blob: cd67eb758912c9fc6969f3cf7a4127e2e231a15b [file] [log] [blame]
/*
Derby - Class org.apache.derby.impl.sql.compile.MatchingClauseNode
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.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import org.apache.derby.catalog.types.DefaultInfoImpl;
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.LocalField;
import org.apache.derby.iapi.services.compiler.MethodBuilder;
import org.apache.derby.iapi.services.context.ContextManager;
import org.apache.derby.iapi.services.io.FormatableBitSet;
import org.apache.derby.shared.common.sanity.SanityManager;
import org.apache.derby.iapi.sql.ResultColumnDescriptor;
import org.apache.derby.iapi.sql.ResultDescription;
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.dictionary.ColumnDescriptor;
import org.apache.derby.iapi.sql.dictionary.ColumnDescriptorList;
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.sql.execute.NoPutResultSet;
import org.apache.derby.iapi.types.DataTypeDescriptor;
import org.apache.derby.iapi.types.DataValueDescriptor;
/**
* Node representing a WHEN MATCHED or WHEN NOT MATCHED clause
* in a MERGE statement.
*
*/
public class MatchingClauseNode extends QueryTreeNode
{
///////////////////////////////////////////////////////////////////////////////////
//
// CONSTANTS
//
///////////////////////////////////////////////////////////////////////////////////
private static final String CURRENT_OF_NODE_NAME = "$MERGE_CURRENT";
///////////////////////////////////////////////////////////////////////////////////
//
// STATE
//
///////////////////////////////////////////////////////////////////////////////////
//
// Filled in by the constructor.
//
private ValueNode _matchingRefinement;
private ResultColumnList _updateColumns;
private ResultColumnList _insertColumns;
private ResultColumnList _insertValues;
//
// Filled in at bind() time.
//
// the INSERT/UPDATE/DELETE statement of this WHEN [ NOT ] MATCHED clause
private DMLModStatementNode _dml;
// the columns in the temporary conglomerate which drives the INSERT/UPDATE/DELETE
private ResultColumnList _thenColumns;
//
// Filled in at generate() time.
//
private int _clauseNumber;
private String _actionMethodName;
private String _resultSetFieldName;
private String _rowMakingMethodName;
///////////////////////////////////////////////////////////////////////////////////
//
// CONSTRUCTORS/FACTORY METHODS
//
///////////////////////////////////////////////////////////////////////////////////
/**
* Constructor called by factory methods.
*/
private MatchingClauseNode
(
ValueNode matchingRefinement,
ResultColumnList updateColumns,
ResultColumnList insertColumns,
ResultColumnList insertValues,
ContextManager cm
)
throws StandardException
{
super( cm );
_matchingRefinement = matchingRefinement;
_updateColumns = updateColumns;
_insertColumns = insertColumns;
_insertValues = insertValues;
}
/** Make a WHEN MATCHED ... THEN UPDATE clause */
static MatchingClauseNode makeUpdateClause
(
ValueNode matchingRefinement,
ResultColumnList updateColumns,
ContextManager cm
)
throws StandardException
{
return new MatchingClauseNode( matchingRefinement, updateColumns, null, null, cm );
}
/** Make a WHEN MATCHED ... THEN DELETE clause */
static MatchingClauseNode makeDeleteClause
(
ValueNode matchingRefinement,
ContextManager cm
)
throws StandardException
{
return new MatchingClauseNode( matchingRefinement, null, null, null, cm );
}
/** Make a WHEN NOT MATCHED ... THEN INSERT clause */
static MatchingClauseNode makeInsertClause
(
ValueNode matchingRefinement,
ResultColumnList insertColumns,
ResultColumnList insertValues,
ContextManager cm
)
throws StandardException
{
return new MatchingClauseNode( matchingRefinement, null, insertColumns, insertValues, cm );
}
///////////////////////////////////////////////////////////////////////////////////
//
// ACCESSORS
//
///////////////////////////////////////////////////////////////////////////////////
/** Return true if this is a WHEN MATCHED ... UPDATE clause */
boolean isUpdateClause() { return (_updateColumns != null); }
/** Return true if this is a WHEN NOT MATCHED ... INSERT clause */
boolean isInsertClause() { return (_insertValues != null); }
/** Return true if this is a WHEN MATCHED ... DELETE clause */
boolean isDeleteClause() { return !( isUpdateClause() || isInsertClause() ); }
/**
* Return the list of columns which form the rows of the ResultSet which drive
* the INSERT/UPDATE/DELETE actions.
*/
ResultColumnList getThenColumns() { return _thenColumns; }
///////////////////////////////////////////////////////////////////////////////////
//
// bind() BEHAVIOR CALLED BY MergeNode
//
///////////////////////////////////////////////////////////////////////////////////
/** Bind this WHEN [ NOT ] MATCHED clause against the parent MergeNode */
void bind
(
DataDictionary dd,
MergeNode mergeNode,
FromList fullFromList,
FromBaseTable targetTable
)
throws StandardException
{
//
// Although the SQL Standard allows subqueries in the WHEN [ NOT ] MATCHED clauses,
// we don't support them yet. That is because code-generation for those clauses breaks
// if they contain subqueries. That, in turn, is because we don't completely optimize those
// clauses. If we improve Derby so that we do completely optimize the WHEN [ NOT ] MATCHED clauses,
// then we can consider enabling subqueries in them.
//
forbidSubqueries();
_thenColumns = new ResultColumnList( getContextManager() );
if ( isDeleteClause() ) { bindDelete( dd, fullFromList, targetTable ); }
if ( isUpdateClause() ) { bindUpdate( dd, mergeNode, fullFromList, targetTable ); }
if ( isInsertClause() ) { bindInsert( dd, mergeNode, fullFromList, targetTable ); }
}
/** Bind the optional refinement condition in the MATCHED clause */
void bindRefinement( MergeNode mergeNode, FromList fullFromList ) throws StandardException
{
if ( _matchingRefinement != null )
{
FromList fromList = fullFromList;
//
// For an INSERT action, the WHEN NOT MATCHED refinement can only
// mention columns in the source table.
//
if ( isInsertClause() )
{
fromList = new FromList( getOptimizerFactory().doJoinOrderOptimization(), getContextManager() );
fromList.addElement( fullFromList.elementAt( MergeNode.SOURCE_TABLE_INDEX ) );
}
mergeNode.bindExpression( _matchingRefinement, fromList );
}
}
/** Collect the columns mentioned by expressions in this MATCHED clause */
void getColumnsInExpressions
(
MergeNode mergeNode,
HashMap<String,ColumnReference> drivingColumnMap
)
throws StandardException
{
if ( _matchingRefinement != null )
{
mergeNode.getColumnsInExpression( drivingColumnMap, _matchingRefinement, ColumnReference.MERGE_UNKNOWN );
}
if ( isUpdateClause() )
{
TableName targetTableName = mergeNode.getTargetTable().getTableName();
//
// Get all columns mentioned on both sides of SET operators in WHEN MATCHED ... THEN UPDATE clauses.
// We need the left side because UPDATE needs before and after images of columns.
// We need the right side because there may be columns in the expressions there.
//
for ( ResultColumn rc : _updateColumns )
{
mergeNode.getColumnsInExpression( drivingColumnMap, rc.getExpression(), ColumnReference.MERGE_UNKNOWN );
ColumnReference leftCR = new ColumnReference( rc.getName(), targetTableName, getContextManager() );
mergeNode.addColumn( drivingColumnMap, leftCR, ColumnReference.MERGE_TARGET );
}
}
else if ( isInsertClause() )
{
// get all columns mentioned in the VALUES subclauses of WHEN NOT MATCHED ... THEN INSERT clauses
for ( ResultColumn rc : _insertValues )
{
mergeNode.getColumnsInExpression( drivingColumnMap, rc.getExpression(), ColumnReference.MERGE_UNKNOWN );
}
}
else if ( isDeleteClause() )
{
// add all of the THEN columns
mergeNode.getColumnsFromList( drivingColumnMap, _thenColumns, ColumnReference.MERGE_TARGET );
}
}
////////////////
//
// BIND UPDATE
//
////////////////
/** Bind a WHEN MATCHED ... THEN UPDATE clause */
private void bindUpdate
(
DataDictionary dd,
MergeNode mergeNode,
FromList fullFromList,
FromBaseTable targetTable
)
throws StandardException
{
ResultColumnList setClauses = realiasSetClauses( targetTable );
bindSetClauses( mergeNode, fullFromList, targetTable, setClauses );
TableName tableName = targetTable.getTableNameField();
FromList selectFromList = fullFromList;
SelectNode selectNode = new SelectNode
(
setClauses,
selectFromList,
null, // where clause
null, // group by list
null, // having clause
null, // window list
null, // optimizer plan override
getContextManager()
);
_dml = new UpdateNode( tableName, selectNode, this, getContextManager() );
_dml.bindStatement();
//
// Don't add USAGE privilege on user-defined types.
//
boolean wasSkippingTypePrivileges = getCompilerContext().skipTypePrivileges( true );
//
// Split the update row into its before and after images.
//
ResultColumnList beforeColumns = new ResultColumnList( getContextManager() );
ResultColumnList afterColumns = new ResultColumnList( getContextManager() );
ResultColumnList fullUpdateRow = getBoundSelectUnderUpdate().getResultColumns();
// the full row is the before image, the after image, and a row location
int rowSize = fullUpdateRow.size() / 2;
// split the row into before and after images
for ( int i = 0; i < rowSize; i++ )
{
ResultColumn origBeforeRC = fullUpdateRow.elementAt( i );
ResultColumn origAfterRC = fullUpdateRow.elementAt( i + rowSize );
ResultColumn beforeRC = origBeforeRC.cloneMe();
ResultColumn afterRC = origAfterRC.cloneMe();
beforeColumns.addResultColumn( beforeRC );
afterColumns.addResultColumn( afterRC );
}
buildThenColumnsForUpdate( fullFromList, targetTable, fullUpdateRow, beforeColumns, afterColumns );
getCompilerContext().skipTypePrivileges( wasSkippingTypePrivileges );
}
/**
* <p>
* Due to discrepancies in how names are resolved by SELECT and UPDATE,
* we have to force the left side of SET clauses to use the same table identifiers
* as the right sides of the SET clauses.
* </p>
*/
private ResultColumnList realiasSetClauses
(
FromBaseTable targetTable
)
throws StandardException
{
ResultColumnList rcl = new ResultColumnList( getContextManager() );
for ( int i = 0; i < _updateColumns.size(); i++ )
{
ResultColumn setRC = _updateColumns.elementAt( i );
TableName tableName = targetTable.getTableName();
ColumnReference newTargetColumn = new ColumnReference
(
setRC.getReference().getColumnName(),
tableName,
getContextManager()
);
newTargetColumn.setMergeTableID( ColumnReference.MERGE_TARGET );
ResultColumn newRC = new ResultColumn
(
newTargetColumn,
setRC.getExpression(),
getContextManager()
);
rcl.addResultColumn( newRC );
}
return rcl;
}
/**
* <p>
* Get the bound SELECT node under the dummy UPDATE node.
* This may not be the source result set of the UPDATE node. That is because a ProjectRestrictNode
* may have been inserted on top of it by DEFAULT handling. This method
* exists to make the UPDATE actions of MERGE statements behave like ordinary
* UPDATE statements in this situation. The behavior is actually wrong. See
* DERBY-6414. Depending on how that bug is addressed, we may be able
* to remove this method eventually.
* </p>
*/
private ResultSetNode getBoundSelectUnderUpdate()
throws StandardException
{
ResultSetNode candidate = _dml.resultSet;
while ( candidate != null )
{
if ( candidate instanceof SelectNode ) { return candidate; }
else if ( candidate instanceof SingleChildResultSetNode )
{
candidate = ((SingleChildResultSetNode) candidate).getChildResult();
}
else { break; }
}
// don't understand what's going on
throw StandardException.newException( SQLState.NOT_IMPLEMENTED );
}
/** Bind the SET clauses of an UPDATE action */
private void bindSetClauses
(
MergeNode mergeNode,
FromList fullFromList,
FromTable targetTable,
ResultColumnList setClauses
)
throws StandardException
{
// needed to make the UpdateNode bind
setClauses.replaceOrForbidDefaults( targetTable.getTableDescriptor(), _updateColumns, true );
bindExpressions( setClauses, fullFromList );
//
// For column resolution later on, columns on the left side
// of SET operators are associated with the TARGET table.
//
for ( int i = 0; i < _updateColumns.size(); i++ )
{
ResultColumn rc = _updateColumns.elementAt( i );
ColumnReference cr = rc.getReference();
cr.setMergeTableID( ColumnReference.MERGE_TARGET );
}
// Now associate the columns on the right side of SET operators.
List<ColumnReference> colRefs = getColumnReferences( _updateColumns );
for ( ColumnReference cr : colRefs )
{
mergeNode.associateColumn( fullFromList, cr, ColumnReference.MERGE_UNKNOWN );
}
}
/**
* <p>
* Construct the row in the temporary table which drives an UPDATE action.
* Unlike a DELETE, whose temporary row is just a list of copied columns, the
* temporary row for UPDATE may contain complex expressions which must
* be code-generated later on.
* </p>
*/
private void buildThenColumnsForUpdate
(
FromList fullFromList,
FromTable targetTable,
ResultColumnList fullRow,
ResultColumnList beforeRow,
ResultColumnList afterValues
)
throws StandardException
{
TableDescriptor td = targetTable.getTableDescriptor();
HashSet<String> changedColumns = getChangedColumnNames();
HashSet<String> changedGeneratedColumns = getChangedGeneratedColumnNames( td, changedColumns );
_thenColumns = fullRow.copyListAndObjects();
//
// Here we set up for the evaluation of expressions in the temporary table
// which drives the INSERT action. If we were actually generating the dummy SELECT
// for the DML action, the work would normally be done there. But we don't generate
// that SELECT. So we do the following here:
//
// 1) If a column has a value specified in the WHEN [ NOT ] MATCHED clause, then we use it.
// There is some special handling to make the DEFAULT value work for identity columns.
//
// 2) Otherwise, if the column has a default, then we plug it in.
//
for ( int i = 0; i < _thenColumns.size(); i++ )
{
ResultColumn origRC = _thenColumns.elementAt( i );
boolean isAfterColumn = (i >= beforeRow.size());
// skip the final RowLocation column of an UPDATE
boolean isRowLocation = isRowLocation( origRC );
ValueNode origExpr = origRC.getExpression();
if ( isRowLocation ) { continue; }
String columnName = origRC.getName();
ColumnDescriptor cd = td.getColumnDescriptor( columnName );
boolean changed = false;
//
// This handles the case that a GENERATED BY DEFAULT identity column is being
// set to the keyword DEFAULT. This causes the UPDATE action of a MERGE statement
// to have the same wrong behavior as a regular UPDATE statement. See derby-6414.
//
if ( cd.isAutoincrement() && (origRC.getExpression() instanceof NumericConstantNode) )
{
DataValueDescriptor numericValue = ((NumericConstantNode) origRC.getExpression()).getValue();
if ( numericValue == null )
{
ResultColumn newRC = makeAutoGenRC( targetTable, origRC, i+1 );
newRC.setVirtualColumnId( origRC.getVirtualColumnId() );
_thenColumns.setElementAt( newRC, i );
continue;
}
}
//
// VirtualColumnNodes are skipped at code-generation time. This can result in
// NPEs when evaluating generation expressions. Replace VirtualColumnNodes with
// UntypedNullConstantNodes, except for identity columns, which require special
// handling below.
//
if ( !origRC.isAutoincrement() && (origRC.getExpression() instanceof VirtualColumnNode) )
{
origRC.setExpression( new UntypedNullConstantNode( getContextManager() ) );
}
//
// Generated columns need special handling. The value needs to be recalculated
// under the following circumstances:
//
// 1) It's the after image of the column
//
// 2) AND the statement causes the value to change.
//
// Otherwise, the value should be set to whatever is in the row coming out
// of the driving left join.
//
if ( cd.hasGenerationClause() )
{
if ( isAfterColumn && changedGeneratedColumns.contains( columnName ) )
{
// Set the expression to something that won't choke ResultColumnList.generateEvaluatedRow().
// The value will be a Java null at execution time, which will cause the value
// to be re-generated.
origRC.setExpression( new UntypedNullConstantNode( getContextManager() ) );
}
else
{
ColumnReference cr = new ColumnReference
( columnName, targetTable.getTableName(), getContextManager() );
origRC.setExpression( cr );
// remove the column descriptor in order to turn off hasGenerationClause()
origRC.setColumnDescriptor( null, null );
}
continue;
}
if ( isAfterColumn )
{
for ( int ic = 0; ic < beforeRow.size(); ic++ )
{
ResultColumn icRC = beforeRow.elementAt( ic );
if ( columnName.equals( icRC.getName() ) )
{
ResultColumn newRC = null;
// replace DEFAULT for a generated or identity column
ResultColumn valueRC = afterValues.elementAt( ic );
if ( valueRC.wasDefaultColumn() || (valueRC.getExpression() instanceof UntypedNullConstantNode ) )
{
if ( !cd.isAutoincrement() )
{
//
// Eliminate column references under identity columns. They
// will mess up the code generation.
//
ValueNode expr = origRC.getExpression();
if ( expr instanceof ColumnReference )
{
origRC.setExpression( new UntypedNullConstantNode( getContextManager() ) );
}
continue;
}
newRC = makeAutoGenRC( targetTable, origRC, i+1 );
}
else
{
newRC = valueRC.cloneMe();
newRC.setType( origRC.getTypeServices() );
}
newRC.setVirtualColumnId( origRC.getVirtualColumnId() );
_thenColumns.setElementAt( newRC, i );
changed = true;
break;
}
}
}
// plug in defaults if we haven't done so already
if ( !changed )
{
DefaultInfoImpl defaultInfo = (DefaultInfoImpl) cd.getDefaultInfo();
if ( (defaultInfo != null) && !defaultInfo.isGeneratedColumn() && !cd.isAutoincrement() )
{
_thenColumns.setDefault( origRC, cd, defaultInfo );
changed = true;
}
}
// set the result column name correctly for buildThenColumnSignature()
ResultColumn finalRC = _thenColumns.elementAt( i );
finalRC.setName( cd.getColumnName() );
//
// Turn off the autogenerated bit for identity columns so that
// ResultColumnList.generateEvaluatedRow() doesn't try to compile
// code to generate values for the before images in UPDATE rows.
// This logic will probably need to be revisited as part of fixing derby-6414.
//
finalRC.resetAutoincrementGenerated();
} // end loop through _thenColumns
}
/** Get the names of the columns explicitly changed by SET clauses */
private HashSet<String> getChangedColumnNames()
throws StandardException
{
HashSet<String> result = new HashSet<String>();
for ( int i = 0; i < _updateColumns.size(); i++ )
{
String columnName = _updateColumns.elementAt( i ).getName();
result.add( columnName );
}
return result;
}
/**
* <p>
* Get the names of the generated columns which are changed
* by the UPDATE statement. These are the generated columns which
* match one of the following conditions:
* </p>
*
* <ul>
* <li>Are explicitly mentioned on the left side of a SET clause.</li>
* <li>Are built from other columns which are explicitly mentioned on the left side of a SET clause.</li>
* </ul>
*/
private HashSet<String> getChangedGeneratedColumnNames
(
TableDescriptor targetTableDescriptor,
HashSet<String> changedColumnNames // columns which are explicitly mentioned on the left side of a SET clause
)
throws StandardException
{
HashSet<String> result = new HashSet<String>();
for ( ColumnDescriptor cd : targetTableDescriptor.getColumnDescriptorList() )
{
if ( !cd.hasGenerationClause() ) { continue; }
if ( changedColumnNames.contains( cd.getColumnName() ) )
{
result.add( cd.getColumnName() );
continue;
}
String[] referencedColumns = cd.getDefaultInfo().getReferencedColumnNames();
for ( String referencedColumnName : referencedColumns )
{
if ( changedColumnNames.contains( referencedColumnName ) )
{
result.add( referencedColumnName );
break;
}
}
}
return result;
}
////////////////
//
// BIND DELETE
//
////////////////
/** Bind a WHEN MATCHED ... THEN DELETE clause */
private void bindDelete
(
DataDictionary dd,
FromList fullFromList,
FromBaseTable targetTable
)
throws StandardException
{
//
// Don't add any privileges until we bind the DELETE.
//
IgnoreFilter ignorePermissions = new IgnoreFilter();
getCompilerContext().addPrivilegeFilter( ignorePermissions );
FromBaseTable deleteTarget = new FromBaseTable
( targetTable.getTableNameField(), null, null, null, getContextManager() );
FromList dummyFromList = new FromList( getContextManager() );
dummyFromList.addFromTable( deleteTarget );
dummyFromList.bindTables( dd, new FromList( getOptimizerFactory().doJoinOrderOptimization(), getContextManager() ) );
CurrentOfNode currentOfNode = CurrentOfNode.makeForMerge
( CURRENT_OF_NODE_NAME, deleteTarget, getContextManager() );
FromList fromList = new FromList( getContextManager() );
fromList.addFromTable( currentOfNode );
SelectNode selectNode = new SelectNode
(
null,
fromList, /* FROM list */
null, /* WHERE clause */
null, /* GROUP BY list */
null, /* having clause */
null, /* window list */
null, /* optimizer plan override */
getContextManager()
);
_dml = new DeleteNode( targetTable.getTableNameField(), selectNode, this, getContextManager() );
// ready to add permissions
getCompilerContext().removePrivilegeFilter( ignorePermissions );
_dml.bindStatement();
buildThenColumnsForDelete();
}
/**
* <p>
* Construct the signature of the temporary table which drives the
* INSERT/UPDATE/DELETE action.
* </p>
*/
private void buildThenColumnsForDelete()
throws StandardException
{
ResultColumnList dmlSignature = _dml.resultSet.getResultColumns();
for ( int i = 0; i < dmlSignature.size(); i++ )
{
ResultColumn origRC = dmlSignature.elementAt( i );
ResultColumn newRC;
ValueNode expression = origRC.getExpression();
if ( expression instanceof ColumnReference )
{
ColumnReference cr = (ColumnReference) ((ColumnReference) expression).getClone();
newRC = new ResultColumn( cr, cr, getContextManager() );
}
else
{
newRC = origRC.cloneMe();
}
_thenColumns.addResultColumn( newRC );
}
}
////////////////
//
// BIND INSERT
//
////////////////
/** Bind a WHEN NOT MATCHED ... THEN INSERT clause */
private void bindInsert
(
DataDictionary dd,
MergeNode mergeNode,
FromList fullFromList,
FromBaseTable targetTable
)
throws StandardException
{
ResultColumnList selectList = new ResultColumnList( getContextManager() );
for ( int i = 0; i < _insertValues.size(); i++ )
{
ResultColumn rc = _insertValues.elementAt( i );
selectList.addResultColumn( rc.cloneMe() );
}
selectList.replaceOrForbidDefaults( targetTable.getTableDescriptor(), _insertColumns, true );
bindExpressions( selectList, fullFromList );
bindInsertValues( fullFromList, targetTable );
// the VALUES clause may not mention columns in the target table
FromList sourceTableFromList = new FromList( getOptimizerFactory().doJoinOrderOptimization(), getContextManager() );
sourceTableFromList.addElement( fullFromList.elementAt( MergeNode.SOURCE_TABLE_INDEX ) );
bindExpressions( _insertValues, sourceTableFromList );
SelectNode selectNode = new SelectNode
(
selectList, // select list
fullFromList,
null, // where clause
null, // group by list
null, // having clause
null, // window list
null, // optimizer plan override
getContextManager()
);
_dml = new InsertNode
(
targetTable.getTableNameField(),
_insertColumns,
selectNode,
this, // in NOT MATCHED clause
null, // targetProperties
null, // order by cols
null, // offset
null, // fetch first
false, // has JDBC limit clause
getContextManager()
);
_dml.bindStatement();
buildThenColumnsForInsert( fullFromList, targetTable, _dml.resultSet.getResultColumns(), _insertColumns, _insertValues );
}
/** Bind the values in the INSERT list */
private void bindInsertValues
(
FromList fullFromList,
FromTable targetTable
)
throws StandardException
{
TableDescriptor td = targetTable.getTableDescriptor();
// construct a full insert column list if insert columns weren't specified
if ( _insertColumns == null ) { _insertColumns = buildFullColumnList( td ); }
if ( _insertColumns.size() != _insertValues.size() )
{
throw StandardException.newException( SQLState.LANG_DB2_INVALID_COLS_SPECIFIED );
}
// forbid illegal values for identity columns
for ( int i = 0; i <_insertValues.size(); i++ )
{
ResultColumn rc = _insertValues.elementAt( i );
String columnName = _insertColumns.elementAt( i ).getName();
ValueNode expr = rc.getExpression();
ColumnDescriptor cd = td.getColumnDescriptor( columnName );
// if the column isn't in the table, this will be sorted out when we bind
// the InsertNode
if ( cd == null ) { continue; }
// DEFAULT is the only value allowed for a GENERATED ALWAYS AS IDENTITY column
if ( cd.isAutoincAlways() && !(expr instanceof DefaultNode) )
{
throw StandardException.newException( SQLState.LANG_AI_CANNOT_MODIFY_AI, columnName );
}
// NULL is illegal as the value for any identity column
if ( cd.isAutoincrement() && (expr instanceof UntypedNullConstantNode) )
{
throw StandardException.newException( SQLState.LANG_NULL_INTO_NON_NULL, columnName );
}
}
// needed to make the SelectNode bind
_insertValues.replaceOrForbidDefaults( targetTable.getTableDescriptor(), _insertColumns, true );
bindExpressions( _insertValues, fullFromList );
}
/**
* <p>
* Build the full column list for a table.
* </p>
*/
private ResultColumnList buildFullColumnList( TableDescriptor td )
throws StandardException
{
ResultColumnList result = new ResultColumnList( getContextManager() );
ColumnDescriptorList cdl = td.getColumnDescriptorList();
int cdlSize = cdl.size();
for ( int index = 0; index < cdlSize; index++ )
{
ColumnDescriptor colDesc = cdl.elementAt( index );
ColumnReference columnRef = new ColumnReference( colDesc.getColumnName(), null, getContextManager() );
ResultColumn resultColumn = new ResultColumn
(
columnRef,
null,
getContextManager()
);
result.addResultColumn( resultColumn );
}
return result;
}
/**
* <p>
* Construct the row in the temporary table which drives an INSERT action.
* Unlike a DELETE, whose temporary row is just a list of copied columns, the
* temporary row for INSERT may contain complex expressions which must
* be code-generated later on.
* </p>
*/
private void buildThenColumnsForInsert
(
FromList fullFromList,
FromTable targetTable,
ResultColumnList fullRow,
ResultColumnList insertColumns,
ResultColumnList insertValues
)
throws StandardException
{
//
// Don't add USAGE privilege on user-defined types just because we're
// building the THEN columns.
//
boolean wasSkippingTypePrivileges = getCompilerContext().skipTypePrivileges( true );
TableDescriptor td = targetTable.getTableDescriptor();
_thenColumns = fullRow.copyListAndObjects();
//
// Here we set up for the evaluation of expressions in the temporary table
// which drives the INSERT action. If we were actually generating the dummy SELECT
// for the DML action, the work would normally be done there. But we don't generate
// that SELECT. So we do the following here:
//
// 1) If a column has a value specified in the WHEN [ NOT ] MATCHED clause, then we use it.
// There is some special handling to make the DEFAULT value work for identity columns.
//
// 2) Otherwise, if the column has a default, then we plug it in.
//
for ( int i = 0; i < _thenColumns.size(); i++ )
{
ResultColumn origRC = _thenColumns.elementAt( i );
String columnName = origRC.getName();
ColumnDescriptor cd = td.getColumnDescriptor( columnName );
boolean changed = false;
//
// VirtualColumnNodes are skipped at code-generation time. This can result in
// NPEs when evaluating generation expressions. Replace VirtualColumnNodes with
// UntypedNullConstantNodes, except for identity columns, which require special
// handling below.
//
if ( !origRC.isAutoincrement() && (origRC.getExpression() instanceof VirtualColumnNode) )
{
origRC.setExpression( new UntypedNullConstantNode( getContextManager() ) );
}
if ( cd.hasGenerationClause() )
{
origRC.setExpression( new UntypedNullConstantNode( getContextManager() ) );
continue;
}
for ( int ic = 0; ic < insertColumns.size(); ic++ )
{
ResultColumn icRC = insertColumns.elementAt( ic );
if ( columnName.equals( icRC.getName() ) )
{
ResultColumn newRC = null;
// replace DEFAULT for a generated or identity column
ResultColumn valueRC = insertValues.elementAt( ic );
if ( valueRC.wasDefaultColumn() || (valueRC.getExpression() instanceof UntypedNullConstantNode ) )
{
if ( !cd.isAutoincrement() )
{
//
// Eliminate column references under identity columns. They
// will mess up the code generation.
//
ValueNode expr = origRC.getExpression();
if ( expr instanceof ColumnReference )
{
origRC.setExpression( new UntypedNullConstantNode( getContextManager() ) );
}
continue;
}
newRC = makeAutoGenRC( targetTable, origRC, i+1 );
}
else
{
newRC = valueRC.cloneMe();
newRC.setType( origRC.getTypeServices() );
}
newRC.setVirtualColumnId( origRC.getVirtualColumnId() );
_thenColumns.setElementAt( newRC, i );
changed = true;
break;
}
}
// plug in defaults if we haven't done so already
if ( !changed )
{
DefaultInfoImpl defaultInfo = (DefaultInfoImpl) cd.getDefaultInfo();
if ( (defaultInfo != null) && !defaultInfo.isGeneratedColumn() && !cd.isAutoincrement() )
{
_thenColumns.setDefault( origRC, cd, defaultInfo );
changed = true;
}
}
// set the result column name correctly for buildThenColumnSignature()
ResultColumn finalRC = _thenColumns.elementAt( i );
finalRC.setName( cd.getColumnName() );
} // end loop through _thenColumns
getCompilerContext().skipTypePrivileges( wasSkippingTypePrivileges );
}
/**
* <p>
* Make a ResultColumn for an identity column which is being set to the DEFAULT
* value. This special ResultColumn will make it through code generation so that it
* will be calculated when the INSERT/UPDATE action is run.
* </p>
*/
private ResultColumn makeAutoGenRC
(
FromTable targetTable,
ResultColumn origRC,
int virtualColumnID
)
throws StandardException
{
String columnName = origRC.getName();
ColumnReference autoGenCR = new ColumnReference( columnName, targetTable.getTableName(), getContextManager() );
ResultColumn autoGenRC = new ResultColumn( autoGenCR, null, getContextManager() );
VirtualColumnNode autoGenVCN = new VirtualColumnNode( targetTable, autoGenRC, virtualColumnID, getContextManager() );
ResultColumn newRC = new ResultColumn( autoGenCR, autoGenVCN, getContextManager() );
// set the type so that buildThenColumnSignature() will function correctly
newRC.setType( origRC.getTypeServices() );
return newRC;
}
/////////////////
//
// BIND MINIONS
//
/////////////////
/** Boilerplate for binding a list of ResultColumns against a FromList */
private void bindExpressions( ResultColumnList rcl, FromList fromList )
throws StandardException
{
CompilerContext cc = getCompilerContext();
final int previousReliability = cc.getReliability();
boolean wasSkippingTypePrivileges = cc.skipTypePrivileges( true );
cc.setReliability( previousReliability | CompilerContext.SQL_IN_ROUTINES_ILLEGAL );
try {
rcl.bindExpressions
(
fromList,
new SubqueryList( getContextManager() ),
new ArrayList<AggregateNode>()
);
}
finally
{
// Restore previous compiler state
cc.setReliability( previousReliability );
cc.skipTypePrivileges( wasSkippingTypePrivileges );
}
}
/**
* <p>
* Forbid subqueries in WHEN [ NOT ] MATCHED clauses.
* </p>
*/
private void forbidSubqueries()
throws StandardException
{
forbidSubqueries( _matchingRefinement );
forbidSubqueries( _updateColumns );
forbidSubqueries( _insertColumns );
forbidSubqueries( _insertValues );
}
private void forbidSubqueries( ResultColumnList rcl )
throws StandardException
{
if ( rcl != null )
{
for ( int i = 0; i < rcl.size(); i++ )
{
forbidSubqueries( rcl.elementAt( i ) );
}
}
}
private void forbidSubqueries( ValueNode expr )
throws StandardException
{
if ( expr != null )
{
CollectNodesVisitor<SubqueryNode> getSubqueries =
new CollectNodesVisitor<SubqueryNode>(SubqueryNode.class);
expr.accept( getSubqueries );
if ( getSubqueries.getList().size() > 0 )
{
throw StandardException.newException( SQLState.LANG_NO_SUBQUERIES_IN_MATCHED_CLAUSE );
}
}
}
///////////////////////////////////////////////////////////////////////////////////
//
// optimize() BEHAVIOR
//
///////////////////////////////////////////////////////////////////////////////////
/**
* <p>
* Optimize the INSERT/UPDATE/DELETE action.
* </p>
*/
void optimize() throws StandardException
{
_dml.optimizeStatement();
}
///////////////////////////////////////////////////////////////////////////////////
//
// generate() BEHAVIOR
//
///////////////////////////////////////////////////////////////////////////////////
ConstantAction makeConstantAction( ActivationClassBuilder acb )
throws StandardException
{
// generate the clause-specific refinement
String refinementName = null;
if ( _matchingRefinement != null )
{
MethodBuilder userExprFun = acb.newUserExprFun();
_matchingRefinement.generateExpression( acb, userExprFun );
userExprFun.methodReturn();
// we are done modifying userExprFun, complete it.
userExprFun.complete();
refinementName = userExprFun.getName();
}
return getGenericConstantActionFactory().getMatchingClauseConstantAction
(
getClauseType(),
refinementName,
buildThenColumnSignature(),
_rowMakingMethodName,
_resultSetFieldName,
_actionMethodName,
_dml.makeConstantAction()
);
}
private int getClauseType()
{
if ( isUpdateClause() ) { return ConstantAction.WHEN_MATCHED_THEN_UPDATE; }
else if ( isInsertClause() ) { return ConstantAction.WHEN_NOT_MATCHED_THEN_INSERT; }
else { return ConstantAction.WHEN_MATCHED_THEN_DELETE; }
}
/**
* <p>
* Build the signature of the row which will go into the temporary table.
* </p>
*/
private ResultDescription buildThenColumnSignature()
throws StandardException
{
ResultColumnDescriptor[] cells = _thenColumns.makeResultDescriptors();
return getLanguageConnectionContext().getLanguageFactory().getResultDescription( cells, "MERGE" );
}
/**
* <p>
* Generate a method to invoke the INSERT/UPDATE/DELETE action. This method
* will be called at runtime by MatchingClauseConstantAction.executeConstantAction().
* </p>
*/
void generate
(
ActivationClassBuilder acb,
ResultColumnList selectList,
ResultSetNode generatedScan,
HalfOuterJoinNode hojn,
int clauseNumber
)
throws StandardException
{
_clauseNumber = clauseNumber;
adjustMatchingRefinement( selectList, generatedScan );
generateInsertUpdateRow( acb, selectList, generatedScan, hojn );
_actionMethodName = "mergeActionMethod_" + _clauseNumber;
MethodBuilder mb = acb.getClassBuilder().newMethodBuilder
(
Modifier.PUBLIC,
ClassName.ResultSet,
_actionMethodName
);
mb.addThrownException(ClassName.StandardException);
remapConstraints();
// now generate the action into this method
_dml.generate( acb, mb );
mb.methodReturn();
mb.complete();
}
/**
* <p>
* Re-map ColumnReferences in constraints to point into the row from the
* temporary table. This is where the row will be stored when constraints
* are being evaluated.
* </p>
*/
private void remapConstraints()
throws StandardException
{
if( isDeleteClause()) { return; }
else
{
ValueNode checkConstraints = isInsertClause() ?
((InsertNode) _dml).checkConstraints :
((UpdateNode) _dml).checkConstraints;
if ( checkConstraints != null )
{
List<ColumnReference> colRefs = getColumnReferences( checkConstraints );
for ( ColumnReference cr : colRefs )
{
cr.getSource().setResultSetNumber( NoPutResultSet.TEMPORARY_RESULT_SET_NUMBER );
}
}
}
}
/**
* <p>
* Adds a field to the generated class to hold the ResultSet of "then" rows
* which drive the INSERT/UPDATE/DELETE action. Generates code to push
* the contents of that field onto the stack.
* </p>
*/
void generateResultSetField( ActivationClassBuilder acb, MethodBuilder mb )
throws StandardException
{
_resultSetFieldName = "mergeResultSetField_" + _clauseNumber;
// make the field public so we can stuff it at execution time
LocalField resultSetField = acb.newFieldDeclaration( Modifier.PUBLIC, ClassName.NoPutResultSet, _resultSetFieldName );
//
// At runtime, MatchingClauseConstantAction.executeConstantAction()
// will stuff the resultSetField with the temporary table which collects
// the rows relevant to this action. We want to push the value of resultSetField
// onto the stack, where it will be the ResultSet argument to the constructor
// of the actual INSERT/UPDATE/DELETE action.
//
mb.getField( resultSetField );
}
/**
* <p>
* Generate a method to build a row for the temporary table for INSERT/UPDATE actions.
* The method stuffs each column in the row with the result of the corresponding
* expression built out of columns in the current row of the driving left join.
* The method returns the stuffed row.
* </p>
*/
private void generateInsertUpdateRow
(
ActivationClassBuilder acb,
ResultColumnList selectList,
ResultSetNode generatedScan,
HalfOuterJoinNode hojn
)
throws StandardException
{
// point expressions in the temporary row at the columns in the
// result column list of the driving left join.
adjustThenColumns( selectList, generatedScan, hojn );
_rowMakingMethodName = "mergeRowMakingMethod_" + _clauseNumber;
MethodBuilder mb = acb.getClassBuilder().newMethodBuilder
(
Modifier.PUBLIC,
ClassName.ExecRow,
_rowMakingMethodName
);
mb.addThrownException(ClassName.StandardException);
_thenColumns.generateEvaluatedRow( acb, mb, false, true );
}
/**
* <p>
* Point the column references in the matching refinement at the corresponding
* columns returned by the driving left join.
* </p>
*/
private void adjustMatchingRefinement
(
ResultColumnList selectList,
ResultSetNode generatedScan
)
throws StandardException
{
if ( _matchingRefinement != null )
{
useGeneratedScan( selectList, generatedScan, _matchingRefinement );
}
}
/**
* <p>
* Point the column references in the temporary row at the corresponding
* columns returned by the driving left join.
* </p>
*/
private void adjustThenColumns
(
ResultColumnList selectList,
ResultSetNode generatedScan,
HalfOuterJoinNode hojn
)
throws StandardException
{
ResultColumnList leftJoinResult = generatedScan.getResultColumns();
useGeneratedScan( selectList, generatedScan, _thenColumns );
//
// For an UPDATE action, the final column in the temporary row is the
// RowLocation. Point it at the last column in the row returned by the left join.
//
int lastRCSlot = _thenColumns.size() - 1;
ResultColumn lastRC = _thenColumns.elementAt( lastRCSlot );
if ( isRowLocation( lastRC ) )
{
ResultColumn lastLeftJoinRC = leftJoinResult.elementAt( leftJoinResult.size() - 1 );
ValueNode value = lastLeftJoinRC.getExpression();
String columnName = lastLeftJoinRC.getName();
ColumnReference rowLocationCR = new ColumnReference
( columnName, hojn.getTableName(), getContextManager() );
rowLocationCR.setSource( lastLeftJoinRC );
ResultColumn rowLocationRC = new ResultColumn( columnName, rowLocationCR, getContextManager() );
_thenColumns.removeElementAt( lastRCSlot );
_thenColumns.addResultColumn( rowLocationRC );
}
}
/**
* <p>
* Point a node's ColumnReferences into the row returned by the driving left join.
* </p>
*/
private void useGeneratedScan
(
ResultColumnList selectList,
ResultSetNode generatedScan,
QueryTreeNode node
)
throws StandardException
{
ResultColumnList leftJoinResult = generatedScan.getResultColumns();
for ( ColumnReference cr : getColumnReferences( node ) )
{
ResultColumn leftJoinRC = leftJoinResult.elementAt( getSelectListOffset( selectList, cr ) - 1 );
cr.setSource( leftJoinRC );
}
}
/**
* <p>
* Find a column reference in the SELECT list of the driving left join
* and return its 1-based offset into that list. Returns -1 if the column
* can't be found.
* </p>
*/
private int getSelectListOffset( ResultColumnList selectList, ValueNode thenExpression )
throws StandardException
{
int selectCount = selectList.size();
if ( thenExpression instanceof ColumnReference )
{
ColumnReference thenCR = (ColumnReference) thenExpression;
String thenCRName = thenCR.getColumnName();
int thenCRMergeTableID = getMergeTableID( thenCR );
// loop through the SELECT list to find this column reference
for ( int sidx = 0; sidx < selectCount; sidx++ )
{
ResultColumn selectRC = selectList.elementAt( sidx );
ValueNode selectExpression = selectRC.getExpression();
ColumnReference selectCR = selectExpression instanceof ColumnReference ?
(ColumnReference) selectExpression : null;
if ( selectCR != null )
{
if (
( getMergeTableID( selectCR ) == thenCRMergeTableID) &&
thenCRName.equals( selectCR.getColumnName() )
)
{
return sidx + 1;
}
}
}
if (SanityManager.DEBUG)
{
SanityManager.THROWASSERT
(
"Can't find select list column corresponding to " + thenCR.getSQLColumnName() +
" with merge table id = " + thenCRMergeTableID
);
}
}
else if ( thenExpression instanceof CurrentRowLocationNode )
{
//
// There is only one RowLocation in the SELECT list, the row location for the
// tuple from the target table. The RowLocation is always the last column in
// the SELECT list.
//
return selectCount;
}
return -1;
}
/** Find the MERGE table id of the indicated column */
private int getMergeTableID( ColumnReference cr )
{
int mergeTableID = cr.getMergeTableID();
if (SanityManager.DEBUG)
{
SanityManager.ASSERT
(
( (mergeTableID == ColumnReference.MERGE_SOURCE) || (mergeTableID == ColumnReference.MERGE_TARGET) ),
"Column " + cr.getSQLColumnName() + " has illegal MERGE table id: " + mergeTableID
);
}
return mergeTableID;
}
///////////////////////////////////////////////////////////////////////////////////
//
// 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
{
super.acceptChildren( v );
if ( _matchingRefinement != null ) { _matchingRefinement.accept( v ); }
if ( _updateColumns != null ) { _updateColumns.accept( v ); }
if ( _insertColumns != null ) { _insertColumns.accept( v ); }
if ( _insertValues != null ) { _insertValues.accept( v ); }
if ( _dml != null ) { _dml.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 );
if ( _matchingRefinement != null )
{
printLabel( depth, "matchingRefinement: " );
_matchingRefinement.treePrint( depth + 1 );
}
if ( _updateColumns != null )
{
printLabel( depth, "updateColumns: " );
_updateColumns.treePrint( depth + 1 );
}
if ( _insertColumns != null )
{
printLabel( depth, "insertColumns: " );
_insertColumns.treePrint( depth + 1 );
}
if ( _insertValues != null )
{
printLabel( depth, "insertValues: " );
_insertValues.treePrint( depth + 1 );
}
}
}
/**
* Convert this object to a String. See comments in QueryTreeNode.java
* for how this should be done for tree printing.
*
* @return This object as a String
*/
@Override
public String toString()
{
if ( isUpdateClause() ) { return "UPDATE"; }
else if ( isInsertClause() ) { return "INSERT"; }
else { return "DELETE"; }
}
///////////////////////////////////////////////////////////////////////////////////
//
// MINIONS
//
///////////////////////////////////////////////////////////////////////////////////
/** 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();
}
/** Return true if the ResultColumn represents a RowLocation */
private boolean isRowLocation( ResultColumn rc ) throws StandardException
{
if ( rc.getExpression() instanceof CurrentRowLocationNode ) { return true; }
DataTypeDescriptor dtd = rc.getTypeServices();
if ( (dtd != null) && (dtd.getTypeId().isRefTypeId()) ) { return true; }
return false;
}
@Override
public boolean referencesSessionSchema() throws StandardException {
return referencesSessionSchema(_matchingRefinement)
|| referencesSessionSchema(_updateColumns)
|| referencesSessionSchema(_insertColumns)
|| referencesSessionSchema(_insertValues);
}
private static boolean referencesSessionSchema(QueryTreeNode node)
throws StandardException {
return node != null && node.referencesSessionSchema();
}
}