blob: 5810153174e4940a6cc61e665173d487e2efc4e8 [file] [log] [blame]
/*
Derby - Class org.apache.derby.impl.sql.execute.InternalTriggerExecutionContext
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.execute;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Hashtable;
import java.util.Map;
import java.util.Vector;
import org.apache.derby.catalog.UUID;
import org.apache.derby.iapi.db.TriggerExecutionContext;
import org.apache.derby.iapi.error.ExceptionSeverity;
import org.apache.derby.iapi.error.PublicAPI;
import org.apache.derby.iapi.error.StandardException;
import org.apache.derby.iapi.jdbc.ConnectionContext;
import org.apache.derby.iapi.reference.SQLState;
import org.apache.derby.iapi.services.i18n.MessageService;
import org.apache.derby.iapi.sql.conn.LanguageConnectionContext;
import org.apache.derby.iapi.sql.dictionary.TriggerDescriptor;
import org.apache.derby.iapi.sql.execute.ConstantAction;
import org.apache.derby.iapi.sql.execute.CursorResultSet;
import org.apache.derby.iapi.sql.execute.ExecRow;
import org.apache.derby.iapi.sql.execute.ExecutionStmtValidator;
import org.apache.derby.iapi.types.DataValueDescriptor;
/**
* There is one of these beasts per INSERT/DELETE/UPDATE
* statement. It fulfills the contract for the externally
* visible trigger execution context and it validates
* that a statement that is about to be executed doesn't
* violate the restrictions placed upon what can be executed
* from a trigger.
* <p>
* Note that it is crucial that cleanup() is called once
* the DML has completed, cleanup() makes sure that users
* can't do something invalid on a tec reference that they
* were holding from when the trigger fired.
*
*/
class InternalTriggerExecutionContext
implements TriggerExecutionContext, ExecutionStmtValidator
{
/*
** Immutable
*/
protected int dmlType;
protected String statementText;
protected ConnectionContext cc;
protected UUID targetTableId;
protected String targetTableName;
protected LanguageConnectionContext lcc;
/*
** Mutable
*/
protected CursorResultSet beforeResultSet;
protected CursorResultSet afterResultSet;
/**
* used exclusively for InsertResultSets which have autoincrement
* columns.
*/
protected ExecRow afterRow;
protected boolean cleanupCalled;
protected TriggerEvent event;
protected TriggerDescriptor triggerd;
/*
** Used to track all the result sets we have given out to
** users. When the trigger context is no longer valid,
** we close all the result sets that may be in the user
** space because they can no longer provide meaningful
** results.
*/
@SuppressWarnings("UseOfObsoleteCollectionType")
private Vector<ResultSet> resultSetVector;
/**
* aiCounters is a vector of AutoincrementCounters used to keep state which
* might be used by the trigger. This is only used by Insert triggers--
* Delete and Update triggers do not use this variable.
*
* @see AutoincrementCounter
*
*/
@SuppressWarnings("UseOfObsoleteCollectionType")
private Vector<AutoincrementCounter> aiCounters;
/**
* aiHT is a hash table of auto increment (key, value) pairs. This is used
* for auto increment values generated by the trigger.
*/
@SuppressWarnings("UseOfObsoleteCollectionType")
private Hashtable<String,Long> aiHT;
/**
* Build a trigger execution context.
* <p>
* About the only thing of real interest to outside observers
* is that it pushes itself as the trigger execution context
* in the lcc. Be sure to call <i>cleanup()</i> when you
* are done, or you will be flogged like the reprobate that
* you are.
*
* @param lcc the lcc
* @param statementText the text of the statement that caused the
* trigger to fire. may be null if we are replicating
* @param targetTableId the UUID of the table upon which the trigger
* fired
* @param targetTableName the name of the table upon which the trigger
* fired
* @param aiCounters A vector of AutoincrementCounters to keep state
* of the ai columns in this insert trigger.a
*
* @exception StandardException on error
*/
@SuppressWarnings({"UseOfObsoleteCollectionType", "LeakingThisInConstructor"})
InternalTriggerExecutionContext
(
LanguageConnectionContext lcc,
ConnectionContext cc,
String statementText,
int dmlType,
UUID targetTableId,
String targetTableName,
Vector<AutoincrementCounter> aiCounters
) throws StandardException
{
this.dmlType = dmlType;
this.statementText = statementText;
this.cc = cc;
this.lcc = lcc;
this.targetTableId = targetTableId;
this.targetTableName = targetTableName;
this.resultSetVector = new Vector<java.sql.ResultSet>();
this.aiCounters = aiCounters;
lcc.pushTriggerExecutionContext(this);
}
void setBeforeResultSet(CursorResultSet rs)
{
beforeResultSet = rs;
}
void setAfterResultSet(CursorResultSet rs)
throws StandardException
{
afterResultSet = rs;
if (aiCounters != null)
{
if (triggerd.isRowTrigger())
{
// An after row trigger needs to see the "first" row inserted
rs.open();
afterRow = rs.getNextRow();
rs.close();
}
else
{
// after statement trigger needs to look at the last value.
if (!triggerd.isBeforeTrigger())
resetAICounters(false);
}
}
}
void setCurrentTriggerEvent(TriggerEvent event)
{
this.event = event;
}
void clearCurrentTriggerEvent()
{
event = null;
}
void setTrigger(TriggerDescriptor triggerd)
{
this.triggerd = triggerd;
}
void clearTrigger() throws StandardException
{
event = null;
triggerd = null;
if (afterResultSet != null)
{
afterResultSet.close();
afterResultSet = null;
}
if (beforeResultSet != null)
{
beforeResultSet.close();
beforeResultSet = null;
}
}
/**
* Cleanup the trigger execution context. <B>MUST</B>
* be called when the caller is done with the trigger
* execution context.
* <p>
* We go to somewhat exaggerated lengths to free up
* all our resources here because a user may hold on
* to a TEC after it is valid, so we clean everything
* up to be on the safe side.
*
* @exception StandardException on unexpected error
*/
protected void cleanup()
throws StandardException
{
if (lcc != null) {
lcc.popTriggerExecutionContext(this);
}
/*
** Explicitly close all result sets that we have
** given out to the user.
*/
if (resultSetVector != null) {
for (ResultSet rs : resultSetVector) {
try {
rs.close();
} catch (SQLException se) {
}
}
}
resultSetVector = null;
/*
** We should have already closed our underlying
** ExecResultSets by closing the jdbc result sets,
** but in case we got an error that we caught and
** ignored, explicitly close them.
*/
if (afterResultSet != null)
{
afterResultSet.close();
afterResultSet = null;
}
if (beforeResultSet != null)
{
beforeResultSet.close();
beforeResultSet = null;
}
lcc = null;
cleanupCalled = true;
}
/**
* Make sure that the user isn't trying to get a result
* set after we have cleaned up.
*/
private void ensureProperContext() throws SQLException
{
if (cleanupCalled)
{
throw new SQLException(
MessageService.getTextMessage(
SQLState.LANG_STATEMENT_CLOSED_NO_REASON),
"XCL31",
ExceptionSeverity.STATEMENT_SEVERITY
);
}
}
/////////////////////////////////////////////////////////
//
// ExecutionStmtValidator
//
/////////////////////////////////////////////////////////
/**
* Make sure that whatever statement is about to be executed
* is ok from the context of this trigger.
* <p>
* Note that we are sub classed in replication for checks
* for replication specific language.
*
* @param constantAction the constant action of the action
* that we are to validate
*
* @exception StandardException on error
*/
public void validateStatement(ConstantAction constantAction) throws StandardException
{
// DDL statements are not allowed in triggers. Direct use of DDL
// statements in a trigger's action statement is disallowed by the
// parser. However, this runtime check is needed to prevent execution
// of DDL statements by procedures within a trigger context.
if (constantAction instanceof DDLConstantAction) {
throw StandardException.newException(SQLState.LANG_NO_DDL_IN_TRIGGER, triggerd.getName());
}
// No INSERT/UPDATE/DELETE for a before trigger. There is no need to
// check this here because parser does not allow these DML statements
// in a trigger's action statement in a before trigger. Parser also
// disallows creation of before triggers calling procedures that modify
// SQL data.
}
/////////////////////////////////////////////////////////
//
// TriggerExectionContext
//
/////////////////////////////////////////////////////////
/**
* Get the target table name upon which the
* trigger event is declared.
*
* @return the target table
*/
public String getTargetTableName()
{
return targetTableName;
}
/**
* Get the target table UUID upon which the
* trigger event is declared.
*
* @return the uuid of the target table
*/
public UUID getTargetTableId()
{
return targetTableId;
}
/**
* Get the type for the event that caused the
* trigger to fire.
*
* @return the event type (e.g. UPDATE_EVENT)
*/
public int getEventType()
{
return dmlType;
}
/**
* Get the text of the statement that caused the
* trigger to fire.
*
* @return the statement text
*/
public String getEventStatementText()
{
return statementText;
}
/**
* Returns a result set row the old images of the changed rows.
* For a row trigger, the result set will have a single row. For
* a statement trigger, this result set has every row that has
* changed or will change. If a statement trigger does not affect
* a row, then the result set will be empty (i.e. ResultSet.next()
* will return false).
*
* @return the ResultSet containing before images of the rows
* changed by the triggering event.
*
* @exception SQLException if called after the triggering event has
* completed
*/
public java.sql.ResultSet getOldRowSet() throws SQLException
{
ensureProperContext();
if (beforeResultSet == null)
{
return null;
}
try
{
CursorResultSet brs = beforeResultSet;
/* We should really shallow clone the result set, because it could be used
* at multiple places independently in trigger action. This is a bug found
* during the fix of beetle 4373.
*/
if (brs instanceof TemporaryRowHolderResultSet)
brs = (CursorResultSet) ((TemporaryRowHolderResultSet) brs).clone();
else if (brs instanceof TableScanResultSet)
brs = (CursorResultSet) ((TableScanResultSet) brs).clone();
brs.open();
java.sql.ResultSet rs = cc.getResultSet(brs);
resultSetVector.addElement(rs);
return rs;
} catch (StandardException se)
{
throw PublicAPI.wrapStandardException(se);
}
}
/**
* Returns a result set row the new images of the changed rows.
* For a row trigger, the result set will have a single row. For
* a statement trigger, this result set has every row that has
* changed or will change. If a statement trigger does not affect
* a row, then the result set will be empty (i.e. ResultSet.next()
* will return false).
*
* @return the ResultSet containing after images of the rows
* changed by the triggering event.
*
* @exception SQLException if called after the triggering event has
* completed
*/
public java.sql.ResultSet getNewRowSet() throws SQLException
{
ensureProperContext();
if (afterResultSet == null)
{
return null;
}
try
{
/* We should really shallow clone the result set, because it could be used
* at multiple places independently in trigger action. This is a bug found
* during the fix of beetle 4373.
*/
CursorResultSet ars = afterResultSet;
if (ars instanceof TemporaryRowHolderResultSet)
ars = (CursorResultSet) ((TemporaryRowHolderResultSet) ars).clone();
else if (ars instanceof TableScanResultSet)
ars = (CursorResultSet) ((TableScanResultSet) ars).clone();
ars.open();
java.sql.ResultSet rs = cc.getResultSet(ars);
resultSetVector.addElement(rs);
return rs;
} catch (StandardException se)
{
throw PublicAPI.wrapStandardException(se);
}
}
/**
* Like getBeforeResultSet(), but returns a result set positioned
* on the first row of the before result set. Used as a convenience
* to get a column for a row trigger. Equivalent to getBeforeResultSet()
* followed by next().
*
* @return the ResultSet positioned on the old row image.
*
* @exception SQLException if called after the triggering event has
* completed
*/
public java.sql.ResultSet getOldRow() throws SQLException
{
java.sql.ResultSet rs = getOldRowSet();
if (rs != null)
rs.next();
return rs;
}
/**
* Like getAfterResultSet(), but returns a result set positioned
* on the first row of the before result set. Used as a convenience
* to get a column for a row trigger. Equivalent to getAfterResultSet()
* followed by next().
*
* @return the ResultSet positioned on the new row image.
*
* @exception SQLException if called after the triggering event has
* completed
*/
public java.sql.ResultSet getNewRow() throws SQLException
{
java.sql.ResultSet rs = getNewRowSet();
if (rs != null)
rs.next();
return rs;
}
public Long getAutoincrementValue(String identity)
{
// first search the hashtable-- this represents the ai values generated
// by this trigger.
if (aiHT != null)
{
Long value = aiHT.get(identity);
if (value != null)
return value;
}
// If we didn't find it in the hashtable search in the counters which
// represent values inherited by trigger from insert statements.
if (aiCounters != null)
{
for (AutoincrementCounter aic : aiCounters)
{
if (identity.equals(aic.getIdentity()))
{
return aic.getCurrentValue();
}
}
}
// didn't find it-- return NULL.
return null;
}
/**
* Copy a map of auto increment values into the trigger
* execution context hash table of auto increment values.
*/
@SuppressWarnings("UseOfObsoleteCollectionType")
public void copyHashtableToAIHT(Map<String,Long> from)
{
if (from == null)
return;
if (aiHT == null)
aiHT = new Hashtable<String,Long>();
aiHT.putAll(from);
}
/**
* Reset Autoincrement counters to the beginning or the end.
*
* @param begin if True, reset the AutoincremnetCounter to the
* beginning-- used to reset the counters for the
* next trigger. If false, reset it to the end--
* this sets up the counter appropriately for a
* AFTER STATEMENT trigger.
*/
public void resetAICounters(boolean begin)
{
if (aiCounters == null)
return;
afterRow = null;
for (AutoincrementCounter aic : aiCounters)
{
aic.reset(begin);
}
}
/**
* Update auto increment counters from the last row inserted.
*
*/
public void updateAICounters() throws StandardException
{
if (aiCounters == null)
return;
for (AutoincrementCounter aic : aiCounters)
{
DataValueDescriptor dvd = afterRow.getColumn(aic.getColumnPosition());
aic.update(dvd.getLong());
}
}
@Override
public String toString() {
return triggerd.getName();
}
}