TableRelation.java

/*
** Module   : TableRelation.java
** Abstract : Representation of a database relation between two tables
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20050325   @20982 Created initial version. Represents a natural relation between two database
**                           tables in a Progress schema.
** 002 ECF 20050616   @21535 Reworked to support notion of the inverse of a relation. Added methods to report
**                           databases affected by the join and to report inverse join type.
** 003 ECF 20070628   @35343 Minor optimization. Replaced StringBuffer with StringBuilder.
** 004 ECF 20081216   @40939 Fixed normalizedTableName(). Temp table database names were the internal versions
**                           ("@temp_db" and "@work_db") used by the SchemaDictionary, but they need to be the
**                           standard "_temp" used everywhere else.
** 005 GES 20090518   @42403 Import change.
** 006 ECF 20131013          Implemented generics.
** 007 ECF 20131215          Changes to support subclassing for metaschema relations.
** 008 ECF 20150325          Added support for cross-schema natural joins.
** 009 OM  20230627          Added support for parent-recid joins.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.schema;

import java.util.*;
import com.goldencode.ast.*;

/**
 * A representation of a <em>natural relation</em> between two Progress
 * tables. A natural relation is a one defined by the following rules:
 * <ul>
 * <li>the tables must have between them one or more field with a common name and data type;
 * <li>the common field(s) must participate in a unique index in at least one of the tables;
 * <li>one table uses a special RECID field to refer a record in the other table. The first table is the
 *       {@code child}, the second is the {@code parent} and the field is named {@code <parent>-recid}.
 * <li>only one such relationship between the two tables is allowed.
 * </ul>
 * The nature of this relation ultimately is used to define an analogous
 * relation between a set of tables in a traditional, relational database.
 * In such a database, the relation is defined by a unique, primary key in
 * one table, referenced by a (not necessarily unique) foreign key in the
 * other.
 */
public class TableRelation
implements Comparable<TableRelation>,
           SchemaParserTokenTypes
{
   /** AST of left-hand table in a join */
   private Aast left = null;
   
   /** AST of right-hand table in a join */
   private Aast right = null;
   
   /** AST of table which represents the primary key end of the relation */
   private Aast primary = null;
   
   /** Normalized left table name */
   private String leftName = null;
   
   /** Normalized right table name */
   private String rightName = null;
   
   /** Names of common fields which define the relation */
   private Set<String> key = null;
   
   /** Set of (lowercase) names of databases affected by this join */
   private final Set<String> databases = new HashSet<>();
   
   /** Type of relation:  many-to-one, one-to-one, or parent-id */
   private int type = -1;
   
   /**
    * Construct a {@code Join}, given a left table, right table, optionally the table which represents the
    * primary key end of the relation, and the set of field names which define the join.
    *
    * @param   left
    *          Left table. May not be {@code null}.
    * @param   right
    *          Right table. May not be {@code null}.
    * @param   primary
    *          Table which represents the primary key end of the join. May be {@code null}.
    * @param   key
    *          Set of names of the fields which comprise the join.
    * @param   type
    *          Type of relation. If this object represents a primary/foreign key relation, type is always
    *          described from the foreign point of view (i.e., {@code MANY_TO_ONE}).<br>
    *          A special case <em>for non-temporary tables</em>, when a record in a child table may refer its
    *          parent record using a {@code <parent>-recid} field is marked as {@code PARENT_ID_RELATION}.<br>
    *          Otherwise, type must be {@code ONE_TO_ONE}.
    *
    * @throws  NullPointerException
    *          if either {@code left} or {@code right} is {@code null}.
    * @throws  IllegalArgumentException
    *          if {@code type} is not a valid relation type.
    */
   TableRelation(Aast left, Aast right, Aast primary, Set<String> key, int type)
   {
      if (left == null || right == null)
      {
         throw new NullPointerException();
      }
      
      switch (type)
      {
         case MANY_TO_ONE:
         case ONE_TO_ONE:
         case PARENT_ID_RELATION:
            break;
         default:
            throw new IllegalArgumentException("Invalid relation type: " + type);
      }
      
      this.primary = primary;
      this.key = key;
      
      if (primary != right)
      {
         this.left = left;
         this.right = right;
      }
      else
      {
         this.left = right;
         this.right = left;
      }
      
      leftName = normalizedTableName(this, this.left);
      rightName = normalizedTableName(this, this.right);
      
      databases.add(getDatabase(this.left).toLowerCase());
      databases.add(getDatabase(this.right).toLowerCase());
      
      this.type = type;
   }
   
   /**
    * Composes a normalized table name, which is the fully qualified table name in all lowercase,
    * in the form: {@code <database_name><table_name>}.
    * <p>
    * The database name is assumed to be the name of the given table's database.
    *
    * @param   table
    *          AST which defines table.
    *
    * @return  Normalized table name as described above.
    */
   static String normalizedTableName(Aast table)
   {
      return normalizedTableName(null, table);
   }
   
   /**
    * Composes a normalized table name, which is the fully qualified table name in all lowercase,
    * in the form: {@code <database_name><table_name>}.
    * 
    * @param   join
    *          Table relation from which the database name will be taken, if not {@code null}.
    * @param   table
    *          AST which defines table.
    *
    * @return  Normalized table name as described above.
    */
   static String normalizedTableName(TableRelation join, Aast table)
   {
      String dbName = (join != null ? join.getDatabase(table) : table.getParent().getText());
      if (SchemaDictionary.TEMP_DB.equals(dbName) || SchemaDictionary.WORK_DB.equals(dbName))
      {
         dbName = SchemaDictionary.TEMP_TABLE_DB;
      }
      
      return (dbName + '.' + table.getText()).toLowerCase();
   }
   
   /**
    * Composes the normalized database name for a table.
    * 
    * @param   table
    *          Table AST.
    * 
    * @return  Lowercase database name.
    */
   static String normalizedDatabaseName(Aast table)
   {
      String dbName = table.getParent().getText();
      if (SchemaDictionary.TEMP_DB.equals(dbName) || SchemaDictionary.WORK_DB.equals(dbName))
      {
         dbName = SchemaDictionary.TEMP_TABLE_DB;
      }
      
      return dbName.toLowerCase();
   }
   
   /**
    * Reports which database(s) is/are affected by this relation as a set
    * of (lowercase) database names. Generally, the returned set will
    * include only a single database name. In the (rare) case of a join
    * across databases, the set will contain two database names.
    *
    * @return  An unmodifiable set of the name(s) of database(s) affected by
    *          this join.
    */
   public Set<String> getDatabases()
   {
      return Collections.unmodifiableSet(databases);
   }
   
   /**
    * A simplified implementation which considers two joins equal if they
    * both contain the same pair of tables, regardless of the left-ness or
    * right-ness of either table in each pair. The premise is that any
    * given pair of tables can only have one joinable relation between
    * them, regardless of the side of the join they are on. Note that this
    * premise does not consider the result set generated by a join (which
    * could vary for an outer join depending on the left-ness or right-ness
    * of the tables), only the nature of the relation between the tables.
    * <p>
    * Two tables are considered equal if their full names match.
    *
    * @param   o
    *          Object with which to compare this <code>Join</code>
    *          instance for equality.
    *
    * @return  <code>true</code> if the joins are equal according to the
    *          above rules, else <code>false</code>.
    */
   @Override
   public boolean equals(Object o)
   {
      if (!(o instanceof TableRelation))
      {
         return false;
      }
      
      TableRelation rel = (TableRelation) o;
      
      String l1 = leftName;
      String l2 = rel.leftName;
      String r1 = rightName;
      String r2 = rel.rightName;
      
      return ((l1.equals(l2) && r1.equals(r2)) || (l1.equals(r2) && r1.equals(l2)));
   }
   
   /**
    * Override the default implementation of this method to be consistent
    * with the simplified implementation of {@link #equals}. It is
    * intentional that we get the same hash code regardless of which table
    * is "left" and which is "right".
    *
    * @return  Hash code for this object.
    */
   @Override
   public int hashCode()
   {
      return leftName.hashCode() + rightName.hashCode();
   }
   
   /**
    * Compares this instance to another <code>TableRelation</code> instance
    * to determine how they are logically sorted:
    * <ol>
    * <li>relations which compare as equivalent by {@link #equals} always
    *     compare as equivalent by this method;
    * <li>relations which define a primary key are considered "greater than"
    *     those which do not;  if this test is inconclusive...
    * <li>the names of the left-hand tables of each relation are compared
    *     lexicographically;  if this test is inconclusive...
    * <li>the names of the right-hand tables of each relation are compared
    *     lexicographically.
    * </ol>
    *
    * @param   other
    *          Another instance of this class to compare against this one.
    *
    * @return  &lt; 0 if <code>this</code> is "less than" <code>o</code>;
    *          &gt; 0 if <code>this</code> is "greater than" <code>o</code>;
    *          0 if both instances compare as equivalent by the above rules.
    */
   public int compareTo(TableRelation other)
   {
      if (this.equals(other))
      {
         return 0;
      }
      
      int res = 0;
      if (primary == null && other.primary != null)
      {
         res = -1;
      }
      else if (primary != null && other.primary == null)
      {
         res = 1;
      }
      
      if (res == 0)
      {
         res = left.getText().compareTo(other.left.getText());
      }
      
      if (res == 0)
      {
         res = right.getText().compareTo(other.right.getText());
      }
      
      return res;
   }
   
   /**
    * Compose and return a string representation of this object, suitable
    * for simple reporting and debug purposes.
    *
    * @return  String representation of this object's state.
    */
   public String toString()
   {
      StringBuilder buf = new StringBuilder();
      
      buf.append(getDatabase(left)).append('.').append(left.getText());
      
      if (primary == left)
      {
         buf.append(" <---- ");
      }
      else if (primary == right)
      {
         buf.append(" ----> ");
      }
      else
      {
         buf.append(" <---> ");
      }
      
      buf.append(getDatabase(right)).append('.').append(right.getText());
      
      switch (type)
      {
         case MANY_TO_ONE:
            buf.append(" (many-to-one on ");
            break;
         case ONE_TO_ONE:
            buf.append(" (one-to-one on ");
            break;
         case PARENT_ID_RELATION:
            buf.append(" (parent-id on ");
            break;
      }
      
      buf.append(getKey()).append(')');
      
      return buf.toString();
   }
   
   /**
    * Get the database schema name for the give table AST.
    * 
    * @param   table
    *          Target table AST.
    * 
    * @return  Name of the table's database schema.
    */
   protected String getDatabase(Aast table)
   {
      return table.getParent().getText();
   }
   
   /**
    * Indicates whether this relation uses a real foreign key join (vs. a Progress-style, natural
    * join).
    * 
    * @return  <code>false</code>.
    */
   protected boolean hasRealForeignKey()
   {
      return false;
   }
   
   /**
    * Get the left-hand table of the join defined by this relation.
    *
    * @return  Left-hand table AST.
    */
   Aast getLeft()
   {
      return left;
   }
   
   /**
    * Get the right-hand table of the join defined by this relation.
    *
    * @return  Right-hand table AST.
    */
   Aast getRight()
   {
      return right;
   }
   
   /**
    * Get the table which defines the primary key end of this relation, if
    * any. If this method returns <code>null</code>, it indicates a
    * one-to-one relationship between the tables
    *
    * @return  Primary key table AST, or <code>null</code> if a two-way,
    *          one-to-one relation was detected.
    */
   Aast getPrimary()
   {
      return primary;
   }
   
   /**
    * Get the table which defines the foreign key end of this relation, if
    * any. If this method returns <code>null</code>, it indicates a
    * one-to-one relationship between the tables
    *
    * @return  Foreign key table AST, or <code>null</code> if a two-way,
    *          one-to-one relation was detected.
    */
   Aast getForeign()
   {
      if (primary == left)
      {
         return right;
      }
      else if (primary == right)
      {
         return left;
      }
      
      return null;
   }
   
   /**
    * Get the set of field names which define the relation described by this object.
    *
    * @return  Set of common field names.
    */
   Set<String> getKey()
   {
      return key;
   }
   
   /**
    * Get the type of relation; will always return a valid type.
    *
    * @return  Relation type; will always be one of {@code MANY_TO_ONE}, {@code ONE_TO_ONE} or
    *          {@code PARENT_ID_RELATION}.
    *
    * @see     #getInverseType
    */
   int getType()
   {
      return type;
   }
   
   /**
    * Get the inverse type of this relation. Whereas {@link #getType}
    * describes the relation from the point of view of the foreign table
    * (or right table, if no foreign end is defined), this method describes
    * the relation from the point of view of the primary table (or left
    * table, if no primary end is defined).
    *
    * @return  Inverse relation type; will always be {@code ONE_TO_MANY}, {@code ONE_TO_ONE},
    *          {@code PARENT_ID_RELATION} or -1 (as an error).
    */
   int getInverseType()
   {
      switch (type)
      {
         case MANY_TO_ONE:
            return ONE_TO_MANY;
         case ONE_TO_ONE:
            return ONE_TO_ONE;
         case PARENT_ID_RELATION:
            return PARENT_ID_RELATION;
         default:
            return -1;
      }
   }
}