# 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;
          }
       }
    }