package jeql.engine.query; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import jeql.api.error.ExecutionException; import jeql.api.error.MissingColumnException; import jeql.api.row.Row; import jeql.api.row.RowIterator; import jeql.api.row.RowSchema; import jeql.api.table.Table; import jeql.engine.CompilationException; import jeql.engine.EngineContext; import jeql.engine.Scope; import jeql.syntax.FromItem; import jeql.syntax.FromList; import jeql.syntax.FunctionNode; import jeql.syntax.ParseTreeNode; /** * The Base Query scope introduces all the variable names * which are valid in the SELECT expressions. * These are provided by the columns in the FROM tables, * and the alias definitions * * @author Martin Davis * */ public class BaseQueryScope implements QueryScope { private static final String AMBIGUOUS_TBL_REF = "Ambiguous table reference"; private Scope parentScope; private Row currentRow; private int rowNum; // table & column info private Map tableMap = new HashMap(); private Table[] fromTbl; private int[] fromTblOffset; private List fromColTypes; private String defaultTableName = null; private boolean hasDefaultTable = false; private Map aliasVarMap = new HashMap(); private Map aliasVarTypeMap = new HashMap(); /** * Indicates whether currently binding an expression tree * which is an argument to an aggregate function */ private boolean isBindingAggFunction = false; private List aggFunList = null; public BaseQueryScope(Scope scope) { this.parentScope = scope; } public Scope getParent() { return parentScope; } public EngineContext getContext() { return parentScope.getContext(); } public Table resolveTable(String name) { if (tableMap.containsKey(name)) { int index = ((Integer) tableMap.get(name)).intValue(); return fromTbl[index]; } return parentScope.resolveTable(name); } public boolean hasVariable(String name) { // built-in variables if (name.equalsIgnoreCase(VAR_SPLITVALUE)) { return true; } if (name.equalsIgnoreCase(VAR_SPLITINDEX)) { return true; } //check if this is a column in this scope int colIndex = getColumnIndexAnyTable(name); if (colIndex != -1) return true; if (aliasVarMap.containsKey(name)) return true; return parentScope.hasVariable(name); } public Object getVariable(String name) { // built-in variables if (name.equalsIgnoreCase(VAR_SPLITVALUE)) { return ((SplitRow) currentRow).getSplitValue(); } if (name.equalsIgnoreCase(VAR_SPLITINDEX)) { return ((SplitRow) currentRow).getSplitIndex(); } if (aliasVarMap.containsKey(name)) return aliasVarMap.get(name); //check if this is a column in this scope int colIndex = getColumnIndexAnyTable(name); if (colIndex != -1) return getColumnValue(colIndex); return parentScope.getVariable(name); } public void setVariable(String name, Object value) { aliasVarMap.put(name, value); } public void setVariableType(String name, Class varType) { aliasVarTypeMap.put(name, varType); } public Class getVariableType(String name) { if (aliasVarMap.containsKey(name)) return (Class) aliasVarTypeMap.get(name); //check if this is a column in this scope int colIndex = getColumnIndexAnyTable(name); if (colIndex != -1) return getColumnType(colIndex); return parentScope.getVariableType(name); } public Table getFromTable(int index) { return fromTbl[0]; } public void setRow(Row row) { currentRow = row; } /** * Gets the current row at the execution point of this select context. * * @return the current row */ public Row getRow() { return currentRow; } /** * Prepares FROM items, binds ON expressions, determines result column types. * * @param fromList */ public void prepareFromItems(FromList fromList) { if (fromList == null || fromList.size() == 0) return; fromTbl = new Table[fromList.size()]; fromTblOffset = new int[fromList.size()]; int colCount = 0; for (int i = 0; i < fromList.size(); i++ ) { FromItem fromItem = fromList.getItem(i); prepareFromItem(fromItem, i); fromTblOffset[i] = colCount; colCount += fromTbl[i].size(); } // bind ON expressions for (int i = 0; i < fromList.size(); i++ ) { FromItem fromItem = fromList.getItem(i); ParseTreeNode onExpr = fromItem.getJoinConditionExpr(); // on expressions are bound in the query scope, not the parent scope if (onExpr != null) onExpr.bind(this); } fromColTypes = findTypes(fromTbl); } private void prepareFromItem(FromItem fromItem, int index) { // bind table expression (in context of parent scope, not this one) fromItem.getTableExpression().bind(parentScope); // bind names (if any) to index into actual tables Integer tblIndex = new Integer(index); String tblName = fromItem.getTableName(); if (tblName != null) addTableName(tblName, tblIndex); String alias = fromItem.getAlias(); if (alias != null) addAliasName(alias, tblIndex); // actual table for index fromTbl[index] = fromItem.getTable(parentScope); // maybe not the best place for this check? if (fromTbl[index] == null) throw new ExecutionException(fromItem, "Unknown table: " + tblName); } private static List findTypes(Table[] tbl) { List colTypes = new ArrayList(); for (int i = 0; i < tbl.length; i++) { RowSchema rs = tbl[i].getRows().getSchema(); for (int j = 0; j < rs.size(); j++) { Class colType = rs.getType(j); colTypes.add(colType); } } return colTypes; } private void addTableName(String name, Integer index) { setDefaultTable(name); /** * a table can appear multiple times in a select, * but references to it by name will then be ambiguous * This condition is flagged by using a sentinel value, * which is checked later (since at this point we don't know * if the table will ever be referred to by name rather than alias) */ if (tableMap.containsKey(name)) tableMap.put(name, AMBIGUOUS_TBL_REF); else tableMap.put(name, index); } private void setDefaultTable(String name) { if (defaultTableName == null && ! hasDefaultTable) { defaultTableName = name; hasDefaultTable = true; return; } hasDefaultTable = false; } public boolean hasDefaultTable() { return fromTbl != null && fromTbl.length == 1; } public Table getDefaultTable() { return fromTbl[0]; } private void addAliasName(String name, Integer index) { // Aliases must be unique if (tableMap.containsKey(name)) throw new CompilationException("Dulicate alias name: " + name); tableMap.put(name, index); } public boolean hasTable(String tblName) { return tableMap.containsKey(tblName); } /** * Gets the index of a table.column reference in this select context. * The index can be used to retrieve the value of the column from * a full row in the {@link RowIterator} generated by the FROM/JOIN specification. * * @param tblName the name of the table (may be null) * @param colName the name of the column * @return */ public int getColumnIndex(String tblName, String colName) { int colIndex = -1; if (tblName != null) { Integer tblIndex = (Integer) tableMap.get(tblName); if (tblIndex == null) { // old logic - now left up to client to check this // throw new ExecutionException("Unknown table: " + tblName); return -1; } Table table = fromTbl[tblIndex.intValue()]; int colTblIndex = table.getColumnIndex(colName); if (colTblIndex < 0) //throw new MissingColumnException(tblName, colName, null); return -1; int colIndexOffset = fromTblOffset[tblIndex.intValue()]; colIndex = colTblIndex + colIndexOffset; } else { colIndex = getColumnIndexAnyTable(colName); } // if (colIndex < 0) // throw new ExecutionException("Unknown column: " + colName); return colIndex; } public int getColumnIndexAnyTable(String colName) { // no from table in query if (fromTbl == null) return -1; for (int i = 0; i < fromTbl.length; i++) { if (! fromTbl[i].hasColumn(colName)) continue; int colIndex = fromTbl[i].getColumnIndex(colName); if (colIndex >= 0) return colIndex + fromTblOffset[i]; } return -1; } public Object getColumnValue(int colIndex) { return currentRow.getValue(colIndex); } public Class getColumnType(int colIndex) { return (Class) fromColTypes.get(colIndex); } private Map valMap = new HashMap(); public boolean hasValue(Object key) { return valMap.containsKey(key); } public void setValue(Object key, Object value) { valMap.put(key, value); } public Object getValue(Object key) { return valMap.get(key); } public void setRowNum(int rowNum) { this.rowNum = rowNum; } public int getRowNum() { return rowNum; } //============ Aggregate function binding ============== public void setBindingAggregateFunction(boolean isBindingAggFunction) { this.isBindingAggFunction = isBindingAggFunction; } public boolean isBindingAggregateFunction() { return isBindingAggFunction; } /** * Records aggregate functions encountered in a select list. * Agg functions form virtual columns when evaluating the outer select list * * @param func */ public void addAggregateFunction(FunctionNode func) { if (aggFunList == null) aggFunList = new ArrayList(); aggFunList.add(func); } /** * * @return List<FunctionNode> */ public List getAggregateFunctionNodes() { return aggFunList; } public boolean hasAggregateFunctions() { return aggFunList != null; } }