package com.tesora.dve.sql.expression;
/*
* #%L
* Tesora Inc.
* Database Virtualization Engine
* %%
* Copyright (C) 2011 - 2014 Tesora Inc.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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 should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import com.tesora.dve.common.MultiMap;
import com.tesora.dve.errmap.AvailableErrors;
import com.tesora.dve.errmap.ErrorInfo;
import com.tesora.dve.sql.ParserException.Pass;
import com.tesora.dve.sql.SchemaException;
import com.tesora.dve.sql.node.LanguageNode;
import com.tesora.dve.sql.node.expression.Alias;
import com.tesora.dve.sql.node.expression.AliasInstance;
import com.tesora.dve.sql.node.expression.ColumnInstance;
import com.tesora.dve.sql.node.expression.ExpressionAlias;
import com.tesora.dve.sql.node.expression.ExpressionNode;
import com.tesora.dve.sql.node.expression.FunctionCall;
import com.tesora.dve.sql.node.expression.NameInstance;
import com.tesora.dve.sql.node.expression.TableInstance;
import com.tesora.dve.sql.node.expression.VariableInstance;
import com.tesora.dve.sql.node.expression.WildcardTable;
import com.tesora.dve.sql.node.structural.JoinedTable;
import com.tesora.dve.sql.parser.LexicalLocation;
import com.tesora.dve.sql.parser.SourceLocation;
import com.tesora.dve.sql.schema.Capability;
import com.tesora.dve.sql.schema.Column;
import com.tesora.dve.sql.schema.LockInfo;
import com.tesora.dve.sql.schema.MultiMapLookup;
import com.tesora.dve.sql.schema.Name;
import com.tesora.dve.sql.schema.PEColumn;
import com.tesora.dve.sql.schema.PETable;
import com.tesora.dve.sql.schema.QualifiedName;
import com.tesora.dve.sql.schema.Schema;
import com.tesora.dve.sql.schema.SchemaContext;
import com.tesora.dve.sql.schema.SchemaLookup;
import com.tesora.dve.sql.schema.SubqueryTable;
import com.tesora.dve.sql.schema.Table;
import com.tesora.dve.sql.schema.TableResolver;
import com.tesora.dve.sql.schema.TableResolver.MissingTableFunction;
import com.tesora.dve.sql.schema.TempTable;
import com.tesora.dve.sql.schema.UnqualifiedName;
import com.tesora.dve.sql.statement.dml.ProjectingStatement;
import com.tesora.dve.sql.util.Functional;
import com.tesora.dve.sql.util.ListSet;
import com.tesora.dve.sql.util.Pair;
import com.tesora.dve.sql.util.UnaryFunction;
public class ScopeEntry implements Scope {
// table namespace
private MultiMap<Name, TableInstance> tableNamespace;
// derived column namespace: aliases declared in the projection
private MultiMap<Name, ExpressionAlias> columnNamespace;
// columns explicitly referenced in the projection at the top level
// used to resolve ambiguity on order by, having, group by which may reference columns from the projection
private ProjectionMap projectionNamespace;
// names referenced in the group bys
private MultiMap<Name, ExpressionNode> groupByNamespace;
private List<ExpressionNode> projection;
private final int scopeID;
// if in the context of building a table definition, record columns by name as they are created here.
// we need them when we look up columns for keys, or for distribution vectors, etc.
private SchemaLookup<PEColumn> columnLookup;
private Table<?> tableInProcess;
private ScopeParsePhase phase;
private ListSet<NameInstance> unresolved;
private ListSet<NameInstance> unresolvedChildren;
private List<Scope> nested;
private ListSet<ProjectingStatement> nestedQueries;
private ListSet<VariableInstance> variables;
private Map<Name, FunctionCall> functions;
public ScopeEntry(boolean doresolution, int id) {
scopeID = id;
tableNamespace = new MultiMap<Name, TableInstance>();
columnNamespace = new MultiMap<Name, ExpressionAlias>();
projectionNamespace = new ProjectionMap();
groupByNamespace = new MultiMap<Name, ExpressionNode>();
columnLookup = new SchemaLookup<PEColumn>(null, false, false);
nested = new ArrayList<Scope>();
nestedQueries = new ListSet<ProjectingStatement>();
variables = new ListSet<VariableInstance>();
unresolved = new ListSet<NameInstance>();
phase = (doresolution ? ScopeParsePhase.RESOLVING: ScopeParsePhase.UNRESOLVING);
functions = new HashMap<Name, FunctionCall>();
}
public int getID() {
return scopeID;
}
@Override
public ScopeParsePhase getPhase() {
return phase;
}
@Override
public void setPhase(ScopeParsePhase spp) {
phase = spp;
}
@Override
public void storeProjection(List<ExpressionNode> proj) {
projection = proj;
phase = ScopeParsePhase.RESOLVING_CURRENT;
}
@Override
public List<Scope> getNested() {
return nested;
}
@Override
public ListSet<ProjectingStatement> getNestedQueries() {
return nestedQueries;
}
@Override
public ListSet<VariableInstance> getVariables() {
return variables;
}
// the errors we throw.
private void objectAmbiguous(String what, Name origName) throws SchemaException {
throw new SchemaException(Pass.SECOND, "Ambiguous " + what + " reference: " + origName.getSQL());
}
private void throwNonUniqueTableException(final Name ambiguousTableName) {
throw new SchemaException(new ErrorInfo(AvailableErrors.NON_UNIQUE_TABLE, ambiguousTableName.getUnquotedName().get()));
}
private static void tableNotFound(SchemaContext sc, Schema<?> schema, Name givenName) throws SchemaException {
ErrorInfo ei = null;
if (givenName.isQualified()) {
QualifiedName qn = (QualifiedName) givenName;
ei = new ErrorInfo(AvailableErrors.TABLE_DNE,
qn.getNamespace().getUnquotedName().get(),
qn.getUnqualified().getUnquotedName().get());
} else {
UnqualifiedName db = schema.getSchemaName(sc);
ei = new ErrorInfo(AvailableErrors.TABLE_DNE,
db.getUnquotedName().get(),
givenName.getUnquotedName().get());
}
throw new SchemaException(ei);
}
private static void columnNotFound(Name columnName, LexicalLocation location) throws SchemaException {
throw new SchemaException(new ErrorInfo(AvailableErrors.COLUMN_DNE,
columnName.getUnquotedName().get(),
location.getExternal()));
}
public static final TableResolver resolver = new TableResolver().withMTChecks()
.withMissingTableFunction(new MissingTableFunction() {
@Override
public void onMissingTable(SchemaContext sc, Schema<?> schema,
Name name) {
tableNotFound(sc,schema,name);
}
});
// add a declared table to the scope, using any alias or the table name as the key. if the key
// is not unique, emit an error
@Override
public TableInstance buildTableInstance(Name inTableName, UnqualifiedName alias, Schema<?> inSchema, SchemaContext sc, LockInfo info) {
TableInstance ti = null;
if (sc.getCapability() == Capability.PARSING_ONLY) {
ti = new TableInstance(null,inTableName,alias,false);
} else {
TableInstance raw = resolver.lookupShowTable(sc, inSchema, inTableName, info);
ti = raw.adapt(inTableName.getUnqualified(), alias, (sc == null ? 0 : sc.getNextTable()),
(sc != null && sc.getOptions().isResolve()));
}
insertTable(ti,alias,inTableName.getUnqualified());
return ti;
}
@Override
public TableInstance buildTableInstance(Name inTableName, UnqualifiedName alias, SchemaContext sc, LockInfo info) {
TableInstance raw = resolver.lookupTable(sc, inTableName, info);
TableInstance ti = raw.adapt(inTableName.getUnqualified(), alias, (sc == null ? 0 : sc.getNextTable()),
(sc != null && sc.getOptions().isResolve()));
insertTable(ti,alias,inTableName.getUnqualified());
return ti;
}
@Override
public void pushVirtualTable(SubqueryTable sqt, UnqualifiedName alias, SchemaContext sc) {
TableInstance ti = new TableInstance(sqt,alias,alias,sc.getNextTable(),false);
insertTable(ti,alias,sqt.getName());
}
private void insertTable(TableInstance ti, Name alias, Name tableName) {
if (alias != null) {
if (tableNamespace.containsKey(alias))
throwNonUniqueTableException(alias);
tableNamespace.put(alias, ti);
} else {
if (tableNamespace.containsKey(tableName))
throwNonUniqueTableException(tableName);
tableNamespace.put(tableName, ti);
}
}
@Override
public void insertTable(TableInstance ti) {
if (ti.getAlias() != null) {
if (tableNamespace.containsKey(ti.getAlias()))
throwNonUniqueTableException(ti.getAlias());
tableNamespace.put(ti.getAlias(), ti);
} else {
if (tableNamespace.containsKey(ti.getTable().getName()))
throwNonUniqueTableException(ti.getTable().getName());
tableNamespace.put(ti.getTable().getName(), ti);
}
}
@Override
public TableInstance lookupTableInstance(SchemaContext sc, Name given, boolean required) {
if (!given.isQualified()) {
Collection<TableInstance> sub = tableNamespace.get(given);
if (sub == null || sub.isEmpty()) {
if (required) {
tableNotFound(sc,sc.getCurrentDatabase().getSchema(),given);
} else {
return null;
}
} else if (sub.size() > 1) {
throwNonUniqueTableException(given);
} else {
return sub.iterator().next();
}
} else {
throw new SchemaException(Pass.SECOND, "Internal error: qualified table name: " + given.getSQL());
}
return null;
}
private ColumnInstance buildColumnInstance(SchemaContext sc, Name given, Column<?> c, TableInstance ti) {
if (sc != null) {
if (sc.getOptions().isRawPlanStep() && ti.getTable() instanceof TempTable)
return new ColumnInstance(c,ti);
}
return new ColumnInstance(given,c,ti);
}
@Override
public ExpressionNode buildColumnInstance(SchemaContext sc, Name igiven) {
Name given = igiven;
// still building the projection - delay resolution until later
if (phase == ScopeParsePhase.UNRESOLVING) {
NameInstance ni = new NameInstance(given,null);
unresolved.add(ni);
return ni;
}
if (given.isQualified()) {
QualifiedName qn = (QualifiedName)given;
UnqualifiedName tableName = qn.getNamespace();
UnqualifiedName columnName = given.getUnqualified();
TableInstance ti = lookupTableInstance(sc, tableName, false);
if (ti == null)
columnNotFound(given,phase.getLocation());
@SuppressWarnings("null")
Column<?> c = ti.getTable().lookup(sc,given.getUnqualified());
if (columnName.isAsterisk()) {
return new WildcardTable(tableName, ti);
}
if (c == null)
columnNotFound(given,phase.getLocation());
return buildColumnInstance(sc,given,c,ti);
}
// if we're in a restricted namespace, try to build by derived first, then try by table
ExpressionNode any = null;
// the order we search depends on the phase
switch (phase) {
case RESOLVING_CURRENT:
case RESOLVING:
// from and where clauses, can only build by table
any = buildColumnRefByTable(sc, given);
break;
case GROUPBY:
// start from table, then look at column ref by projection, finally column ref by derived
// once found, put into the group by namespace
any = buildColumnRefByDerived(given);
if (any == null)
any = buildColumnRefByProjection(given);
if (any == null)
any = buildColumnRefByTable(sc, given);
if (any != null)
groupByNamespace.put(given, any);
break;
case HAVING:
// start from group by namespace, column ref by projection, column ref by derived
// if we have an expression, we can't use the group by column
any = buildColumnRefByTable(sc, given);
if (any == null)
any = buildColumnRefByProjection(given);
if (any == null)
any = buildColumnRefByDerived(given/*, false*/);
if (any == null)
any = buildColumnRefByGroupBy(given);
break;
case TRAILING:
// start from column ref by derived, then column ref by projection
any = buildColumnRefByDerived(given);
if (any == null)
any = buildColumnRefByProjection(given);
if (any == null)
any = buildColumnRefByTable(sc, given);
break;
default:
throw new IllegalArgumentException("Invalid scope parse phase: " + phase);
}
if (any == null)
columnNotFound(given,phase.getLocation());
return any;
}
@Override
public void resolveProjection(SchemaContext sc) {
if (unresolved != null && unresolved.size() > 0 && tableNamespace.keySet().size() == 0) {
// will not resolve so try later
return;
}
phase = ScopeParsePhase.RESOLVING;
// look for any top level name instances
HashMap<NameInstance,Integer> offsets = new HashMap<NameInstance,Integer>();
for(int i = 0; i < projection.size(); i++) {
ExpressionNode en = projection.get(i);
if (en instanceof NameInstance) {
offsets.put((NameInstance)en,i);
}
}
for(NameInstance ni : unresolved) {
Name embedded = ni.getName();
ExpressionNode ci = buildColumnInstance(sc, embedded);
if (ni.isNegated()) {
ci.setNegated();
}
if (ni.getParent() == null) {
Integer index = offsets.get(ni);
if (index != null) {
projection.remove(index.intValue());
projection.add(index.intValue(), ci);
if (ci instanceof ColumnInstance) {
ColumnInstance nci = (ColumnInstance) ci;
projectionNamespace.add(nci);
} else if (ci instanceof WildcardTable) {
WildcardTable wct = (WildcardTable) ci;
for(Column<?> c : wct.getTableInstance().getTable().getColumns(sc)) {
projectionNamespace.add(new ColumnInstance(c,wct.getTableInstance()));
}
}
} else {
index = null;
for(int i = 0; i < getUnresolvedChildren().size(); i++) {
NameInstance urcni = getUnresolvedChildren().get(i);
if (StringUtils.equals(urcni.getName().get(), ni.getName().get())) {
index = i;
urcni.getParentEdge().set(ci);
getUnresolvedChildren().remove(index.intValue());
break;
}
}
}
if (index == null)
throw new SchemaException(Pass.SECOND, "Lost unresolved column: " + embedded.getSQL());
} else {
ni.getParentEdge().set(ci);
}
}
unresolved.clear();
}
// find a column ref by looking at ref'd tables first
private ExpressionNode buildColumnRefByTable(SchemaContext sc, Name given) {
// look up among the table namespaces
// we might have two unaliased tables with the same name - so keep looking after we find our first candidate
Pair<TableInstance,Column<?>> candidate = null;
for(TableInstance ti : tableNamespace.values()) {
Column<?> c = ti.getTable().lookup(sc,given);
if (c != null) {
if (candidate != null) {
final LanguageNode parent = ti.getParent();
/*
* MySQL now treats the common columns of NATURAL or USING
* joins as a single column, so when a query refers to such
* columns, the query compiler does not consider them as
* ambiguous.
*/
if ((parent instanceof JoinedTable) && ((JoinedTable) parent).getJoinType().isNaturalJoin()) {
continue;
}
objectAmbiguous("Column", given);
} else {
candidate = new Pair<TableInstance, Column<?>>(ti, c);
}
}
}
if (candidate == null) return null;
// build the reference
return buildColumnInstance(sc,given,candidate.getSecond(),candidate.getFirst());
}
private ExpressionNode buildColumnRefByDerived(Name given) {
Collection<ExpressionAlias> sub = columnNamespace.get(given);
if (sub == null || sub.isEmpty())
return null;
if (sub.size() > 1)
objectAmbiguous("Derived column",given);
ExpressionAlias targ = sub.iterator().next();
return new AliasInstance(targ, given.getOrig());
}
private ExpressionNode buildColumnRefByProjection(Name given) {
Collection<ColumnInstance> sub = projectionNamespace.lookup(given);
if (sub == null || sub.isEmpty())
return null;
if (sub.size() > 1)
objectAmbiguous("Referenced column", given);
ColumnInstance ci = sub.iterator().next();
return (ExpressionNode) ci.copy(null);
}
private ExpressionNode buildColumnRefByGroupBy(Name given) {
Collection<ExpressionNode> sub = groupByNamespace.get(given);
if (sub == null || sub.isEmpty())
return null;
if (sub.size() > 1)
objectAmbiguous("Referenced column", given);
ExpressionNode en = sub.iterator().next();
return (ExpressionNode)en.copy(null);
}
@Override
public void insertColumn(UnqualifiedName alias, ExpressionAlias e) {
columnNamespace.put(alias, e);
}
@Override
public ExpressionNode buildExpressionAlias(ExpressionNode e, Alias alias, SourceLocation sloc) {
ExpressionAlias ea = new ExpressionAlias(e, alias,sloc);
if (alias.isName()) {
columnNamespace.put(alias.getNameAlias(), ea);
}
return ea;
}
@Override
public Set<String> getAllAliases() {
HashSet<String> ret = new HashSet<String>();
ret.addAll(Functional.apply(tableNamespace.keySet(), buildUnquoted));
ret.addAll(Functional.apply(columnNamespace.keySet(), buildUnquoted));
return ret;
}
@Override
public ListSet<TableKey> getLocalTables() {
ListSet<TableKey> tabs = new ListSet<TableKey>();
for(TableInstance ti : tableNamespace.values()) {
if (ti.getTable() instanceof PETable) {
PETable pet = (PETable) ti.getTable();
if (pet.isVirtualTable()) continue;
}
tabs.add(ti.getTableKey());
}
return tabs;
}
@Override
public ListSet<TableKey> getAllVisibleTables() {
ListSet<TableKey> out = new ListSet<TableKey>();
out.addAll(getLocalTables());
for(Scope c : nested)
out.addAll(c.getAllVisibleTables());
return out;
}
@Override
public ListSet<FunctionCall> getFunctions() {
ListSet<FunctionCall> funcs = new ListSet<FunctionCall>(functions.values());
return funcs;
}
@Override
public void registerFunction(FunctionCall fc) {
functions.put(fc.getFunctionName().getUnquotedName(), fc);
}
@Override
public PEColumn registerColumn(PEColumn c) {
columnLookup.add(c);
return c;
}
@Override
public void registerAlterColumns(SchemaContext sc, PETable tab) {
tableInProcess = tab;
for(PEColumn c : tab.getColumns(sc)) {
registerColumn(c);
}
}
@Override
public PEColumn lookupInProcessColumn(Name n) {
return columnLookup.lookup(n);
}
@Override
public Table<?> getAlteredTable() {
return tableInProcess;
}
public ListSet<NameInstance> getUnresolved() {
return unresolved;
}
@Override
public ListSet<NameInstance> getUnresolvedChildren() {
if (unresolvedChildren == null) {
unresolvedChildren = new ListSet<NameInstance>();
}
return unresolvedChildren;
}
private static final UnaryFunction<String, Name> buildUnquoted = new UnaryFunction<String, Name>() {
@Override
public String evaluate(Name object) {
return object.get();
}
};
private static class ProjectionMap extends MultiMapLookup<ColumnInstance> {
public ProjectionMap() {
super(false, false, new UnaryFunction<Name[], ColumnInstance>() {
@Override
public Name[] evaluate(ColumnInstance object) {
return new Name[] { object.getColumn().getName() };
}
});
}
public void add(ColumnInstance ci) {
add(Collections.singleton(ci));
}
}
}