| /* |
| |
| Derby - Class org.apache.derby.impl.sql.compile.MergeNode |
| |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to you under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| |
| */ |
| |
| package org.apache.derby.impl.sql.compile; |
| |
| import java.util.Arrays; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| |
| import org.apache.derby.iapi.error.StandardException; |
| import org.apache.derby.iapi.reference.ClassName; |
| import org.apache.derby.iapi.reference.SQLState; |
| import org.apache.derby.iapi.services.classfile.VMOpcode; |
| import org.apache.derby.iapi.services.compiler.MethodBuilder; |
| import org.apache.derby.iapi.services.context.ContextManager; |
| import org.apache.derby.iapi.sql.compile.CompilerContext; |
| import org.apache.derby.iapi.sql.compile.IgnoreFilter; |
| import org.apache.derby.iapi.sql.compile.Visitor; |
| import org.apache.derby.iapi.sql.conn.Authorizer; |
| import org.apache.derby.iapi.sql.dictionary.ColumnDescriptor; |
| import org.apache.derby.iapi.sql.dictionary.DataDictionary; |
| import org.apache.derby.iapi.sql.dictionary.TableDescriptor; |
| import org.apache.derby.iapi.sql.execute.ConstantAction; |
| import org.apache.derby.iapi.util.IdUtil; |
| import org.apache.derby.shared.common.sanity.SanityManager; |
| |
| /** |
| * <p> |
| * A MergeNode represents a MERGE statement. The statement looks like |
| * this... |
| * </p> |
| * |
| * <pre> |
| * MERGE INTO targetTable |
| * USING sourceTable |
| * ON searchCondition |
| * matchingClause1 ... matchingClauseN |
| * </pre> |
| * |
| * <p> |
| * ...where each matching clause looks like this... |
| * </p> |
| * |
| * <pre> |
| * WHEN MATCHED [ AND matchingRefinement ] THEN DELETE |
| * </pre> |
| * |
| * <p> |
| * ...or |
| * </p> |
| * |
| * <pre> |
| * WHEN MATCHED [ AND matchingRefinement ] THEN UPDATE SET col1 = expr1, ... colM = exprM |
| * </pre> |
| * |
| * <p> |
| * ...or |
| * </p> |
| * |
| * <pre> |
| * WHEN NOT MATCHED [ AND matchingRefinement ] THEN INSERT columnList VALUES valueList |
| * </pre> |
| * |
| * <p> |
| * The Derby compiler essentially rewrites this statement into a driving left join |
| * followed by a series of DELETE/UPDATE/INSERT actions. The left join looks like |
| * this: |
| * </p> |
| * |
| * <pre> |
| * SELECT selectList FROM sourceTable LEFT OUTER JOIN targetTable ON searchCondition |
| * </pre> |
| * |
| * <p> |
| * The selectList of the driving left join consists of the following: |
| * </p> |
| * |
| * <ul> |
| * <li>All of the columns mentioned in the searchCondition.</li> |
| * <li>All of the columns mentioned in the matchingRefinement clauses.</li> |
| * <li>All of the columns mentioned in the SET clauses and the INSERT columnLists and valueLists.</li> |
| * <li>All additional columns needed for the triggers and foreign keys fired by the DeleteResultSets |
| * and UpdateResultSets constructed for the WHEN MATCHED clauses.</li> |
| * <li>All additional columns needed to build index rows and evaluate generated columns |
| * needed by the UpdateResultSets constructed for the WHEN MATCHED...THEN UPDATE clauses.</li> |
| * <li>A trailing targetTable.RowLocation column.</li> |
| * </ul> |
| * |
| * <p> |
| * The driving left join's selectList then looks like this... |
| * </p> |
| * |
| * <pre> |
| * sc1, ..., scN, tc1, ..., tcM, targetTable.RowLocation |
| * </pre> |
| * |
| * <p> |
| * Where sc1...scN are the columns we need from the source table (in alphabetical |
| * order) and tc1...tcM are the columns we need from the target table (in alphabetical |
| * order). |
| * </p> |
| * |
| * <p> |
| * The matchingRefinement expressions are bound and generated against the |
| * FromList of the driving left join. Dummy DeleteNode, UpdateNode, and InsertNode |
| * statements are independently constructed in order to bind and generate the DELETE/UPDATE/INSERT |
| * actions. |
| * </p> |
| * |
| * <p> |
| * At execution time, the targetTable.RowLocation column is used to determine |
| * whether a given driving row matches. The row matches iff targetTable.RowLocation is not null. |
| * The driving row is then assigned to the |
| * first DELETE/UPDATE/INSERT action to which it applies. The relevant columns from |
| * the driving row are extracted and buffered in a temporary table (the "then" rows) specific to that |
| * DELETE/UPDATE/INSERT action. After the driving left join has been processed, |
| * the DELETE/UPDATE/INSERT actions are run in order, each taking its corresponding |
| * temporary table as its source ResultSet. |
| * </p> |
| * |
| * <p> |
| * Name resolution was a particularly thorny problem. This is because name resolution |
| * behaves differently for SELECTs and UPDATEs. In particular, while processing UPDATEs, |
| * the compiler throws away name resolution information; this happens as a consequence |
| * of work done on DERBY-1043. In the end, I had to invent more name resolution machinery |
| * in order to compensate for the differences in the handling of SELECTs and UPDATEs. |
| * If we are to allow subqueries in matching refinement clauses and in the values expressions |
| * of INSERT and UPDATE actions, then we probably need to remove this special name |
| * resolution machinery. And that, in turn, probably means revisiting DERBY-1043. |
| * </p> |
| * |
| * <p> |
| * The special name resolution machinery involves marking source and target column references |
| * in order to make it clear which table they belong to. This is done in associateColumn(). The markers |
| * are consulted at code-generation time in order to resolve column references when we |
| * generate the expressions needed to populate the rows which go into the temporary tables. |
| * That resolution happens in MatchingClauseNode.getSelectListOffset(). |
| * </p> |
| */ |
| |
| public final class MergeNode extends DMLModStatementNode |
| { |
| /////////////////////////////////////////////////////////////////////////////////// |
| // |
| // CONSTANTS |
| // |
| /////////////////////////////////////////////////////////////////////////////////// |
| |
| public static final int SOURCE_TABLE_INDEX = 0; |
| public static final int TARGET_TABLE_INDEX = 1; |
| |
| private static final String TARGET_ROW_LOCATION_NAME = "###TargetRowLocation"; |
| |
| /////////////////////////////////////////////////////////////////////////////////// |
| // |
| // STATE |
| // |
| /////////////////////////////////////////////////////////////////////////////////// |
| |
| // |
| // Filled in by the constructor. |
| // |
| private FromBaseTable _targetTable; |
| private FromTable _sourceTable; |
| private ValueNode _searchCondition; |
| private QueryTreeNodeVector<MatchingClauseNode> _matchingClauses; |
| |
| // |
| // Filled in at bind() time. |
| // |
| private ResultColumnList _selectList; |
| private FromList _leftJoinFromList; |
| private HalfOuterJoinNode _hojn; |
| |
| // |
| // Filled in at generate() time. |
| // |
| private ConstantAction _constantAction; |
| private CursorNode _leftJoinCursor; |
| |
| /////////////////////////////////////////////////////////////////////////////////// |
| // |
| // CONSTRUCTOR |
| // |
| /////////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * <p> |
| * Constructor for a MergeNode. |
| * </p> |
| */ |
| public MergeNode |
| ( |
| FromTable targetTable, |
| FromTable sourceTable, |
| ValueNode searchCondition, |
| QueryTreeNodeVector<MatchingClauseNode> matchingClauses, |
| ContextManager cm |
| ) |
| throws StandardException |
| { |
| super( null, null, cm ); |
| |
| if ( !( targetTable instanceof FromBaseTable) ) { notBaseTable(); } |
| else { _targetTable = (FromBaseTable) targetTable; } |
| |
| _sourceTable = sourceTable; |
| _searchCondition = searchCondition; |
| _matchingClauses = matchingClauses; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////////// |
| // |
| // BIND-TIME ENTRY POINTS CALLED BY MatchingClauseNode |
| // |
| /////////////////////////////////////////////////////////////////////////////////// |
| |
| /** Get the target table for the MERGE statement */ |
| FromBaseTable getTargetTable() { return _targetTable; } |
| |
| /** |
| * <p> |
| * Associate a column with the SOURCE or TARGET table. This is |
| * part of the special name resolution machinery which smooths over |
| * the differences between name resolution for SELECTs and UPDATEs. |
| * </p> |
| */ |
| void associateColumn( FromList fromList, ColumnReference cr, int mergeTableID ) |
| throws StandardException |
| { |
| if ( mergeTableID != ColumnReference.MERGE_UNKNOWN ) { cr.setMergeTableID( mergeTableID ); } |
| else |
| { |
| // we have to figure out which table the column is in |
| String columnsTableName = cr.getTableName(); |
| |
| if ( ((FromTable) fromList.elementAt( SOURCE_TABLE_INDEX )).getMatchingColumn( cr ) != null ) |
| { |
| cr.setMergeTableID( ColumnReference.MERGE_SOURCE ); |
| } |
| else if ( ((FromTable) fromList.elementAt( TARGET_TABLE_INDEX )).getMatchingColumn( cr ) != null ) |
| { |
| cr.setMergeTableID( ColumnReference.MERGE_TARGET ); |
| } |
| } |
| |
| // Don't raise an error if a column in another table is referenced and we |
| // don't know how to handle it here. If the column is not in the SOURCE or TARGET |
| // table, then it will be caught by other bind-time logic. Columns which ought |
| // to be associated, but aren't, will be caught later on by MatchingClauseNode.getMergeTableID(). |
| } |
| |
| /** Boilerplate for binding an expression against a FromList */ |
| void bindExpression( ValueNode value, FromList fromList ) |
| throws StandardException |
| { |
| CompilerContext cc = getCompilerContext(); |
| final int previousReliability = cc.getReliability(); |
| |
| cc.setReliability( previousReliability | CompilerContext.SQL_IN_ROUTINES_ILLEGAL ); |
| cc.pushCurrentPrivType( Authorizer.SELECT_PRIV ); |
| |
| try { |
| // this adds SELECT priv on referenced columns and EXECUTE privs on referenced routines |
| value.bindExpression |
| ( |
| fromList, |
| new SubqueryList( getContextManager() ), |
| new ArrayList<AggregateNode>() |
| ); |
| } |
| finally |
| { |
| // Restore previous compiler state |
| cc.popCurrentPrivType(); |
| cc.setReliability( previousReliability ); |
| } |
| } |
| |
| /** |
| * <p> |
| * Add the columns in the matchingRefinement clause to the evolving map. |
| * This is called when we're building the SELECT list for the driving left join. |
| * </p> |
| */ |
| void getColumnsInExpression |
| ( HashMap<String,ColumnReference> map, ValueNode expression, int mergeTableID ) |
| throws StandardException |
| { |
| if ( expression == null ) { return; } |
| |
| List<ColumnReference> colRefs = getColumnReferences( expression ); |
| |
| getColumnsFromList( map, colRefs, mergeTableID ); |
| } |
| |
| /** |
| * <p> |
| * Add a list of columns to the the evolving map. |
| * This is called when we're building the SELECT list for the driving left join. |
| * </p> |
| */ |
| void getColumnsFromList |
| ( HashMap<String,ColumnReference> map, ResultColumnList rcl, int mergeTableID ) |
| throws StandardException |
| { |
| List<ColumnReference> colRefs = getColumnReferences( rcl ); |
| |
| getColumnsFromList( map, colRefs, mergeTableID ); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////////// |
| // |
| // bind() BEHAVIOR |
| // |
| /////////////////////////////////////////////////////////////////////////////////// |
| |
| @Override |
| public void bindStatement() throws StandardException |
| { |
| DataDictionary dd = getDataDictionary(); |
| |
| // source table must be a vti or base table |
| if ( |
| !(_sourceTable instanceof FromVTI) && |
| !(_sourceTable instanceof FromBaseTable) |
| ) |
| { |
| throw StandardException.newException( SQLState.LANG_SOURCE_NOT_BASE_OR_VTI ); |
| } |
| |
| // source and target may not have the same correlation names |
| if ( getExposedName( _targetTable ).equals( getExposedName( _sourceTable ) ) ) |
| { |
| throw StandardException.newException( SQLState.LANG_SAME_EXPOSED_NAME ); |
| } |
| |
| // don't allow derived column lists right now |
| forbidDerivedColumnLists(); |
| |
| // synonyms not allowed |
| forbidSynonyms(); |
| |
| // |
| // Don't add any privileges until we bind the matching clauses. |
| // |
| IgnoreFilter ignorePermissions = new IgnoreFilter(); |
| getCompilerContext().addPrivilegeFilter( ignorePermissions ); |
| |
| FromList dfl = new FromList( getContextManager() ); |
| FromTable dflSource = cloneFromTable( _sourceTable ); |
| FromBaseTable dflTarget = (FromBaseTable) cloneFromTable( _targetTable ); |
| dfl.addFromTable( dflSource ); |
| dfl.addFromTable( dflTarget ); |
| dfl.bindTables( dd, new FromList( getOptimizerFactory().doJoinOrderOptimization(), getContextManager() ) ); |
| |
| // target table must be a base table |
| if ( !targetIsBaseTable( dflTarget ) ) { notBaseTable(); } |
| |
| // ready to add permissions |
| getCompilerContext().removePrivilegeFilter( ignorePermissions ); |
| |
| for ( MatchingClauseNode mcn : _matchingClauses ) |
| { |
| FromList dummyFromList = cloneFromList( dd, dflTarget ); |
| FromBaseTable dummyTargetTable = (FromBaseTable) dummyFromList.elementAt( TARGET_TABLE_INDEX ); |
| mcn.bind( dd, this, dummyFromList, dummyTargetTable ); |
| |
| // window function not allowed |
| SelectNode.checkNoWindowFunctions(mcn, "matching clause"); |
| |
| // aggregates not allowed |
| checkNoAggregates(mcn); |
| } |
| |
| bindLeftJoin( dd ); |
| } |
| |
| |
| static void checkNoAggregates(QueryTreeNode clause) |
| throws StandardException { |
| |
| // Clause cannot contain window aggregates except inside subqueries |
| HasNodeVisitor visitor = new HasNodeVisitor(AggregateNode.class, |
| SubqueryNode.class); |
| clause.accept(visitor); |
| |
| if (visitor.hasNode()) { |
| throw StandardException.newException( |
| SQLState.LANG_NO_AGGREGATES_IN_MERGE_MATCHING_CLAUSE); |
| } |
| } |
| |
| |
| ///////////////////////////////////// |
| // |
| // TABLE AND CORRELATION CHECKS |
| // |
| ///////////////////////////////////// |
| |
| /** Get the exposed name of a FromTable */ |
| private String getExposedName( FromTable ft ) throws StandardException |
| { |
| return ft.getTableName().getTableName(); |
| } |
| |
| @Override |
| public boolean referencesSessionSchema() throws StandardException { |
| return _sourceTable.referencesSessionSchema() |
| || _targetTable.referencesSessionSchema() |
| || _searchCondition.referencesSessionSchema() |
| || _matchingClauses.referencesSessionSchema(); |
| } |
| |
| /** |
| *<p> |
| * Because of name resolution complexities, we do not allow derived column lists |
| * on source or target tables. These lists arise in queries like the following: |
| * </p> |
| * |
| * <pre> |
| * merge into t1 r( x ) |
| * using t2 on r.x = t2.a |
| * when matched then delete; |
| * |
| * merge into t1 |
| * using t2 r( x ) on t1.a = r.x |
| * when matched then delete; |
| * </pre> |
| */ |
| private void forbidDerivedColumnLists() throws StandardException |
| { |
| if ( (_sourceTable.getResultColumns() != null) || (_targetTable.getResultColumns() != null) ) |
| { |
| throw StandardException.newException( SQLState.LANG_NO_DCL_IN_MERGE ); |
| } |
| } |
| |
| /** Neither the source nor the target table may be a synonym */ |
| private void forbidSynonyms() throws StandardException |
| { |
| forbidSynonyms(_targetTable.getTableNameField().cloneMe()); |
| if ( _sourceTable instanceof FromBaseTable ) |
| { |
| forbidSynonyms( |
| ((FromBaseTable) _sourceTable).getTableNameField().cloneMe()); |
| } |
| } |
| |
| private void forbidSynonyms(TableName tableName) throws StandardException |
| { |
| tableName.bind(); |
| |
| TableName synonym = resolveTableToSynonym( tableName ); |
| if ( synonym != null ) |
| { |
| throw StandardException.newException( SQLState.LANG_NO_SYNONYMS_IN_MERGE ); |
| } |
| } |
| |
| /** Throw a "not base table" exception */ |
| private void notBaseTable() throws StandardException |
| { |
| throw StandardException.newException( SQLState.LANG_TARGET_NOT_BASE_TABLE ); |
| } |
| |
| /** Return true if the target table is a base table */ |
| private boolean targetIsBaseTable( FromBaseTable targetTable ) throws StandardException |
| { |
| FromBaseTable fbt = targetTable; |
| TableDescriptor desc = fbt.getTableDescriptor(); |
| if ( desc == null ) { return false; } |
| |
| switch( desc.getTableType() ) |
| { |
| case TableDescriptor.BASE_TABLE_TYPE: |
| case TableDescriptor.GLOBAL_TEMPORARY_TABLE_TYPE: |
| return true; |
| |
| default: |
| return false; |
| } |
| } |
| |
| /** Return true if the source table is a base table, view, or table function */ |
| private boolean sourceIsBase_or_VTI() throws StandardException |
| { |
| if ( _sourceTable instanceof FromVTI ) { return true; } |
| if ( !( _sourceTable instanceof FromBaseTable) ) { return false; } |
| |
| FromBaseTable fbt = (FromBaseTable) _sourceTable; |
| TableDescriptor desc = fbt.getTableDescriptor(); |
| if ( desc == null ) { return false; } |
| |
| switch( desc.getTableType() ) |
| { |
| case TableDescriptor.BASE_TABLE_TYPE: |
| case TableDescriptor.SYSTEM_TABLE_TYPE: |
| case TableDescriptor.GLOBAL_TEMPORARY_TABLE_TYPE: |
| return true; |
| |
| default: |
| return false; |
| } |
| } |
| |
| /////////////////////////// |
| // |
| // BINDING THE LEFT JOIN |
| // |
| /////////////////////////// |
| |
| /** |
| * Bind the driving left join select. |
| * Stuffs the left join SelectNode into the resultSet variable. |
| */ |
| private void bindLeftJoin( DataDictionary dd ) throws StandardException |
| { |
| CompilerContext cc = getCompilerContext(); |
| final int previousReliability = cc.getReliability(); |
| |
| try { |
| cc.setReliability( previousReliability | CompilerContext.SQL_IN_ROUTINES_ILLEGAL ); |
| |
| // |
| // Don't add any privileges until we bind the matching refinement clauses. |
| // |
| IgnoreFilter ignorePermissions = new IgnoreFilter(); |
| getCompilerContext().addPrivilegeFilter( ignorePermissions ); |
| |
| _hojn = new HalfOuterJoinNode |
| ( |
| _sourceTable, |
| _targetTable, |
| _searchCondition, |
| null, |
| false, |
| null, |
| getContextManager() |
| ); |
| |
| _leftJoinFromList = _hojn.makeFromList( true, true ); |
| _leftJoinFromList.bindTables( dd, new FromList( getOptimizerFactory().doJoinOrderOptimization(), getContextManager() ) ); |
| |
| if ( !sourceIsBase_or_VTI() ) |
| { |
| throw StandardException.newException( SQLState.LANG_SOURCE_NOT_BASE_OR_VTI ); |
| } |
| |
| FromList topFromList = new FromList( getOptimizerFactory().doJoinOrderOptimization(), getContextManager() ); |
| topFromList.addFromTable( _hojn ); |
| |
| // ready to add permissions |
| getCompilerContext().removePrivilegeFilter( ignorePermissions ); |
| |
| // preliminary binding of the matching clauses to resolve column |
| // references. this ensures that we can add all of the columns from |
| // the matching refinements to the SELECT list of the left join. |
| // we re-bind the matching clauses when we're done binding the left join |
| // because, at that time, we have result set numbers needed for |
| // code generation. |
| for ( MatchingClauseNode mcn : _matchingClauses ) |
| { |
| mcn.bindRefinement( this, _leftJoinFromList ); |
| } |
| |
| ResultColumnList selectList = buildSelectList(); |
| |
| // save a copy so that we can remap column references when generating the temporary rows |
| _selectList = selectList.copyListAndObjects(); |
| |
| resultSet = new SelectNode |
| ( |
| selectList, |
| topFromList, |
| null, // where clause |
| null, // group by list |
| null, // having clause |
| null, // window list |
| null, // optimizer plan override |
| getContextManager() |
| ); |
| |
| // Wrap the SELECT in a CursorNode in order to finish binding it. |
| _leftJoinCursor = new CursorNode |
| ( |
| "SELECT", |
| resultSet, |
| null, |
| null, |
| null, |
| null, |
| false, |
| CursorNode.READ_ONLY, |
| null, |
| true, |
| getContextManager() |
| ); |
| |
| // |
| // We're only interested in privileges related to the ON clause. |
| // Otherwise, the driving left join should not contribute any |
| // privilege requirements. |
| // |
| getCompilerContext().addPrivilegeFilter( ignorePermissions ); |
| |
| _leftJoinCursor.bindStatement(); |
| |
| // ready to add permissions again |
| getCompilerContext().removePrivilegeFilter( ignorePermissions ); |
| |
| // now figure out what privileges are needed for the ON clause |
| addOnClausePrivileges(); |
| } |
| finally |
| { |
| // Restore previous compiler state |
| cc.setReliability( previousReliability ); |
| } |
| } |
| |
| //////////////////////////// |
| // |
| // CLONING THE FROM LIST |
| // |
| //////////////////////////// |
| |
| /** Create a FromList for binding a WHEN [ NOT ] MATCHED clause */ |
| private FromList cloneFromList( DataDictionary dd, FromBaseTable targetTable ) |
| throws StandardException |
| { |
| FromList dummyFromList = new FromList( getContextManager() ); |
| FromBaseTable dummyTargetTable = new FromBaseTable |
| ( |
| targetTable.getTableNameField(), |
| targetTable.correlationName, |
| null, |
| null, |
| getContextManager() |
| ); |
| FromTable dummySourceTable = cloneFromTable( _sourceTable ); |
| |
| dummyTargetTable.setMergeTableID( ColumnReference.MERGE_TARGET ); |
| dummySourceTable.setMergeTableID ( ColumnReference.MERGE_SOURCE ); |
| |
| dummyFromList.addFromTable( dummySourceTable ); |
| dummyFromList.addFromTable( dummyTargetTable ); |
| |
| // |
| // Don't add any privileges while binding the tables. |
| // |
| IgnoreFilter ignorePermissions = new IgnoreFilter(); |
| getCompilerContext().addPrivilegeFilter( ignorePermissions ); |
| |
| dummyFromList.bindTables( dd, new FromList( getOptimizerFactory().doJoinOrderOptimization(), getContextManager() ) ); |
| |
| // ready to add permissions |
| getCompilerContext().removePrivilegeFilter( ignorePermissions ); |
| |
| return dummyFromList; |
| } |
| |
| /** Clone a FromTable to avoid binding the original */ |
| private FromTable cloneFromTable( FromTable fromTable ) throws StandardException |
| { |
| if ( fromTable instanceof FromVTI ) |
| { |
| FromVTI source = (FromVTI) fromTable; |
| |
| return new FromVTI |
| ( |
| source.methodCall, |
| source.correlationName, |
| source.getResultColumns(), |
| null, |
| source.exposedName, |
| getContextManager() |
| ); |
| } |
| else if ( fromTable instanceof FromBaseTable ) |
| { |
| FromBaseTable source = (FromBaseTable) fromTable; |
| return new FromBaseTable |
| ( |
| source.tableName, |
| source.correlationName, |
| null, |
| null, |
| getContextManager() |
| ); |
| } |
| else |
| { |
| throw StandardException.newException( SQLState.LANG_SOURCE_NOT_BASE_OR_VTI ); |
| } |
| } |
| |
| /////////////////////////// |
| // |
| // PRIVILEGE MANAGEMENT |
| // |
| /////////////////////////// |
| |
| /** |
| * <p> |
| * Add the privileges required by the ON clause. |
| * </p> |
| */ |
| private void addOnClausePrivileges() throws StandardException |
| { |
| // add SELECT privilege on columns |
| for ( ColumnReference cr : getColumnReferences( _searchCondition ) ) |
| { |
| addColumnPrivilege( cr ); |
| } |
| |
| // add EXECUTE privilege on routines |
| for ( StaticMethodCallNode routine : getRoutineReferences( _searchCondition ) ) |
| { |
| addRoutinePrivilege( routine ); |
| } |
| |
| // add USAGE privilege on CASTs to user-defined types |
| for ( CastNode value : getCastNodes( _searchCondition ) ) |
| { |
| addUDTUsagePriv( value ); |
| } |
| } |
| |
| /** |
| * <p> |
| * Add SELECT privilege on the indicated column. |
| * </p> |
| */ |
| private void addColumnPrivilege( ColumnReference cr ) |
| throws StandardException |
| { |
| CompilerContext cc = getCompilerContext(); |
| ResultColumn rc = cr.getSource(); |
| |
| if ( rc != null ) |
| { |
| ColumnDescriptor colDesc = rc.getColumnDescriptor(); |
| |
| if ( colDesc != null ) |
| { |
| cc.pushCurrentPrivType( Authorizer.SELECT_PRIV ); |
| cc.addRequiredColumnPriv( colDesc ); |
| cc.popCurrentPrivType(); |
| } |
| } |
| } |
| |
| /** |
| * <p> |
| * Add EXECUTE privilege on the indicated routine. |
| * </p> |
| */ |
| private void addRoutinePrivilege( StaticMethodCallNode routine ) |
| throws StandardException |
| { |
| CompilerContext cc = getCompilerContext(); |
| |
| cc.pushCurrentPrivType( Authorizer.EXECUTE_PRIV ); |
| cc.addRequiredRoutinePriv( routine.ad ); |
| cc.popCurrentPrivType(); |
| } |
| |
| /** Get a list of CastNodes in an expression */ |
| private List<CastNode> getCastNodes( QueryTreeNode expression ) |
| throws StandardException |
| { |
| CollectNodesVisitor<CastNode> getCNs = |
| new CollectNodesVisitor<CastNode>(CastNode.class); |
| |
| expression.accept(getCNs); |
| |
| return getCNs.getList(); |
| } |
| |
| /** Get a list of routines in an expression */ |
| private List<StaticMethodCallNode> getRoutineReferences( QueryTreeNode expression ) |
| throws StandardException |
| { |
| CollectNodesVisitor<StaticMethodCallNode> getSMCNs = |
| new CollectNodesVisitor<StaticMethodCallNode>(StaticMethodCallNode.class); |
| |
| expression.accept(getSMCNs); |
| |
| return getSMCNs.getList(); |
| } |
| |
| /////////////////////////////// |
| // |
| // BUILD THE SELECT LIST |
| // FOR THE DRIVING LEFT JOIN. |
| // |
| /////////////////////////////// |
| |
| /** Build the select list for the left join */ |
| private ResultColumnList buildSelectList() throws StandardException |
| { |
| HashMap<String,ColumnReference> drivingColumnMap = new HashMap<String,ColumnReference>(); |
| getColumnsInExpression( drivingColumnMap, _searchCondition, ColumnReference.MERGE_UNKNOWN ); |
| |
| for ( MatchingClauseNode mcn : _matchingClauses ) |
| { |
| mcn.getColumnsInExpressions( this, drivingColumnMap ); |
| |
| int mergeTableID = mcn.isDeleteClause() ? ColumnReference.MERGE_TARGET : ColumnReference.MERGE_UNKNOWN; |
| getColumnsFromList( drivingColumnMap, mcn.getThenColumns(), mergeTableID ); |
| } |
| |
| ResultColumnList selectList = new ResultColumnList( getContextManager() ); |
| |
| // add all of the columns from the source table which are mentioned |
| addColumns |
| ( |
| (FromTable) _leftJoinFromList.elementAt( SOURCE_TABLE_INDEX ), |
| drivingColumnMap, |
| selectList, |
| ColumnReference.MERGE_SOURCE |
| ); |
| // add all of the columns from the target table which are mentioned |
| addColumns |
| ( |
| (FromTable) _leftJoinFromList.elementAt( TARGET_TABLE_INDEX ), |
| drivingColumnMap, |
| selectList, |
| ColumnReference.MERGE_TARGET |
| ); |
| |
| addTargetRowLocation( selectList ); |
| |
| return selectList; |
| } |
| |
| /** Add the target table's row location to the left join's select list */ |
| private void addTargetRowLocation( ResultColumnList selectList ) |
| throws StandardException |
| { |
| // tell the target table to generate a row location column |
| _targetTable.setRowLocationColumnName( TARGET_ROW_LOCATION_NAME ); |
| |
| TableName fromTableName = _targetTable.getTableName(); |
| ColumnReference cr = new ColumnReference |
| ( TARGET_ROW_LOCATION_NAME, fromTableName, getContextManager() ); |
| cr.setMergeTableID( ColumnReference.MERGE_TARGET ); |
| ResultColumn rowLocationColumn = new ResultColumn( (String) null, cr, getContextManager() ); |
| rowLocationColumn.markGenerated(); |
| |
| selectList.addResultColumn( rowLocationColumn ); |
| } |
| |
| /** |
| * <p> |
| * Add to an evolving select list the columns from the indicated table. |
| * </p> |
| */ |
| private void addColumns |
| ( |
| FromTable fromTable, |
| HashMap<String,ColumnReference> drivingColumnMap, |
| ResultColumnList selectList, |
| int mergeTableID |
| ) |
| throws StandardException |
| { |
| String[] columnNames = getColumns( mergeTableID, drivingColumnMap ); |
| TableName tableName = fromTable.getTableName(); |
| |
| for ( int i = 0; i < columnNames.length; i++ ) |
| { |
| ColumnReference cr = new ColumnReference |
| ( columnNames[ i ], tableName, getContextManager() ); |
| cr.setMergeTableID( mergeTableID ); |
| ResultColumn rc = new ResultColumn( (String) null, cr, getContextManager() ); |
| selectList.addResultColumn( rc ); |
| } |
| } |
| |
| /** Get the column names from the table with the given table number, in sorted order */ |
| private String[] getColumns( int mergeTableID, HashMap<String,ColumnReference> map ) |
| { |
| HashSet<String> set = new HashSet<String>(); |
| |
| for ( ColumnReference cr : map.values() ) |
| { |
| if ( cr.getMergeTableID() == mergeTableID ) { set.add( cr.getColumnName() ); } |
| } |
| |
| String[] retval = new String[ set.size() ]; |
| set.toArray( retval ); |
| Arrays.sort( retval ); |
| |
| return retval; |
| } |
| |
| /** Get a list of column references in an expression */ |
| private List<ColumnReference> getColumnReferences( QueryTreeNode expression ) |
| throws StandardException |
| { |
| CollectNodesVisitor<ColumnReference> getCRs = |
| new CollectNodesVisitor<ColumnReference>(ColumnReference.class); |
| |
| expression.accept(getCRs); |
| |
| return getCRs.getList(); |
| } |
| |
| /** Add a list of columns to the the evolving map */ |
| private void getColumnsFromList |
| ( HashMap<String,ColumnReference> map, List<ColumnReference> colRefs, int mergeTableID ) |
| throws StandardException |
| { |
| for ( ColumnReference cr : colRefs ) |
| { |
| addColumn( map, cr, mergeTableID ); |
| } |
| } |
| |
| /** Add a column to the evolving map of referenced columns */ |
| void addColumn |
| ( |
| HashMap<String,ColumnReference> map, |
| ColumnReference cr, |
| int mergeTableID |
| ) |
| throws StandardException |
| { |
| if ( cr.getTableName() == null ) |
| { |
| cr = cr.bindExpression( |
| _leftJoinFromList, |
| new SubqueryList(getContextManager()), |
| new ArrayList<AggregateNode>()); |
| TableName tableName = cr.getQualifiedTableName(); |
| cr = new ColumnReference( cr.getColumnName(), tableName, getContextManager() ); |
| } |
| |
| associateColumn( _leftJoinFromList, cr, mergeTableID ); |
| |
| String key = makeDCMKey( cr.getTableName(), cr.getColumnName() ); |
| |
| ColumnReference mapCR = map.get( key ); |
| if ( mapCR != null ) |
| { |
| mapCR.setMergeTableID( cr.getMergeTableID() ); |
| } |
| else |
| { |
| map.put( key, cr ); |
| } |
| } |
| |
| /** Make a HashMap key for a column in the driving column map of the LEFT JOIN */ |
| private String makeDCMKey( String tableName, String columnName ) |
| { |
| return IdUtil.mkQualifiedName( tableName, columnName ); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////////// |
| // |
| // optimize() BEHAVIOR |
| // |
| /////////////////////////////////////////////////////////////////////////////////// |
| |
| @Override |
| public void optimizeStatement() throws StandardException |
| { |
| // |
| // Don't add any privileges during optimization. |
| // |
| IgnoreFilter ignorePermissions = new IgnoreFilter(); |
| getCompilerContext().addPrivilegeFilter( ignorePermissions ); |
| |
| /* First optimize the left join */ |
| _leftJoinCursor.optimizeStatement(); |
| |
| // |
| // No need to set lockMode in the master MergeNode. The individual |
| // actions and the driving left-join will set their own lock modes. |
| // |
| |
| // now optimize the INSERT/UPDATE/DELETE actions |
| for ( MatchingClauseNode mcn : _matchingClauses ) |
| { |
| mcn.optimize(); |
| } |
| |
| // ready to add permissions again |
| getCompilerContext().removePrivilegeFilter( ignorePermissions ); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////////// |
| // |
| // generate() BEHAVIOR |
| // |
| /////////////////////////////////////////////////////////////////////////////////// |
| |
| @Override |
| void generate( ActivationClassBuilder acb, MethodBuilder mb ) |
| throws StandardException |
| { |
| int clauseCount = _matchingClauses.size(); |
| |
| /* generate the parameters */ |
| generateParameterValueSet(acb); |
| |
| acb.pushGetResultSetFactoryExpression( mb ); |
| |
| // arg 1: the driving left join |
| _leftJoinCursor.generate( acb, mb ); |
| |
| // dig up the actual result set which was generated and which will drive the MergeResultSet |
| ScrollInsensitiveResultSetNode sirs = (ScrollInsensitiveResultSetNode) _leftJoinCursor.resultSet; |
| ResultSetNode generatedScan = sirs.getChildResult(); |
| |
| ConstantAction[] clauseActions = new ConstantAction[ clauseCount ]; |
| for ( int i = 0; i < clauseCount; i++ ) |
| { |
| MatchingClauseNode mcn = _matchingClauses.elementAt(i); |
| |
| mcn.generate( acb, _selectList, generatedScan, _hojn, i ); |
| clauseActions[ i ] = mcn.makeConstantAction( acb ); |
| } |
| _constantAction = getGenericConstantActionFactory().getMergeConstantAction( clauseActions ); |
| |
| mb.callMethod |
| ( VMOpcode.INVOKEINTERFACE, (String) null, "getMergeResultSet", ClassName.ResultSet, 1 ); |
| } |
| |
| @Override |
| public ConstantAction makeConstantAction() throws StandardException |
| { |
| return _constantAction; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////////// |
| // |
| // Visitable BEHAVIOR |
| // |
| /////////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * Accept the visitor for all visitable children of this node. |
| * |
| * @param v the visitor |
| * |
| * @exception StandardException on error |
| */ |
| @Override |
| void acceptChildren(Visitor v) |
| throws StandardException |
| { |
| if ( _leftJoinCursor != null ) |
| { |
| _leftJoinCursor.acceptChildren( v ); |
| } |
| else |
| { |
| super.acceptChildren( v ); |
| |
| _targetTable.accept( v ); |
| _sourceTable.accept( v ); |
| _searchCondition.accept( v ); |
| } |
| |
| for ( MatchingClauseNode mcn : _matchingClauses ) |
| { |
| mcn.accept( v ); |
| } |
| } |
| |
| /** |
| * Prints the sub-nodes of this object. See QueryTreeNode.java for |
| * how tree printing is supposed to work. |
| * |
| * @param depth The depth of this node in the tree |
| */ |
| @Override |
| void printSubNodes( int depth ) |
| { |
| if (SanityManager.DEBUG) |
| { |
| super.printSubNodes( depth ); |
| |
| printLabel( depth, "targetTable: " ); |
| _targetTable.treePrint( depth + 1 ); |
| |
| printLabel( depth, "sourceTable: " ); |
| _sourceTable.treePrint( depth + 1 ); |
| |
| if ( _searchCondition != null ) |
| { |
| printLabel( depth, "searchCondition: " ); |
| _searchCondition.treePrint( depth + 1 ); |
| } |
| |
| for ( MatchingClauseNode mcn : _matchingClauses ) |
| { |
| printLabel( depth, mcn.toString() ); |
| mcn.treePrint( depth + 1 ); |
| } |
| } |
| } |
| |
| @Override |
| String statementToString() |
| { |
| return "MERGE"; |
| } |
| } |