blob: 72e2eab281a7018da2a8fd63ed3adb46e1cbc677 [file] [log] [blame]
/*
Derby - Class org.apache.derby.impl.sql.compile.CreateTriggerNode
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.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import org.apache.derby.catalog.UUID;
import org.apache.derby.iapi.error.StandardException;
import org.apache.derby.iapi.reference.SQLState;
import org.apache.derby.iapi.services.context.ContextManager;
import org.apache.derby.shared.common.sanity.SanityManager;
import org.apache.derby.iapi.sql.compile.CompilerContext;
import org.apache.derby.iapi.sql.compile.Visitable;
import org.apache.derby.iapi.sql.compile.Visitor;
import org.apache.derby.iapi.sql.conn.Authorizer;
import org.apache.derby.iapi.sql.conn.LanguageConnectionContext;
import org.apache.derby.iapi.sql.depend.DependencyManager;
import org.apache.derby.iapi.sql.depend.ProviderInfo;
import org.apache.derby.iapi.sql.depend.ProviderList;
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.SchemaDescriptor;
import org.apache.derby.iapi.sql.dictionary.TableDescriptor;
import org.apache.derby.iapi.sql.dictionary.TriggerDescriptor;
import org.apache.derby.iapi.sql.execute.ConstantAction;
/**
* A CreateTriggerNode is the root of a QueryTree
* that represents a CREATE TRIGGER
* statement.
*
*/
class CreateTriggerNode extends DDLStatementNode
{
private TableName triggerName;
private TableName tableName;
private int triggerEventMask;
private ResultColumnList triggerCols;
private boolean isBefore;
private boolean isRow;
private boolean isEnabled;
private List<TriggerReferencingStruct> refClause;
private ValueNode whenClause;
private String whenText;
private StatementNode actionNode;
private String actionText;
private String originalWhenText;
private String originalActionText;
private ProviderInfo[] providerInfo;
private SchemaDescriptor triggerSchemaDescriptor;
private SchemaDescriptor compSchemaDescriptor;
/*
* The following arrary will include columns that will cause the trigger to
* fire. This information will get saved in SYSTRIGGERS.
*
* The array will be null for all kinds of insert and delete triggers but
* it will be non-null for a subset of update triggers.
*
* For update triggers, the array will be null if no column list is
* supplied in the CREATE TRIGGER trigger column clause as shown below.
* The UPDATE trigger below will fire no matter which column in table1
* gets updated.
* eg
* CREATE TRIGGER tr1 AFTER UPDATE ON table1
* REFERENCING OLD AS oldt NEW AS newt
* FOR EACH ROW UPDATE table2 SET c24=oldt.c14;
*
* For update triggers, this array will be non-null if specific trigger
* column(s) has been specified in the CREATE TRIGGER sql. The UPDATE
* trigger below will fire when an update happens on column c12 in table1.
* eg
* CREATE TRIGGER tr1 AFTER UPDATE OF c12 ON table1
* REFERENCING OLD AS oldt NEW AS newt
* FOR EACH ROW UPDATE table2 SET c24=oldt.c14;
*
* Array referencedColInts along with referencedColsInTriggerAction will
* be used to determine which columns from the triggering table need to
* be read in when the trigger fires, thus making sure that we do not
* read the columns from the trigger table that are not required for
* trigger execution.
*/
private int[] referencedColInts;
/*
* The following array (which was added as part of DERBY-1482) will
* include columns referenced in the trigger action through the
* REFERENCING clause(old/new transition variables), in other trigger
* action columns. This information will get saved in SYSTRIGGERS
* (with the exception of triggers created in pre-10.7 dbs. For
* pre-10.7 dbs, this information will not get saved in SYSTRIGGERS
* in order to maintain backward compatibility.
*
* Unlike referencedColInts, this array can be non-null for all 3 types
* of triggers, namely, INSERT, UPDATE AND DELETE triggers. This array
* will be null if no columns in the trigger action are referencing
* old/new transition variables
*
* eg of a trigger in 10.7 and higher dbs which will cause
* referencedColsInTriggerAction to be null
* CREATE TRIGGER tr1 NO CASCADE BEFORE UPDATE of c12 ON table1
* SELECT c24 FROM table2 WHERE table2.c21 = 1
*
* eg of a trigger in 10.7 and higher dbs which will cause
* referencedColsInTriggerAction to be non-null
* For the trigger below, old value of column c14 from trigger table is
* used in the trigger action through old/new transition variables. A
* note of this requirement to read c14 will be made in
* referencedColsInTriggerAction array.
* eg
* CREATE TRIGGER tr1 AFTER UPDATE ON table1
* REFERENCING OLD AS oldt NEW AS newt
* FOR EACH ROW UPDATE table2 SET c24=oldt.c14;
*
* The exception to the rules above for trigger action columns information
* in referencedColsInTriggerAction is a trigger that was created with
* pre-10.7 release. Prior to 10.7, we did not collect any information
* about trigger action columns. So, any of the 2 kinds of trigger shown
* above prior to 10.7 will not have any trigger action column info on
* them in SYSTRIGGERS table. In order to cover the pre-existing pre-10.7
* triggers and all the other kinds of triggers, we will follow following
* 4 rules during trigger execution.
* Rule1)If trigger column information is null, then read all the
* columns from trigger table into memory irrespective of whether
* there is any trigger action column information. 2 egs of such
* triggers
* create trigger tr1 after update on t1 for each row values(1);
* create trigger tr1 after update on t1 referencing old as oldt
* for each row insert into t2 values(2,oldt.j,-2);
* Rule2)If trigger column information is available but no trigger
* action column information is found and no REFERENCES clause is
* used for the trigger, then only read the columns identified by
* the trigger column. eg
* create trigger tr1 after update of c1 on t1
* for each row values(1);
* Rule3)If trigger column information and trigger action column
* information both are not null, then only those columns will be
* read into memory. This is possible only for triggers created in
* release 10.7 or higher. Because prior to that we did not collect
* trigger action column informatoin. eg
* create trigger tr1 after update of c1 on t1
* referencing old as oldt for each row
* insert into t2 values(2,oldt.j,-2);
* Rule4)If trigger column information is available but no trigger
* action column information is found but REFERENCES clause is used
* for the trigger, then read all the columns from the trigger
* table. This will cover soft-upgrade and hard-upgrade scenario
* for triggers created pre-10.7. This rule prevents us from having
* special logic for soft-upgrade. Additionally, this logic makes
* invalidation of existing triggers unnecessary during
* hard-upgrade. The pre-10.7 created triggers will work just fine
* even though for some triggers, they would have trigger action
* columns missing from SYSTRIGGERS. A user can choose to drop and
* recreate such triggers to take advantage of Rule 3 which will
* avoid unnecessary column reads during trigger execution.
* eg trigger created prior to 10.7
* create trigger tr1 after update of c1 on t1
* referencing old as oldt for each row
* insert into t2 values(2,oldt.j,-2);
* To reiterate, Rule4) is there to cover triggers created with
* pre-10,7 releases but now that database has been
* hard/soft-upgraded to 10.7 or higher version. Prior to 10.7,
* we did not collect any information about trigger action columns.
*
* The only place we will need special code for soft-upgrade is during
* trigger creation. If we are in soft-upgrade mode, we want to make sure
* that we do not save information about trigger action columns in
* SYSTRIGGERS because the releases prior to 10.7 do not understand
* trigger action column information.
*
* Array referencedColInts along with referencedColsInTriggerAction will
* be used to determine which columns from the triggering table needs to
* be read in when the trigger fires, thus making sure that we do not
* read the columns from the trigger table that are not required for
* trigger execution.
*/
private int[] referencedColsInTriggerAction;
private TableDescriptor triggerTableDescriptor;
/*
** Names of old and new table. By default we have
** OLD/old and NEW/new. The casing is dependent on
** the language connection context casing as the rest
** of other code. Therefore we will set the value of the
** String at the init() time.
** However, if there is a referencing clause
** we will reset these values to be whatever the user
** wants.
*/
private String oldTableName;
private String newTableName;
private boolean oldTableInReferencingClause;
private boolean newTableInReferencingClause;
/**
* <p>
* A list that describes how the original SQL text of the trigger action
* statement was modified when transition tables and transition variables
* were replaced by VTI calls. Each element in the list contains four
* integers describing positions where modifications have happened. The
* first two integers are begin and end positions of a transition table
* or transition variable in {@link #originalActionText the original SQL
* text}. The last two integers are begin and end positions of the
* corresponding replacement in {@link #actionText the transformed SQL
* text}.
* </p>
*
* <p>
* Begin positions are inclusive and end positions are exclusive.
* </p>
*/
private final ArrayList<int[]>
actionTransformations = new ArrayList<int[]>();
/**
* Structure that has the same shape as {@code actionTransformations},
* except that it describes the transformations in the WHEN clause.
*/
private final ArrayList<int[]>
whenClauseTransformations = new ArrayList<int[]>();
/**
* Constructor for a CreateTriggerNode
*
* @param triggerName name of the trigger
* @param tableName name of the table which the trigger is declared upon
* @param triggerEventMask TriggerDescriptor.TRIGGER_EVENT_XXX
* @param triggerCols columns trigger is to fire upon. Valid
* for UPDATE case only.
* @param isBefore is before trigger (false for after)
* @param isRow true for row trigger, false for statement
* @param isEnabled true if enabled
* @param refClause the referencing clause
* @param whenClause the WHEN clause tree
* @param whenText the text of the WHEN clause
* @param actionNode the trigger action tree
* @param actionText the text of the trigger action
* @param cm context manager
*
* @exception StandardException Thrown on error
*/
CreateTriggerNode
(
TableName triggerName,
TableName tableName,
int triggerEventMask,
ResultColumnList triggerCols,
boolean isBefore,
boolean isRow,
boolean isEnabled,
List<TriggerReferencingStruct> refClause,
ValueNode whenClause,
String whenText,
StatementNode actionNode,
String actionText,
ContextManager cm
) throws StandardException
{
super(triggerName, cm);
this.triggerName = triggerName;
this.tableName = tableName;
this.triggerEventMask = triggerEventMask;
this.triggerCols = triggerCols;
this.isBefore = isBefore;
this.isRow = isRow;
this.isEnabled = isEnabled;
this.refClause = refClause;
this.whenClause = whenClause;
this.originalWhenText = whenText;
this.whenText = (whenText == null) ? null : whenText.trim();
this.actionNode = actionNode;
this.originalActionText = actionText;
this.actionText = (actionText == null) ? null : actionText.trim();
this.implicitCreateSchema = true;
}
String statementToString()
{
return "CREATE TRIGGER";
}
/**
* 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 (triggerCols != null)
{
printLabel(depth, "triggerColumns: ");
triggerCols.treePrint(depth + 1);
}
if (whenClause != null)
{
printLabel(depth, "whenClause: ");
whenClause.treePrint(depth + 1);
}
if (actionNode != null)
{
printLabel(depth, "actionNode: ");
actionNode.treePrint(depth + 1);
}
}
}
// accessors
// We inherit the generate() method from DDLStatementNode.
/**
* Bind this CreateTriggerNode. This means doing any static error
* checking that can be done before actually creating the table.
*
*
* @exception StandardException Thrown on error
*/
@Override
public void bindStatement() throws StandardException
{
CompilerContext compilerContext = getCompilerContext();
DataDictionary dd = getDataDictionary();
/*
** Grab the current schema. We will use that for
** sps compilation
*/
LanguageConnectionContext lcc = getLanguageConnectionContext();
compSchemaDescriptor = lcc.getDefaultSchema();
/*
** Get and check the schema descriptor for this
** trigger. This check will throw the proper exception
** if someone tries to create a trigger in the SYS
** schema.
*/
triggerSchemaDescriptor = getSchemaDescriptor();
/*
** Get the trigger table.
*/
triggerTableDescriptor = getTableDescriptor(tableName);
//throw an exception if user is attempting to create a trigger on a temporary table
if (isSessionSchema(triggerTableDescriptor.getSchemaDescriptor()))
{
throw StandardException.newException(SQLState.LANG_OPERATION_NOT_ALLOWED_ON_SESSION_SCHEMA_TABLES);
}
if (isPrivilegeCollectionRequired())
{
compilerContext.pushCurrentPrivType(Authorizer.TRIGGER_PRIV);
compilerContext.addRequiredTablePriv(triggerTableDescriptor);
compilerContext.popCurrentPrivType();
}
/*
** Regenerates the actionText and actionNode if necessary.
*/
boolean needInternalSQL = bindReferencesClause(dd);
// Get all the names of SQL objects referenced by the triggered
// SQL statement and the WHEN clause. Since some of the TableName
// nodes may be eliminated from the node tree during the bind phase,
// we collect the nodes before the nodes have been bound. The
// names will be used later when we normalize the trigger text
// that will be stored in the system tables.
SortedSet<TableName> actionNames =
actionNode.getOffsetOrderedNodes(TableName.class);
SortedSet<TableName> whenNames = (whenClause != null)
? whenClause.getOffsetOrderedNodes(TableName.class)
: null;
ProviderList prevAPL =
compilerContext.getCurrentAuxiliaryProviderList();
ProviderList apl = new ProviderList();
lcc.pushTriggerTable(triggerTableDescriptor);
try
{
compilerContext.setCurrentAuxiliaryProviderList(apl);
/*
** Bind the trigger action and the trigger
** when clause to make sure that they are
** ok. Note that we have already substituted
** in various replacements for OLD/NEW transition
** tables/variables and reparsed if necessary.
*/
if (needInternalSQL)
compilerContext.setReliability(CompilerContext.INTERNAL_SQL_LEGAL);
// For before triggers, the action statement cannot contain calls
// to procedures that modify SQL data. If the action statement
// contains a procedure call, this reliability will be used during
// bind of the call statement node.
if(isBefore)
compilerContext.setReliability(CompilerContext.MODIFIES_SQL_DATA_PROCEDURE_ILLEGAL);
actionNode.bindStatement();
if (whenClause != null)
{
ContextManager cm = getContextManager();
whenClause = whenClause.bindExpression(
new FromList(cm), new SubqueryList(cm),
new ArrayList<AggregateNode>(0));
// The WHEN clause must be a BOOLEAN expression.
whenClause.checkIsBoolean();
}
}
finally
{
lcc.popTriggerTable(triggerTableDescriptor);
compilerContext.setCurrentAuxiliaryProviderList(prevAPL);
}
// Qualify identifiers before storing them (DERBY-5901/DERBY-6370).
qualifyNames(actionNames, whenNames);
/*
** Statement is dependent on the TableDescriptor
*/
compilerContext.createDependency(triggerTableDescriptor);
/*
** If there is a list of columns, then no duplicate columns,
** and all columns must be found.
*/
if (triggerCols != null && triggerCols.size() != 0)
{
HashSet<String> columnNames = new HashSet<String>();
for (ResultColumn rc : triggerCols)
{
if (!columnNames.add(rc.getName()))
{
throw StandardException.newException(SQLState.LANG_DUPLICATE_COLUMN_IN_TRIGGER_UPDATE,
rc.getName(),
triggerName);
}
ColumnDescriptor cd = triggerTableDescriptor.getColumnDescriptor(rc.getName());
if (cd == null)
{
throw StandardException.newException(SQLState.LANG_COLUMN_NOT_FOUND_IN_TABLE,
rc.getName(),
tableName);
}
}
}
// Throw an exception if the WHEN clause or the triggered SQL
// statement references a table in the SESSION schema.
if (referencesSessionSchema()) {
throw StandardException.newException(SQLState.LANG_OPERATION_NOT_ALLOWED_ON_SESSION_SCHEMA_TABLES);
}
DependencyManager dm = dd.getDependencyManager();
providerInfo = dm.getPersistentProviderInfos(apl);
dm.clearColumnInfoInProviders(apl);
}
/**
* Return true if the node references SESSION schema tables (temporary or permanent)
*
* @return true if references SESSION schema tables, else false
*
* @exception StandardException Thrown on error
*/
@Override
public boolean referencesSessionSchema()
throws StandardException
{
//If create trigger is part of create statement and the trigger is defined on or it references SESSION schema tables,
//it will get caught in the bind phase of trigger and exception will be thrown by the trigger bind.
return isSessionSchema(triggerTableDescriptor.getSchemaName())
|| actionNode.referencesSessionSchema()
|| (whenClause != null && whenClause.referencesSessionSchema());
}
/**
* Comparator that can be used for sorting lists of FromBaseTables
* on the position they have in the SQL query string.
*/
private static final Comparator<FromBaseTable> OFFSET_COMPARATOR = new Comparator<FromBaseTable>() {
public int compare(FromBaseTable o1, FromBaseTable o2) {
// Return negative int, zero, or positive int if the offset of the
// first table is less than, equal to, or greater than the offset
// of the second table.
return o1.getTableNameField().getBeginOffset() -
o2.getTableNameField().getBeginOffset();
}
};
/*
** BIND OLD/NEW TRANSITION TABLES/VARIABLES AND collect TRIGGER ACTION
** COLUMNS referenced through REFERECING CLAUSE in CREATE TRIGGER statement
**
** 1) validate the referencing clause (if any)
**
** 2) convert trigger action text and WHEN clause text. e.g.
** DELETE FROM t WHERE c = old.c
** turns into
** DELETE FROM t WHERE c = org.apache.derby.iapi.db.Factory::
** getTriggerExecutionContext().getOldRow().
** getInt(columnNumberFor'C'inRuntimeResultset);
** or
** DELETE FROM t WHERE c in (SELECT c FROM OLD)
** turns into
** DELETE FROM t WHERE c in (
** SELECT c FROM new TriggerOldTransitionTable OLD)
**
** 3) check all column references against new/old transition
** variables (since they are no longer 'normal' column references
** that will be checked during bind)
**
** 4) collect all column references in trigger action through new/old
** transition variables. Information about them will be saved in
** SYSTRIGGERS table DERBY-1482(if we are dealing with pre-10.7 db, then we
** will not put any information about trigger action columns in the system
** table to ensure backward compatibility). This information along with the
** trigger columns will decide what columns from the trigger table will be
** fetched into memory during trigger execution.
**
** 5) reparse the new action text
**
** You might be wondering why we regenerate the text and reparse
** instead of just reworking the tree to have the nodes we want.
** Well, the primary reason is that if we screwed with the tree,
** then we would have a major headache if this trigger action
** was ever recompiled -- spses don't really know that they are
** triggers so it would be quite arduous to figure out that an
** sps is a trigger and munge up its query tree after figuring
** out what its OLD/NEW tables are, etc. Also, it is just plain
** easier to just generate the sql and rebind.
**
*/
private boolean bindReferencesClause(DataDictionary dd) throws StandardException
{
validateReferencesClause(dd);
// the actions of before triggers may not reference generated columns
if ( isBefore ) { forbidActionsOnGenCols(); }
String transformedActionText;
String transformedWhenText = null;
if (triggerCols != null && triggerCols.size() != 0) {
//If the trigger is defined on speific columns, then collect
//their column positions and ensure that those columns do
//indeed exist in the trigger table.
referencedColInts = new int[triggerCols.size()];
//This is the most interesting case for us. If we are here,
//then it means that a set of trigger columns are specified
//in the CREATE TRIGGER statement. This can only happen for
//an UPDATE trigger.
//eg
//CREATE TRIGGER tr1 AFTER UPDATE OF c12 ON table1
// REFERENCING OLD AS oldt NEW AS newt
// FOR EACH ROW UPDATE table2 SET c24=oldt.c14;
for (int i=0; i < triggerCols.size(); i++){
ResultColumn rc = triggerCols.elementAt(i);
ColumnDescriptor cd =
triggerTableDescriptor.getColumnDescriptor(rc.getName());
//Following will catch the case where an invalid trigger column
//has been specified in CREATE TRIGGER statement.
//CREATE TRIGGER tr1 AFTER UPDATE OF c1678 ON table1
// REFERENCING OLD AS oldt NEW AS newt
// FOR EACH ROW UPDATE table2 SET c24=oldt.c14;
if (cd == null)
{
throw StandardException.newException(SQLState.LANG_COLUMN_NOT_FOUND_IN_TABLE,
rc.getName(),
tableName);
}
referencedColInts[i] = cd.getPosition();
}
// sort the list
java.util.Arrays.sort(referencedColInts);
}
if (isRow)
{
//Create an array for column positions of columns referenced in
//trigger action. Initialize it to -1. The call to
//DataDictoinary.getTriggerActionSPS will find out the actual
//columns, if any, referenced in the trigger action and put their
//column positions in the array.
referencedColsInTriggerAction = new int[triggerTableDescriptor.getNumberOfColumns()];
java.util.Arrays.fill(referencedColsInTriggerAction, -1);
int[] cols;
cols = getDataDictionary().examineTriggerNodeAndCols(actionNode,
oldTableName,
newTableName,
originalActionText,
referencedColInts,
referencedColsInTriggerAction,
actionNode.getBeginOffset(),
triggerTableDescriptor,
triggerEventMask,
true,
actionTransformations);
if (whenClause != null)
{
cols = getDataDictionary().examineTriggerNodeAndCols(whenClause,
oldTableName,
newTableName,
originalActionText,
referencedColInts,
referencedColsInTriggerAction,
actionNode.getBeginOffset(),
triggerTableDescriptor,
triggerEventMask,
true,
actionTransformations);
}
//Now that we have verified that are no invalid column references
//for trigger columns, let's go ahead and transform the OLD/NEW
//transient table references in the trigger action sql.
transformedActionText = getDataDictionary().getTriggerActionString(actionNode,
oldTableName,
newTableName,
originalActionText,
referencedColInts,
referencedColsInTriggerAction,
actionNode.getBeginOffset(),
triggerTableDescriptor,
triggerEventMask,
true,
actionTransformations, cols);
// If there is a WHEN clause, we need to transform its text too.
if (whenClause != null) {
transformedWhenText =
getDataDictionary().getTriggerActionString(
whenClause, oldTableName, newTableName,
originalWhenText, referencedColInts,
referencedColsInTriggerAction,
whenClause.getBeginOffset(),
triggerTableDescriptor, triggerEventMask, true,
whenClauseTransformations, cols);
}
//Now that we know what columns we need for REFERENCEd columns in
//trigger action, we can get rid of -1 entries for the remaining
//columns from trigger table. This information will be saved in
//SYSTRIGGERS and will be used at trigger execution time to decide
//which columns need to be read into memory for trigger action
referencedColsInTriggerAction = justTheRequiredColumns(
referencedColsInTriggerAction);
}
else
{
//This is a table level trigger
transformedActionText = transformStatementTriggerText(
actionNode, originalActionText, actionTransformations);
if (whenClause != null) {
transformedWhenText = transformStatementTriggerText(
whenClause, originalWhenText,
whenClauseTransformations);
}
}
if (referencedColsInTriggerAction != null)
java.util.Arrays.sort(referencedColsInTriggerAction);
/*
** Parse the new action text with the substitutions.
** Also, we reset the actionText to this new value. This
** is what we are going to stick in the system tables.
*/
boolean regenNode = false;
if (!transformedActionText.equals(actionText))
{
regenNode = true;
actionText = transformedActionText;
actionNode = parseStatement(actionText, true);
}
if (whenClause != null && !transformedWhenText.equals(whenText)) {
regenNode = true;
whenText = transformedWhenText;
whenClause = parseSearchCondition(whenText, true);
}
return regenNode;
}
/**
* Make sure all references to SQL schema objects (such as tables and
* functions) in the SQL fragments that will be stored in the SPS and
* in the trigger descriptor, are fully qualified with a schema name.
*
* @param actionNames all the TableName nodes found in the triggered
* SQL statement
* @param whenNames all the Table Name nodes found in the WHEN clause
*/
private void qualifyNames(SortedSet<TableName> actionNames,
SortedSet<TableName> whenNames)
throws StandardException {
StringBuilder original = new StringBuilder();
StringBuilder transformed = new StringBuilder();
// Qualify the names in the action text.
qualifyNames(actionNode, actionNames, originalActionText, actionText,
actionTransformations, original, transformed);
originalActionText = original.toString();
actionText = transformed.toString();
// Do the same for the WHEN clause, if there is one.
if (whenClause != null) {
original.setLength(0);
transformed.setLength(0);
qualifyNames(whenClause, whenNames, originalWhenText, whenText,
whenClauseTransformations, original, transformed);
originalWhenText = original.toString();
whenText = transformed.toString();
}
}
/**
* Qualify all names SQL object names in original and transformed SQL
* text for an action or a WHEN clause.
*
* @param node the query tree node for the transformed version of the
* SQL text, in a bound state
* @param tableNames all the TableName nodes in the transformed text,
* in the order in which they appear in the SQL text
* @param originalText the original SQL text
* @param transformedText the transformed SQL text (with VTI calls for
* transition tables or transition variables)
* @param replacements a data structure that describes how {@code
* originalText} was transformed into {@code transformedText}
* @param newOriginal where to store the normalized version of the
* original text
* @param newTransformed where to store the normalized version of the
* transformed text
*/
private void qualifyNames(
QueryTreeNode node,
SortedSet<TableName> tableNames,
String originalText,
String transformedText,
List<int[]> replacements,
StringBuilder newOriginal,
StringBuilder newTransformed) throws StandardException {
int originalPos = 0;
int transformedPos = 0;
for (TableName name : tableNames) {
String qualifiedName = name.getFullSQLName();
int beginOffset = name.getBeginOffset() - node.getBeginOffset();
int tokenLength = name.getEndOffset() + 1 - name.getBeginOffset();
// For the transformed text, use the positions from the node.
newTransformed.append(transformedText, transformedPos, beginOffset);
newTransformed.append(qualifiedName);
transformedPos = beginOffset + tokenLength;
// For the original text, we need to adjust the positions to
// compensate for the changes in the transformed text.
Integer origBeginOffset =
getOriginalPosition(replacements, beginOffset);
if (origBeginOffset != null) {
newOriginal.append(originalText, originalPos, origBeginOffset);
newOriginal.append(qualifiedName);
originalPos = origBeginOffset + tokenLength;
}
}
newTransformed.append(
transformedText, transformedPos, transformedText.length());
newOriginal.append(originalText, originalPos, originalText.length());
}
/**
* Translate a position from the transformed trigger text
* ({@link #actionText} or {@link #whenText}) to the corresponding
* position in the original trigger text ({@link #originalActionText}
* or {@link #originalWhenText}).
*
* @param replacements a data structure that describes the relationship
* between positions in the original and the transformed text
* @param transformedPosition the position to translate
* @return the position in the original text, or {@code null} if there
* is no corresponding position in the original text (for example if
* it points to a token that was added to the transformed text and
* does not exist in the original text)
*/
private static Integer getOriginalPosition(
List<int[]> replacements, int transformedPosition) {
// Find the last change before the position we want to translate.
for (int i = replacements.size() - 1; i >= 0; i--) {
int[] offsets = replacements.get(i);
// offset[0] is the begin offset of the replaced text
// offset[1] is the end offset of the replaced text
// offset[2] is the begin offset of the replacement text
// offset[3] is the end offset of the replacement text
// Skip those changes that come after the position we
// want to translate.
if (transformedPosition >= offsets[2]) {
if (transformedPosition < offsets[3]) {
// The position points inside a changed portion of the
// SQL text, so there's no corresponding position in the
// original text. Return null.
return null;
} else {
// The position points after the end of the changed text,
// which means it's in a portion that's common to the
// original and the transformed text. Translate between
// the two.
return offsets[1] + (transformedPosition - offsets[3]);
}
}
}
// The position is before any of the transformations, so the position
// is the same in the original and the transformed text.
return transformedPosition;
}
/*
* The arrary passed will have either -1 or a column position as it's
* elements. If the array only has -1 as for all it's elements, then
* this method will return null. Otherwise, the method will create a
* new arrary with all -1 entries removed from the original arrary.
*/
private int[] justTheRequiredColumns(int[] columnsArrary) {
int countOfColsRefedInArray = 0;
int numberOfColsInTriggerTable = triggerTableDescriptor.getNumberOfColumns();
//Count number of non -1 entries
for (int i=0; i < numberOfColsInTriggerTable; i++) {
if (columnsArrary[i] != -1)
countOfColsRefedInArray++;
}
if (countOfColsRefedInArray > 0){
int[] tempArrayOfNeededColumns = new int[countOfColsRefedInArray];
int j=0;
for (int i=0; i < numberOfColsInTriggerTable; i++) {
if (columnsArrary[i] != -1)
tempArrayOfNeededColumns[j++] = columnsArrary[i];
}
return tempArrayOfNeededColumns;
} else
return null;
}
/**
* Transform the WHEN clause or the triggered SQL statement of a
* statement trigger from its original shape to internal syntax where
* references to transition tables are replaced with VTIs that return
* the before or after image of the changed rows.
*
* @param node the syntax tree of the WHEN clause or the triggered
* SQL statement
* @param originalText the original text of the WHEN clause or the
* triggered SQL statement
* @param replacements list that will be populated with int arrays that
* describe how the original text was transformed. The int arrays
* contain the begin (inclusive) and end (exclusive) positions of the
* original text that got replaced and of the replacement text, so that
* positions in the transformed text can be mapped to positions in the
* original text.
* @return internal syntax for accessing before or after image of
* the changed rows
* @throws StandardException if an error happens while performing the
* transformation
*/
private String transformStatementTriggerText(
QueryTreeNode node, String originalText, List<int[]> replacements)
throws StandardException
{
final int offset = node.getBeginOffset();
int start = 0;
StringBuilder newText = new StringBuilder();
// For a statement trigger, we find all FromBaseTable nodes. If
// the from table is NEW or OLD (or user designated alternates
// REFERENCING), we turn them into a trigger table VTI.
for (FromBaseTable fromTable : getTransitionTables(node)) {
String baseTableName = fromTable.getBaseTableName();
int tokBeginOffset = fromTable.getTableNameField().getBeginOffset();
int tokEndOffset = fromTable.getTableNameField().getEndOffset();
int nextTokenStart = tokEndOffset - offset + 1;
// Check if this transition table is allowed in this trigger type.
checkInvalidTriggerReference(baseTableName);
// The text up to the transition table name should be kept.
newText.append(originalText, start, tokBeginOffset - offset);
// Replace the transition table name with a VTI.
final int replacementOffset = newText.length();
newText.append(baseTableName.equals(oldTableName)
? "new org.apache.derby.catalog.TriggerOldTransitionRows() "
: "new org.apache.derby.catalog.TriggerNewTransitionRows() ");
// If the user supplied a correlation, then just
// pick it up automatically; otherwise, supply
// the default.
if (fromTable.getCorrelationName() == null) {
newText.append(baseTableName).append(' ');
}
// Record that we have made a change.
replacements.add(new int[] {
tokBeginOffset - offset, // offset to original token
nextTokenStart, // offset to next token
replacementOffset, // offset to replacement
newText.length() // offset to token after replacement
});
start = nextTokenStart;
}
// Finally, add everything found after the last transition table
// unchanged.
newText.append(originalText, start, originalText.length());
return newText.toString();
}
/**
* Get all transition tables referenced by a given node, sorted in the
* order in which they appear in the SQL text.
*
* @param node the node in which to search for transition tables
* @return a sorted set of {@code FromBaseTable}s that represent
* transition tables
* @throws StandardException if an error occurs
*/
private SortedSet<FromBaseTable> getTransitionTables(Visitable node)
throws StandardException {
CollectNodesVisitor<FromBaseTable> visitor =
new CollectNodesVisitor<FromBaseTable>(FromBaseTable.class);
node.accept(visitor);
TreeSet<FromBaseTable> tables =
new TreeSet<FromBaseTable>(OFFSET_COMPARATOR);
for (FromBaseTable fbt : visitor.getList()) {
if (!isTransitionTable(fbt)) {
// The from table is not the NEW or OLD table, so no need
// to do anything. Skip this table.
continue;
}
int tokBeginOffset = fbt.getTableNameField().getBeginOffset();
if (tokBeginOffset == -1) {
// Unknown offset. Skip this table.
continue;
}
tables.add(fbt);
}
return tables;
}
/**
* Check if a table represents one of the transition tables.
*
* @param fbt the table to check
* @return {@code true} if {@code fbt} represents either the old or
* the new transition table, {@code false} otherwise
*/
private boolean isTransitionTable(FromBaseTable fbt) {
// DERBY-6540: It can only be a transition table if the name
// is not schema qualified.
if (!fbt.getOrigTableName().hasSchema()) {
String baseTableName = fbt.getBaseTableName();
if (baseTableName != null) {
return baseTableName.equals(oldTableName) ||
baseTableName.equals(newTableName);
}
}
// Table name didn't match a transition table.
return false;
}
/*
* Forbid references to generated columns in the actions of BEFORE triggers.
* This is DERBY-3948, enforcing the following section of the SQL standard:
* part 2, section 11.39 (<trigger definition>), syntax rule 12c:
*
* <blockquote>
* 12) If BEFORE is specified, then:
* :
* c) The <triggered action> shall not contain a <field reference> that
* references a field in the new transition variable corresponding to a
* generated column of T.
* </blockquote>
*/
private void forbidActionsOnGenCols()
throws StandardException
{
ColumnDescriptorList generatedColumns = triggerTableDescriptor.getGeneratedColumns();
int genColCount = generatedColumns.size();
if ( genColCount == 0 ) { return; }
CollectNodesVisitor<ColumnReference> visitor =
new CollectNodesVisitor<ColumnReference>(ColumnReference.class);
actionNode.accept( visitor );
if (whenClause != null) {
whenClause.accept(visitor);
}
for (ColumnReference cr : visitor.getList())
{
String colRefName = cr.getColumnName();
String tabRefName = cr.getTableName();
for ( int gc_idx = 0; gc_idx < genColCount; gc_idx++ )
{
String genColName = generatedColumns.elementAt( gc_idx ).getColumnName();
if ( genColName.equals( colRefName ) && equals( newTableName, tabRefName ) )
{
throw StandardException.newException( SQLState.LANG_GEN_COL_BEFORE_TRIG, genColName );
}
}
}
}
/*
* Compare two strings.
*/
private boolean equals( String left, String right )
{
if ( left == null ) { return (right == null); }
else
{
return left.equals( right );
}
}
/*
** Check for illegal combinations here: insert & old or
** delete and new
*/
private void checkInvalidTriggerReference(String tableName) throws StandardException
{
if (tableName.equals(oldTableName) &&
(triggerEventMask & TriggerDescriptor.TRIGGER_EVENT_INSERT) == TriggerDescriptor.TRIGGER_EVENT_INSERT)
{
throw StandardException.newException(SQLState.LANG_TRIGGER_BAD_REF_MISMATCH, "INSERT", "new");
}
else if (tableName.equals(newTableName) &&
(triggerEventMask & TriggerDescriptor.TRIGGER_EVENT_DELETE) == TriggerDescriptor.TRIGGER_EVENT_DELETE)
{
throw StandardException.newException(SQLState.LANG_TRIGGER_BAD_REF_MISMATCH, "DELETE", "old");
}
}
/*
** Make sure that the referencing clause is legitimate.
** While we are at it we set the new/oldTableName to
** be whatever the user wants.
*/
private void validateReferencesClause(DataDictionary dd) throws StandardException
{
if ((refClause == null) || refClause.isEmpty())
{
return;
}
for (TriggerReferencingStruct trn : refClause)
{
/*
** 1) Make sure that we don't try to refer
** to a table for a row trigger or a row for
** a table trigger.
*/
if (isRow && !trn.isRow)
{
throw StandardException.newException(SQLState.LANG_TRIGGER_BAD_REF_MISMATCH, "ROW", "row");
}
else if (!isRow && trn.isRow)
{
throw StandardException.newException(SQLState.LANG_TRIGGER_BAD_REF_MISMATCH, "STATEMENT", "table");
}
/*
** 2) Make sure we have no dups
*/
if (trn.isNew)
{
if (newTableInReferencingClause)
{
throw StandardException.newException(SQLState.LANG_TRIGGER_BAD_REF_CLAUSE_DUPS);
}
/*
** 3a) No NEW reference in delete trigger
*/
if ((triggerEventMask & TriggerDescriptor.TRIGGER_EVENT_DELETE) == TriggerDescriptor.TRIGGER_EVENT_DELETE)
{
throw StandardException.newException(SQLState.LANG_TRIGGER_BAD_REF_MISMATCH, "DELETE", "old");
}
newTableName = trn.identifier;
newTableInReferencingClause = true;
}
else
{
if (oldTableInReferencingClause)
{
throw StandardException.newException(SQLState.LANG_TRIGGER_BAD_REF_CLAUSE_DUPS);
}
/*
** 3b) No OLD reference in insert trigger
*/
if ((triggerEventMask & TriggerDescriptor.TRIGGER_EVENT_INSERT) == TriggerDescriptor.TRIGGER_EVENT_INSERT)
{
throw StandardException.newException(SQLState.LANG_TRIGGER_BAD_REF_MISMATCH, "INSERT", "new");
}
oldTableName = trn.identifier;
oldTableInReferencingClause = true;
}
/*
** 4) Additional restriction on BEFORE triggers
*/
if (this.isBefore && !trn.isRow) {
// OLD TABLE and NEW TABLE not allowed for BEFORE triggers.
throw StandardException.newException(SQLState.LANG_TRIGGER_BAD_REF_MISMATCH, "BEFORE", "row");
}
}
}
/**
* Create the Constant information that will drive the guts of Execution.
*
* @exception StandardException Thrown on failure
*/
@Override
public ConstantAction makeConstantAction() throws StandardException
{
String oldReferencingName = (oldTableInReferencingClause) ? oldTableName : null;
String newReferencingName = (newTableInReferencingClause) ? newTableName : null;
return getGenericConstantActionFactory().getCreateTriggerConstantAction(
triggerSchemaDescriptor.getSchemaName(),
getRelativeName(),
triggerEventMask,
isBefore,
isRow,
isEnabled,
triggerTableDescriptor,
(UUID)null, // when SPSID
whenText,
(UUID)null, // action SPSid
actionText,
compSchemaDescriptor.getUUID(),
referencedColInts,
referencedColsInTriggerAction,
originalWhenText,
originalActionText,
oldTableInReferencingClause,
newTableInReferencingClause,
oldReferencingName,
newReferencingName,
providerInfo
);
}
/**
* 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 (SanityManager.DEBUG)
{
String refString = "null";
if (refClause != null)
{
StringBuilder buf = new StringBuilder();
for (TriggerReferencingStruct trn : refClause)
{
buf.append("\t");
buf.append(trn.toString());
buf.append("\n");
}
refString = buf.toString();
}
return super.toString() +
"tableName: "+tableName+
"\ntriggerEventMask: "+triggerEventMask+
"\nisBefore: "+isBefore+
"\nisRow: "+isRow+
"\nisEnabled: "+isEnabled+
"\nwhenText: "+whenText+
"\nrefClause: "+refString+
"\nactionText: "+actionText+
"\n";
}
else
{
return "";
}
}
@Override
void acceptChildren(Visitor v) throws StandardException {
super.acceptChildren(v);
if (triggerName != null) {
triggerName = (TableName) triggerName.accept(v);
}
if (tableName != null) {
tableName = (TableName) tableName.accept(v);
}
}
}