blob: 42831c6850c5a0cb336d93d87861e542d1c968fc [file] [log] [blame]
/* Copyright (c) 2001-2010, The HSQL Development Group
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the HSQL Development Group nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hsqldb.sample;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.Trigger;
import org.hsqldb.lib.StringUtil;
// peterhudson@users 20020130 - patch 478657 by peterhudson - new class
// fredt@users 20030727 - signature altered
// boucherb@users 20040315 - sample updated
/**
* <P>Sample code for use of triggers in hsqldb.
*
* This class org.hsqldb.sample package, but a typical implementation is in
* users's class hierarchy.
*
* SQL to invoke is:<p>
* CREATE TRIGGER triggerSample BEFORE|AFTER INSERT|UPDATE|DELETE
* ON myTable [FOR EACH ROW] [QUEUE n] [NOWAIT] CALL "myPackage.trigClass"<br>
*
* This will create a thread that will wait for its firing event to occur;
* when this happens, the trigger's thread runs the 'trigClass.fire'
* Note that this is still in the same Java Virtual Machine as the
* database, so make sure the fired method does not hang.<p>
*
* There is a queue of events waiting to be run by each trigger thread.
* This is particularly useful for 'FOR EACH ROW' triggers, when a large
* number of trigger events occur in rapid succession, without the trigger
* thread getting a chance to run. If the queue becomes full, subsequent
* additions to it cause the database engine to suspend awaiting space
* in the queue. Take great care to avoid this situation if the trigger
* action involves accessing the database, as deadlock will occur.
* This can be avoided either by ensuring the QUEUE parameter makes a large
* enough queue, or by using the NOWAIT parameter, which causes a new
* trigger event to overwrite the most recent event in the queue.
* The default queue size is 1024.<p>
*
* Ensure that "myPackage.trigClass" is present in the classpath which
* you use to start hsql.<p>
*
* If the method wants to access the database, it must establish
* a JDBC connection.<p>
*
* When the 'fire' method is called, it is passed the following arguments: <p>
*
* fire (int type, String trigName, String tabName, Object oldRow[],
* Object[] newRow) <p>
*
* where 'type' is one of the values enumerated in the Trigger interface and
* the 'oldRow'/'newRow' pair represents the rows acted on. The first
* length - 1 array slots contain column values and the final slot contains
* either null or the value of the internally assigned row identity, if
* the concerned table has no primary key. The final slot must _never_ be
* modified. <p>
*
* The mapping of row classes to database types is specified in
* /doc/hsqlSyntax.html#Datatypes. <p>
*
* To be done:<p>
*
* <ol>
* <li> Implement the "jdbc:default:connection: URL to provide transparent
* and portable access to internal connections for use in triggers and
* stored procedures. <p>
*
* <li> Implement declaritive column to trigger method argument
* mapping, conditional execution (WHEN clause), etc. <p>
*
* <li> Investigate and refine synchronous and asynchronous trigger models. <p>
*
* Because certain combinations of trigger create parameters cause the
* individual triggered actions of a multirow update to run in different
* threads, it is possible for an 'after' trigger to run before its
* corresponding 'before' trigger; the acceptability and implications
* of this needs to be investigated, documented and the behaviour of
* the engine fully specified.
*
* <li> Investigate and implement the SQL 200n specified execution stack under
* arbitrary triggered action and SQL-invoked routine call graphs.
* </ol>
*
* @author Peter Hudson
* @author boucherb@users
* @version 1.7.2
* @since 1.7.0
*/
public class TriggerSample implements Trigger {
static final PrintWriter out = new PrintWriter(System.out);
static final String drv = "org.hsqldb.jdbc.JDBCDriver";
static final String url = "jdbc:hsqldb:mem:trigger-sample";
static final String usr = "SA";
static final String pwd = "";
static final String impl = TriggerSample.class.getName();
static final String tn = "trig_test";
static final String drop_test_table_stmt = "DROP TABLE " + tn
+ " IF EXISTS";
static final String create_test_table_stmt = "CREATE TABLE " + tn
+ "(id INTEGER PRIMARY KEY, value VARCHAR(20))";
static final String drop_audit_table_stmt = "DROP TABLE audit IF EXISTS";
static final String create_audit_table_stmt = "CREATE TABLE audit("
+ "id INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1), "
+ "op VARCHAR(6), " + "tn VARCHAR(20), " + "ors LONGVARCHAR, "
+ "nrs LONGVARCHAR, " + "ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP)";
static final String audit_insert_stmt =
"INSERT INTO audit(op, tn, ors, nrs) VALUES(?, ?, ?, ?)";
/**
* A sample HSQLDB Trigger interface implementation. <p>
*
* This sample prints information about the firing trigger and records
* actions in an audit table. <p>
*
* The techniques used here are simplified dramatically for demonstration
* purposes and are in no way recommended as a model upon which to build
* actual installations involving triggered actions.
*
* @param typ trigger type
* @param trn trigger name
* @param tn table name
* @param or old row
* @param nr new row
*/
public void fire(int typ, String trn, String tn, Object[] or,
Object[] nr) {
synchronized (TriggerSample.class) {
String ors = or == null ? "null"
: StringUtil.arrayToString(or);
String nrs = nr == null ? "null"
: StringUtil.arrayToString(nr);
out.println("----------------------------------------");
out.println(getTriggerDescriptor(trn, typ, tn));
out.println("old row : " + ors);
out.println("new row : " + nrs);
out.flush();
if ("TRIG_TEST".equals(tn)) {
switch (typ) {
case INSERT_BEFORE_ROW : {
// Business rule: ID shall be less than 11.
// (Marti DiBergi, we love you ;-)
// You can cast row[i] given your knowledge of what
// the table format is:
final int ID = ((Number) nr[0]).intValue();
doAssert(ID < 11, "ID < 11");
break;
}
case UPDATE_BEFORE_ROW : {
// Business rule: ignore update of VALUE 'unchangable'.
if ("unchangable".equals(or[1])) {
nr[1] = or[1]; // short-circuit the update
}
// !!!Warning!!!
// The engine does not check the class of substituted
// values; it's up to you to use the correct class.
// For example, this will cause database curruption:
// nr[1] = new Integer(5);
break;
}
}
}
doAuditStep(typ, tn, ors, nrs);
}
}
private static void doAssert(boolean b, String msg) {
if (b) {
// do nothing
} else {
throw org.hsqldb.error.Error.error(ErrorCode.GENERAL_ERROR,
msg);
}
}
private static void doAuditStep(int typ, String tn, String ors,
String nrs) {
Connection conn;
PreparedStatement stmt;
switch (typ) {
case INSERT_AFTER_ROW :
case UPDATE_AFTER_ROW :
case DELETE_AFTER_ROW : {
try {
conn = getConnection();
stmt = conn.prepareStatement(audit_insert_stmt);
stmt.setString(1, getOperationSpec(typ));
stmt.setString(2, tn);
stmt.setString(3, ors);
stmt.setString(4, nrs);
stmt.executeUpdate();
conn.close();
} catch (SQLException se) {
se.printStackTrace();
}
}
}
}
public static String getWhenSpec(int type) {
switch (type) {
case INSERT_BEFORE_ROW :
case UPDATE_BEFORE_ROW :
case DELETE_BEFORE_ROW : {
return "BEFORE";
}
case INSERT_AFTER :
case INSERT_AFTER_ROW :
case UPDATE_AFTER :
case UPDATE_AFTER_ROW :
case DELETE_AFTER :
case DELETE_AFTER_ROW : {
return "AFTER";
}
default : {
return "";
}
}
}
public static String getOperationSpec(int type) {
switch (type) {
case INSERT_AFTER :
case INSERT_AFTER_ROW :
case INSERT_BEFORE_ROW : {
return "INSERT";
}
case UPDATE_AFTER :
case UPDATE_AFTER_ROW :
case UPDATE_BEFORE_ROW : {
return "UPDATE";
}
case DELETE_AFTER :
case DELETE_AFTER_ROW :
case DELETE_BEFORE_ROW : {
return "DELETE";
}
default : {
return "";
}
}
}
public static String getQueueSpec(int qs) {
return (qs < 0) ? ""
: ("QUEUE " + qs);
}
public static String getForEachSpec(int type) {
switch (type) {
case INSERT_BEFORE_ROW :
case INSERT_AFTER_ROW :
case UPDATE_BEFORE_ROW :
case UPDATE_AFTER_ROW :
case DELETE_AFTER_ROW :
case DELETE_BEFORE_ROW : {
return "FOR EACH ROW";
}
default : {
return "FOR EACH STATEMENT";
}
}
}
public static String getTriggerDDL(String trn, int typ, String tab,
int qs,
String impl) throws SQLException {
StringBuffer sb = new StringBuffer();
sb.append("CREATE TRIGGER ");
sb.append(trn);
sb.append(' ');
sb.append(getWhenSpec(typ));
sb.append(' ');
sb.append(getOperationSpec(typ));
sb.append(" ON ");
sb.append(tab);
sb.append(' ');
sb.append(getForEachSpec(typ));
sb.append(' ');
sb.append(getQueueSpec(qs));
sb.append(" CALL \"");
sb.append(impl);
sb.append("\"");
return sb.toString();
}
public static String getTriggerDescriptor(String trn, int typ,
String tab) {
StringBuffer sb = new StringBuffer();
sb.append("TRIGGER : ");
sb.append(trn);
sb.append(' ');
sb.append(getWhenSpec(typ));
sb.append(' ');
sb.append(getOperationSpec(typ));
sb.append(" ON ");
sb.append(tab);
sb.append(' ');
sb.append(getForEachSpec(typ));
return sb.toString();
}
private static Connection getConnection() throws SQLException {
try {
Class.forName(drv).newInstance();
return DriverManager.getConnection(url, usr, pwd);
} catch (SQLException se) {
throw se;
} catch (Exception e) {
throw new SQLException(e.toString());
}
}
private static void createTrigger(Statement stmt, String trn,
int typ) throws SQLException {
stmt.execute(getTriggerDDL(trn, typ, tn, 0, impl));
}
private static void setup() throws SQLException {
Connection conn = getConnection();
Statement stmt = conn.createStatement();
stmt.execute(drop_test_table_stmt);
stmt.execute(create_test_table_stmt);
stmt.execute(drop_audit_table_stmt);
stmt.execute(create_audit_table_stmt);
createTrigger(stmt, "tibr_" + tn, INSERT_BEFORE_ROW);
createTrigger(stmt, "tia_" + tn, INSERT_AFTER);
createTrigger(stmt, "tiar_" + tn, INSERT_AFTER_ROW);
createTrigger(stmt, "tubr_" + tn, UPDATE_BEFORE_ROW);
createTrigger(stmt, "tua_" + tn, UPDATE_AFTER);
createTrigger(stmt, "tuar_" + tn, UPDATE_AFTER_ROW);
createTrigger(stmt, "tdbr_" + tn, DELETE_BEFORE_ROW);
createTrigger(stmt, "tda_" + tn, DELETE_AFTER);
createTrigger(stmt, "tdar_" + tn, DELETE_AFTER_ROW);
stmt.close();
conn.close();
}
private static void doSomeWork() throws SQLException {
Connection conn = getConnection();
Statement stmt = conn.createStatement();
conn.setAutoCommit(false);
stmt.execute("INSERT INTO trig_test VALUES (1, 'hello')");
stmt.execute("INSERT INTO trig_test VALUES (2, 'now what?')");
stmt.execute("INSERT INTO trig_test VALUES (3, 'unchangable')");
stmt.execute("INSERT INTO trig_test VALUES (4, 'goodbye')");
conn.commit();
dumpTable("trig_test");
stmt.execute("UPDATE trig_test SET value = 'all done'");
conn.commit();
dumpTable("trig_test");
stmt.execute("DELETE FROM trig_test");
conn.rollback();
dumpTable("trig_test");
try {
stmt.execute("INSERT INTO trig_test VALUES(11, 'whatever')");
} catch (SQLException se) {
se.printStackTrace();
}
stmt.execute("INSERT INTO trig_test VALUES(10, 'whatever')");
conn.commit();
dumpTable("trig_test");
stmt.close();
conn.close();
}
private static void dumpTable(String tn) throws SQLException {
Connection conn = getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from " + tn);
ResultSetMetaData rsmd = rs.getMetaData();
int count = rsmd.getColumnCount();
out.println();
out.println("****************************************");
out.println("DUMP FOR TABLE: " + tn);
out.println("****************************************");
out.flush();
while (rs.next()) {
out.print("[");
for (int i = 1; i <= count; i++) {
out.print(rs.getString(i));
if (i < count) {
out.print(" : ");
}
}
out.println("]");
}
out.println();
out.flush();
rs.close();
stmt.close();
conn.close();
}
private static void runSample() throws SQLException {
setup();
doSomeWork();
dumpTable("audit");
}
public static void main(String[] args) throws SQLException {
runSample();
}
}
/*
test SQL
CREATE CACHED TABLE trig_test (int_field integer)
CREATE TRIGGER ins_before BEFORE INSERT ON trig_test CALL "org.hsqldb.sample.TriggerSample"
CREATE TRIGGER ins_after AFTER INSERT ON trig_test CALL "org.hsqldb.sample.TriggerSample"
CREATE TRIGGER upd_before BEFORE UPDATE ON trig_test CALL "org.hsqldb.sample.TriggerSample"
CREATE TRIGGER upd_after AFTER UPDATE ON trig_test CALL "org.hsqldb.sample.TriggerSample"
CREATE TRIGGER upd_before_row BEFORE UPDATE ON trig_test FOR EACH ROW CALL "org.hsqldb.sample.TriggerSample"
CREATE TRIGGER upd_after_row AFTER UPDATE ON trig_test FOR EACH ROW CALL "org.hsqldb.sample.TriggerSample"
CREATE TRIGGER del_before BEFORE DELETE ON trig_test CALL "org.hsqldb.sample.TriggerSample"
CREATE TRIGGER del_after AFTER DELETE ON trig_test CALL "org.hsqldb.sample.TriggerSample"
CREATE TRIGGER del_before_row BEFORE DELETE ON trig_test FOR EACH ROW CALL "org.hsqldb.sample.TriggerSample"
CREATE TRIGGER del_after_row AFTER DELETE ON trig_test FOR EACH ROW CALL "org.hsqldb.sample.TriggerSample"
INSERT INTO trig_test VALUES (1)
INSERT INTO trig_test VALUES (2)
INSERT INTO trig_test VALUES (3)
UPDATE trig_test SET int_field = int_field + 3
DELETE FROM trig_test
*/