Package adipe.translate.impl

Source Code of adipe.translate.impl.TableReferenceTranslation

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();
    }
}
TOP

Related Classes of adipe.translate.impl.TableReferenceTranslation

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.