timescaledb/tsl/test/sql/dist_triggers.sql
Erik Nordström 38e6c0aee5 Improve trigger handling on distributed hypertables
Triggers on distributed hypertables can now be renamed due to having
the rename statement forwarded to data nodes. This also applies to
other objects on such tables, like constraints and indexes, since they
share the same DDL "rename" machinery. Tests are added for these
cases.

For convenience, trigger functions on distributed hypertables will now
also be created automatically on the data nodes. In other words, it is
no longer necessary to pre-create the trigger function on all data
nodes.

This change also fixes an issue with statement-level triggers, which
weren't properly propagated to data nodes during `CREATE TRIGGER`.

Fixes #3238
2021-06-16 14:12:04 +02:00

408 lines
12 KiB
PL/PgSQL

-- This file and its contents are licensed under the Timescale License.
-- Please see the included NOTICE for copyright information and
-- LICENSE-TIMESCALE for a copy of the license.
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
\unset ECHO
\o /dev/null
\ir include/filter_exec.sql
\ir include/remote_exec.sql
\o
\set ECHO all
\set DATA_NODE_1 :TEST_DBNAME _1
\set DATA_NODE_2 :TEST_DBNAME _2
\set DATA_NODE_3 :TEST_DBNAME _3
\set TABLESPACE_1 :TEST_DBNAME _1
\set TABLESPACE_2 :TEST_DBNAME _2
SELECT
test.make_tablespace_path(:'TEST_TABLESPACE1_PREFIX', :'TEST_DBNAME') AS spc1path,
test.make_tablespace_path(:'TEST_TABLESPACE2_PREFIX', :'TEST_DBNAME') AS spc2path
\gset
SELECT (add_data_node (name, host => 'localhost', DATABASE => name)).*
FROM (VALUES (:'DATA_NODE_1'), (:'DATA_NODE_2'), (:'DATA_NODE_3')) v (name);
GRANT USAGE ON FOREIGN SERVER :DATA_NODE_1, :DATA_NODE_2, :DATA_NODE_3 TO PUBLIC;
-- Import testsupport.sql file to data nodes
\unset ECHO
\o /dev/null
\c :DATA_NODE_1
SET client_min_messages TO ERROR;
\ir :TEST_SUPPORT_FILE
\c :DATA_NODE_2
SET client_min_messages TO ERROR;
\ir :TEST_SUPPORT_FILE
\c :DATA_NODE_3
SET client_min_messages TO ERROR;
\ir :TEST_SUPPORT_FILE
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
\o
SET client_min_messages TO NOTICE;
\set ECHO all
SET ROLE :ROLE_1;
CREATE TABLE hyper (
time BIGINT NOT NULL,
device_id TEXT NOT NULL,
sensor_1 NUMERIC NULL DEFAULT 1
);
-- Table to log trigger events
CREATE TABLE trigger_events (
tg_when text,
tg_level text,
tg_op text,
tg_name text
);
CREATE OR REPLACE FUNCTION test_trigger()
RETURNS TRIGGER LANGUAGE PLPGSQL AS
$BODY$
BEGIN
INSERT INTO public.trigger_events VALUES (TG_WHEN, TG_LEVEL, TG_OP, TG_NAME);
RETURN NEW;
END
$BODY$;
CALL distributed_exec($$
CREATE TABLE trigger_events (
tg_when text,
tg_level text,
tg_op text,
tg_name text
);
$$, ARRAY[:'DATA_NODE_1']);
-- row triggers: BEFORE
CREATE TRIGGER _0_test_trigger_insert
BEFORE INSERT ON hyper
FOR EACH ROW EXECUTE FUNCTION test_trigger();
CREATE TRIGGER _0_test_trigger_update
BEFORE UPDATE ON hyper
FOR EACH ROW EXECUTE FUNCTION test_trigger();
CREATE TRIGGER _0_test_trigger_delete
BEFORE DELETE ON hyper
FOR EACH ROW EXECUTE FUNCTION test_trigger();
CREATE TRIGGER z_test_trigger_all
BEFORE INSERT OR UPDATE OR DELETE ON hyper
FOR EACH ROW EXECUTE FUNCTION test_trigger();
-- row triggers: AFTER
CREATE TRIGGER _0_test_trigger_insert_after
AFTER INSERT ON hyper
FOR EACH ROW EXECUTE FUNCTION test_trigger();
CREATE TRIGGER _0_test_trigger_insert_after_when_dev1
AFTER INSERT ON hyper
FOR EACH ROW
WHEN (NEW.device_id = 'dev1')
EXECUTE FUNCTION test_trigger();
CREATE TRIGGER _0_test_trigger_update_after
AFTER UPDATE ON hyper
FOR EACH ROW EXECUTE FUNCTION test_trigger();
CREATE TRIGGER _0_test_trigger_delete_after
AFTER DELETE ON hyper
FOR EACH ROW EXECUTE FUNCTION test_trigger();
CREATE TRIGGER z_test_trigger_all_after
AFTER INSERT OR UPDATE OR DELETE ON hyper
FOR EACH ROW EXECUTE FUNCTION test_trigger();
--- Create some triggers before we turn the table into a distributed
--- hypertable and some triggers after so that we test both cases.
SELECT * FROM create_distributed_hypertable('hyper', 'time', 'device_id', 3, chunk_time_interval => 10, data_nodes => ARRAY[:'DATA_NODE_1', :'DATA_NODE_2']);
-- FAILURE cases
\set ON_ERROR_STOP 0
-- Check that CREATE TRIGGER fails if a trigger already exists on a data node.
CALL distributed_exec($$
CREATE TRIGGER _0_test_trigger_insert_s_before
BEFORE INSERT ON hyper
FOR EACH STATEMENT EXECUTE FUNCTION test_trigger();
$$, ARRAY[:'DATA_NODE_1']);
CREATE TRIGGER _0_test_trigger_insert_s_before
BEFORE INSERT ON hyper
FOR EACH STATEMENT EXECUTE FUNCTION test_trigger();
CALL distributed_exec($$
DROP TRIGGER _0_test_trigger_insert_s_before ON hyper;
$$, ARRAY[:'DATA_NODE_1']);
-- Test that trigger execution fails if trigger_events table doesn't
-- exist on all nodes. Insert should fail
INSERT INTO hyper(time, device_id,sensor_1) VALUES
(1257987600000000000, 'dev1', 1);
\set ON_ERROR_STOP 1
-- Now, create trigger_events on the other nodes
CALL distributed_exec($$
CREATE TABLE trigger_events (
tg_when text,
tg_level text,
tg_op text,
tg_name text
);
$$, ARRAY[:'DATA_NODE_2', :'DATA_NODE_3']);
-- Test that trigger fails if the user isn't the owner of the trigger
-- function on one of the nodes.
RESET ROLE;
CALL distributed_exec($$
ALTER FUNCTION test_trigger OWNER TO current_user;
$$, ARRAY[:'DATA_NODE_1']);
SET ROLE :ROLE_1;
\set ON_ERROR_STOP 0
-- Insert should fail since the trigger function on DN1 isn't owned by
-- the user.
INSERT INTO hyper(time, device_id,sensor_1) VALUES
(1257987600000000000, 'dev1', 1);
\set ON_ERROR_STOP 1
-- Reset the owner of the trigger function on DN1 to the non-superuser
RESET ROLE;
CALL distributed_exec('ALTER FUNCTION test_trigger OWNER TO ' || :'ROLE_1', ARRAY[:'DATA_NODE_1']);
SET ROLE :ROLE_1;
-- Add more triggers after the distributed hypertable is created
-- statement triggers: BEFORE
CREATE TRIGGER _0_test_trigger_insert_s_before
BEFORE INSERT ON hyper
FOR EACH STATEMENT EXECUTE FUNCTION test_trigger();
CREATE TRIGGER _0_test_trigger_update_s_before
BEFORE UPDATE ON hyper
FOR EACH STATEMENT EXECUTE FUNCTION test_trigger();
CREATE TRIGGER _0_test_trigger_delete_s_before
BEFORE DELETE ON hyper
FOR EACH STATEMENT EXECUTE FUNCTION test_trigger();
-- statement triggers: AFTER
CREATE TRIGGER _0_test_trigger_insert_s_after
AFTER INSERT ON hyper
FOR EACH STATEMENT EXECUTE FUNCTION test_trigger();
CREATE TRIGGER _0_test_trigger_update_s_after
AFTER UPDATE ON hyper
FOR EACH STATEMENT EXECUTE FUNCTION test_trigger();
CREATE TRIGGER _0_test_trigger_delete_s_after
AFTER DELETE ON hyper
FOR EACH STATEMENT EXECUTE FUNCTION test_trigger();
--test triggers before create_distributed_hypertable
INSERT INTO hyper(time, device_id,sensor_1) VALUES
(1257987600000000000, 'dev1', 1);
-- Show trigger count on access node. Only statement-level triggers
-- fire on the access node.
SELECT tg_when, tg_level, tg_op, tg_name, count(*)
FROM trigger_events
GROUP BY 1,2,3,4
ORDER BY 1,2,3,4;
-- Show trigger counts on data nodes. Both statement-level and
-- row-level triggers fire on the data nodes.
SELECT * FROM test.remote_exec(ARRAY[:'DATA_NODE_1', :'DATA_NODE_2'], $$
SELECT tg_when, tg_level, tg_op, tg_name, count(*)
FROM trigger_events
GROUP BY 1,2,3,4
ORDER BY 1,2,3,4;
$$);
TRUNCATE trigger_events;
CALL distributed_exec($$
TRUNCATE trigger_events;
$$);
INSERT INTO hyper(time, device_id,sensor_1) VALUES
(1257987700000000000, 'dev2', 1), (1257987800000000000, 'dev2', 1);
UPDATE hyper SET sensor_1 = 2;
DELETE FROM hyper;
SELECT tg_when, tg_level, tg_op, tg_name, count(*)
FROM trigger_events
GROUP BY 1,2,3,4
ORDER BY 1,2,3,4;
SELECT * FROM test.remote_exec(ARRAY[:'DATA_NODE_1', :'DATA_NODE_2'], $$
SELECT tg_when, tg_level, tg_op, tg_name, count(*)
FROM trigger_events
GROUP BY 1,2,3,4
ORDER BY 1,2,3,4;
$$);
-- Attach a new data node and show that the hypertable is created on
-- the node, including its triggers.
SELECT attach_data_node(:'DATA_NODE_3', 'hyper');
-- Show that triggers are created on the new data node after attaching
SELECT * FROM test.remote_exec(ARRAY[:'DATA_NODE_3'],
$$
SELECT test.show_triggers('hyper');
$$);
-- Insert data on the new data node to create a chunk and fire
-- triggers.
INSERT INTO hyper(time, device_id,sensor_1) VALUES
(1257987700000000000, 'dev4', 1);
SELECT * FROM test.remote_exec(ARRAY[:'DATA_NODE_3'], $$
SELECT tg_when, tg_level, tg_op, tg_name, count(*)
FROM trigger_events
GROUP BY 1,2,3,4
ORDER BY 1,2,3,4;
$$);
--test drop trigger
DROP TRIGGER _0_test_trigger_insert ON hyper;
DROP TRIGGER _0_test_trigger_insert_s_before ON hyper;
DROP TRIGGER _0_test_trigger_insert_after ON hyper;
DROP TRIGGER _0_test_trigger_insert_s_after ON hyper;
DROP TRIGGER _0_test_trigger_update ON hyper;
DROP TRIGGER _0_test_trigger_update_s_before ON hyper;
DROP TRIGGER _0_test_trigger_update_after ON hyper;
DROP TRIGGER _0_test_trigger_update_s_after ON hyper;
DROP TRIGGER _0_test_trigger_delete ON hyper;
DROP TRIGGER _0_test_trigger_delete_s_before ON hyper;
DROP TRIGGER _0_test_trigger_delete_after ON hyper;
DROP TRIGGER _0_test_trigger_delete_s_after ON hyper;
DROP TRIGGER z_test_trigger_all ON hyper;
DROP TRIGGER z_test_trigger_all_after ON hyper;
DROP TRIGGER _0_test_trigger_insert_after_when_dev1 ON hyper;
-- Triggers are dropped on all data nodes:
SELECT * FROM test.remote_exec(ARRAY[:'DATA_NODE_1', :'DATA_NODE_2', :'DATA_NODE_3'], $$
SELECT st."Child" as chunk_relid, test.show_triggers((st)."Child")
FROM test.show_subtables('hyper') st;
$$);
-- Test triggers that modify tuples and make sure RETURNING is done
-- properly (i.e., the modified tuple is returned).
-- Add serial (autoincrement) and DEFAULT value columns to test that
-- these work with custom insert nodes.
CREATE TABLE disttable(
id serial,
time timestamptz NOT NULL,
device int DEFAULT 100,
temp_c float
);
SELECT * FROM create_distributed_hypertable('disttable', 'time', 'device');
-- Create a datatable to source data from. Add array of composite data
-- type to test switching to text mode below. Arrays include the type
-- Oid when serialized in binary format. Since the Oid of a
-- user-created type can differ across data nodes, such serialization
-- is not safe.
CREATE TABLE datatable (LIKE disttable);
INSERT INTO datatable (id, time, device, temp_c) VALUES
(1, '2017-01-01 06:01', 1, 1),
(2, '2017-01-01 09:11', 3, 2),
(3, '2017-01-01 08:01', 1, 3),
(4, '2017-01-02 08:01', 2, 4),
(5, '2018-07-02 08:01', 87, 5),
(6, '2018-07-01 06:01', 13, 6),
(7, '2018-07-01 09:11', 90, 7),
(8, '2018-07-01 08:01', 29, 8);
CREATE OR REPLACE FUNCTION temp_increment_trigger()
RETURNS TRIGGER LANGUAGE PLPGSQL AS
$BODY$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.temp_c = NEW.temp_c+1.0;
END IF;
RETURN NEW;
END
$BODY$;
-- Add a BEFORE INSERT trigger to see that plan reverts to
-- DataNodeDispatch when using RETURNING
CREATE TRIGGER _0_temp_increment
BEFORE INSERT ON disttable
FOR EACH ROW EXECUTE FUNCTION temp_increment_trigger();
-- Show that the trigger exists on a data node
SELECT test.remote_exec(ARRAY[:'DATA_NODE_3'], $$ SELECT test.show_triggers('disttable') $$);
TRUNCATE disttable;
-- Show EXPLAINs for INSERT first with DataNodeCopy disabled. Should
-- always use DataNodeDispatch
SET timescaledb.enable_distributed_insert_with_copy=false;
-- Without RETURNING
EXPLAIN VERBOSE
INSERT INTO disttable (time, device, temp_c)
SELECT time, device, temp_c FROM datatable;
-- With RETURNING
EXPLAIN VERBOSE
INSERT INTO disttable (time, device, temp_c)
SELECT time, device, temp_c FROM datatable RETURNING *;
-- With DataNodeCopy enabled, should use DataNodeCopy when there's no
-- RETURNING clause, but with RETURNING it should use DataNodeDispatch
-- due to the modifying trigger.
SET timescaledb.enable_distributed_insert_with_copy=true;
-- Without RETURNING
EXPLAIN VERBOSE
INSERT INTO disttable (time, device, temp_c)
SELECT time, device, temp_c FROM datatable;
-- With RETURNING
EXPLAIN VERBOSE
INSERT INTO disttable (time, device, temp_c)
SELECT time, device, temp_c FROM datatable RETURNING *;
-- Do the actual INSERT, but wrap in CTE to ensure ordered output in
-- order to avoid flakiness. The returned rows should have temp_c
-- incremented by the trigger
WITH inserted AS (
INSERT INTO disttable (time, device, temp_c)
SELECT time, device, temp_c FROM datatable RETURNING *
) SELECT * FROM inserted ORDER BY 1;
-- Show that the RETURNING rows are the same as those stored after
-- INSERT. Expect temp_c to be incremented by one compared to the
-- original data.
SELECT di.id, di.time, di.device, di.temp_c AS temp_c, da.temp_c AS temp_c_orig
FROM disttable di, datatable da
WHERE di.id = da.id
ORDER BY 1;
-- Rename a trigger
ALTER TRIGGER _0_temp_increment ON disttable RENAME TO _1_temp_increment;
-- Show that remote chunks have the new trigger name
SELECT * FROM test.remote_exec(NULL, $$
SELECT st."Child" as chunk_relid, test.show_triggers((st)."Child")
FROM test.show_subtables('disttable') st;
$$);
-- Drop the trigger and show that it is dropped on data nodes
DROP TRIGGER _1_temp_increment ON disttable;
SELECT * FROM test.remote_exec(NULL, $$
SELECT st."Child" as chunk_relid, test.show_triggers((st)."Child")
FROM test.show_subtables('disttable') st;
$$);