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 < 0 if <code>this</code> is "less than" <code>o</code>;
* > 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;
}
}
}