/* * Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, * Version 1.0, and under the Eclipse Public License, Version 1.0 * (http://h2database.com/html/license.html). * Initial Developer: H2 Group */ package org.h2.test.db; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Arrays; import java.util.HashSet; import org.h2.api.Trigger; import org.h2.constant.ErrorCode; import org.h2.test.TestBase; import org.h2.tools.TriggerAdapter; import org.h2.util.Task; /** * Tests for trigger and constraints. */ public class TestTriggersConstraints extends TestBase implements Trigger { private static boolean mustNotCallTrigger; private String triggerName; /** * Run just this test. * * @param a ignored */ public static void main(String... a) throws Exception { TestBase.createCaller().init().test(); } public void test() throws Exception { deleteDb("trigger"); testTriggerDeadlock(); testDeleteInTrigger(); testTriggerAdapter(); testViewTrigger(); testTriggerBeforeSelect(); testTriggerAlterTable(); testTriggers(); testConstraints(); testMultiPartForeignKeys(); deleteDb("trigger"); } /** * A trigger that deletes all rows in the test table. */ public static class DeleteTrigger extends TriggerAdapter { public void fire(Connection conn, ResultSet oldRow, ResultSet newRow) throws SQLException { conn.createStatement().execute("delete from test"); } } private void testTriggerDeadlock() throws Exception { final Connection conn, conn2; final Statement stat, stat2; conn = getConnection("trigger"); conn2 = getConnection("trigger"); stat = conn.createStatement(); stat2 = conn2.createStatement(); stat.execute("create table test(id int) as select 1"); stat.execute("create table test2(id int) as select 1"); stat.execute("create trigger test_u before update on test2 " + "for each row call \"" + DeleteTrigger.class.getName() + "\""); conn.setAutoCommit(false); conn2.setAutoCommit(false); stat2.execute("update test set id = 2"); Task task = new Task() { public void call() throws Exception { Thread.sleep(300); stat2.execute("update test2 set id = 4"); } }; task.execute(); Thread.sleep(100); try { stat.execute("update test2 set id = 3"); task.get(); } catch (SQLException e) { assertEquals(ErrorCode.LOCK_TIMEOUT_1, e.getErrorCode()); } conn2.rollback(); conn.rollback(); stat.execute("drop table test"); stat.execute("drop table test2"); conn.close(); conn2.close(); } private void testDeleteInTrigger() throws SQLException { if (config.mvcc) { return; } Connection conn; Statement stat; conn = getConnection("trigger"); stat = conn.createStatement(); stat.execute("create table test(id int) as select 1"); stat.execute("create trigger test_u before update on test " + "for each row call \"" + DeleteTrigger.class.getName() + "\""); // this threw a NullPointerException assertThrows(ErrorCode.ROW_NOT_FOUND_WHEN_DELETING_1, stat). execute("update test set id = 2"); stat.execute("drop table test"); conn.close(); } private void testTriggerAdapter() throws SQLException { Connection conn; Statement stat; conn = getConnection("trigger"); stat = conn.createStatement(); stat.execute("drop table if exists test"); stat.execute("create table test(id int)"); stat.execute("create table message(name varchar)"); stat.execute("create trigger test_insert before insert, update, delete on test " + "for each row call \"" + TestTriggerAdapter.class.getName() + "\""); stat.execute("insert into test values(1)"); ResultSet rs; rs = stat.executeQuery("select * from test"); rs.next(); assertEquals(10, rs.getInt(1)); stat.execute("update test set id = 2"); rs = stat.executeQuery("select * from test"); rs.next(); assertEquals(20, rs.getInt(1)); stat.execute("delete from test"); rs = stat.executeQuery("select * from message"); assertTrue(rs.next()); assertEquals("+1;", rs.getString(1)); assertTrue(rs.next()); assertEquals("-10;+2;", rs.getString(1)); assertTrue(rs.next()); assertEquals("-20;", rs.getString(1)); assertFalse(rs.next()); stat.execute("drop table test, message"); conn.close(); } private void testViewTrigger() throws SQLException { Connection conn; Statement stat; conn = getConnection("trigger"); stat = conn.createStatement(); stat.execute("drop table if exists test"); stat.execute("create table test(id int)"); stat.execute("create view test_view as select * from test"); stat.execute("create trigger test_view_insert " + "instead of insert on test_view for each row call \"" + TestView.class.getName() + "\""); stat.execute("create trigger test_view_delete " + "instead of delete on test_view for each row call \"" + TestView.class.getName() + "\""); if (!config.memory) { conn.close(); conn = getConnection("trigger"); stat = conn.createStatement(); } int count = stat.executeUpdate("insert into test_view values(1)"); assertEquals(1, count); ResultSet rs; rs = stat.executeQuery("select * from test"); assertTrue(rs.next()); assertFalse(rs.next()); count = stat.executeUpdate("delete from test_view"); assertEquals(1, count); stat.execute("drop view test_view"); stat.execute("drop table test"); conn.close(); } /** * A test trigger adapter implementation. */ public static class TestTriggerAdapter extends TriggerAdapter { public void fire(Connection conn, ResultSet oldRow, ResultSet newRow) throws SQLException { StringBuilder buff = new StringBuilder(); if (oldRow != null) { buff.append("-").append(oldRow.getString("id")).append(';'); } if (newRow != null) { buff.append("+").append(newRow.getString("id")).append(';'); } if (!"TEST_INSERT".equals(triggerName)) { throw new RuntimeException("Wrong trigger name: " + triggerName); } if (!"TEST".equals(tableName)) { throw new RuntimeException("Wrong table name: " + tableName); } if (!"PUBLIC".equals(schemaName)) { throw new RuntimeException("Wrong schema name: " + schemaName); } if (type != (Trigger.INSERT | Trigger.UPDATE | Trigger.DELETE)) { throw new RuntimeException("Wrong type: " + type); } if (newRow != null) { if (oldRow == null) { if (newRow.getInt(1) != 1) { throw new RuntimeException("Expected: 1 got: " + newRow.getString(1)); } } else { if (newRow.getInt(1) != 2) { throw new RuntimeException("Expected: 2 got: " + newRow.getString(1)); } } newRow.updateInt(1, newRow.getInt(1) * 10); } conn.createStatement().execute("insert into message values('" + buff.toString() + "')"); } } /** * A test trigger implementation. */ public static class TestView implements Trigger { PreparedStatement prepInsert; public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException { prepInsert = conn.prepareStatement("insert into test values(?)"); } public void fire(Connection conn, Object[] oldRow, Object[] newRow) throws SQLException { if (newRow != null) { prepInsert.setInt(1, (Integer) newRow[0]); prepInsert.execute(); } } public void close() { // ignore } public void remove() { // ignore } } private void testTriggerBeforeSelect() throws SQLException { Connection conn; Statement stat; conn = getConnection("trigger"); stat = conn.createStatement(); stat.execute("drop table if exists meta_tables"); stat.execute("create table meta_tables(name varchar)"); stat.execute("create trigger meta_tables_select " + "before select on meta_tables call \"" + TestSelect.class.getName() + "\""); ResultSet rs; rs = stat.executeQuery("select * from meta_tables"); assertTrue(rs.next()); assertFalse(rs.next()); stat.execute("create table test(id int)"); rs = stat.executeQuery("select * from meta_tables"); assertTrue(rs.next()); assertTrue(rs.next()); assertFalse(rs.next()); conn.close(); if (!config.memory) { conn = getConnection("trigger"); stat = conn.createStatement(); stat.execute("create table test2(id int)"); rs = stat.executeQuery("select * from meta_tables"); assertTrue(rs.next()); assertTrue(rs.next()); assertTrue(rs.next()); assertFalse(rs.next()); conn.close(); } } /** * A test trigger implementation. */ public static class TestSelect implements Trigger { PreparedStatement prepMeta; public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException { prepMeta = conn.prepareStatement("insert into meta_tables " + "select table_name from information_schema.tables " + "where table_schema='PUBLIC'"); } public void fire(Connection conn, Object[] oldRow, Object[] newRow) throws SQLException { if (oldRow != null || newRow != null) { throw new SQLException("old and new must be null"); } conn.createStatement().execute("delete from meta_tables"); prepMeta.execute(); } public void close() { // ignore } public void remove() { // ignore } } /** * A test trigger implementation. */ public static class Test implements Trigger { public void fire(Connection conn, Object[] oldRow, Object[] newRow) throws SQLException { conn.createStatement().execute("call seq.nextval"); } public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) { // nothing to do } public void close() { // ignore } public void remove() { // ignore } } private void testTriggerAlterTable() throws SQLException { Connection conn = getConnection("trigger"); Statement stat = conn.createStatement(); stat.execute("DROP TABLE IF EXISTS TEST"); stat.execute("create sequence seq"); stat.execute("create table test(id int primary key)"); assertSingleValue(stat, "call seq.nextval", 1); conn.setAutoCommit(false); stat.execute("create trigger test_upd before insert on test call \"" + Test.class.getName() + "\""); stat.execute("insert into test values(1)"); assertSingleValue(stat, "call seq.nextval", 3); stat.execute("alter table test add column name varchar"); assertSingleValue(stat, "call seq.nextval", 4); stat.execute("drop sequence seq"); stat.execute("drop table test"); conn.close(); } private void testConstraints() throws SQLException { Connection conn = getConnection("trigger"); Statement stat = conn.createStatement(); stat.execute("DROP TABLE IF EXISTS TEST"); stat.execute("create table test(id int primary key, parent int)"); stat.execute("alter table test add constraint test_parent_id " + "foreign key(parent) references test (id) on delete cascade"); stat.execute("insert into test select x, x/2 from system_range(0, 100)"); stat.execute("delete from test"); assertSingleValue(stat, "select count(*) from test", 0); stat.execute("drop table test"); conn.close(); } /** * Regression test: we had a bug where the AlterTableAddConstraint class * used to sometimes pick the wrong unique index for a foreign key. */ private void testMultiPartForeignKeys() throws SQLException { Connection conn = getConnection("trigger"); Statement stat = conn.createStatement(); stat.execute("DROP TABLE IF EXISTS TEST1"); stat.execute("DROP TABLE IF EXISTS TEST2"); stat.execute("create table test1(id int primary key, col1 int)"); stat.execute("alter table test1 add constraint unique_test1 " + "unique (id,col1)"); stat.execute("create table test2(id int primary key, col1 int)"); stat.execute("alter table test2 add constraint fk_test2 " + "foreign key(id,col1) references test1 (id,col1)"); stat.execute("insert into test1 values (1,1)"); stat.execute("insert into test1 values (2,2)"); stat.execute("insert into test1 values (3,3)"); stat.execute("insert into test2 values (1,1)"); assertThrows(23506, stat).execute("insert into test2 values (2,1)"); assertSingleValue(stat, "select count(*) from test1", 3); assertSingleValue(stat, "select count(*) from test2", 1); stat.execute("drop table test1"); stat.execute("drop table test2"); conn.close(); } private void testTriggers() throws SQLException { mustNotCallTrigger = false; Connection conn = getConnection("trigger"); Statement stat = conn.createStatement(); stat.execute("DROP TABLE IF EXISTS TEST"); stat.execute("CREATE TABLE TEST(ID INT PRIMARY KEY, NAME VARCHAR(255))"); // CREATE TRIGGER trigger {BEFORE|AFTER} // {INSERT|UPDATE|DELETE|ROLLBACK} ON table // [FOR EACH ROW] [QUEUE n] [NOWAIT] CALL triggeredClass stat.execute("CREATE TRIGGER IF NOT EXISTS INS_BEFORE " + "BEFORE INSERT ON TEST " + "FOR EACH ROW NOWAIT CALL \"" + getClass().getName() + "\""); stat.execute("CREATE TRIGGER IF NOT EXISTS INS_BEFORE " + "BEFORE INSERT ON TEST " + "FOR EACH ROW NOWAIT CALL \"" + getClass().getName() + "\""); stat.execute("CREATE TRIGGER INS_AFTER " + "" + "AFTER INSERT ON TEST " + "FOR EACH ROW NOWAIT CALL \"" + getClass().getName() + "\""); stat.execute("CREATE TRIGGER UPD_BEFORE " + "BEFORE UPDATE ON TEST " + "FOR EACH ROW NOWAIT CALL \"" + getClass().getName() + "\""); stat.execute("CREATE TRIGGER INS_AFTER_ROLLBACK " + "AFTER INSERT, ROLLBACK ON TEST " + "FOR EACH ROW NOWAIT CALL \"" + getClass().getName() + "\""); stat.execute("INSERT INTO TEST VALUES(1, 'Hello')"); ResultSet rs; rs = stat.executeQuery("SCRIPT"); checkRows(rs, new String[] { "CREATE FORCE TRIGGER PUBLIC.INS_BEFORE " + "BEFORE INSERT ON PUBLIC.TEST " + "FOR EACH ROW NOWAIT CALL \"" + getClass().getName() + "\";", "CREATE FORCE TRIGGER PUBLIC.INS_AFTER " + "AFTER INSERT ON PUBLIC.TEST " + "FOR EACH ROW NOWAIT CALL \"" + getClass().getName() + "\";", "CREATE FORCE TRIGGER PUBLIC.UPD_BEFORE " + "BEFORE UPDATE ON PUBLIC.TEST " + "FOR EACH ROW NOWAIT CALL \"" + getClass().getName() + "\";", "CREATE FORCE TRIGGER PUBLIC.INS_AFTER_ROLLBACK " + "AFTER INSERT, ROLLBACK ON PUBLIC.TEST " + "FOR EACH ROW NOWAIT CALL \"" + getClass().getName() + "\";", }); while (rs.next()) { String sql = rs.getString(1); if (sql.startsWith("CREATE TRIGGER")) { System.out.println(sql); } } rs = stat.executeQuery("SELECT * FROM TEST"); rs.next(); assertEquals("Hello-updated", rs.getString(2)); assertFalse(rs.next()); stat.execute("UPDATE TEST SET NAME=NAME||'-upd'"); rs = stat.executeQuery("SELECT * FROM TEST"); rs.next(); assertEquals("Hello-updated-upd-updated2", rs.getString(2)); assertFalse(rs.next()); mustNotCallTrigger = true; stat.execute("DROP TRIGGER IF EXISTS INS_BEFORE"); stat.execute("DROP TRIGGER IF EXISTS INS_BEFORE"); stat.execute("DROP TRIGGER IF EXISTS INS_AFTER_ROLLBACK"); assertThrows(ErrorCode.TRIGGER_NOT_FOUND_1, stat). execute("DROP TRIGGER INS_BEFORE"); stat.execute("DROP TRIGGER INS_AFTER"); stat.execute("DROP TRIGGER UPD_BEFORE"); stat.execute("UPDATE TEST SET NAME=NAME||'-upd-no_trigger'"); stat.execute("INSERT INTO TEST VALUES(100, 'Insert-no_trigger')"); conn.close(); conn = getConnection("trigger"); mustNotCallTrigger = false; conn.close(); } private void checkRows(ResultSet rs, String[] expected) throws SQLException { HashSet<String> set = new HashSet<String>(Arrays.asList(expected)); while (rs.next()) { set.remove(rs.getString(1)); } if (set.size() > 0) { fail("set should be empty: " + set); } } public void fire(Connection conn, Object[] oldRow, Object[] newRow) throws SQLException { if (mustNotCallTrigger) { throw new AssertionError("must not be called now"); } if (conn == null) { throw new AssertionError("connection is null"); } if (triggerName.startsWith("INS_BEFORE")) { newRow[1] = newRow[1] + "-updated"; } else if (triggerName.startsWith("INS_AFTER")) { if (!newRow[1].toString().endsWith("-updated")) { throw new AssertionError("supposed to be updated"); } checkCommit(conn); } else if (triggerName.startsWith("UPD_BEFORE")) { newRow[1] = newRow[1] + "-updated2"; } else if (triggerName.startsWith("UPD_AFTER")) { if (!newRow[1].toString().endsWith("-updated2")) { throw new AssertionError("supposed to be updated2"); } checkCommit(conn); } } public void close() { // ignore } public void remove() { // ignore } private void checkCommit(Connection conn) throws SQLException { assertThrows(ErrorCode.COMMIT_ROLLBACK_NOT_ALLOWED, conn).commit(); assertThrows(ErrorCode.COMMIT_ROLLBACK_NOT_ALLOWED, conn.createStatement()). execute("CREATE TABLE X(ID INT)"); } public void init(Connection conn, String schemaName, String trigger, String tableName, boolean before, int type) { this.triggerName = trigger; if (!"TEST".equals(tableName)) { throw new AssertionError("supposed to be TEST"); } if ((trigger.endsWith("AFTER") && before) || (trigger.endsWith("BEFORE") && !before)) { throw new AssertionError("triggerName: " + trigger + " before:" + before); } if ((trigger.startsWith("UPD") && type != UPDATE) || (trigger.startsWith("INS") && type != INSERT) || (trigger.startsWith("DEL") && type != DELETE)) { throw new AssertionError("triggerName: " + trigger + " type:" + type); } } }