package adipe.translate.impl;
import static adipe.translate.impl.DnfExcludable.checkContainsSubquery;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.List;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.antlr.v4.runtime.tree.TerminalNode;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import adipe.translate.AmbiguousNameException;
import adipe.translate.TranslationException;
import adipe.translate.TypeCheckException;
import adipe.translate.WrappedException;
import adipe.translate.ra.RaTermBuilder;
import adipe.translate.ra.Relation;
import adipe.translate.ra.Schema;
import adipe.translate.sql.ColumnIndexesImpl;
import adipe.translate.sql.ColumnIndexes;
import adipe.translate.sql.ColumnNamesImpl;
import adipe.translate.sql.ColumnNamesLookup;
import adipe.translate.sql.State2;
import adipe.translate.sql.SimpleColumn;
import adipe.translate.sql.parser.SqlParser.*;
/**
* This represents the process and state of translating an SQL table reference (or table name, etc.) into the library's internal representation.
*
* Examples of table references in the FROM clause:
* FROM p
* FROM p JOIN q ON b=q.a
*
* Examples of queries that require parsing by name:
* TABLE p
*
* Parsing by JoinedTableContext:
* p JOIN q on p.a=q.a
*
*/
abstract class TableReferenceTranslation {
protected final StoresRelations stores;
private final Scope scope;
private final Schema schema;
private final TranslatesSubqueries translates;
protected LinkedHashMultimap<String, ColumnNamesImpl> relationsFound;
private TableReferenceTranslation(StoresRelations stores, TranslatesSubqueries translates, Schema schema) {
this.stores = stores;
this.scope = new Scope(new ColumnScope(null), null, new TableScope(null));
this.schema = schema;
this.translates = translates;
this.relationsFound = LinkedHashMultimap.create();
}
/**
* Prepare for interpreting the table reference in {@link ctx}.
*
* @param ctx the term describing the table reference
* @param stores a StoresRelations instance to handle results
* @param translates a TranslatesSubqueries instance to translate any subqueries
* @param schema the database schema
* @return a new instance
*/
static TableReferenceTranslation of(TableReferenceContext ctx, StoresRelations stores, TranslatesSubqueries translates, Schema schema) {
return new ByTableReferenceContext(ctx, stores, translates, schema);
}
/**
* Prepare for interpreting the table, referenced by name
* @param tableName the name describing the table
* @param stores a StoresRelations instance to handle results
* @param schema the database schema
* @return a new instance
*/
static TableReferenceTranslation of(String tableName, StoresRelations stores, Schema schema) {
return new ByName(tableName, stores, schema);
}
/**
* Prepare for interpreting the joined table in {@link ctx}.
*
* @param ctx the term describing the joined table
* @param stores a StoresRelations instance to handle results
* @param translates a TranslatesSubqueries instance to translate any subqueries
* @param schema the database schema
* @return a new instance
*/
static TableReferenceTranslation of(JoinedTableContext ctx, StoresRelations stores, TranslatesSubqueries translates, Schema schema) {
return new ByJoinedTableContext(ctx, stores, translates, schema);
}
/**
* Interpret a tableReference into a {@link Relation} instance
*/
private static class ByTableReferenceContext extends TableReferenceTranslation {
private final TableReferenceContext ctx;
ByTableReferenceContext(TableReferenceContext ctx, StoresRelations stores, TranslatesSubqueries translates,
Schema schema) {
super(stores, translates, schema);
this.ctx = ctx;
}
/**
* @throws IndexOutOfBoundsException if the name in the table reference names no table from the schema
* @throws TranslationException when a derived table is parser, but has a derived column list of the wrong size
*/
@Override
protected State2 interpretEtc() throws IndexOutOfBoundsException, TranslationException {
return tableReference(ctx);
}
}
/**
* Interpret a table name into a {@link Relation} instance
*/
private static class ByName extends TableReferenceTranslation {
private final String tableName;
ByName(String tableName, StoresRelations stores, Schema schema) {
super(stores, null, schema);
this.tableName = tableName;
}
@Override
protected State2 interpretEtc() throws TranslationException {
return registerTableByName(tableName, tableName, null);
}
}
/**
* Interpret a joinedTableReference into a {@link Relation} instance
*/
private static class ByJoinedTableContext extends TableReferenceTranslation {
private final JoinedTableContext ctx;
public ByJoinedTableContext(JoinedTableContext ctx, StoresRelations stores, TranslatesSubqueries translates, Schema schema) {
super(stores, translates, schema);
this.ctx = ctx;
}
/**
* @throws TranslationException when a derived table is parsed, but has a derived column list of the wrong size
*/
@Override
protected State2 interpretEtc() throws TranslationException {
return visitJoinedTable(ctx);
}
}
/**
* Interpret the table reference and store results
* @return the resulting relation
*
* @side stores the relation and formula using {@link #stores}
* @side clobbers {@link #scopeIncludingAliases} (used elsewhere, e.g., in {@link #processSelectList})
* @throw IndexOutOfBoundsException when no such table is found in the schema
* @throw TranslationException when a derived table is parsed with a derived column list of the wrong size
* TODO update
*/
final State2 interpret() throws IndexOutOfBoundsException, TranslationException
{
State2 st = interpretEtc();
stores.onVisibleRelations(relationsFound);
stores.onRelationFormula(st);
return st;
}
abstract protected State2 interpretEtc() throws IndexOutOfBoundsException, TranslationException;
private void registerRelation(String tableName, ColumnNamesImpl colNames) {
relationsFound.put(tableName, colNames);
scope.addTable(tableName, colNames);
}
/**
* @side adds the table to the scope
* @side clobbers {@link #scopeIncludingAliases} (used elsewhere, e.g., in {@link #processSelectList})
* @throws IndexOutOfBoundsException if the name of the referenced table is not found in the schema
* @throws TranslationException if a derived table is parsed with a derived column list of the wrong size
* @throws TranslationException the query is malformed
*
* @return the relation resulting from parsing
* TODO update
*/
protected State2 tableReference(TableReferenceContext ctx)
throws IndexOutOfBoundsException, TranslationException
{
if (ctx.tableName() != null) {
String primaryName = ctx.tableName().getText();
String knownAs = primaryName;
List<String> derivedColumnList = null;
if (ctx.correlationSpecification() != null)
{
derivedColumnList = visitDerivedColumnList(ctx.correlationSpecification().derivedColumnList());
knownAs = ctx.correlationSpecification().correlationName().getText();
}
return registerTableByName(primaryName, knownAs, derivedColumnList);
} else if (ctx.derivedTable() != null) {
return derivedTable(ctx.derivedTable(), ctx.correlationSpecification());
} else {
return joinedTableInlinedInTableReference(ctx);
}
}
/**
* @param derivedColumnList may be null which means no list
* @return the list of column names, or null if no {@code derivedColumnList == null}
*/
private List<String> visitDerivedColumnList(DerivedColumnListContext derivedColumnList)
{
if (derivedColumnList == null) return null;
return visitColumnNameList(derivedColumnList.columnNameList());
}
private List<String> visitColumnNameList(ColumnNameListContext columnNameList)
{
List<String> names = Lists.newArrayList();
for (ColumnNameContext cnctx : columnNameList.columnName()) {
names.add(cnctx.getText());
}
return names;
}
/**
* @throws IndexOutOfBoundsException if no table by the name {@code primaryName} is found in the schema
* @throws TranslationException if {@code derivedColumnList} has different number of names than there are columns
* TODO update
*/
protected State2 registerTableByName(String primaryName, String knownAs, List<String> derivedColumnList)
throws IndexOutOfBoundsException, TranslationException
{
Relation r;
ColumnNamesImpl[] colNamesWr = new ColumnNamesImpl[1];
try {
r = schema.instantiateTable(primaryName, knownAs, derivedColumnList, colNamesWr);
} catch (IllegalStateException exc) {
throw new TranslationException(exc.getMessage(), exc);
}
registerRelation(r.alias(), colNamesWr[0]);
return makeState2(r, colNamesWr[0].asColumnIndexesLookup(), addQualifiedColumnNames(knownAs == null?primaryName:knownAs, colNamesWr[0]));
}
private State2 makeState2(Relation r, ColumnIndexesImpl indexes, ColumnNamesImpl named) {
return new State2(r, indexes, named, scope.tableScope());
}
/**
* Return the {@code Relation} instance representing the derived table and the subquery.
* @throws TranslationException when a derived column list is specified in {@code correlationSpecification},
* but it specifies an incorrect number of column names
* TODO update
*/
private State2 derivedTable(
DerivedTableContext derivedTable,
CorrelationSpecificationContext correlationSpecificationCtx)
throws TranslationException
{
String alias;
SubqueryContext subqueryCtx;
List<String> derivedColumnList;
ColumnNamesImpl[] colNamesWr = new ColumnNamesImpl[1];
alias = correlationSpecificationCtx.correlationName().getText();
derivedColumnList = visitDerivedColumnList(correlationSpecificationCtx.derivedColumnList());
subqueryCtx = derivedTable.tableSubquery().subquery();
Relation subquery = translates.translate(subqueryCtx, alias, derivedColumnList, colNamesWr);
registerRelation(subquery.alias(), colNamesWr[0]);
return makeState2(subquery, colNamesWr[0].asColumnIndexesLookup(), addQualifiedColumnNames(subquery.alias(), colNamesWr[0]));
}
private ColumnNamesImpl addQualifiedColumnNames(String tableName, ColumnNamesImpl withoutQualified) {
ColumnNamesImpl named = new ColumnNamesImpl(withoutQualified);
for (Entry<String, SimpleColumn> entry : withoutQualified.columnsMultimap().entries()) {
named.addColumn(tableName+"."+entry.getKey(), entry.getValue());
}
return named;
}
/**
* @side adds tables to the scope
* @side clobbers {@link #scopeIncludingAliases} (used elsewhere, e.g., in {@link #processSelectList})
* @throws IndexOutOfBoundsException when a table name not found in the schema is referenced
* @throws TranslationException when a derived table is parsed, but has a derived column list of the wrong size
* TODO update
*/
private State2 joinedTableInlinedInTableReference(TableReferenceContext ctx)
throws IndexOutOfBoundsException, TranslationException {
if (ctx.joinedTable() != null) {
return visitJoinedTable(ctx.joinedTable());
} else if (ctx.CROSS() != null) {
return crossJoin(ctx.tableReference());
} else {
checkNotNull(ctx.JOIN());
return qualifiedJoin(ctx.tableReference(), ctx.NATURAL(), ctx.joinType(), ctx.joinSpecification());
}
}
/**
* @side adds tables to the scope
* @side clobbers {@link #scopeIncludingAliases} (used elsewhere, e.g., in {@link #processSelectList})
* @throw {@link IndexOutOfBoundsException} when a table name not found in the schema is referenced
* @throws TranslationException when a derived table is parsed, but has a derived column list of the wrong size
* TODO update
*/
protected State2 visitJoinedTable(JoinedTableContext ctx)
throws IndexOutOfBoundsException, TranslationException
{
if (ctx.joinedTable() != null) {
return visitJoinedTable(ctx.joinedTable());
} else if (ctx.crossJoin() != null) {
return crossJoin(ctx.crossJoin().tableReference());
} else {
checkNotNull(ctx.qualifiedJoin());
return qualifiedJoin(ctx.qualifiedJoin().tableReference(),
ctx.qualifiedJoin().NATURAL(),
ctx.qualifiedJoin().joinType(),
ctx.qualifiedJoin().joinSpecification());
}
}
private State2 crossJoin(List<TableReferenceContext> tableReference) {
throw new RuntimeException("not implemented"); // TODO
}
private final static Pattern EQ_PATTERN = Pattern.compile("#(\\d+)=#(\\d+)");
/**
* @side adds tables to the scope
* @side clobbers {@link #scopeIncludingAliases} (used elsewhere, e.g., in {@link #processSelectList})
* @throws IndexOutOfBoundsException when a table name is referenced that is not found in the schema
* @throws TranslationException when a derived table is parsed, but has a derived column list of the wrong size
*/
private State2 qualifiedJoin(List<TableReferenceContext> tableReference,
TerminalNode natural, JoinTypeContext joinType,
JoinSpecificationContext joinSpecification)
throws IndexOutOfBoundsException, TranslationException
{
StoresRelations storesForJoin = new StoresRelations() {
@Override
public void onVisibleRelations(LinkedHashMultimap<String, ColumnNamesImpl> relationsFound) {
if (TableReferenceTranslation.this.relationsFound.isEmpty()) {
TableReferenceTranslation.this.relationsFound = relationsFound;
} else {
TableReferenceTranslation.this.relationsFound.putAll(relationsFound);
}
for (Entry<String, ColumnNamesImpl> entry : relationsFound.entries()) {
scope.addTable(entry.getKey(), entry.getValue());
}
}
@Override
public void onRelationFormula(State2 relation) { }
};
State2 r1 = TableReferenceTranslation.of(tableReference.get(0), storesForJoin, translates, schema).interpret();
State2 r2 = TableReferenceTranslation.of(tableReference.get(1), storesForJoin, translates, schema).interpret();
if (natural != null) {
throw new RuntimeException("NATURAL JOIN is not supported"); // TODO
}
if (joinSpecification == null) {
// TODO
throw new RuntimeException("JOIN specification missing");
}
boolean isInnerJoin = joinType == null || joinType.INNER() != null;
ColumnNamesImpl named = new ColumnNamesImpl();
named.addAll(r1.named().inner());
named.addAll(r2.named().inner());
if (! isInnerJoin) {
r1.overwriteExpand();
r2.overwriteExpand();
}
Proposition p = visitJoinSpecification(joinSpecification, columnNamesLookupForJoin2(r1, r2));
// TODO move the check inside {@link Proposition} instead of matching like this
boolean pIsEq;
String pcode = p.code(columnIndexesForJoin2(r1, r2));
Matcher eqMatcher = EQ_PATTERN.matcher(pcode);
pIsEq = eqMatcher.matches();
if (pIsEq) {
// TODO refactor
int index1 = Integer.parseInt(eqMatcher.group(1));
int index2 = Integer.parseInt(eqMatcher.group(2));
if (index1 > index2) {
int tmp = index1;
index1 = index2;
index2 = tmp;
}
boolean bothColumnsFromSameOperand = (index1 > r1.relation().columns().size() || index2 <= r1.relation().columns().size());
if (bothColumnsFromSameOperand) {
pIsEq = false;
} else if (isInnerJoin) {
index2 = index2 - r1.relation().columns().size();
SimpleColumn replacedColumn = r2.relation().columns().get(index2 - 1);
SimpleColumn replacedBy = r1.relation().columns().get(index1 - 1);
for (ColumnNamesImpl cibn : relationsFound.values()) {
// TODO improve time complexity
cibn.replace(replacedColumn, replacedBy);
}
named.replace(replacedColumn, replacedBy);
}
}
if (checkContainsSubquery(p)) {
throw new RuntimeException("not implemented (EXISTS in JOIN condition)");
}
Relation rel;
if (! isInnerJoin) {
if (joinType.UNION() != null) {
throw new RuntimeException("UNION JOIN is not supported");// TODO
} else {
checkNotNull(joinType.outerJoinType());
if (joinType.outerJoinType().LEFT() != null) {
rel = r1.relation().leftOuterJoin(r2.relation(), pcode, pIsEq);
} else if (joinType.outerJoinType().RIGHT() != null) {
rel = r1.relation().rightOuterJoin(r2.relation(), pcode, pIsEq);
} else {
checkNotNull(joinType.outerJoinType().FULL());
rel = r1.relation().fullOuterJoin(r2.relation(), pcode, pIsEq);
}
}
} else {
rel = r1.relation().join(r2.relation(), pcode, pIsEq);
}
return makeState2(rel, rel.columns(), named);
}
private Proposition visitJoinSpecification(JoinSpecificationContext ctx, ColumnNamesLookup columnNamesLookup) throws WrappedException, TypeCheckException, RuntimeException {
checkNotNull(ctx.joinCondition());
// TODO
// zawodzi dla SELECT * FROM p JOIN q USING ...
return visitJoinCondition(ctx.joinCondition(), columnNamesLookup);
}
/**
* @throws RuntimeException
* @throws TypeCheckException
* @throws WrappedException
* @side none
*/
private Proposition visitJoinCondition(JoinConditionContext ctx, ColumnNamesLookup columnNamesLookup) throws WrappedException, TypeCheckException, RuntimeException {
RaTermBuilder raBuilder = RaTermBuilder.create(); // TODO use a special case builder that throws on registration of aggregates
return ConditionTranslation.of(
columnNamesLookup,
raBuilder,
ctx.searchCondition()
).proposition();
}
private ColumnNamesLookup columnNamesLookupForJoin2(final State2 r1, final State2 r2) {
return new ColumnNamesLookup() {
// TODO maybe get a base class covering the common {@link #indexFromColumnName} code
@Override
public SimpleColumn byColumnName(String columnName) throws WrappedException {
try {
return findColumnInEither(columnName);
} catch (AmbiguousNameException exc) {
throw new WrappedException(exc);
}
}
private SimpleColumn findColumnInEither(String columnName) throws AmbiguousNameException, WrappedException {
if (r1.getTableIfQualifiedNameAndExists(columnName) != null && r2.getTableIfQualifiedNameAndExists(columnName) != null) {
throw new AmbiguousNameException(String.format("The table name '%s' is ambiguous", TableScope.getTableNameIfQualifiedName(columnName)));
}
SimpleColumn matchInLeftState = r1.getColumnIcShdNwrapAmbNindNull(columnName);
if (matchInLeftState != null) {
try {
r2.getColumnIcShdNwrapAmbInd(columnName);
throw new AmbiguousNameException(String.format("The column name '%s' is ambiguous", columnName));
} catch (IndexOutOfBoundsException exc) {
return matchInLeftState;
}
} else {
try {
return r2.getColumnIcShdNwrapAmbInd(columnName);
} catch (IndexOutOfBoundsException exc2) {
throw new WrappedException(exc2);
}
}
}
};
}
private ColumnIndexes columnIndexesForJoin2(final State2 r1, final State2 r2) {
return new ColumnIndexes() {
@Override
public int index(SimpleColumn column) throws WrappedException {
try {
return 1 + r1.relation().columns().find(column);
} catch (IndexOutOfBoundsException ex) {
return 1 + r1.relation().columns().size() + r2.relation().columns().find(column);
}
}
};
}
@Override
public String toString() {
return relationsFound.toString();
}
}