diff --git a/pkg/ccl/logictestccl/testdata/logic_test/triggers b/pkg/ccl/logictestccl/testdata/logic_test/triggers index 02f00405bb7..7a4757cf274 100644 --- a/pkg/ccl/logictestccl/testdata/logic_test/triggers +++ b/pkg/ccl/logictestccl/testdata/logic_test/triggers @@ -1,3 +1,5 @@ +# LogicTest: !local-mixed-24.1 !local-mixed-24.2 !local-legacy-schema-changer + # ============================================================================== # Trigger functions cannot be directly invoked. # ============================================================================== @@ -452,4 +454,847 @@ $$ statement ok DROP FUNCTION f; +# ============================================================================== +# Test invalid target tables, views, and functions. +# ============================================================================== + +subtest invalid_targets + +statement ok +CREATE TABLE xy (x INT, y INT); + +statement ok +CREATE VIEW v AS SELECT * FROM xy; + +statement ok +CREATE MATERIALIZED VIEW mv AS SELECT * FROM xy; + +statement ok +CREATE FUNCTION f() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + RAISE NOTICE 'foo!'; + RETURN NULL; + END; +$$; + +# Nonexistent table. +statement error pgcode 42P01 pq: relation "nonexistent" does not exist +CREATE TRIGGER foo AFTER INSERT ON nonexistent FOR EACH ROW EXECUTE FUNCTION f(); + +# System tables cannot have triggers. +statement error pgcode 42501 pq: user root does not have TRIGGER privilege on relation jobs +CREATE TRIGGER foo BEFORE UPDATE ON system.jobs EXECUTE FUNCTION f(); + +# Virtual tables cannot have triggers. +statement error pgcode 42501 pq: user root does not have TRIGGER privilege on relation pg_roles +CREATE TRIGGER foo BEFORE UPDATE ON pg_catalog.pg_roles EXECUTE FUNCTION f(); + +# Materialized views cannot have triggers. +statement error pgcode 42809 pq: relation "mv" cannot have triggers\nDETAIL: This operation is not supported for materialized views. +CREATE TRIGGER foo AFTER DELETE ON mv FOR EACH ROW EXECUTE FUNCTION f(); + +# Nonexistent function. +statement error pgcode 42883 pq: unknown function: nonexistent() +CREATE TRIGGER foo BEFORE UPDATE ON xy FOR EACH ROW EXECUTE FUNCTION nonexistent(); + +statement ok +CREATE FUNCTION not_trigger() RETURNS INT LANGUAGE SQL AS $$ SELECT 1 $$; + +# The function must be a trigger function. +statement error pgcode 42P17 pq: function not_trigger must return type trigger +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION not_trigger(); + +# ============================================================================== +# Test invalid trigger options. +# ============================================================================== + +subtest options + +statement error pgcode 42809 pq: "xy" is a table\nDETAIL: Tables cannot have INSTEAD OF triggers. +CREATE TRIGGER foo INSTEAD OF INSERT ON xy EXECUTE FUNCTION f(); + +statement error pgcode 42809 pq: "xy" is a table\nDETAIL: Tables cannot have INSTEAD OF triggers. +CREATE TRIGGER foo INSTEAD OF UPDATE ON xy FOR EACH ROW EXECUTE FUNCTION f(); + +statement error pgcode 42809 pq: "v" is a view\nDETAIL: Views cannot have row-level BEFORE or AFTER triggers. +CREATE TRIGGER foo BEFORE UPDATE ON v FOR EACH ROW EXECUTE FUNCTION f(); + +statement error pgcode 42809 pq: "v" is a view\nDETAIL: Views cannot have row-level BEFORE or AFTER triggers. +CREATE TRIGGER foo AFTER INSERT ON v FOR EACH ROW EXECUTE FUNCTION f(); + +statement error pgcode 42809 pq: "v" is a view\nDETAIL: Views cannot have TRUNCATE triggers. +CREATE TRIGGER foo INSTEAD OF TRUNCATE ON v EXECUTE FUNCTION f(); + +statement error pgcode 0A000 pq: INSTEAD OF triggers must be FOR EACH ROW +CREATE TRIGGER foo INSTEAD OF INSERT ON v FOR EACH STATEMENT EXECUTE FUNCTION f(); + +statement error pgcode 0A000 pq: INSTEAD OF triggers cannot have WHEN conditions +CREATE TRIGGER foo INSTEAD OF INSERT ON v FOR EACH ROW WHEN (1 = 1) EXECUTE FUNCTION f(); + +statement error pgcode 0A000 pq: INSTEAD OF triggers cannot have column lists +CREATE TRIGGER foo INSTEAD OF UPDATE OF x, y ON v FOR EACH ROW EXECUTE FUNCTION f(); + +# Only UPDATE triggers can have column lists. +statement error pgcode 42601 pq: at or near "of": syntax error +CREATE TRIGGER foo BEFORE INSERT OF x, y ON xy FOR EACH ROW EXECUTE FUNCTION f(); + +statement error pgcode 42P17 pq: NEW TABLE can only be specified for an INSERT or UPDATE trigger +CREATE TRIGGER foo AFTER DELETE ON xy REFERENCING NEW TABLE AS nt EXECUTE FUNCTION f(); + +statement error pgcode 42P17 pq: OLD TABLE can only be specified for a DELETE or UPDATE trigger +CREATE TRIGGER foo AFTER INSERT ON xy REFERENCING OLD TABLE AS ot EXECUTE FUNCTION f(); + +statement error pgcode 42601 pq: cannot specify NEW more than once +CREATE TRIGGER foo AFTER UPDATE ON xy REFERENCING NEW TABLE AS nt NEW TABLE AS nt2 EXECUTE FUNCTION f(); + +statement error pgcode 42601 pq: cannot specify OLD more than once +CREATE TRIGGER foo AFTER UPDATE ON xy REFERENCING OLD TABLE AS ot OLD TABLE AS ot2 EXECUTE FUNCTION f(); + +statement error pgcode 0A000 pq: ROW variable naming in the REFERENCING clause is not supported +CREATE TRIGGER foo AFTER UPDATE ON xy REFERENCING OLD ROW AS ot EXECUTE FUNCTION f(); + +statement error pgcode 42P17 pq: OLD TABLE name and NEW TABLE name cannot be the same +CREATE TRIGGER foo AFTER UPDATE ON xy REFERENCING OLD TABLE AS nt NEW TABLE AS nt EXECUTE FUNCTION f(); + +statement error pgcode 42P17 pq: transition table name can only be specified for an AFTER trigger +CREATE TRIGGER foo BEFORE UPDATE ON xy REFERENCING NEW TABLE AS nt EXECUTE FUNCTION f(); + +statement error pgcode 42P17 pq: TRUNCATE triggers cannot specify transition tables +CREATE TRIGGER foo AFTER TRUNCATE ON xy REFERENCING NEW TABLE AS nt EXECUTE FUNCTION f(); + +statement error pgcode 0A000 pq: transition tables cannot be specified for triggers with more than one event +CREATE TRIGGER foo AFTER INSERT OR UPDATE ON xy REFERENCING NEW TABLE AS nt EXECUTE FUNCTION f(); + +statement error pgcode 0A000 pq: transition tables cannot be specified for triggers with column lists +CREATE TRIGGER foo AFTER UPDATE OF x ON xy REFERENCING NEW TABLE AS nt EXECUTE FUNCTION f(); + +# ============================================================================== +# Test invalid trigger WHEN clause. +# ============================================================================== + +subtest when_clause + +# The WHEN clause must be of type BOOL. +statement error pgcode 42804 pq: argument of WHEN must be type bool, not type int +CREATE TRIGGER foo AFTER INSERT ON xy WHEN (1) EXECUTE FUNCTION f(); + +# The WHEN clause cannot reference table columns. +statement error pgcode 42703 pq: column "x" does not exist +CREATE TRIGGER foo AFTER INSERT ON xy WHEN (x = 1) EXECUTE FUNCTION f(); + +# The WHEN clause cannot contain a subquery. +statement error pgcode 0A000 pq: subqueries are not allowed in WHEN +CREATE TRIGGER foo AFTER INSERT ON xy WHEN (SELECT 1) EXECUTE FUNCTION f(); + +statement error pgcode 42P17 pq: statement trigger's WHEN condition cannot reference column values +CREATE TRIGGER foo AFTER INSERT ON xy WHEN (NEW IS NULL) EXECUTE FUNCTION f(); + +statement error pgcode 42P17 pq: statement trigger's WHEN condition cannot reference column values +CREATE TRIGGER foo AFTER INSERT ON xy WHEN (OLD IS NULL) EXECUTE FUNCTION f(); + +statement error pgcode 42P17 pq: DELETE trigger's WHEN condition cannot reference NEW values +CREATE TRIGGER foo AFTER DELETE ON xy FOR EACH ROW WHEN (NEW IS NULL) EXECUTE FUNCTION f(); + +statement error pgcode 42P17 pq: INSERT trigger's WHEN condition cannot reference OLD values +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW WHEN (OLD IS NULL) EXECUTE FUNCTION f(); + +# ============================================================================== +# Test early binding/validation on trigger creation. +# ============================================================================== + +subtest early_binding + +# SQL statements and expressions within a trigger function are lazily validated. +# This means that trigger function creation will catch syntax errors in SQL, but +# not other types of errors. +# +# Case with a nonexistent table. +statement ok +CREATE FUNCTION g() RETURNS TRIGGER AS $$ + BEGIN + INSERT INTO nonexistent VALUES (1, 2); + RETURN NULL; + END; +$$ LANGUAGE PLpgSQL; + +statement error pgcode 42P01 pq: relation "nonexistent" does not exist +CREATE TRIGGER foo BEFORE INSERT ON xy EXECUTE FUNCTION g(); + +statement ok +DROP FUNCTION g; +CREATE FUNCTION g() RETURNS TRIGGER AS $$ + BEGIN + IF (SELECT count(*) FROM nonexistent) > 0 THEN + RETURN NULL; + ELSE + RETURN NEW; + END IF; + END; +$$ LANGUAGE PLpgSQL; + +statement error pgcode 42P01 pq: relation "nonexistent" does not exist +CREATE TRIGGER foo AFTER UPDATE ON xy FOR EACH ROW EXECUTE FUNCTION g(); + +# Case with a nonexistent function reference. +statement ok +DROP FUNCTION g; +CREATE FUNCTION g() RETURNS TRIGGER AS $$ + BEGIN + RAISE NOTICE '%', f_nonexistent(); + RETURN NEW; + END; +$$ LANGUAGE PLpgSQL; + +statement error pgcode 42883 pq: unknown function: f_nonexistent() +CREATE TRIGGER foo AFTER DELETE ON xy EXECUTE FUNCTION g(); + +# Case with a nonexistent type reference. +statement ok +DROP FUNCTION g; +CREATE FUNCTION g() RETURNS TRIGGER AS $$ + BEGIN + RETURN ROW(1, 2)::typ_nonexistent; + END; +$$ LANGUAGE PLpgSQL; + +statement error pgcode 42704 pq: type "typ_nonexistent" does not exist +CREATE TRIGGER foo BEFORE INSERT ON xy EXECUTE FUNCTION g(); + +# Incorrect type in a SQL expression. +statement ok +DROP FUNCTION g; +CREATE FUNCTION g() RETURNS TRIGGER AS $$ + BEGIN + IF 'not a bool' THEN + RETURN NEW; + ELSE + RETURN NULL; + END IF; + END; +$$ LANGUAGE PLpgSQL; + +statement error pgcode 22P02 pq: could not parse "not a bool" as type bool: invalid bool value +CREATE TRIGGER foo AFTER UPDATE ON xy FOR EACH ROW EXECUTE FUNCTION g(); + +# Disallowed SQL statement. +statement ok +DROP FUNCTION g; +CREATE FUNCTION g() RETURNS TRIGGER AS $$ + BEGIN + CREATE TABLE foo (x INT, y INT); + RETURN NEW; + END; +$$ LANGUAGE PLpgSQL; + +statement error pgcode 0A000 pq: unimplemented: CREATE TABLE usage inside a function definition +CREATE TRIGGER foo AFTER DELETE ON xy EXECUTE FUNCTION g(); + +statement ok +DROP FUNCTION g; + +# Incorrect function volatility. +statement ok +CREATE TABLE t (a INT, b INT); +CREATE FUNCTION g() RETURNS TRIGGER LANGUAGE PLpgSQL IMMUTABLE AS $$ + BEGIN + SELECT count(*) FROM t; + RETURN NEW; + END; +$$; + +statement error pgcode 22023 pq: referencing relations is not allowed in immutable function +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION g(); + +statement ok +DROP FUNCTION g(); +DROP TABLE t; + +# ============================================================================== +# Test duplicate and nonexistent triggers as CREATE/DROP targets. +# ============================================================================== + +subtest duplicate_nonexistent + +statement ok +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION f(); + +statement error pgcode 42710 pq: trigger "foo" for relation "xy" already exists +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION f(); + +# It is possible to create another trigger with a different name. +statement ok +CREATE TRIGGER bar AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION f(); + +statement ok +DROP TRIGGER foo ON xy; + +statement ok +DROP TRIGGER bar ON xy; + +# Dropping a nonexistent trigger is an error. +statement error pgcode 42704 pq: trigger "foo" of relation "xy" does not exist +DROP TRIGGER foo ON xy; + +# The IF EXISTS syntax allows dropping a nonexistent trigger without error. +statement ok +DROP TRIGGER IF EXISTS foo ON xy; + +# ============================================================================== +# Test dependency tracking for a relation reference. +# ============================================================================== + +subtest relation_dependency + +statement ok +CREATE TABLE t (a INT, b INT); + +statement ok +CREATE FUNCTION g() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + INSERT INTO t VALUES (1, 2); + RETURN NULL; + END; +$$; + +statement ok +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION g(); + +statement error pgcode 2BP01 pq: cannot drop table t because other objects depend on it +DROP TABLE t; + +statement error pgcode 2BP01 pq: cannot drop function "g" because other objects \(\[test.public.xy\]\) still depend on it +DROP FUNCTION g; + +statement ok +DROP TRIGGER foo ON xy; + +statement ok +DROP TABLE t; + +# Now, the trigger function refers to a nonexistent relation, so the trigger +# cannot be created. +statement error pgcode 42P01 pq: relation "t" does not exist +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION g(); + +statement ok +DROP FUNCTION g; + +# ============================================================================== +# Test dependency tracking for a user-defined type reference. +# ============================================================================== + +subtest type_dependency + +statement ok +CREATE TYPE typ AS (x INT, y INT); + +statement ok +CREATE FUNCTION g() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + DECLARE + a typ; + BEGIN + RETURN a; + END; +$$; + +statement ok +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION g(); + +statement error pgcode 2BP01 pq: cannot drop type "typ" because other objects \(\[test.public.xy\]\) still depend on it +DROP TYPE typ; + +statement error pgcode 2BP01 pq: cannot drop function "g" because other objects \(\[test.public.xy\]\) still depend on it +DROP FUNCTION g; + +statement ok +DROP TRIGGER foo ON xy; + +statement ok +DROP TYPE typ; + +# Now, the trigger function refers to a nonexistent type, so the trigger +# cannot be created. +statement error pgcode 42704 pq: type "typ" does not exist +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION g(); + +statement ok +DROP FUNCTION g; + +# ============================================================================== +# Test dependency tracking for a routine reference. +# ============================================================================== + +subtest routine_dependency + +statement ok +CREATE FUNCTION g() RETURNS INT LANGUAGE SQL AS $$ SELECT 1; $$; + +statement ok +CREATE FUNCTION g2() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + RAISE NOTICE '%', g(); + RETURN NULL; + END; +$$; + +statement ok +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION g2(); + +statement error pgcode 2BP01 pq: cannot drop function "g" because other objects \(\[test.public.xy\]\) still depend on it +DROP FUNCTION g; + +statement ok +DROP TRIGGER foo ON xy; + +statement ok +DROP FUNCTION g; + +# Now, the trigger function refers to a nonexistent routine, so the trigger +# cannot be created. +statement error pgcode 42883 pq: unknown function: g() +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION g2(); + +statement ok +DROP FUNCTION g2; + +# ============================================================================== +# Test renaming referenced objects. +# ============================================================================== + +subtest renaming + +statement ok +CREATE TABLE t (a INT, b INT); + +statement ok +CREATE SEQUENCE s; + +statement ok +CREATE TYPE typ AS (x INT, y INT); + +statement ok +CREATE FUNCTION g() RETURNS INT LANGUAGE SQL AS $$ SELECT 1; $$; + +statement ok +CREATE FUNCTION trigger_func() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + DECLARE + a typ; + BEGIN + RAISE NOTICE '%, %', g(), nextval('s'); + INSERT INTO t VALUES (1, 2); + RETURN a; + END; +$$; + +statement ok +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION trigger_func(); + +# Relations are referenced by name, so renaming the table is not allowed. +statement error pgcode 2BP01 cannot rename relation "t" because view "xy" depends on it +ALTER TABLE t RENAME TO t2; + +# Sequences are remapped to their IDs, so renaming is allowed. +statement ok +ALTER SEQUENCE s RENAME TO s2; + +# Types are remapped to their IDs, so renaming is allowed. +statement ok +ALTER TYPE typ RENAME TO typ2; + +# Routines are referenced by name, so renaming is not allowed. +statement ok +ALTER FUNCTION g RENAME TO g2; + +statement ok +DROP TRIGGER foo ON xy; + +statement ok +DROP FUNCTION trigger_func; +DROP FUNCTION g2; +DROP TYPE typ2; +DROP SEQUENCE s2; +DROP TABLE t; + +# ============================================================================== +# Test privilege checks. +# ============================================================================== + +subtest privileges + +statement ok +CREATE TABLE t (a INT, b INT); + +statement ok +CREATE FUNCTION g() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ BEGIN RETURN NULL; END $$; + +statement ok +REVOKE EXECUTE ON FUNCTION g() FROM PUBLIC; + +user testuser + +# Trigger creation requires the TRIGGER privilege on the target table. +statement error pgcode 42501 pq: user testuser does not have TRIGGER privilege on relation t +CREATE TRIGGER foo AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION g(); + +user root + +statement ok +GRANT TRIGGER ON TABLE t TO testuser; + +user testuser + +# Trigger creation requires the EXECUTE privilege on the trigger function. +statement error pgcode 42501 pq: user testuser does not have EXECUTE privilege on function g +CREATE TRIGGER foo AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION g(); + +user root + +statement ok +GRANT EXECUTE ON FUNCTION g TO testuser; + +user testuser + +# With TRIGGER on the table and EXECUTE on the function, the user can create +# a trigger. The user does not have to own the table or function. +statement ok +CREATE TRIGGER foo AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION g(); + +# The user can only drop the trigger if they own the table. +statement error pgcode 42501 pq: must be owner of relation t +DROP TRIGGER foo ON t; + +user root + +statement ok +ALTER TABLE t OWNER TO testuser; + +statement ok +REVOKE ALL ON TABLE t FROM testuser; +REVOKE ALL ON FUNCTION g FROM testuser; + +user testuser + +# Now the user can drop the trigger, despite having no privileges on either the +# function or the table. +statement ok +DROP TRIGGER foo ON t; + +user root + +statement ok +DROP FUNCTION g; +DROP TABLE t; + +# ============================================================================== +# Test cascading drops with a trigger. +# ============================================================================== + +subtest cascade + +statement ok +CREATE DATABASE db; + +statement ok +USE db; + +statement ok +CREATE TABLE t (a INT, b INT); + +statement ok +CREATE FUNCTION g() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ BEGIN RETURN NULL; END; $$; + +statement ok +CREATE TRIGGER foo AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION g(); + +statement ok +USE test; + +statement ok +DROP DATABASE db CASCADE; + +statement ok +CREATE SCHEMA s; + +statement ok +CREATE TABLE s.t (a INT, b INT); + +statement ok +CREATE FUNCTION s.g() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ BEGIN RETURN NULL; END; $$; + +statement ok +CREATE TRIGGER foo AFTER INSERT ON s.t FOR EACH ROW EXECUTE FUNCTION s.g(); + +statement ok +DROP SCHEMA s CASCADE; + +# ============================================================================== +# Test references across schemas and databases. +# ============================================================================== + +subtest cross_schema_database + +statement ok +CREATE SCHEMA s; + +statement ok +CREATE DATABASE db; + +statement ok +CREATE TABLE s.xy (x INT, y INT); + +statement ok +CREATE FUNCTION s.f() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + RAISE NOTICE 'bar!'; + RETURN NULL; + END; +$$; + +statement ok +CREATE TYPE s.typ AS (x INT, y INT); + +statement ok +USE db; + +statement ok +CREATE TABLE xy (x INT, y INT); + +statement ok +CREATE FUNCTION f() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + RAISE NOTICE 'baz!'; + RETURN NULL; + END; +$$; + +statement ok +CREATE TYPE typ AS (x INT, y INT); + +statement ok +USE test; + +statement ok +CREATE TRIGGER foo AFTER INSERT ON s.xy FOR EACH ROW EXECUTE FUNCTION s.f(); + +statement ok +DROP TRIGGER foo ON s.xy; + +statement ok +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION s.f(); + +statement ok +DROP TRIGGER foo ON xy; + +statement ok +CREATE TRIGGER foo AFTER INSERT ON s.xy FOR EACH ROW EXECUTE FUNCTION f(); + +statement ok +DROP TRIGGER foo ON s.xy; + +statement error pgcode 0A000 pq: cross-database function references not allowed +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION db.public.f(); + +statement error pgcode 0A000 pq: unimplemented: cross-db references not supported +CREATE TRIGGER foo AFTER INSERT ON db.public.xy FOR EACH ROW EXECUTE FUNCTION f(); + +statement error pgcode 0A000 pq: unimplemented: cross-db references not supported +CREATE TRIGGER foo AFTER INSERT ON db.public.xy FOR EACH ROW EXECUTE FUNCTION db.public.f(); + +statement ok +CREATE FUNCTION g() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + INSERT INTO db.xy VALUES (1, 2); + RETURN NULL; + END; +$$; + +statement error pgcode 0A000 pq: dependent relation xy cannot be from another database +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION g(); + +statement ok +DROP FUNCTION g; + +statement ok +CREATE FUNCTION g() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + RAISE NOTICE '%', pg_typeof(ROW(1, 2)::db.typ); + RETURN NULL; + END; +$$; + +statement error pgcode 0A000 pq: cross database type references are not supported: db.public.typ +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION g(); + +statement ok +DROP FUNCTION g; + +statement ok +DROP SCHEMA s CASCADE; + +statement ok +DROP DATABASE db CASCADE; + +# ============================================================================== +# Test cyclical table references. +# ============================================================================== + +subtest cyclical + +statement ok +CREATE TABLE t (a INT, b INT); + +statement ok +CREATE FUNCTION g() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + DECLARE + cnt INT := 0; + BEGIN + SELECT count(*) INTO cnt FROM t; + IF cnt < 10 THEN + INSERT INTO t VALUES (1, 2); + END IF; + RAISE NOTICE 'HERE'; + RETURN NULL; + END; +$$; + +# NOTE: the trigger is both attached to table "t", and references it via the +# trigger function. This should not prevent dropping the trigger or table. +statement ok +CREATE TRIGGER foo AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION g(); + +statement ok +DROP TRIGGER foo ON t; + +statement ok +CREATE TRIGGER foo AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION g(); + +statement ok +DROP TABLE t; + +statement ok +DROP FUNCTION g; + +# ============================================================================== +# Test changing search path. +# ============================================================================== + +subtest search_path + +let $xy_oid +SELECT oid FROM pg_class WHERE relname = 'xy'; + +statement ok +CREATE PROCEDURE show_triggers() LANGUAGE PLpgSQL AS $$ + DECLARE + foo JSON; + name JSON; + body JSON; + curs REFCURSOR; + BEGIN + SELECT + crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', + descriptor, + false + ) INTO foo + FROM + system.descriptor + WHERE id = $xy_oid; + foo := foo->'table'->'triggers'; + OPEN curs FOR SELECT value->'name', value->'funcBody' FROM jsonb_array_elements(foo); + LOOP + FETCH curs INTO name, body; + IF name IS NULL THEN + EXIT; + END IF; + RAISE NOTICE '%->%', name, split_part(split_part(body::TEXT, ' ', 7), ')', 1); + END LOOP; + END; +$$; + +statement ok +CREATE SCHEMA s; + +statement ok +CREATE TABLE t (a INT, b INT); + +statement ok +CREATE TABLE s.t (a INT, b INT); + +statement ok +CREATE FUNCTION g() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + RAISE NOTICE '%', (SELECT max(a) FROM t); + RETURN NULL; + END +$$; + +statement ok +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION g(); + +# The trigger function body stored with the trigger should reference the table +# on the public schema. +query T noticetrace +CALL show_triggers(); +---- +NOTICE: "foo"->test.public.t + +statement ok +SET search_path = s,public; + +statement ok +CREATE TRIGGER bar AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION g(); + +# The first trigger should still reference the table on the public schema, but +# the second should reference the table on schema "s". +query T noticetrace +CALL show_triggers(); +---- +NOTICE: "foo"->test.public.t +NOTICE: "bar"->test.s.t + +# The trigger function is still unqualified. +query TT +SHOW CREATE FUNCTION g; +---- +g CREATE FUNCTION public.g() + RETURNS TRIGGER + VOLATILE + NOT LEAKPROOF + CALLED ON NULL INPUT + LANGUAGE plpgsql + SECURITY INVOKER + AS $$ + BEGIN + RAISE NOTICE '%', (SELECT max(a) FROM t); + RETURN NULL; + END; + $$ + +statement ok +RESET search_path; + +statement ok +DROP TRIGGER foo ON xy; + +statement ok +DROP TRIGGER bar ON xy; + +statement ok +DROP SCHEMA s CASCADE; +DROP TABLE t; +DROP FUNCTION g; + +# ============================================================================== +# Test unsupported syntax. +# ============================================================================== + +subtest unsupported + +statement error pgcode 0A000 pq: unimplemented: CREATE OR REPLACE TRIGGER is not supported +CREATE OR REPLACE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION f(); + +statement error pgcode 0A000 pq: unimplemented: cascade dropping triggers +DROP TRIGGER foo ON xy CASCADE; + subtest end diff --git a/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/BUILD.bazel b/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/BUILD.bazel index 229112f58c0..2d52175c814 100644 --- a/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/BUILD.bazel @@ -9,7 +9,7 @@ go_test( "//pkg/ccl/logictestccl:testdata", # keep ], exec_properties = {"test.Pool": "large"}, - shard_count = 30, + shard_count = 29, tags = ["cpu:1"], deps = [ "//pkg/base", diff --git a/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/generated_test.go b/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/generated_test.go index 31a878a902d..800b90104d4 100644 --- a/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/generated_test.go +++ b/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/generated_test.go @@ -236,13 +236,6 @@ func TestCCLLogic_subject( runCCLLogicTest(t, "subject") } -func TestCCLLogic_triggers( - t *testing.T, -) { - defer leaktest.AfterTest(t)() - runCCLLogicTest(t, "triggers") -} - func TestCCLLogic_udf_params( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/local-mixed-24.1/BUILD.bazel b/pkg/ccl/logictestccl/tests/local-mixed-24.1/BUILD.bazel index dcf119ffe57..3ea21bf8c1e 100644 --- a/pkg/ccl/logictestccl/tests/local-mixed-24.1/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/local-mixed-24.1/BUILD.bazel @@ -9,7 +9,7 @@ go_test( "//pkg/ccl/logictestccl:testdata", # keep ], exec_properties = {"test.Pool": "large"}, - shard_count = 29, + shard_count = 28, tags = ["cpu:1"], deps = [ "//pkg/base", diff --git a/pkg/ccl/logictestccl/tests/local-mixed-24.1/generated_test.go b/pkg/ccl/logictestccl/tests/local-mixed-24.1/generated_test.go index fcfec77090e..6790e3150e8 100644 --- a/pkg/ccl/logictestccl/tests/local-mixed-24.1/generated_test.go +++ b/pkg/ccl/logictestccl/tests/local-mixed-24.1/generated_test.go @@ -236,13 +236,6 @@ func TestCCLLogic_subject( runCCLLogicTest(t, "subject") } -func TestCCLLogic_triggers( - t *testing.T, -) { - defer leaktest.AfterTest(t)() - runCCLLogicTest(t, "triggers") -} - func TestCCLLogic_udf_params( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/local-mixed-24.2/BUILD.bazel b/pkg/ccl/logictestccl/tests/local-mixed-24.2/BUILD.bazel index c856d3edbf0..11838adc6da 100644 --- a/pkg/ccl/logictestccl/tests/local-mixed-24.2/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/local-mixed-24.2/BUILD.bazel @@ -9,7 +9,7 @@ go_test( "//pkg/ccl/logictestccl:testdata", # keep ], exec_properties = {"test.Pool": "large"}, - shard_count = 30, + shard_count = 29, tags = ["cpu:1"], deps = [ "//pkg/base", diff --git a/pkg/ccl/logictestccl/tests/local-mixed-24.2/generated_test.go b/pkg/ccl/logictestccl/tests/local-mixed-24.2/generated_test.go index 9937e90d4e3..0903f53762a 100644 --- a/pkg/ccl/logictestccl/tests/local-mixed-24.2/generated_test.go +++ b/pkg/ccl/logictestccl/tests/local-mixed-24.2/generated_test.go @@ -236,13 +236,6 @@ func TestCCLLogic_subject( runCCLLogicTest(t, "subject") } -func TestCCLLogic_triggers( - t *testing.T, -) { - defer leaktest.AfterTest(t)() - runCCLLogicTest(t, "triggers") -} - func TestCCLLogic_udf_params( t *testing.T, ) { diff --git a/pkg/ccl/schemachangerccl/ccl_generated_test.go b/pkg/ccl/schemachangerccl/ccl_generated_test.go index 9af221c3713..2e255bf84c4 100644 --- a/pkg/ccl/schemachangerccl/ccl_generated_test.go +++ b/pkg/ccl/schemachangerccl/ccl_generated_test.go @@ -50,6 +50,13 @@ func TestBackupRollbacks_ccl_create_index(t *testing.T) { sctest.BackupRollbacks(t, path, MultiRegionTestClusterFactory{}) } +func TestBackupRollbacks_ccl_create_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger" + sctest.BackupRollbacks(t, path, MultiRegionTestClusterFactory{}) +} + func TestBackupRollbacks_ccl_drop_database_multiregion_primary_region(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -71,6 +78,20 @@ func TestBackupRollbacks_ccl_drop_table_multiregion_primary_region(t *testing.T) sctest.BackupRollbacks(t, path, MultiRegionTestClusterFactory{}) } +func TestBackupRollbacks_ccl_drop_table_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger" + sctest.BackupRollbacks(t, path, MultiRegionTestClusterFactory{}) +} + +func TestBackupRollbacks_ccl_drop_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger" + sctest.BackupRollbacks(t, path, MultiRegionTestClusterFactory{}) +} + func TestBackupRollbacksMixedVersion_ccl_alter_index_configure_zone(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -106,6 +127,13 @@ func TestBackupRollbacksMixedVersion_ccl_create_index(t *testing.T) { sctest.BackupRollbacksMixedVersion(t, path, MultiRegionTestClusterFactory{}) } +func TestBackupRollbacksMixedVersion_ccl_create_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger" + sctest.BackupRollbacksMixedVersion(t, path, MultiRegionTestClusterFactory{}) +} + func TestBackupRollbacksMixedVersion_ccl_drop_database_multiregion_primary_region(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -127,6 +155,20 @@ func TestBackupRollbacksMixedVersion_ccl_drop_table_multiregion_primary_region(t sctest.BackupRollbacksMixedVersion(t, path, MultiRegionTestClusterFactory{}) } +func TestBackupRollbacksMixedVersion_ccl_drop_table_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger" + sctest.BackupRollbacksMixedVersion(t, path, MultiRegionTestClusterFactory{}) +} + +func TestBackupRollbacksMixedVersion_ccl_drop_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger" + sctest.BackupRollbacksMixedVersion(t, path, MultiRegionTestClusterFactory{}) +} + func TestBackupSuccess_ccl_alter_index_configure_zone(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -162,6 +204,13 @@ func TestBackupSuccess_ccl_create_index(t *testing.T) { sctest.BackupSuccess(t, path, MultiRegionTestClusterFactory{}) } +func TestBackupSuccess_ccl_create_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger" + sctest.BackupSuccess(t, path, MultiRegionTestClusterFactory{}) +} + func TestBackupSuccess_ccl_drop_database_multiregion_primary_region(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -183,6 +232,20 @@ func TestBackupSuccess_ccl_drop_table_multiregion_primary_region(t *testing.T) { sctest.BackupSuccess(t, path, MultiRegionTestClusterFactory{}) } +func TestBackupSuccess_ccl_drop_table_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger" + sctest.BackupSuccess(t, path, MultiRegionTestClusterFactory{}) +} + +func TestBackupSuccess_ccl_drop_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger" + sctest.BackupSuccess(t, path, MultiRegionTestClusterFactory{}) +} + func TestBackupSuccessMixedVersion_ccl_alter_index_configure_zone(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -218,6 +281,13 @@ func TestBackupSuccessMixedVersion_ccl_create_index(t *testing.T) { sctest.BackupSuccessMixedVersion(t, path, MultiRegionTestClusterFactory{}) } +func TestBackupSuccessMixedVersion_ccl_create_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger" + sctest.BackupSuccessMixedVersion(t, path, MultiRegionTestClusterFactory{}) +} + func TestBackupSuccessMixedVersion_ccl_drop_database_multiregion_primary_region(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -239,6 +309,20 @@ func TestBackupSuccessMixedVersion_ccl_drop_table_multiregion_primary_region(t * sctest.BackupSuccessMixedVersion(t, path, MultiRegionTestClusterFactory{}) } +func TestBackupSuccessMixedVersion_ccl_drop_table_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger" + sctest.BackupSuccessMixedVersion(t, path, MultiRegionTestClusterFactory{}) +} + +func TestBackupSuccessMixedVersion_ccl_drop_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger" + sctest.BackupSuccessMixedVersion(t, path, MultiRegionTestClusterFactory{}) +} + func TestEndToEndSideEffects_ccl_alter_index_configure_zone(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -274,6 +358,13 @@ func TestEndToEndSideEffects_ccl_create_index(t *testing.T) { sctest.EndToEndSideEffects(t, path, MultiRegionTestClusterFactory{}) } +func TestEndToEndSideEffects_ccl_create_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger" + sctest.EndToEndSideEffects(t, path, MultiRegionTestClusterFactory{}) +} + func TestEndToEndSideEffects_ccl_drop_database_multiregion_primary_region(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -295,6 +386,20 @@ func TestEndToEndSideEffects_ccl_drop_table_multiregion_primary_region(t *testin sctest.EndToEndSideEffects(t, path, MultiRegionTestClusterFactory{}) } +func TestEndToEndSideEffects_ccl_drop_table_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger" + sctest.EndToEndSideEffects(t, path, MultiRegionTestClusterFactory{}) +} + +func TestEndToEndSideEffects_ccl_drop_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger" + sctest.EndToEndSideEffects(t, path, MultiRegionTestClusterFactory{}) +} + func TestExecuteWithDMLInjection_ccl_alter_index_configure_zone(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -330,6 +435,13 @@ func TestExecuteWithDMLInjection_ccl_create_index(t *testing.T) { sctest.ExecuteWithDMLInjection(t, path, MultiRegionTestClusterFactory{}) } +func TestExecuteWithDMLInjection_ccl_create_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger" + sctest.ExecuteWithDMLInjection(t, path, MultiRegionTestClusterFactory{}) +} + func TestExecuteWithDMLInjection_ccl_drop_database_multiregion_primary_region(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -351,6 +463,20 @@ func TestExecuteWithDMLInjection_ccl_drop_table_multiregion_primary_region(t *te sctest.ExecuteWithDMLInjection(t, path, MultiRegionTestClusterFactory{}) } +func TestExecuteWithDMLInjection_ccl_drop_table_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger" + sctest.ExecuteWithDMLInjection(t, path, MultiRegionTestClusterFactory{}) +} + +func TestExecuteWithDMLInjection_ccl_drop_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger" + sctest.ExecuteWithDMLInjection(t, path, MultiRegionTestClusterFactory{}) +} + func TestGenerateSchemaChangeCorpus_ccl_alter_index_configure_zone(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -386,6 +512,13 @@ func TestGenerateSchemaChangeCorpus_ccl_create_index(t *testing.T) { sctest.GenerateSchemaChangeCorpus(t, path, MultiRegionTestClusterFactory{}) } +func TestGenerateSchemaChangeCorpus_ccl_create_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger" + sctest.GenerateSchemaChangeCorpus(t, path, MultiRegionTestClusterFactory{}) +} + func TestGenerateSchemaChangeCorpus_ccl_drop_database_multiregion_primary_region(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -407,6 +540,20 @@ func TestGenerateSchemaChangeCorpus_ccl_drop_table_multiregion_primary_region(t sctest.GenerateSchemaChangeCorpus(t, path, MultiRegionTestClusterFactory{}) } +func TestGenerateSchemaChangeCorpus_ccl_drop_table_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger" + sctest.GenerateSchemaChangeCorpus(t, path, MultiRegionTestClusterFactory{}) +} + +func TestGenerateSchemaChangeCorpus_ccl_drop_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger" + sctest.GenerateSchemaChangeCorpus(t, path, MultiRegionTestClusterFactory{}) +} + func TestPause_ccl_alter_index_configure_zone(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -442,6 +589,13 @@ func TestPause_ccl_create_index(t *testing.T) { sctest.Pause(t, path, MultiRegionTestClusterFactory{}) } +func TestPause_ccl_create_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger" + sctest.Pause(t, path, MultiRegionTestClusterFactory{}) +} + func TestPause_ccl_drop_database_multiregion_primary_region(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -463,6 +617,20 @@ func TestPause_ccl_drop_table_multiregion_primary_region(t *testing.T) { sctest.Pause(t, path, MultiRegionTestClusterFactory{}) } +func TestPause_ccl_drop_table_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger" + sctest.Pause(t, path, MultiRegionTestClusterFactory{}) +} + +func TestPause_ccl_drop_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger" + sctest.Pause(t, path, MultiRegionTestClusterFactory{}) +} + func TestPauseMixedVersion_ccl_alter_index_configure_zone(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -498,6 +666,13 @@ func TestPauseMixedVersion_ccl_create_index(t *testing.T) { sctest.PauseMixedVersion(t, path, MultiRegionTestClusterFactory{}) } +func TestPauseMixedVersion_ccl_create_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger" + sctest.PauseMixedVersion(t, path, MultiRegionTestClusterFactory{}) +} + func TestPauseMixedVersion_ccl_drop_database_multiregion_primary_region(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -519,6 +694,20 @@ func TestPauseMixedVersion_ccl_drop_table_multiregion_primary_region(t *testing. sctest.PauseMixedVersion(t, path, MultiRegionTestClusterFactory{}) } +func TestPauseMixedVersion_ccl_drop_table_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger" + sctest.PauseMixedVersion(t, path, MultiRegionTestClusterFactory{}) +} + +func TestPauseMixedVersion_ccl_drop_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger" + sctest.PauseMixedVersion(t, path, MultiRegionTestClusterFactory{}) +} + func TestRollback_ccl_alter_index_configure_zone(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -554,6 +743,13 @@ func TestRollback_ccl_create_index(t *testing.T) { sctest.Rollback(t, path, MultiRegionTestClusterFactory{}) } +func TestRollback_ccl_create_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger" + sctest.Rollback(t, path, MultiRegionTestClusterFactory{}) +} + func TestRollback_ccl_drop_database_multiregion_primary_region(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -574,3 +770,17 @@ func TestRollback_ccl_drop_table_multiregion_primary_region(t *testing.T) { const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_multiregion_primary_region" sctest.Rollback(t, path, MultiRegionTestClusterFactory{}) } + +func TestRollback_ccl_drop_table_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger" + sctest.Rollback(t, path, MultiRegionTestClusterFactory{}) +} + +func TestRollback_ccl_drop_trigger(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger" + sctest.Rollback(t, path, MultiRegionTestClusterFactory{}) +} diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.definition b/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.definition new file mode 100644 index 00000000000..3528cd8bbef --- /dev/null +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.definition @@ -0,0 +1,13 @@ +setup +CREATE TABLE defaultdb.t (id INT PRIMARY KEY, name VARCHAR(256), money INT); +CREATE FUNCTION f() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + RAISE NOTICE '%: % -> %', TG_OP, OLD, NEW; + RETURN COALESCE(OLD, NEW); + END; +$$; +---- + +test +CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON defaultdb.t FOR EACH ROW EXECUTE FUNCTION f(); +---- diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.explain b/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.explain new file mode 100644 index 00000000000..88c8c1a919f --- /dev/null +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.explain @@ -0,0 +1,62 @@ +/* setup */ +CREATE TABLE defaultdb.t (id INT PRIMARY KEY, name VARCHAR(256), money INT); +CREATE FUNCTION f() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + RAISE NOTICE '%: % -> %', TG_OP, OLD, NEW; + RETURN COALESCE(OLD, NEW); + END; +$$; + +/* test */ +EXPLAIN (DDL) CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON defaultdb.t FOR EACH ROW EXECUTE FUNCTION f(); +---- +Schema change plan for CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON ‹defaultdb›.‹t› FOR EACH ROW EXECUTE FUNCTION ‹f›(); + ├── StatementPhase + │ └── Stage 1 of 1 in StatementPhase + │ ├── 7 elements transitioning toward PUBLIC + │ │ ├── ABSENT → PUBLIC Trigger:{DescID: 104 (t), TriggerID: 1} + │ │ ├── ABSENT → PUBLIC TriggerName:{DescID: 104 (t), TriggerID: 1} + │ │ ├── ABSENT → PUBLIC TriggerEnabled:{DescID: 104 (t), TriggerID: 1} + │ │ ├── ABSENT → PUBLIC TriggerTiming:{DescID: 104 (t), TriggerID: 1} + │ │ ├── ABSENT → PUBLIC TriggerEvents:{DescID: 104 (t), TriggerID: 1} + │ │ ├── ABSENT → PUBLIC TriggerFunctionCall:{DescID: 104 (t), TriggerID: 1} + │ │ └── ABSENT → PUBLIC TriggerDeps:{DescID: 104 (t), TriggerID: 1} + │ └── 8 Mutation operations + │ ├── AddTrigger {"Trigger":{"TableID":104,"TriggerID":1}} + │ ├── SetTriggerName {"Name":{"Name":"tr","TableID":104,"TriggerID":1}} + │ ├── SetTriggerEnabled {"Enabled":{"Enabled":true,"TableID":104,"TriggerID":1}} + │ ├── SetTriggerTiming {"Timing":{"ActionTime":1,"ForEachRow":true,"TableID":104,"TriggerID":1}} + │ ├── SetTriggerEvents {"Events":{"TableID":104,"TriggerID":1}} + │ ├── SetTriggerFunctionCall {"FunctionCall":{"FuncBody":"BEGIN\nRAISE NOTI...","FuncID":105,"TableID":104,"TriggerID":1}} + │ ├── SetTriggerForwardReferences {"Deps":{"TableID":104,"TriggerID":1}} + │ └── AddTriggerBackReferencesInRoutines {"BackReferencedTableID":104,"BackReferencedTriggerID":1} + └── PreCommitPhase + ├── Stage 1 of 2 in PreCommitPhase + │ ├── 7 elements transitioning toward PUBLIC + │ │ ├── PUBLIC → ABSENT Trigger:{DescID: 104 (t), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerName:{DescID: 104 (t), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerEnabled:{DescID: 104 (t), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerTiming:{DescID: 104 (t), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerEvents:{DescID: 104 (t), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerFunctionCall:{DescID: 104 (t), TriggerID: 1} + │ │ └── PUBLIC → ABSENT TriggerDeps:{DescID: 104 (t), TriggerID: 1} + │ └── 1 Mutation operation + │ └── UndoAllInTxnImmediateMutationOpSideEffects + └── Stage 2 of 2 in PreCommitPhase + ├── 7 elements transitioning toward PUBLIC + │ ├── ABSENT → PUBLIC Trigger:{DescID: 104 (t), TriggerID: 1} + │ ├── ABSENT → PUBLIC TriggerName:{DescID: 104 (t), TriggerID: 1} + │ ├── ABSENT → PUBLIC TriggerEnabled:{DescID: 104 (t), TriggerID: 1} + │ ├── ABSENT → PUBLIC TriggerTiming:{DescID: 104 (t), TriggerID: 1} + │ ├── ABSENT → PUBLIC TriggerEvents:{DescID: 104 (t), TriggerID: 1} + │ ├── ABSENT → PUBLIC TriggerFunctionCall:{DescID: 104 (t), TriggerID: 1} + │ └── ABSENT → PUBLIC TriggerDeps:{DescID: 104 (t), TriggerID: 1} + └── 8 Mutation operations + ├── AddTrigger {"Trigger":{"TableID":104,"TriggerID":1}} + ├── SetTriggerName {"Name":{"Name":"tr","TableID":104,"TriggerID":1}} + ├── SetTriggerEnabled {"Enabled":{"Enabled":true,"TableID":104,"TriggerID":1}} + ├── SetTriggerTiming {"Timing":{"ActionTime":1,"ForEachRow":true,"TableID":104,"TriggerID":1}} + ├── SetTriggerEvents {"Events":{"TableID":104,"TriggerID":1}} + ├── SetTriggerFunctionCall {"FunctionCall":{"FuncBody":"BEGIN\nRAISE NOTI...","FuncID":105,"TableID":104,"TriggerID":1}} + ├── SetTriggerForwardReferences {"Deps":{"TableID":104,"TriggerID":1}} + └── AddTriggerBackReferencesInRoutines {"BackReferencedTableID":104,"BackReferencedTriggerID":1} diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.explain_shape b/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.explain_shape new file mode 100644 index 00000000000..266bcb765b2 --- /dev/null +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.explain_shape @@ -0,0 +1,14 @@ +/* setup */ +CREATE TABLE defaultdb.t (id INT PRIMARY KEY, name VARCHAR(256), money INT); +CREATE FUNCTION f() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + RAISE NOTICE '%: % -> %', TG_OP, OLD, NEW; + RETURN COALESCE(OLD, NEW); + END; +$$; + +/* test */ +EXPLAIN (DDL, SHAPE) CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON defaultdb.t FOR EACH ROW EXECUTE FUNCTION f(); +---- +Schema change plan for CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON ‹defaultdb›.‹t› FOR EACH ROW EXECUTE FUNCTION ‹f›(); + └── execute 1 system table mutations transaction diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.side_effects b/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.side_effects new file mode 100644 index 00000000000..d65425d0c5e --- /dev/null +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.side_effects @@ -0,0 +1,129 @@ +/* setup */ +CREATE TABLE defaultdb.t (id INT PRIMARY KEY, name VARCHAR(256), money INT); +CREATE FUNCTION f() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + RAISE NOTICE '%: % -> %', TG_OP, OLD, NEW; + RETURN COALESCE(OLD, NEW); + END; +$$; +---- +... ++object {100 101 t} -> 104 + +/* test */ +CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON defaultdb.t FOR EACH ROW EXECUTE FUNCTION f(); +---- +begin transaction #1 +# begin StatementPhase +checking for feature: CREATE TRIGGER +increment telemetry for sql.schema.create_trigger +## StatementPhase stage 1 of 1 with 8 MutationType ops +upsert descriptor #104 + ... + nextIndexId: 2 + nextMutationId: 1 + + nextTriggerId: 2 + parentId: 100 + primaryIndex: + ... + replacementOf: + time: {} + + triggers: + + - actionTime: BEFORE + + dependsOn: [] + + dependsOnRoutines: + + - 105 + + enabled: true + + events: + + - columnNames: [] + + type: INSERT + + - columnNames: [] + + type: UPDATE + + - columnNames: [] + + type: DELETE + + forEachRow: true + + funcArgs: [] + + funcBody: | + + BEGIN + + RAISE NOTICE '%: % -> %', tg_op, old, new; + + RETURN COALESCE(old, new); + + END; + + funcId: 105 + + id: 1 + + name: tr + unexposedParentSchemaId: 101 + - version: "1" + + version: "2" +upsert descriptor #105 + function: + + dependedOnBy: + + - id: 104 + + triggerIds: + + - 1 + functionBody: | + BEGIN + ... + family: TriggerFamily + oid: 2279 + - version: "1" + + version: "2" + volatility: VOLATILE +# end StatementPhase +# begin PreCommitPhase +## PreCommitPhase stage 1 of 2 with 1 MutationType op +undo all catalog changes within txn #1 +persist all catalog changes to storage +## PreCommitPhase stage 2 of 2 with 8 MutationType ops +upsert descriptor #104 + ... + nextIndexId: 2 + nextMutationId: 1 + + nextTriggerId: 2 + parentId: 100 + primaryIndex: + ... + replacementOf: + time: {} + + triggers: + + - actionTime: BEFORE + + dependsOn: [] + + dependsOnRoutines: + + - 105 + + enabled: true + + events: + + - columnNames: [] + + type: INSERT + + - columnNames: [] + + type: UPDATE + + - columnNames: [] + + type: DELETE + + forEachRow: true + + funcArgs: [] + + funcBody: | + + BEGIN + + RAISE NOTICE '%: % -> %', tg_op, old, new; + + RETURN COALESCE(old, new); + + END; + + funcId: 105 + + id: 1 + + name: tr + unexposedParentSchemaId: 101 + - version: "1" + + version: "2" +upsert descriptor #105 + function: + + dependedOnBy: + + - id: 104 + + triggerIds: + + - 1 + functionBody: | + BEGIN + ... + family: TriggerFamily + oid: 2279 + - version: "1" + + version: "2" + volatility: VOLATILE +persist all catalog changes to storage +# end PreCommitPhase +commit transaction #1 diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.definition b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.definition new file mode 100644 index 00000000000..be0112d1cb1 --- /dev/null +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.definition @@ -0,0 +1,17 @@ +setup +CREATE TABLE defaultdb.t (id INT PRIMARY KEY, name VARCHAR(256), money INT); +CREATE FUNCTION f() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + RAISE NOTICE '%: % -> %', TG_OP, OLD, NEW; + RETURN COALESCE(OLD, NEW); + END; +$$; +---- + +setup +CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON defaultdb.t FOR EACH ROW EXECUTE FUNCTION f(); +---- + +test +DROP TABLE defaultdb.t +---- diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.explain b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.explain new file mode 100644 index 00000000000..ef5bfa78ba6 --- /dev/null +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.explain @@ -0,0 +1,259 @@ +/* setup */ +CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON defaultdb.t FOR EACH ROW EXECUTE FUNCTION f(); + +/* test */ +EXPLAIN (DDL) DROP TABLE defaultdb.t; +---- +Schema change plan for DROP TABLE ‹defaultdb›.‹public›.‹t›; + ├── StatementPhase + │ └── Stage 1 of 1 in StatementPhase + │ ├── 41 elements transitioning toward ABSENT + │ │ ├── PUBLIC → ABSENT Namespace:{DescID: 104 (t-), Name: "t", ReferencedDescID: 100 (defaultdb)} + │ │ ├── PUBLIC → ABSENT Owner:{DescID: 104 (t-)} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 104 (t-), Name: "admin"} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 104 (t-), Name: "root"} + │ │ ├── PUBLIC → DROPPED Table:{DescID: 104 (t-)} + │ │ ├── PUBLIC → ABSENT SchemaChild:{DescID: 104 (t-), ReferencedDescID: 101 (public)} + │ │ ├── PUBLIC → ABSENT ColumnFamily:{DescID: 104 (t-), Name: "primary", ColumnFamilyID: 0 (primary-)} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 1 (id-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "id", ColumnID: 1 (id-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 1 (id-), TypeName: "INT8"} + │ │ ├── PUBLIC → ABSENT ColumnNotNull:{DescID: 104 (t-), ColumnID: 1 (id-), IndexID: 0} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 2 (name-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "name", ColumnID: 2 (name-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 2 (name-), TypeName: "VARCHAR(256)"} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 3 (money-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "money", ColumnID: 3 (money-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 3 (money-), TypeName: "INT8"} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 4294967295 (crdb_internal_mvcc_timestamp-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "crdb_internal_mvcc_timestamp", ColumnID: 4294967295 (crdb_internal_mvcc_timestamp-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 4294967295 (crdb_internal_mvcc_timestamp-), TypeName: "DECIMAL"} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 4294967294 (tableoid-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "tableoid", ColumnID: 4294967294 (tableoid-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 4294967294 (tableoid-), TypeName: "OID"} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 4294967293 (crdb_internal_origin_id-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "crdb_internal_origin_id", ColumnID: 4294967293 (crdb_internal_origin_id-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 4294967293 (crdb_internal_origin_id-), TypeName: "INT4"} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 4294967292 (crdb_internal_origin_timestamp-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "crdb_internal_origin_timestamp", ColumnID: 4294967292 (crdb_internal_origin_timestamp-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 4294967292 (crdb_internal_origin_timestamp-), TypeName: "DECIMAL"} + │ │ ├── PUBLIC → ABSENT IndexColumn:{DescID: 104 (t-), ColumnID: 1 (id-), IndexID: 1 (t_pkey-)} + │ │ ├── PUBLIC → ABSENT IndexColumn:{DescID: 104 (t-), ColumnID: 2 (name-), IndexID: 1 (t_pkey-)} + │ │ ├── PUBLIC → ABSENT IndexColumn:{DescID: 104 (t-), ColumnID: 3 (money-), IndexID: 1 (t_pkey-)} + │ │ ├── PUBLIC → ABSENT PrimaryIndex:{DescID: 104 (t-), IndexID: 1 (t_pkey-), ConstraintID: 1} + │ │ ├── PUBLIC → ABSENT IndexName:{DescID: 104 (t-), Name: "t_pkey", IndexID: 1 (t_pkey-)} + │ │ ├── PUBLIC → ABSENT Trigger:{DescID: 104 (t-), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerName:{DescID: 104 (t-), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerEnabled:{DescID: 104 (t-), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerTiming:{DescID: 104 (t-), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerEvents:{DescID: 104 (t-), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerFunctionCall:{DescID: 104 (t-), TriggerID: 1} + │ │ └── PUBLIC → ABSENT TriggerDeps:{DescID: 104 (t-), TriggerID: 1} + │ └── 51 Mutation operations + │ ├── MarkDescriptorAsDropped {"DescriptorID":104} + │ ├── RemoveObjectParent {"ObjectID":104,"ParentSchemaID":101} + │ ├── MakePublicColumnNotNullValidated {"ColumnID":1,"TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":2,"TableID":104} + │ ├── SetColumnName {"ColumnID":2,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":3,"TableID":104} + │ ├── SetColumnName {"ColumnID":3,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":4294967295,"TableID":104} + │ ├── SetColumnName {"ColumnID":4294967295,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":4294967294,"TableID":104} + │ ├── SetColumnName {"ColumnID":4294967294,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":4294967293,"TableID":104} + │ ├── SetColumnName {"ColumnID":4294967293,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":4294967292,"TableID":104} + │ ├── SetColumnName {"ColumnID":4294967292,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakePublicPrimaryIndexWriteOnly {"IndexID":1,"TableID":104} + │ ├── SetIndexName {"IndexID":1,"Name":"crdb_internal_in...","TableID":104} + │ ├── RemoveTrigger {"Trigger":{"TableID":104,"TriggerID":1}} + │ ├── NotImplementedForPublicObjects {"DescID":104,"ElementType":"scpb.TriggerName"} + │ ├── NotImplementedForPublicObjects {"DescID":104,"ElementType":"scpb.TriggerEnab..."} + │ ├── NotImplementedForPublicObjects {"DescID":104,"ElementType":"scpb.TriggerTimi..."} + │ ├── NotImplementedForPublicObjects {"DescID":104,"ElementType":"scpb.TriggerEven..."} + │ ├── NotImplementedForPublicObjects {"DescID":104,"ElementType":"scpb.TriggerFunc..."} + │ ├── RemoveTriggerBackReferencesInRoutines {"BackReferencedTableID":104,"BackReferencedTriggerID":1} + │ ├── DrainDescriptorName {"Namespace":{"DatabaseID":100,"DescriptorID":104,"Name":"t","SchemaID":101}} + │ ├── NotImplementedForPublicObjects {"DescID":104,"ElementType":"scpb.Owner"} + │ ├── RemoveUserPrivileges {"DescriptorID":104,"User":"admin"} + │ ├── RemoveUserPrivileges {"DescriptorID":104,"User":"root"} + │ ├── RemoveColumnNotNull {"ColumnID":1,"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":2,"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":3,"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":4294967295,"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":4294967294,"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":4294967293,"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":4294967292,"TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":1,"TableID":104} + │ ├── SetColumnName {"ColumnID":1,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":4294967295,"TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":4294967294,"TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":4294967293,"TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":4294967292,"TableID":104} + │ ├── MakeWriteOnlyIndexDeleteOnly {"IndexID":1,"TableID":104} + │ ├── AssertColumnFamilyIsRemoved {"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":1,"TableID":104} + │ ├── RemoveColumnFromIndex {"ColumnID":1,"IndexID":1,"TableID":104} + │ ├── RemoveColumnFromIndex {"ColumnID":2,"IndexID":1,"Kind":2,"TableID":104} + │ ├── RemoveColumnFromIndex {"ColumnID":3,"IndexID":1,"Kind":2,"Ordinal":1,"TableID":104} + │ ├── MakeIndexAbsent {"IndexID":1,"TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":1,"TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":2,"TableID":104} + │ └── MakeDeleteOnlyColumnAbsent {"ColumnID":3,"TableID":104} + ├── PreCommitPhase + │ ├── Stage 1 of 2 in PreCommitPhase + │ │ ├── 41 elements transitioning toward ABSENT + │ │ │ ├── ABSENT → PUBLIC Namespace:{DescID: 104 (t-), Name: "t", ReferencedDescID: 100 (defaultdb)} + │ │ │ ├── ABSENT → PUBLIC Owner:{DescID: 104 (t-)} + │ │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 104 (t-), Name: "admin"} + │ │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 104 (t-), Name: "root"} + │ │ │ ├── DROPPED → PUBLIC Table:{DescID: 104 (t-)} + │ │ │ ├── ABSENT → PUBLIC SchemaChild:{DescID: 104 (t-), ReferencedDescID: 101 (public)} + │ │ │ ├── ABSENT → PUBLIC ColumnFamily:{DescID: 104 (t-), Name: "primary", ColumnFamilyID: 0 (primary-)} + │ │ │ ├── ABSENT → PUBLIC Column:{DescID: 104 (t-), ColumnID: 1 (id-)} + │ │ │ ├── ABSENT → PUBLIC ColumnName:{DescID: 104 (t-), Name: "id", ColumnID: 1 (id-)} + │ │ │ ├── ABSENT → PUBLIC ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 1 (id-), TypeName: "INT8"} + │ │ │ ├── ABSENT → PUBLIC ColumnNotNull:{DescID: 104 (t-), ColumnID: 1 (id-), IndexID: 0} + │ │ │ ├── ABSENT → PUBLIC Column:{DescID: 104 (t-), ColumnID: 2 (name-)} + │ │ │ ├── ABSENT → PUBLIC ColumnName:{DescID: 104 (t-), Name: "name", ColumnID: 2 (name-)} + │ │ │ ├── ABSENT → PUBLIC ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 2 (name-), TypeName: "VARCHAR(256)"} + │ │ │ ├── ABSENT → PUBLIC Column:{DescID: 104 (t-), ColumnID: 3 (money-)} + │ │ │ ├── ABSENT → PUBLIC ColumnName:{DescID: 104 (t-), Name: "money", ColumnID: 3 (money-)} + │ │ │ ├── ABSENT → PUBLIC ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 3 (money-), TypeName: "INT8"} + │ │ │ ├── ABSENT → PUBLIC Column:{DescID: 104 (t-), ColumnID: 4294967295 (crdb_internal_mvcc_timestamp-)} + │ │ │ ├── ABSENT → PUBLIC ColumnName:{DescID: 104 (t-), Name: "crdb_internal_mvcc_timestamp", ColumnID: 4294967295 (crdb_internal_mvcc_timestamp-)} + │ │ │ ├── ABSENT → PUBLIC ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 4294967295 (crdb_internal_mvcc_timestamp-), TypeName: "DECIMAL"} + │ │ │ ├── ABSENT → PUBLIC Column:{DescID: 104 (t-), ColumnID: 4294967294 (tableoid-)} + │ │ │ ├── ABSENT → PUBLIC ColumnName:{DescID: 104 (t-), Name: "tableoid", ColumnID: 4294967294 (tableoid-)} + │ │ │ ├── ABSENT → PUBLIC ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 4294967294 (tableoid-), TypeName: "OID"} + │ │ │ ├── ABSENT → PUBLIC Column:{DescID: 104 (t-), ColumnID: 4294967293 (crdb_internal_origin_id-)} + │ │ │ ├── ABSENT → PUBLIC ColumnName:{DescID: 104 (t-), Name: "crdb_internal_origin_id", ColumnID: 4294967293 (crdb_internal_origin_id-)} + │ │ │ ├── ABSENT → PUBLIC ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 4294967293 (crdb_internal_origin_id-), TypeName: "INT4"} + │ │ │ ├── ABSENT → PUBLIC Column:{DescID: 104 (t-), ColumnID: 4294967292 (crdb_internal_origin_timestamp-)} + │ │ │ ├── ABSENT → PUBLIC ColumnName:{DescID: 104 (t-), Name: "crdb_internal_origin_timestamp", ColumnID: 4294967292 (crdb_internal_origin_timestamp-)} + │ │ │ ├── ABSENT → PUBLIC ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 4294967292 (crdb_internal_origin_timestamp-), TypeName: "DECIMAL"} + │ │ │ ├── ABSENT → PUBLIC IndexColumn:{DescID: 104 (t-), ColumnID: 1 (id-), IndexID: 1 (t_pkey-)} + │ │ │ ├── ABSENT → PUBLIC IndexColumn:{DescID: 104 (t-), ColumnID: 2 (name-), IndexID: 1 (t_pkey-)} + │ │ │ ├── ABSENT → PUBLIC IndexColumn:{DescID: 104 (t-), ColumnID: 3 (money-), IndexID: 1 (t_pkey-)} + │ │ │ ├── ABSENT → PUBLIC PrimaryIndex:{DescID: 104 (t-), IndexID: 1 (t_pkey-), ConstraintID: 1} + │ │ │ ├── ABSENT → PUBLIC IndexName:{DescID: 104 (t-), Name: "t_pkey", IndexID: 1 (t_pkey-)} + │ │ │ ├── ABSENT → PUBLIC Trigger:{DescID: 104 (t-), TriggerID: 1} + │ │ │ ├── ABSENT → PUBLIC TriggerName:{DescID: 104 (t-), TriggerID: 1} + │ │ │ ├── ABSENT → PUBLIC TriggerEnabled:{DescID: 104 (t-), TriggerID: 1} + │ │ │ ├── ABSENT → PUBLIC TriggerTiming:{DescID: 104 (t-), TriggerID: 1} + │ │ │ ├── ABSENT → PUBLIC TriggerEvents:{DescID: 104 (t-), TriggerID: 1} + │ │ │ ├── ABSENT → PUBLIC TriggerFunctionCall:{DescID: 104 (t-), TriggerID: 1} + │ │ │ └── ABSENT → PUBLIC TriggerDeps:{DescID: 104 (t-), TriggerID: 1} + │ │ └── 1 Mutation operation + │ │ └── UndoAllInTxnImmediateMutationOpSideEffects + │ └── Stage 2 of 2 in PreCommitPhase + │ ├── 41 elements transitioning toward ABSENT + │ │ ├── PUBLIC → ABSENT Namespace:{DescID: 104 (t-), Name: "t", ReferencedDescID: 100 (defaultdb)} + │ │ ├── PUBLIC → ABSENT Owner:{DescID: 104 (t-)} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 104 (t-), Name: "admin"} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 104 (t-), Name: "root"} + │ │ ├── PUBLIC → DROPPED Table:{DescID: 104 (t-)} + │ │ ├── PUBLIC → ABSENT SchemaChild:{DescID: 104 (t-), ReferencedDescID: 101 (public)} + │ │ ├── PUBLIC → ABSENT ColumnFamily:{DescID: 104 (t-), Name: "primary", ColumnFamilyID: 0 (primary-)} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 1 (id-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "id", ColumnID: 1 (id-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 1 (id-), TypeName: "INT8"} + │ │ ├── PUBLIC → ABSENT ColumnNotNull:{DescID: 104 (t-), ColumnID: 1 (id-), IndexID: 0} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 2 (name-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "name", ColumnID: 2 (name-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 2 (name-), TypeName: "VARCHAR(256)"} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 3 (money-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "money", ColumnID: 3 (money-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 3 (money-), TypeName: "INT8"} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 4294967295 (crdb_internal_mvcc_timestamp-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "crdb_internal_mvcc_timestamp", ColumnID: 4294967295 (crdb_internal_mvcc_timestamp-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 4294967295 (crdb_internal_mvcc_timestamp-), TypeName: "DECIMAL"} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 4294967294 (tableoid-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "tableoid", ColumnID: 4294967294 (tableoid-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 4294967294 (tableoid-), TypeName: "OID"} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 4294967293 (crdb_internal_origin_id-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "crdb_internal_origin_id", ColumnID: 4294967293 (crdb_internal_origin_id-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 4294967293 (crdb_internal_origin_id-), TypeName: "INT4"} + │ │ ├── PUBLIC → ABSENT Column:{DescID: 104 (t-), ColumnID: 4294967292 (crdb_internal_origin_timestamp-)} + │ │ ├── PUBLIC → ABSENT ColumnName:{DescID: 104 (t-), Name: "crdb_internal_origin_timestamp", ColumnID: 4294967292 (crdb_internal_origin_timestamp-)} + │ │ ├── PUBLIC → ABSENT ColumnType:{DescID: 104 (t-), ColumnFamilyID: 0 (primary-), ColumnID: 4294967292 (crdb_internal_origin_timestamp-), TypeName: "DECIMAL"} + │ │ ├── PUBLIC → ABSENT IndexColumn:{DescID: 104 (t-), ColumnID: 1 (id-), IndexID: 1 (t_pkey-)} + │ │ ├── PUBLIC → ABSENT IndexColumn:{DescID: 104 (t-), ColumnID: 2 (name-), IndexID: 1 (t_pkey-)} + │ │ ├── PUBLIC → ABSENT IndexColumn:{DescID: 104 (t-), ColumnID: 3 (money-), IndexID: 1 (t_pkey-)} + │ │ ├── PUBLIC → ABSENT PrimaryIndex:{DescID: 104 (t-), IndexID: 1 (t_pkey-), ConstraintID: 1} + │ │ ├── PUBLIC → ABSENT IndexName:{DescID: 104 (t-), Name: "t_pkey", IndexID: 1 (t_pkey-)} + │ │ ├── PUBLIC → ABSENT Trigger:{DescID: 104 (t-), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerName:{DescID: 104 (t-), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerEnabled:{DescID: 104 (t-), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerTiming:{DescID: 104 (t-), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerEvents:{DescID: 104 (t-), TriggerID: 1} + │ │ ├── PUBLIC → ABSENT TriggerFunctionCall:{DescID: 104 (t-), TriggerID: 1} + │ │ └── PUBLIC → ABSENT TriggerDeps:{DescID: 104 (t-), TriggerID: 1} + │ └── 54 Mutation operations + │ ├── MarkDescriptorAsDropped {"DescriptorID":104} + │ ├── RemoveObjectParent {"ObjectID":104,"ParentSchemaID":101} + │ ├── MakePublicColumnNotNullValidated {"ColumnID":1,"TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":2,"TableID":104} + │ ├── SetColumnName {"ColumnID":2,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":3,"TableID":104} + │ ├── SetColumnName {"ColumnID":3,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":4294967295,"TableID":104} + │ ├── SetColumnName {"ColumnID":4294967295,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":4294967294,"TableID":104} + │ ├── SetColumnName {"ColumnID":4294967294,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":4294967293,"TableID":104} + │ ├── SetColumnName {"ColumnID":4294967293,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":4294967292,"TableID":104} + │ ├── SetColumnName {"ColumnID":4294967292,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakePublicPrimaryIndexWriteOnly {"IndexID":1,"TableID":104} + │ ├── SetIndexName {"IndexID":1,"Name":"crdb_internal_in...","TableID":104} + │ ├── RemoveTrigger {"Trigger":{"TableID":104,"TriggerID":1}} + │ ├── NotImplementedForPublicObjects {"DescID":104,"ElementType":"scpb.TriggerName"} + │ ├── NotImplementedForPublicObjects {"DescID":104,"ElementType":"scpb.TriggerEnab..."} + │ ├── NotImplementedForPublicObjects {"DescID":104,"ElementType":"scpb.TriggerTimi..."} + │ ├── NotImplementedForPublicObjects {"DescID":104,"ElementType":"scpb.TriggerEven..."} + │ ├── NotImplementedForPublicObjects {"DescID":104,"ElementType":"scpb.TriggerFunc..."} + │ ├── RemoveTriggerBackReferencesInRoutines {"BackReferencedTableID":104,"BackReferencedTriggerID":1} + │ ├── DrainDescriptorName {"Namespace":{"DatabaseID":100,"DescriptorID":104,"Name":"t","SchemaID":101}} + │ ├── NotImplementedForPublicObjects {"DescID":104,"ElementType":"scpb.Owner"} + │ ├── RemoveUserPrivileges {"DescriptorID":104,"User":"admin"} + │ ├── RemoveUserPrivileges {"DescriptorID":104,"User":"root"} + │ ├── RemoveColumnNotNull {"ColumnID":1,"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":2,"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":3,"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":4294967295,"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":4294967294,"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":4294967293,"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":4294967292,"TableID":104} + │ ├── MakePublicColumnWriteOnly {"ColumnID":1,"TableID":104} + │ ├── SetColumnName {"ColumnID":1,"Name":"crdb_internal_co...","TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":4294967295,"TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":4294967294,"TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":4294967293,"TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":4294967292,"TableID":104} + │ ├── MakeWriteOnlyIndexDeleteOnly {"IndexID":1,"TableID":104} + │ ├── AssertColumnFamilyIsRemoved {"TableID":104} + │ ├── MakeWriteOnlyColumnDeleteOnly {"ColumnID":1,"TableID":104} + │ ├── RemoveColumnFromIndex {"ColumnID":1,"IndexID":1,"TableID":104} + │ ├── RemoveColumnFromIndex {"ColumnID":2,"IndexID":1,"Kind":2,"TableID":104} + │ ├── RemoveColumnFromIndex {"ColumnID":3,"IndexID":1,"Kind":2,"Ordinal":1,"TableID":104} + │ ├── MakeIndexAbsent {"IndexID":1,"TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":1,"TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":2,"TableID":104} + │ ├── MakeDeleteOnlyColumnAbsent {"ColumnID":3,"TableID":104} + │ ├── SetJobStateOnDescriptor {"DescriptorID":104,"Initialize":true} + │ ├── SetJobStateOnDescriptor {"DescriptorID":105,"Initialize":true} + │ └── CreateSchemaChangerJob {"NonCancelable":true,"RunningStatus":"PostCommitNonRev..."} + └── PostCommitNonRevertiblePhase + └── Stage 1 of 1 in PostCommitNonRevertiblePhase + ├── 3 elements transitioning toward ABSENT + │ ├── DROPPED → ABSENT Table:{DescID: 104 (t-)} + │ ├── PUBLIC → ABSENT IndexData:{DescID: 104 (t-), IndexID: 1 (t_pkey-)} + │ └── PUBLIC → ABSENT TableData:{DescID: 104 (t-), ReferencedDescID: 100 (defaultdb)} + └── 5 Mutation operations + ├── CreateGCJobForTable {"DatabaseID":100,"TableID":104} + ├── CreateGCJobForIndex {"IndexID":1,"TableID":104} + ├── RemoveJobStateFromDescriptor {"DescriptorID":104} + ├── RemoveJobStateFromDescriptor {"DescriptorID":105} + └── UpdateSchemaChangerJob {"IsNonCancelable":true,"RunningStatus":"all stages compl..."} diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.explain_shape b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.explain_shape new file mode 100644 index 00000000000..d79f1409be4 --- /dev/null +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.explain_shape @@ -0,0 +1,8 @@ +/* setup */ +CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON defaultdb.t FOR EACH ROW EXECUTE FUNCTION f(); + +/* test */ +EXPLAIN (DDL, SHAPE) DROP TABLE defaultdb.t; +---- +Schema change plan for DROP TABLE ‹defaultdb›.‹public›.‹t›; + └── execute 2 system table mutations transactions diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.side_effects b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.side_effects new file mode 100644 index 00000000000..6063d8f962a --- /dev/null +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.side_effects @@ -0,0 +1,210 @@ +/* setup */ +CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON defaultdb.t FOR EACH ROW EXECUTE FUNCTION f(); +---- +... + +/* test */ +DROP TABLE defaultdb.t; +---- +begin transaction #1 +# begin StatementPhase +checking for feature: DROP TABLE +increment telemetry for sql.schema.drop_table +write *eventpb.DropTable to event log: + sql: + descriptorId: 104 + statement: DROP TABLE ‹defaultdb›.‹public›.‹t› + tag: DROP TABLE + user: root + tableName: defaultdb.public.t +## StatementPhase stage 1 of 1 with 51 MutationType ops +delete object namespace entry {100 101 t} -> 104 +upsert descriptor #104 + ... + createAsOfTime: + wallTime: "1640995200000000000" + + dropTime: " + families: + - columnIds: + ... + replacementOf: + time: {} + - triggers: + - - actionTime: BEFORE + - dependsOnRoutines: + - - 105 + - enabled: true + - events: + - - type: INSERT + - - type: UPDATE + - - type: DELETE + - forEachRow: true + - funcBody: | + - BEGIN + - RAISE NOTICE '%: % -> %', tg_op, old, new; + - RETURN COALESCE(old, new); + - END; + - funcId: 105 + - id: 1 + - name: tr + + state: DROP + + triggers: [] + unexposedParentSchemaId: 101 + - version: "2" + + version: "3" +upsert descriptor #105 + function: + - dependedOnBy: + - - id: 104 + - triggerIds: + - - 1 + functionBody: | + BEGIN + ... + family: TriggerFamily + oid: 2279 + - version: "2" + + version: "3" + volatility: VOLATILE +# end StatementPhase +# begin PreCommitPhase +## PreCommitPhase stage 1 of 2 with 1 MutationType op +undo all catalog changes within txn #1 +persist all catalog changes to storage +## PreCommitPhase stage 2 of 2 with 54 MutationType ops +delete object namespace entry {100 101 t} -> 104 +upsert descriptor #104 + ... + createAsOfTime: + wallTime: "1640995200000000000" + + declarativeSchemaChangerState: + + authorization: + + userName: root + + currentStatuses: + + jobId: "1" + + nameMapping: + + id: 104 + + name: t + + relevantStatements: + + - statement: + + redactedStatement: DROP TABLE ‹defaultdb›.‹public›.‹t› + + statement: DROP TABLE defaultdb.t + + statementTag: DROP TABLE + + targetRanks: + + targets: + + dropTime: " + families: + - columnIds: + ... + replacementOf: + time: {} + - triggers: + - - actionTime: BEFORE + - dependsOnRoutines: + - - 105 + - enabled: true + - events: + - - type: INSERT + - - type: UPDATE + - - type: DELETE + - forEachRow: true + - funcBody: | + - BEGIN + - RAISE NOTICE '%: % -> %', tg_op, old, new; + - RETURN COALESCE(old, new); + - END; + - funcId: 105 + - id: 1 + - name: tr + + state: DROP + + triggers: [] + unexposedParentSchemaId: 101 + - version: "2" + + version: "3" +upsert descriptor #105 + function: + - dependedOnBy: + - - id: 104 + - triggerIds: + - - 1 + + declarativeSchemaChangerState: + + authorization: + + userName: root + + jobId: "1" + + nameMapping: + + id: 105 + + name: f + functionBody: | + BEGIN + ... + family: TriggerFamily + oid: 2279 + - version: "2" + + version: "3" + volatility: VOLATILE +persist all catalog changes to storage +create job #1 (non-cancelable: true): "DROP TABLE defaultdb.public.t" + descriptor IDs: [104 105] +# end PreCommitPhase +commit transaction #1 +notified job registry to adopt jobs: [1] +# begin PostCommitPhase +begin transaction #2 +commit transaction #2 +begin transaction #3 +## PostCommitNonRevertiblePhase stage 1 of 1 with 5 MutationType ops +upsert descriptor #104 + ... + createAsOfTime: + wallTime: "1640995200000000000" + - declarativeSchemaChangerState: + - authorization: + - userName: root + - currentStatuses: + - jobId: "1" + - nameMapping: + - id: 104 + - name: t + - relevantStatements: + - - statement: + - redactedStatement: DROP TABLE ‹defaultdb›.‹public›.‹t› + - statement: DROP TABLE defaultdb.t + - statementTag: DROP TABLE + - targetRanks: + - targets: + dropTime: " + families: + ... + triggers: [] + unexposedParentSchemaId: 101 + - version: "3" + + version: "4" +upsert descriptor #105 + function: + - declarativeSchemaChangerState: + - authorization: + - userName: root + - jobId: "1" + - nameMapping: + - id: 105 + - name: f + functionBody: | + BEGIN + ... + family: TriggerFamily + oid: 2279 + - version: "3" + + version: "4" + volatility: VOLATILE +persist all catalog changes to storage +create job #2 (non-cancelable: true): "GC for DROP TABLE defaultdb.public.t" + descriptor IDs: [104] +update progress of schema change job #1: "all stages completed" +set schema change job #1 to non-cancellable +updated schema change job #1 descriptor IDs to [] +write *eventpb.FinishSchemaChange to event log: + sc: + descriptorId: 104 +commit transaction #3 +notified job registry to adopt jobs: [2] +# end PostCommitPhase diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.definition b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.definition new file mode 100644 index 00000000000..6060ff6df87 --- /dev/null +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.definition @@ -0,0 +1,17 @@ +setup +CREATE TABLE defaultdb.t (id INT PRIMARY KEY, name VARCHAR(256), money INT); +CREATE FUNCTION f() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + RAISE NOTICE '%: % -> %', TG_OP, OLD, NEW; + RETURN COALESCE(OLD, NEW); + END; +$$; +---- + +setup +CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON defaultdb.t FOR EACH ROW EXECUTE FUNCTION f(); +---- + +test +DROP TRIGGER tr ON defaultdb.t; +---- diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.explain b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.explain new file mode 100644 index 00000000000..9dd7c2ef020 --- /dev/null +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.explain @@ -0,0 +1,29 @@ +/* setup */ +CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON defaultdb.t FOR EACH ROW EXECUTE FUNCTION f(); + +/* test */ +EXPLAIN (DDL) DROP TRIGGER tr ON defaultdb.t; +---- +Schema change plan for DROP TRIGGER ‹tr› ON ‹defaultdb›.‹t›; + ├── StatementPhase + │ └── Stage 1 of 1 in StatementPhase + │ ├── 2 elements transitioning toward ABSENT + │ │ ├── PUBLIC → ABSENT Trigger:{DescID: 104 (t), TriggerID: 1} + │ │ └── PUBLIC → ABSENT TriggerDeps:{DescID: 104 (t), TriggerID: 1} + │ └── 2 Mutation operations + │ ├── RemoveTrigger {"Trigger":{"TableID":104,"TriggerID":1}} + │ └── RemoveTriggerBackReferencesInRoutines {"BackReferencedTableID":104,"BackReferencedTriggerID":1} + └── PreCommitPhase + ├── Stage 1 of 2 in PreCommitPhase + │ ├── 2 elements transitioning toward ABSENT + │ │ ├── ABSENT → PUBLIC Trigger:{DescID: 104 (t), TriggerID: 1} + │ │ └── ABSENT → PUBLIC TriggerDeps:{DescID: 104 (t), TriggerID: 1} + │ └── 1 Mutation operation + │ └── UndoAllInTxnImmediateMutationOpSideEffects + └── Stage 2 of 2 in PreCommitPhase + ├── 2 elements transitioning toward ABSENT + │ ├── PUBLIC → ABSENT Trigger:{DescID: 104 (t), TriggerID: 1} + │ └── PUBLIC → ABSENT TriggerDeps:{DescID: 104 (t), TriggerID: 1} + └── 2 Mutation operations + ├── RemoveTrigger {"Trigger":{"TableID":104,"TriggerID":1}} + └── RemoveTriggerBackReferencesInRoutines {"BackReferencedTableID":104,"BackReferencedTriggerID":1} diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.explain_shape b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.explain_shape new file mode 100644 index 00000000000..b47ce34f00a --- /dev/null +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.explain_shape @@ -0,0 +1,8 @@ +/* setup */ +CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON defaultdb.t FOR EACH ROW EXECUTE FUNCTION f(); + +/* test */ +EXPLAIN (DDL, SHAPE) DROP TRIGGER tr ON defaultdb.t; +---- +Schema change plan for DROP TRIGGER ‹tr› ON ‹defaultdb›.‹t›; + └── execute 1 system table mutations transaction diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.side_effects b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.side_effects new file mode 100644 index 00000000000..7c32416397e --- /dev/null +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.side_effects @@ -0,0 +1,101 @@ +/* setup */ +CREATE TRIGGER tr BEFORE INSERT OR UPDATE OR DELETE ON defaultdb.t FOR EACH ROW EXECUTE FUNCTION f(); +---- +... + +/* test */ +DROP TRIGGER tr ON defaultdb.t; +---- +begin transaction #1 +# begin StatementPhase +checking for feature: DROP TRIGGER +## StatementPhase stage 1 of 1 with 2 MutationType ops +upsert descriptor #104 + ... + replacementOf: + time: {} + - triggers: + - - actionTime: BEFORE + - dependsOnRoutines: + - - 105 + - enabled: true + - events: + - - type: INSERT + - - type: UPDATE + - - type: DELETE + - forEachRow: true + - funcBody: | + - BEGIN + - RAISE NOTICE '%: % -> %', tg_op, old, new; + - RETURN COALESCE(old, new); + - END; + - funcId: 105 + - id: 1 + - name: tr + + triggers: [] + unexposedParentSchemaId: 101 + - version: "2" + + version: "3" +upsert descriptor #105 + function: + - dependedOnBy: + - - id: 104 + - triggerIds: + - - 1 + functionBody: | + BEGIN + ... + family: TriggerFamily + oid: 2279 + - version: "2" + + version: "3" + volatility: VOLATILE +# end StatementPhase +# begin PreCommitPhase +## PreCommitPhase stage 1 of 2 with 1 MutationType op +undo all catalog changes within txn #1 +persist all catalog changes to storage +## PreCommitPhase stage 2 of 2 with 2 MutationType ops +upsert descriptor #104 + ... + replacementOf: + time: {} + - triggers: + - - actionTime: BEFORE + - dependsOnRoutines: + - - 105 + - enabled: true + - events: + - - type: INSERT + - - type: UPDATE + - - type: DELETE + - forEachRow: true + - funcBody: | + - BEGIN + - RAISE NOTICE '%: % -> %', tg_op, old, new; + - RETURN COALESCE(old, new); + - END; + - funcId: 105 + - id: 1 + - name: tr + + triggers: [] + unexposedParentSchemaId: 101 + - version: "2" + + version: "3" +upsert descriptor #105 + function: + - dependedOnBy: + - - id: 104 + - triggerIds: + - - 1 + functionBody: | + BEGIN + ... + family: TriggerFamily + oid: 2279 + - version: "2" + + version: "3" + volatility: VOLATILE +persist all catalog changes to storage +# end PreCommitPhase +commit transaction #1 diff --git a/pkg/sql/distsql_spec_exec_factory.go b/pkg/sql/distsql_spec_exec_factory.go index 6dff062bcf4..1416edd5ae5 100644 --- a/pkg/sql/distsql_spec_exec_factory.go +++ b/pkg/sql/distsql_spec_exec_factory.go @@ -1069,6 +1069,10 @@ func (e *distSQLSpecExecFactory) ConstructCreateFunction( return nil, unimplemented.NewWithIssue(47473, "experimental opt-driven distsql planning: create function") } +func (e *distSQLSpecExecFactory) ConstructCreateTrigger(_ *tree.CreateTrigger) (exec.Node, error) { + return nil, unimplemented.NewWithIssue(47473, "experimental opt-driven distsql planning: create trigger") +} + func (e *distSQLSpecExecFactory) ConstructSequenceSelect(sequence cat.Sequence) (exec.Node, error) { return nil, unimplemented.NewWithIssue(47473, "experimental opt-driven distsql planning: sequence select") } diff --git a/pkg/sql/opt/cat/BUILD.bazel b/pkg/sql/opt/cat/BUILD.bazel index c4fcbeb8780..19cda59e173 100644 --- a/pkg/sql/opt/cat/BUILD.bazel +++ b/pkg/sql/opt/cat/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "schema.go", "sequence.go", "table.go", + "trigger.go", "utils.go", "view.go", "zone.go", diff --git a/pkg/sql/opt/cat/table.go b/pkg/sql/opt/cat/table.go index 4ad7ce9faa7..a9fa530745c 100644 --- a/pkg/sql/opt/cat/table.go +++ b/pkg/sql/opt/cat/table.go @@ -175,6 +175,12 @@ type Table interface { // IsHypothetical returns true if this is a hypothetical table (used when // searching for index recommendations). IsHypothetical() bool + + // TriggerCount returns the number of triggers present on the table. + TriggerCount() int + + // Trigger returns the ith trigger, where i < TriggerCount. + Trigger(i int) Trigger } // CheckConstraint represents a check constraint on a table. Check constraints diff --git a/pkg/sql/opt/cat/trigger.go b/pkg/sql/opt/cat/trigger.go new file mode 100644 index 00000000000..6cea38e50f2 --- /dev/null +++ b/pkg/sql/opt/cat/trigger.go @@ -0,0 +1,59 @@ +// Copyright 2024 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package cat + +import "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + +// Trigger is an interface to a trigger on a table or view, which executes a +// trigger function in response to a pre-defined mutation of the table. +type Trigger interface { + // Name is the name of the trigger. It is unique within a given table, and + // cannot be qualified. + Name() tree.Name + + // ActionTime defines whether the trigger should be fired before, after, or + // instead of the triggering event. + ActionTime() tree.TriggerActionTime + + // EventCount returns the number of events that the trigger is configured to + // fire on. + EventCount() int + + // Event returns the ith event that the trigger is configured to fire on. + Event(i int) tree.TriggerEvent + + // NewTransitionAlias is the name to be used for the NEW transition table. + // If no alias was specified, the result is the empty string. + NewTransitionAlias() tree.Name + + // OldTransitionAlias is the name to be used for the OLD transition table. + // If no alias was specified, the result is the empty string. + OldTransitionAlias() tree.Name + + // ForEachRow returns true if the trigger function should be invoked once for + // each row affected by the triggering event. If false, the trigger function + // is invoked once per statement. + ForEachRow() bool + + // WhenExpr is the optional filter expression that determines whether the + // trigger function should be invoked. If no WHEN clause was specified, the + // result is the empty string. + WhenExpr() string + + // FuncID is the ID of the function that will be called when the trigger + // fires. + FuncID() StableID + + // FuncArgs is a list of constant string arguments for the trigger function. + FuncArgs() tree.Datums + + // FuncBody is the set of body statements for the trigger function with + // fully-qualified names, resolved when the trigger was created. + FuncBody() string + + // Enabled is true if the trigger is currently enabled. + Enabled() bool +} diff --git a/pkg/sql/opt/cat/view.go b/pkg/sql/opt/cat/view.go index f6e88852d9c..fc5d80f9333 100644 --- a/pkg/sql/opt/cat/view.go +++ b/pkg/sql/opt/cat/view.go @@ -33,6 +33,12 @@ type View interface { // IsSystemView returns true if this view is a system view (like // crdb_internal.ranges). IsSystemView() bool + + // TriggerCount returns the number of triggers present on the view. + TriggerCount() int + + // Trigger returns the ith trigger, where i < TriggerCount. + Trigger(i int) Trigger } // FormatView nicely formats a catalog view using a treeprinter for debugging diff --git a/pkg/sql/opt/exec/execbuilder/relational.go b/pkg/sql/opt/exec/execbuilder/relational.go index dd494134d8a..db6e17ca335 100644 --- a/pkg/sql/opt/exec/execbuilder/relational.go +++ b/pkg/sql/opt/exec/execbuilder/relational.go @@ -280,6 +280,9 @@ func (b *Builder) buildRelational(e memo.RelExpr) (_ execPlan, outputCols colOrd case *memo.CreateFunctionExpr: ep, outputCols, err = b.buildCreateFunction(t) + case *memo.CreateTriggerExpr: + ep, outputCols, err = b.buildCreateTrigger(t) + case *memo.WithExpr: ep, outputCols, err = b.buildWith(t) diff --git a/pkg/sql/opt/exec/execbuilder/statement.go b/pkg/sql/opt/exec/execbuilder/statement.go index c0c9a91e3b1..c7f25a9a993 100644 --- a/pkg/sql/opt/exec/execbuilder/statement.go +++ b/pkg/sql/opt/exec/execbuilder/statement.go @@ -84,6 +84,13 @@ func (b *Builder) buildCreateFunction( return execPlan{root: root}, colOrdMap{}, err } +func (b *Builder) buildCreateTrigger( + ct *memo.CreateTriggerExpr, +) (_ execPlan, outputCols colOrdMap, err error) { + root, err := b.factory.ConstructCreateTrigger(ct.Syntax) + return execPlan{root: root}, colOrdMap{}, err +} + func (b *Builder) buildExplainOpt( explain *memo.ExplainExpr, ) (_ execPlan, outputCols colOrdMap, err error) { diff --git a/pkg/sql/opt/exec/explain/emit.go b/pkg/sql/opt/exec/explain/emit.go index 92b8e2035fa..b8c371fa508 100644 --- a/pkg/sql/opt/exec/explain/emit.go +++ b/pkg/sql/opt/exec/explain/emit.go @@ -335,6 +335,7 @@ var nodeNames = [...]string{ createFunctionOp: "create function", createTableOp: "create table", createTableAsOp: "create table as", + createTriggerOp: "create trigger", createViewOp: "create view", deleteOp: "delete", deleteRangeOp: "delete range", @@ -1035,6 +1036,7 @@ func (e *emitter) emitNodeAttributes(n *Node) error { createFunctionOp, createTableOp, createTableAsOp, + createTriggerOp, createViewOp, sequenceSelectOp, saveTableOp, diff --git a/pkg/sql/opt/exec/explain/plan_gist_factory.go b/pkg/sql/opt/exec/explain/plan_gist_factory.go index 12201f3773d..1763818ca53 100644 --- a/pkg/sql/opt/exec/explain/plan_gist_factory.go +++ b/pkg/sql/opt/exec/explain/plan_gist_factory.go @@ -31,7 +31,7 @@ import ( ) func init() { - if numOperators != 62 { + if numOperators != 63 { // This error occurs when an operator has been added or removed in // pkg/sql/opt/exec/explain/factory.opt. If an operator is added at the // end of factory.opt, simply adjust the hardcoded value above. If an @@ -623,6 +623,16 @@ func (u *unknownTable) IsHypothetical() bool { return false } +// TriggerCount is part of the cat.Table interface. +func (u *unknownTable) TriggerCount() int { + return 0 +} + +// Trigger is part of the cat.Table interface. +func (u *unknownTable) Trigger(i int) cat.Trigger { + panic(errors.AssertionFailedf("not implemented")) +} + var _ cat.Table = &unknownTable{} // unknownTable implements the cat.Index interface and is used to represent diff --git a/pkg/sql/opt/exec/explain/result_columns.go b/pkg/sql/opt/exec/explain/result_columns.go index 995579b15e7..5c00a5139fc 100644 --- a/pkg/sql/opt/exec/explain/result_columns.go +++ b/pkg/sql/opt/exec/explain/result_columns.go @@ -229,7 +229,7 @@ func getResultColumns( case createTableOp, createTableAsOp, createViewOp, controlJobsOp, controlSchedulesOp, cancelQueriesOp, cancelSessionsOp, createStatisticsOp, errorIfRowsOp, deleteRangeOp, - createFunctionOp, callOp: + createFunctionOp, createTriggerOp, callOp: // These operations produce no columns. return nil, nil diff --git a/pkg/sql/opt/exec/factory.opt b/pkg/sql/opt/exec/factory.opt index 7d323cbbfa1..bbe989992bb 100644 --- a/pkg/sql/opt/exec/factory.opt +++ b/pkg/sql/opt/exec/factory.opt @@ -794,3 +794,8 @@ define ShowCompletions { define Call { Proc *tree.RoutineExpr } + +# CreateTrigger implements CREATE TRIGGER. +define CreateTrigger { + Ct *tree.CreateTrigger +} diff --git a/pkg/sql/opt/memo/expr_format.go b/pkg/sql/opt/memo/expr_format.go index 11df8256671..286f3bc4874 100644 --- a/pkg/sql/opt/memo/expr_format.go +++ b/pkg/sql/opt/memo/expr_format.go @@ -754,6 +754,10 @@ func (f *ExprFmtCtx) formatRelational(e RelExpr, tp treeprinter.Node) { tp.Child(tree.AsStringWithFlags(t.Syntax, fmtFlags)) f.formatDependencies(tp, t.Deps, t.TypeDeps) + case *CreateTriggerExpr: + tp.Child(t.Syntax.String()) + f.formatDependencies(tp, t.Deps, t.TypeDeps) + case *CreateStatisticsExpr: tp.Child(t.Syntax.String()) diff --git a/pkg/sql/opt/memo/logical_props_builder.go b/pkg/sql/opt/memo/logical_props_builder.go index 3934bd75a8b..ad6c691c7cf 100644 --- a/pkg/sql/opt/memo/logical_props_builder.go +++ b/pkg/sql/opt/memo/logical_props_builder.go @@ -1610,6 +1610,12 @@ func (b *logicalPropsBuilder) buildCreateFunctionProps( BuildSharedProps(cf, &rel.Shared, b.evalCtx) } +func (b *logicalPropsBuilder) buildCreateTriggerProps( + ct *CreateTriggerExpr, rel *props.Relational, +) { + BuildSharedProps(ct, &rel.Shared, b.evalCtx) +} + func (b *logicalPropsBuilder) buildFiltersItemProps(item *FiltersItem, scalar *props.Scalar) { BuildSharedProps(item.Condition, &scalar.Shared, b.evalCtx) diff --git a/pkg/sql/opt/ops/statement.opt b/pkg/sql/opt/ops/statement.opt index 90741649457..d471bc1a613 100644 --- a/pkg/sql/opt/ops/statement.opt +++ b/pkg/sql/opt/ops/statement.opt @@ -84,6 +84,27 @@ define CreateFunctionPrivate { FuncDeps SchemaFunctionDeps } +# CreateTrigger represents a CREATE TRIGGER statement. +[Relational, DDL, Mutation] +define CreateTrigger { + _ CreateTriggerPrivate +} + +[Private] +define CreateTriggerPrivate { + # Syntax is the CREATE TRIGGER AST node. + Syntax CreateTrigger + + # Deps contains the data source dependencies of the trigger. + Deps SchemaDeps + + # TypeDeps contains the type dependencies of the trigger. + TypeDeps SchemaTypeDeps + + # FuncDeps contains the function dependencies of the trigger. + FuncDeps SchemaFunctionDeps +} + # Explain returns information about the execution plan of the "input" # expression. [Relational] diff --git a/pkg/sql/opt/optbuilder/builder.go b/pkg/sql/opt/optbuilder/builder.go index 64e45821ac9..b2627ea1026 100644 --- a/pkg/sql/opt/optbuilder/builder.go +++ b/pkg/sql/opt/optbuilder/builder.go @@ -138,6 +138,10 @@ type Builder struct { // are disabled and only statements whitelisted are allowed. insideFuncDef bool + // If set, we are processing a trigger definition; in this case catalog caches + // are disabled. + insideTriggerDef bool + // insideUDF is true when the current expressions are being built within a // UDF. insideUDF bool diff --git a/pkg/sql/opt/optbuilder/create_function.go b/pkg/sql/opt/optbuilder/create_function.go index a29821f62e7..62f165e9906 100644 --- a/pkg/sql/opt/optbuilder/create_function.go +++ b/pkg/sql/opt/optbuilder/create_function.go @@ -522,8 +522,8 @@ func (b *Builder) buildCreateFunction(cf *tree.CreateRoutine, inScope *scope) (o // function creation, when the type of the NEW and OLD variables is not yet // known. var createTriggerFuncParams = append([]routineParam{ - {name: "new", typ: types.Unknown, class: tree.RoutineParamIn}, - {name: "old", typ: types.Unknown, class: tree.RoutineParamIn}, + {name: triggerColNew, typ: types.Unknown, class: tree.RoutineParamIn}, + {name: triggerColOld, typ: types.Unknown, class: tree.RoutineParamIn}, }, triggerFuncStaticParams...) func formatFuncBodyStmt( diff --git a/pkg/sql/opt/optbuilder/create_trigger.go b/pkg/sql/opt/optbuilder/create_trigger.go index eb58d8d9b5e..6cb71234f56 100644 --- a/pkg/sql/opt/optbuilder/create_trigger.go +++ b/pkg/sql/opt/optbuilder/create_trigger.go @@ -6,10 +6,464 @@ package optbuilder import ( + "github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb" + "github.com/cockroachdb/cockroach/pkg/sql/catalog/typedesc" + "github.com/cockroachdb/cockroach/pkg/sql/opt" + "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" + "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" + "github.com/cockroachdb/cockroach/pkg/sql/opt/props" + "github.com/cockroachdb/cockroach/pkg/sql/opt/props/physical" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/cockroach/pkg/sql/plpgsql/parser" + "github.com/cockroachdb/cockroach/pkg/sql/privilege" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/sql/sem/volatility" + "github.com/cockroachdb/cockroach/pkg/sql/types" "github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented" + "github.com/cockroachdb/cockroach/pkg/util/intsets" + "github.com/cockroachdb/errors" ) -func (b *Builder) buildCreateTrigger(cf *tree.CreateTrigger, inScope *scope) (outScope *scope) { - panic(unimplemented.NewWithIssue(126359, "CREATE TRIGGER")) +func (b *Builder) buildCreateTrigger(ct *tree.CreateTrigger, inScope *scope) (outScope *scope) { + b.insideTriggerDef = true + b.trackSchemaDeps = true + // Make sure datasource names are qualified. + b.qualifyDataSourceNamesInAST = true + oldEvalCtxAnn := b.evalCtx.Annotations + oldSemaCtxAnn := b.semaCtx.Annotations + defer func() { + b.insideTriggerDef = false + b.trackSchemaDeps = false + b.schemaDeps = nil + b.schemaTypeDeps = intsets.Fast{} + b.schemaFunctionDeps = intsets.Fast{} + b.qualifyDataSourceNamesInAST = false + b.evalCtx.Annotations = oldEvalCtxAnn + b.semaCtx.Annotations = oldSemaCtxAnn + }() + + // Resolve the table/view and check its privileges. + tn := ct.TableName.ToTableName() + if tn.ExplicitCatalog { + if string(tn.CatalogName) != b.evalCtx.SessionData().Database { + panic(unimplemented.New("CREATE TRIGGER", "cross-db references not supported")) + } + } + ds, _, _ := b.resolveDataSource(&tn, privilege.TRIGGER) + + // Resolve the trigger function and check its privileges. + funcExpr := tree.FuncExpr{Func: tree.ResolvableFunctionReference{FunctionReference: ct.FuncName}} + typedExpr := inScope.resolveType(&funcExpr, types.Any) + f, ok := typedExpr.(*tree.FuncExpr) + if !ok { + panic(errors.AssertionFailedf("%s is not a function", funcExpr.Func.String())) + } + o := f.ResolvedOverload() + if err := b.catalog.CheckExecutionPrivilege(b.ctx, o.Oid, b.checkPrivilegeUser); err != nil { + panic(err) + } + + var allEventTypes tree.TriggerEventTypeSet + for i := range ct.Events { + allEventTypes.Add(ct.Events[i].EventType) + } + + // Validate the CREATE TRIGGER statement. + b.validateCreateTrigger(ct, ds, allEventTypes) + + // Lookup the implicit table type. This must happen after the above checks, + // since virtual/system tables do not have an implicit type. + typeID := typedesc.TableIDToImplicitTypeOID(descpb.ID(ds.ID())) + tableTyp, err := b.semaCtx.TypeResolver.ResolveTypeByOID(b.ctx, typeID) + if err != nil { + panic(err) + } + + // Build and validate the WHEN expression. + if ct.When != nil { + b.buildWhenForTrigger(ct, tableTyp, allEventTypes) + } + + // Build and validate the trigger function body. + // TODO(#128536): pass the qualified function body through the + // CreateTriggerPrivate instead. + ct.FuncBody = b.buildFunctionForTrigger(ct, tableTyp, f) + + // Add the resolved and validated CREATE TRIGGER statement to the memo. + outScope = b.allocScope() + outScope.expr = b.factory.ConstructCreateTrigger( + &memo.CreateTriggerPrivate{ + Syntax: ct, + Deps: b.schemaDeps, + TypeDeps: b.schemaTypeDeps, + FuncDeps: b.schemaFunctionDeps, + }, + ) + return outScope } + +// validateCreateTrigger checks that the CREATE TRIGGER statement is valid. +func (b *Builder) validateCreateTrigger( + ct *tree.CreateTrigger, ds cat.DataSource, allEventTypes tree.TriggerEventTypeSet, +) { + var hasTargetCols bool + for i := range ct.Events { + if len(ct.Events[i].Columns) > 0 { + hasTargetCols = true + break + } + } + + // Check that the target table/view is valid. + switch t := ds.(type) { + case cat.Table: + if t.IsSystemTable() || t.IsVirtualTable() { + panic(pgerror.Newf(pgcode.InsufficientPrivilege, + "permission denied: \"%s\" is a system catalog", t.Name())) + } + if t.IsMaterializedView() { + panic(errors.WithDetail(pgerror.Newf(pgcode.WrongObjectType, + "relation \"%s\" cannot have triggers", t.Name()), + "This operation is not supported for materialized views.")) + } + if ct.ActionTime == tree.TriggerActionTimeInsteadOf { + panic(errors.WithDetail(pgerror.Newf(pgcode.WrongObjectType, + "\"%s\" is a table", t.Name()), + "Tables cannot have INSTEAD OF triggers.")) + } + if !ct.Replace { + for i := 0; i < t.TriggerCount(); i++ { + if t.Trigger(i).Name() == ct.Name { + panic(pgerror.Newf(pgcode.DuplicateObject, + "trigger \"%s\" for relation \"%s\" already exists", ct.Name, t.Name())) + } + } + } + case cat.View: + if t.IsSystemView() { + panic(pgerror.Newf(pgcode.InsufficientPrivilege, + "permission denied: \"%s\" is a system catalog", t.Name())) + } + // Views can only use row-level INSTEAD OF, or statement-level BEFORE or + // AFTER timing. The former is checked below. + if ct.ActionTime != tree.TriggerActionTimeInsteadOf && ct.ForEach == tree.TriggerForEachRow { + panic(errors.WithDetail(pgerror.Newf(pgcode.WrongObjectType, + "\"%s\" is a view", t.Name()), + "Views cannot have row-level BEFORE or AFTER triggers.")) + } + if allEventTypes.Contains(tree.TriggerEventTruncate) { + panic(errors.WithDetail(pgerror.Newf(pgcode.WrongObjectType, + "\"%s\" is a view", t.Name()), + "Views cannot have TRUNCATE triggers.")) + } + if !ct.Replace { + for i := 0; i < t.TriggerCount(); i++ { + if t.Trigger(i).Name() == ct.Name { + panic(pgerror.Newf(pgcode.DuplicateObject, + "trigger \"%s\" for relation \"%s\" already exists", ct.Name, t.Name())) + } + } + } + default: + panic(pgerror.Newf(pgcode.WrongObjectType, "relation \"%s\" cannot have triggers", t.Name())) + } + + // TRUNCATE is not compatible with FOR EACH ROW. + if ct.ForEach == tree.TriggerForEachRow && allEventTypes.Contains(tree.TriggerEventTruncate) { + panic(pgerror.New(pgcode.FeatureNotSupported, + "TRUNCATE FOR EACH ROW triggers are not supported")) + } + + // Validate usage of INSTEAD OF timing. + if ct.ActionTime == tree.TriggerActionTimeInsteadOf { + if ct.ForEach != tree.TriggerForEachRow { + panic(pgerror.New(pgcode.FeatureNotSupported, + "INSTEAD OF triggers must be FOR EACH ROW")) + } + if ct.When != nil { + panic(pgerror.New(pgcode.FeatureNotSupported, + "INSTEAD OF triggers cannot have WHEN conditions")) + } + if hasTargetCols { + panic(pgerror.New(pgcode.FeatureNotSupported, + "INSTEAD OF triggers cannot have column lists")) + } + } + + // Validate usage of transition tables. + if len(ct.Transitions) > 0 { + if ct.ActionTime != tree.TriggerActionTimeAfter { + panic(pgerror.New(pgcode.InvalidObjectDefinition, + "transition table name can only be specified for an AFTER trigger")) + } + if allEventTypes.Contains(tree.TriggerEventTruncate) { + panic(pgerror.New(pgcode.InvalidObjectDefinition, + "TRUNCATE triggers cannot specify transition tables")) + } + if len(ct.Events) > 1 { + panic(pgerror.New(pgcode.FeatureNotSupported, + "transition tables cannot be specified for triggers with more than one event")) + } + if hasTargetCols { + panic(pgerror.New(pgcode.FeatureNotSupported, + "transition tables cannot be specified for triggers with column lists")) + } + } + if len(ct.Transitions) == 2 && ct.Transitions[0].Name == ct.Transitions[1].Name { + panic(pgerror.Newf(pgcode.InvalidObjectDefinition, + "OLD TABLE name and NEW TABLE name cannot be the same")) + } + var sawOld, sawNew bool + for i := range ct.Transitions { + if ct.Transitions[i].IsNew { + if !allEventTypes.Contains(tree.TriggerEventInsert) && + !allEventTypes.Contains(tree.TriggerEventUpdate) { + panic(pgerror.New(pgcode.InvalidObjectDefinition, + "NEW TABLE can only be specified for an INSERT or UPDATE trigger")) + } + if sawNew { + panic(pgerror.Newf(pgcode.Syntax, "cannot specify NEW more than once")) + } + sawNew = true + } else { + if !allEventTypes.Contains(tree.TriggerEventDelete) && + !allEventTypes.Contains(tree.TriggerEventUpdate) { + panic(pgerror.New(pgcode.InvalidObjectDefinition, + "OLD TABLE can only be specified for a DELETE or UPDATE trigger")) + } + if sawOld { + panic(pgerror.Newf(pgcode.Syntax, "cannot specify OLD more than once")) + } + sawOld = true + } + if ct.Transitions[i].IsRow { + // NOTE: Postgres also returns an "unimplemented" error here. + panic(errors.WithHint(pgerror.New(pgcode.FeatureNotSupported, + "ROW variable naming in the REFERENCING clause is not supported"), + "Use OLD TABLE or NEW TABLE for naming transition tables.")) + } + } +} + +// buildWhenForTrigger builds and validates the WHEN clause of a trigger. +func (b *Builder) buildWhenForTrigger( + ct *tree.CreateTrigger, tableTyp *types.T, allEventTypes tree.TriggerEventTypeSet, +) { + // The WHEN clause can reference the OLD and NEW implicit variables, + // although only in specific contexts. The other implicit variables are not + // allowed. + whenScope := b.allocScope() + whenScope.context = exprKindWhen + tup := b.makeAllNullsTuple(tableTyp) + newName, oldName := scopeColName(triggerColNew), scopeColName(triggerColOld) + newCol := b.synthesizeColumn(whenScope, newName, tableTyp, nil /* expr */, tup) + oldCol := b.synthesizeColumn(whenScope, oldName, tableTyp, nil /* expr */, tup) + + // Check that the expression is of type bool. Disallow subqueries inside the + // WHEN clause. + defer b.semaCtx.Properties.Restore(b.semaCtx.Properties) + b.semaCtx.Properties.Require("WHEN", tree.RejectSubqueries) + typedWhen := whenScope.resolveAndRequireType(ct.When, types.Bool) + + // Check for invalid NEW or OLD variable references. Also resolve + // user-defined type and function reference. + var colRefs opt.ColSet + b.buildScalar(typedWhen, whenScope, nil /* outScope */, nil /* outCol */, &colRefs) + if colRefs.Contains(newCol.id) { + if ct.ForEach == tree.TriggerForEachStatement { + panic(pgerror.New(pgcode.InvalidObjectDefinition, + "statement trigger's WHEN condition cannot reference column values")) + } + if allEventTypes.Contains(tree.TriggerEventDelete) { + panic(pgerror.New(pgcode.InvalidObjectDefinition, + "DELETE trigger's WHEN condition cannot reference NEW values")) + } + } + if colRefs.Contains(oldCol.id) { + if ct.ForEach == tree.TriggerForEachStatement { + panic(pgerror.New(pgcode.InvalidObjectDefinition, + "statement trigger's WHEN condition cannot reference column values")) + } + if allEventTypes.Contains(tree.TriggerEventInsert) { + panic(pgerror.New(pgcode.InvalidObjectDefinition, + "INSERT trigger's WHEN condition cannot reference OLD values")) + } + } +} + +// buildFunctionForTrigger builds and validates the trigger function that will +// be executed by the trigger. The validated function body will be serialized +// and returned as a string. +func (b *Builder) buildFunctionForTrigger( + ct *tree.CreateTrigger, tableTyp *types.T, f *tree.FuncExpr, +) string { + b.insideFuncDef = true + defer func() { + b.insideFuncDef = false + }() + o := f.ResolvedOverload() + funcScope := b.allocScope() + if !f.ResolvedType().Identical(types.Trigger) { + panic(pgerror.Newf(pgcode.InvalidObjectDefinition, + "function %s must return type trigger", ct.FuncName)) + } + if o.Language == tree.RoutineLangSQL { + // NOTE: Trigger functions never use SQL. + panic(errors.AssertionFailedf("SQL language not supported for triggers")) + } + // The trigger always references the trigger function. + b.schemaFunctionDeps.Add(int(o.Oid)) + + // The trigger function can reference the NEW and OLD transition relations, + // aliased in the trigger definition. + for _, transition := range ct.Transitions { + // Build a fake relational expression with a column corresponding to each + // column from the table. + outCols, presentation := b.makeColsForLabeledTupleType(tableTyp) + fakeRelPrivate := &memo.FakeRelPrivate{Props: &props.Relational{OutputCols: outCols}} + fakeExpr := b.factory.ConstructFakeRel(fakeRelPrivate) + + // Add the fake relational expression to the memo as a CTE, and make it + // available in the trigger function's scope. + id := b.factory.Memo().NextWithID() + b.factory.Metadata().AddWithBinding(id, fakeExpr) + cte := &cteSource{ + name: tree.AliasClause{Alias: transition.Name}, + cols: presentation, + expr: fakeExpr, + id: id, + mtr: tree.CTEMaterializeAlways, + } + if funcScope.ctes == nil { + funcScope.ctes = make(map[string]*cteSource) + } + funcScope.ctes[string(transition.Name)] = cte + b.addCTE(cte) + } + if len(ct.Transitions) > 0 { + defer func() { + // Reset the CTEs in the builder after the function body is built. + b.ctes = nil + }() + } + + // The trigger function takes a set of implicitly-defined parameters, two of + // which are determined by the table's record type. Add them to the trigger + // function scope. + numStaticParams := len(triggerFuncStaticParams) + triggerFuncParams := make([]routineParam, numStaticParams, numStaticParams+2) + copy(triggerFuncParams, triggerFuncStaticParams) + triggerFuncParams = append(triggerFuncParams, routineParam{name: triggerColNew, typ: tableTyp}) + triggerFuncParams = append(triggerFuncParams, routineParam{name: triggerColOld, typ: tableTyp}) + for i, param := range triggerFuncParams { + paramColName := funcParamColName(param.name, i) + col := b.synthesizeColumn(funcScope, paramColName, param.typ, nil /* expr */, nil /* scalar */) + col.setParamOrd(i) + } + + // Now that the transition relations and table type are known, fully build and + // validate the trigger function's body statements. + // + // We need to disable stable function folding because we want to catch the + // volatility of stable functions. If folded, we only get a scalar and lose + // the volatility. + stmt, err := parser.Parse(o.Body) + if err != nil { + panic(err) + } + b.factory.FoldingControl().TemporarilyDisallowStableFolds(func() { + plBuilder := newPLpgSQLBuilder( + b, ct.FuncName.String(), stmt.AST.Label, nil /* colRefs */, triggerFuncParams, tableTyp, + false /* isProcedure */, true /* buildSQL */, nil, /* outScope */ + ) + funcScope = plBuilder.buildRootBlock(stmt.AST, funcScope, triggerFuncParams) + }) + var vol tree.RoutineVolatility + switch o.Volatility { + case volatility.Leakproof, volatility.Immutable: + vol = tree.RoutineImmutable + case volatility.Stable: + vol = tree.RoutineStable + case volatility.Volatile: + vol = tree.RoutineVolatile + } + checkStmtVolatility(vol, funcScope, stmt) + + // Validate that the result type of the last statement matches the + // return type of the function. + // TODO(mgartner): stmtScope.cols does not describe the result + // columns of the statement. We should use physical.Presentation + // instead. + err = validateReturnType(b.ctx, b.semaCtx, tableTyp, funcScope.cols) + if err != nil { + panic(err) + } + if vol == tree.RoutineImmutable && len(b.schemaDeps) > 0 { + panic( + pgerror.Newf( + pgcode.InvalidParameterValue, + "referencing relations is not allowed in immutable function", + ), + ) + } + + // Return the function body with fully-qualified names. + fmtCtx := tree.NewFmtCtx(tree.FmtSerializable) + fmtCtx.FormatNode(stmt.AST) + return fmtCtx.CloseAndGetString() +} + +// makeAllNullsTuple constructs a tuple with the given type, with all NULL +// elements. +func (b *Builder) makeAllNullsTuple(typ *types.T) opt.ScalarExpr { + if len(typ.TupleContents()) == 0 { + panic(errors.AssertionFailedf("expected nonzero tuple contents")) + } + elems := make(memo.ScalarListExpr, len(typ.TupleContents())) + for i := range elems { + elems[i] = memo.NullSingleton + } + return b.factory.ConstructTuple(elems, typ) +} + +// makeColsForLabeledTupleType adds a column to the metadata for each element of +// the given tuple type. The elements of the tuple type must have labels. The +// set of newly constructed columns is returned, as well as a presentation for +// those columns. +func (b *Builder) makeColsForLabeledTupleType(typ *types.T) (opt.ColSet, physical.Presentation) { + if len(typ.TupleContents()) == 0 { + panic(errors.AssertionFailedf("expected nonzero tuple contents")) + } + if len(typ.TupleLabels()) != len(typ.TupleContents()) { + panic(errors.AssertionFailedf("expected labeled tuple elements")) + } + var cols opt.ColSet + presentation := make(physical.Presentation, len(typ.TupleContents())) + for i, colTyp := range typ.TupleContents() { + colName := typ.TupleLabels()[i] + colID := b.factory.Metadata().AddColumn(colName, colTyp) + cols.Add(colID) + presentation[i] = opt.AliasedColumn{Alias: colName, ID: colID} + } + return cols, presentation +} + +// triggerFuncStaticParams is the set of implicitly-defined parameters for a +// PL/pgSQL trigger function, excluding the NEW and OLD parameters which are +// determined by the table when a trigger is created. +var triggerFuncStaticParams = []routineParam{ + {name: "tg_name", typ: types.Name, class: tree.RoutineParamIn}, + {name: "tg_when", typ: types.String, class: tree.RoutineParamIn}, + {name: "tg_level", typ: types.String, class: tree.RoutineParamIn}, + {name: "tg_op", typ: types.String, class: tree.RoutineParamIn}, + {name: "tg_relid", typ: types.Oid, class: tree.RoutineParamIn}, + {name: "tg_relname", typ: types.Name, class: tree.RoutineParamIn}, + {name: "tg_table_name", typ: types.Name, class: tree.RoutineParamIn}, + {name: "tg_table_schema", typ: types.Name, class: tree.RoutineParamIn}, + {name: "tg_nargs", typ: types.Int, class: tree.RoutineParamIn}, + {name: "tg_argv", typ: types.StringArray, class: tree.RoutineParamIn}, +} + +const triggerColNew = "new" +const triggerColOld = "old" diff --git a/pkg/sql/opt/optbuilder/routine.go b/pkg/sql/opt/optbuilder/routine.go index cb75981cadf..fdabfd76584 100644 --- a/pkg/sql/opt/optbuilder/routine.go +++ b/pkg/sql/opt/optbuilder/routine.go @@ -802,19 +802,3 @@ func (b *Builder) withinNestedPLpgSQLCall(fn func()) { b.insideNestedPLpgSQLCall = true fn() } - -// triggerFuncStaticParams is the set of implicitly-defined parameters for a -// PL/pgSQL trigger function, excluding the NEW and OLD parameters which are -// determined by the table when a trigger is created. -var triggerFuncStaticParams = []routineParam{ - {name: "tg_name", typ: types.Name, class: tree.RoutineParamIn}, - {name: "tg_when", typ: types.String, class: tree.RoutineParamIn}, - {name: "tg_level", typ: types.String, class: tree.RoutineParamIn}, - {name: "tg_op", typ: types.String, class: tree.RoutineParamIn}, - {name: "tg_relid", typ: types.Oid, class: tree.RoutineParamIn}, - {name: "tg_relname", typ: types.Name, class: tree.RoutineParamIn}, - {name: "tg_table_name", typ: types.Name, class: tree.RoutineParamIn}, - {name: "tg_table_schema", typ: types.Name, class: tree.RoutineParamIn}, - {name: "tg_nargs", typ: types.Int, class: tree.RoutineParamIn}, - {name: "tg_argv", typ: types.StringArray, class: tree.RoutineParamIn}, -} diff --git a/pkg/sql/opt/optbuilder/scalar.go b/pkg/sql/opt/optbuilder/scalar.go index 5cbc19aee2b..2d9f91b37bd 100644 --- a/pkg/sql/opt/optbuilder/scalar.go +++ b/pkg/sql/opt/optbuilder/scalar.go @@ -589,7 +589,7 @@ func (b *Builder) buildFunction( var ds cat.DataSource if seqIdentifier.IsByID() { flags := cat.Flags{ - AvoidDescriptorCaches: b.insideViewDef || b.insideFuncDef, + AvoidDescriptorCaches: b.insideViewDef || b.insideFuncDef || b.insideTriggerDef, } ds, _, err = b.catalog.ResolveDataSourceByID(b.ctx, flags, cat.StableID(seqIdentifier.SeqID)) if err != nil { diff --git a/pkg/sql/opt/optbuilder/scope.go b/pkg/sql/opt/optbuilder/scope.go index 8fe4ece1c2a..88a8a33fecb 100644 --- a/pkg/sql/opt/optbuilder/scope.go +++ b/pkg/sql/opt/optbuilder/scope.go @@ -137,6 +137,7 @@ const ( exprKindWhere exprKindWindowFrameStart exprKindWindowFrameEnd + exprKindWhen ) var exprKindName = [...]string{ @@ -160,6 +161,7 @@ var exprKindName = [...]string{ exprKindWhere: "WHERE", exprKindWindowFrameStart: "WINDOW FRAME START", exprKindWindowFrameEnd: "WINDOW FRAME END", + exprKindWhen: "WHEN", } func (k exprKind) String() string { diff --git a/pkg/sql/opt/optbuilder/testdata/create_trigger b/pkg/sql/opt/optbuilder/testdata/create_trigger new file mode 100644 index 00000000000..5f1d88857b2 --- /dev/null +++ b/pkg/sql/opt/optbuilder/testdata/create_trigger @@ -0,0 +1,72 @@ +exec-ddl +CREATE TABLE xy (x INT, y INT); +---- + +exec-ddl +CREATE TABLE ab (a INT, b INT); +---- + +exec-ddl +CREATE TYPE typ AS ENUM ('a', 'b'); +---- + +exec-ddl +CREATE FUNCTION f_basic() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ BEGIN RETURN NULL; END $$; +---- + +# TODO(#128150): Display the dependency on f_basic(). +build +CREATE TRIGGER tr BEFORE INSERT OR UPDATE ON xy FOR EACH ROW EXECUTE FUNCTION f_basic(); +---- +create-trigger + ├── CREATE TRIGGER tr BEFORE INSERT OR UPDATE ON xy FOR EACH ROW EXECUTE FUNCTION f_basic() + └── no dependencies + +build +CREATE TRIGGER foo AFTER DELETE ON xy REFERENCING OLD TABLE AS foo WHEN (1 = 1) EXECUTE FUNCTION f_basic(); +---- +create-trigger + ├── CREATE TRIGGER foo AFTER DELETE ON xy REFERENCING OLD TABLE AS foo FOR EACH STATEMENT WHEN (1 = 1) EXECUTE FUNCTION f_basic() + └── no dependencies + +build +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION f_basic(); +---- +create-trigger + ├── CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION f_basic() + └── no dependencies + +exec-ddl +CREATE FUNCTION f_typ() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + DECLARE + a typ; + BEGIN + RETURN a; + END; +$$; +---- + +build +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION f_typ(); +---- +create-trigger + ├── CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION f_typ() + └── dependencies + └── typ + +exec-ddl +CREATE FUNCTION f_relation() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ + BEGIN + INSERT INTO ab VALUES (1, 2); + RETURN NULL; + END; +$$; +---- + +build +CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION f_relation(); +---- +create-trigger + ├── CREATE TRIGGER foo AFTER INSERT ON xy FOR EACH ROW EXECUTE FUNCTION f_relation() + └── dependencies + └── ab [columns: a b] diff --git a/pkg/sql/opt/optbuilder/util.go b/pkg/sql/opt/optbuilder/util.go index 14288249f19..12f59f3b1b3 100644 --- a/pkg/sql/opt/optbuilder/util.go +++ b/pkg/sql/opt/optbuilder/util.go @@ -657,7 +657,7 @@ func (b *Builder) resolveDataSource( tn *tree.TableName, priv privilege.Kind, ) (cat.DataSource, opt.MDDepName, cat.DataSourceName) { var flags cat.Flags - if b.insideViewDef || b.insideFuncDef { + if b.insideViewDef || b.insideFuncDef || b.insideTriggerDef { // Avoid taking descriptor leases when we're creating a view or a // function. flags.AvoidDescriptorCaches = true @@ -685,7 +685,7 @@ func (b *Builder) resolveDataSourceRef( ref *tree.TableRef, priv privilege.Kind, ) (cat.DataSource, opt.MDDepName) { var flags cat.Flags - if b.insideViewDef || b.insideFuncDef { + if b.insideViewDef || b.insideFuncDef || b.insideTriggerDef { // Avoid taking table leases when we're creating a view or a function. flags.AvoidDescriptorCaches = true } diff --git a/pkg/sql/opt/optgen/cmd/optgen/metadata.go b/pkg/sql/opt/optgen/cmd/optgen/metadata.go index 7bf5a19b1d2..4d0c89f2abb 100644 --- a/pkg/sql/opt/optgen/cmd/optgen/metadata.go +++ b/pkg/sql/opt/optgen/cmd/optgen/metadata.go @@ -218,6 +218,7 @@ func newMetadata(compiled *lang.CompiledExpr, pkg string) *metadata { "Subquery": {fullName: "tree.Subquery", isPointer: true, usePointerIntern: true}, "CreateTable": {fullName: "tree.CreateTable", isPointer: true, usePointerIntern: true}, "CreateRoutine": {fullName: "tree.CreateRoutine", isPointer: true, usePointerIntern: true}, + "CreateTrigger": {fullName: "tree.CreateTrigger", isPointer: true, usePointerIntern: true}, "CreateStats": {fullName: "tree.CreateStats", isPointer: true, usePointerIntern: true}, "TableName": {fullName: "tree.TableName", isPointer: true, usePointerIntern: true}, "Constraint": {fullName: "constraint.Constraint", isPointer: true, usePointerIntern: true}, diff --git a/pkg/sql/opt/testutils/testcat/test_catalog.go b/pkg/sql/opt/testutils/testcat/test_catalog.go index 4f0c6ccec31..0c573b2949d 100644 --- a/pkg/sql/opt/testutils/testcat/test_catalog.go +++ b/pkg/sql/opt/testutils/testcat/test_catalog.go @@ -735,6 +735,16 @@ func (tv *View) CollectTypes(ord int) (descpb.IDs, error) { return nil, nil } +// TriggerCount is a part of the cat.View interface. +func (tv *View) TriggerCount() int { + return 0 +} + +// Trigger is a part of the cat.View interface. +func (tv *View) Trigger(i int) cat.Trigger { + panic("not implemented") +} + // Table implements the cat.Table interface for testing purposes. type Table struct { TabID cat.StableID @@ -1035,6 +1045,16 @@ func (tt *Table) IsRefreshViewRequired() bool { return false } +// TriggerCount is a part of the cat.Table interface. +func (tt *Table) TriggerCount() int { + return 0 +} + +// Trigger is a part of the cat.Table interface. +func (tt *Table) Trigger(i int) cat.Trigger { + panic("not implemented") +} + // Index implements the cat.Index interface for testing purposes. type Index struct { IdxName string diff --git a/pkg/sql/opt/testutils/testcat/types.go b/pkg/sql/opt/testutils/testcat/types.go index 8b20eff50e1..00442a74ed0 100644 --- a/pkg/sql/opt/testutils/testcat/types.go +++ b/pkg/sql/opt/testutils/testcat/types.go @@ -8,6 +8,8 @@ package testcat import ( "context" + "github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb" + "github.com/cockroachdb/cockroach/pkg/sql/catalog/typedesc" "github.com/cockroachdb/cockroach/pkg/sql/enum" "github.com/cockroachdb/cockroach/pkg/sql/oidext" "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" @@ -77,6 +79,31 @@ func (tc *Catalog) ResolveType( } // ResolveTypeByOID is part of the cat.Catalog interface. -func (tc *Catalog) ResolveTypeByOID(context.Context, oid.Oid) (*types.T, error) { - return nil, errors.Newf("ResolveTypeByOID not supported in the test catalog") +func (tc *Catalog) ResolveTypeByOID(ctx context.Context, typID oid.Oid) (*types.T, error) { + // First look for a matching user-defined enum type. + for _, typ := range tc.enumTypes { + if typ.Oid() == typID { + return typ, nil + } + } + // Otherwise look for a matching implicit record type. + for _, ds := range tc.testSchema.dataSources { + if tab, ok := ds.(*Table); ok { + implicitTypID := typedesc.TableIDToImplicitTypeOID(descpb.ID(tab.ID())) + if implicitTypID != typID { + continue + } + contents := make([]*types.T, 0, tab.ColumnCount()) + labels := make([]string, 0, tab.ColumnCount()) + for i, n := 0, tab.ColumnCount(); i < n; i++ { + col := tab.Column(i) + if col.Kind() == cat.Ordinary { + contents = append(contents, col.DatumType()) + labels = append(labels, string(col.ColName())) + } + } + return types.MakeLabeledTuple(contents, labels), nil + } + } + return nil, errors.Newf("type %d does not exist", typID) } diff --git a/pkg/sql/opt_catalog.go b/pkg/sql/opt_catalog.go index d0f0b05f84f..7044ad71295 100644 --- a/pkg/sql/opt_catalog.go +++ b/pkg/sql/opt_catalog.go @@ -629,13 +629,17 @@ func (oc *optCatalog) codec() keys.SQLCodec { // optView is a wrapper around catalog.TableDescriptor that implements // the cat.Object, cat.DataSource, and cat.View interfaces. type optView struct { - desc catalog.TableDescriptor + desc catalog.TableDescriptor + triggers []optTrigger } var _ cat.View = &optView{} func newOptView(desc catalog.TableDescriptor) *optView { - return &optView{desc: desc} + return &optView{ + desc: desc, + triggers: getOptTriggers(desc.GetTriggers()), + } } // ID is part of the cat.Object interface. @@ -688,6 +692,16 @@ func (ov *optView) CollectTypes(ord int) (descpb.IDs, error) { return collectTypes(col) } +// TriggerCount is part of the cat.View interface. +func (ov *optView) TriggerCount() int { + return len(ov.triggers) +} + +// Trigger is part of the cat.View interface. +func (ov *optView) Trigger(i int) cat.Trigger { + return &ov.triggers[i] +} + // optSequence is a wrapper around catalog.TableDescriptor that // implements the cat.Object and cat.DataSource interfaces. type optSequence struct { @@ -783,6 +797,8 @@ type optTable struct { // constraints for user defined types. checkConstraints []optCheckConstraint + triggers []optTrigger + // colMap is a mapping from unique ColumnID to column ordinal within the // table. This is a common lookup that needs to be fast. colMap catalog.TableColMap @@ -1094,6 +1110,9 @@ func newOptTable( } ot.checkConstraints = append(ot.checkConstraints, synthesizedChecks...) + // Move all triggers into the opt table. + ot.triggers = getOptTriggers(desc.GetTriggers()) + // Add stats last, now that other metadata is initialized. if stats != nil { ot.stats = make([]optTableStat, len(stats)) @@ -1404,6 +1423,16 @@ func (ot *optTable) IsHypothetical() bool { return false } +// TriggerCount is part of the cat.Table interface. +func (ot *optTable) TriggerCount() int { + return len(ot.triggers) +} + +// Trigger is part of the cat.Table interface. +func (ot *optTable) Trigger(i int) cat.Trigger { + return &ot.triggers[i] +} + // lookupColumnOrdinal returns the ordinal of the column with the given ID. A // cache makes the lookup O(1). func (ot *optTable) lookupColumnOrdinal(colID descpb.ColumnID) (int, error) { @@ -2475,6 +2504,16 @@ func (ot *optVirtualTable) IsRefreshViewRequired() bool { return false } +// TriggerCount is part of the cat.Table interface. +func (ot *optVirtualTable) TriggerCount() int { + return 0 +} + +// Trigger is part of the cat.Table interface. +func (ot *optVirtualTable) Trigger(i int) cat.Trigger { + panic(errors.AssertionFailedf("no triggers")) +} + // optVirtualIndex is a dummy implementation of cat.Index for the indexes // reported by a virtual table. The index assumes that table column 0 is a dummy // PK column. @@ -2704,6 +2743,118 @@ type optCatalogTableInterface interface { var _ optCatalogTableInterface = &optTable{} var _ optCatalogTableInterface = &optVirtualTable{} +// optTrigger is a wrapper around descpb.TriggerDescriptor that implements the +// cat.Trigger interface. +type optTrigger struct { + name tree.Name + actionTime tree.TriggerActionTime + events []tree.TriggerEvent + newTransitionAlias tree.Name + oldTransitionAlias tree.Name + forEachRow bool + whenExpr string + funcID cat.StableID + funcArgs tree.Datums + funcBody string + enabled bool +} + +var _ cat.Trigger = &optTrigger{} + +// Name is part of the cat.Trigger interface. +func (o *optTrigger) Name() tree.Name { + return o.name +} + +// ActionTime is part of the cat.Trigger interface. +func (o *optTrigger) ActionTime() tree.TriggerActionTime { + return o.actionTime +} + +// EventCount is part of the cat.Trigger interface. +func (o *optTrigger) EventCount() int { + return len(o.events) +} + +// Event is part of the cat.Trigger interface. +func (o *optTrigger) Event(i int) tree.TriggerEvent { + return o.events[i] +} + +// NewTransitionAlias is part of the cat.Trigger interface. +func (o *optTrigger) NewTransitionAlias() tree.Name { + return o.newTransitionAlias +} + +// OldTransitionAlias is part of the cat.Trigger interface. +func (o *optTrigger) OldTransitionAlias() tree.Name { + return o.oldTransitionAlias +} + +// ForEachRow is part of the cat.Trigger interface. +func (o *optTrigger) ForEachRow() bool { + return o.forEachRow +} + +// WhenExpr is part of the cat.Trigger interface. +func (o *optTrigger) WhenExpr() string { + return o.whenExpr +} + +// FuncID is part of the cat.Trigger interface. +func (o *optTrigger) FuncID() cat.StableID { + return o.funcID +} + +// FuncArgs is part of the cat.Trigger interface. +func (o *optTrigger) FuncArgs() tree.Datums { + return o.funcArgs +} + +func (o *optTrigger) FuncBody() string { + return o.funcBody +} + +// Enabled is part of the cat.Trigger interface. +func (o *optTrigger) Enabled() bool { + return o.enabled +} + +// getOptTriggers maps from descpb.TriggerDescriptor to optTrigger. +func getOptTriggers(descTriggers []descpb.TriggerDescriptor) []optTrigger { + triggers := make([]optTrigger, len(descTriggers)) + for i := range triggers { + descTrigger := &descTriggers[i] + optEvents := make([]tree.TriggerEvent, len(descTrigger.Events)) + for j := range optEvents { + descEvent := descTrigger.Events[j] + optEvents[j].EventType = tree.TriggerEventType(descEvent.Type) + optEvents[j].Columns = make(tree.NameList, 0, len(descEvent.ColumnNames)) + for _, colName := range descEvent.ColumnNames { + optEvents[j].Columns = append(optEvents[j].Columns, tree.Name(colName)) + } + } + funcArgs := make(tree.Datums, len(descTrigger.FuncArgs)) + for j := range funcArgs { + funcArgs[j] = tree.NewDString(descTrigger.FuncArgs[j]) + } + triggers[i] = optTrigger{ + name: tree.Name(descTrigger.Name), + actionTime: tree.TriggerActionTime(descTrigger.ActionTime), + events: optEvents, + newTransitionAlias: tree.Name(descTrigger.NewTransitionAlias), + oldTransitionAlias: tree.Name(descTrigger.OldTransitionAlias), + forEachRow: descTrigger.ForEachRow, + whenExpr: descTrigger.WhenExpr, + funcID: cat.StableID(descTrigger.FuncID), + funcArgs: funcArgs, + funcBody: descTrigger.FuncBody, + enabled: descTrigger.Enabled, + } + } + return triggers +} + // collectTypes walks the given column's default and computed expression, // and collects any user defined types it finds. If the column itself is of // a user defined type, it will also be added to the set of user defined types. diff --git a/pkg/sql/opt_exec_factory.go b/pkg/sql/opt_exec_factory.go index ea236c0be2e..af638dc31bc 100644 --- a/pkg/sql/opt_exec_factory.go +++ b/pkg/sql/opt_exec_factory.go @@ -27,6 +27,8 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/opt/constraint" "github.com/cockroachdb/cockroach/pkg/sql/opt/exec" "github.com/cockroachdb/cockroach/pkg/sql/opt/exec/explain" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" "github.com/cockroachdb/cockroach/pkg/sql/row" "github.com/cockroachdb/cockroach/pkg/sql/rowcontainer" "github.com/cockroachdb/cockroach/pkg/sql/sem/builtins" @@ -1911,6 +1913,26 @@ func (ef *execFactory) ConstructCreateFunction( }, nil } +// ConstructCreateTrigger is part of the exec.Factory interface. +func (ef *execFactory) ConstructCreateTrigger(ct *tree.CreateTrigger) (exec.Node, error) { + if err := checkSchemaChangeEnabled( + ef.ctx, + ef.planner.ExecCfg(), + "CREATE TRIGGER", + ); err != nil { + return nil, err + } + plan, err := ef.planner.SchemaChange(ef.ctx, ct) + if err != nil { + return nil, err + } + if plan == nil { + return nil, pgerror.New(pgcode.FeatureNotSupported, + "CREATE TRIGGER is only implemented in the declarative schema changer") + } + return plan, nil +} + func toPlanDependencies( deps opt.SchemaDeps, typeDeps opt.SchemaTypeDeps, funcDeps opt.SchemaFunctionDeps, ) (planDependencies, typeDependencies, functionDependencies, error) { diff --git a/pkg/sql/reference_provider.go b/pkg/sql/reference_provider.go index 2d93538f179..7f99ab83029 100644 --- a/pkg/sql/reference_provider.go +++ b/pkg/sql/reference_provider.go @@ -17,6 +17,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/opt/optbuilder" "github.com/cockroachdb/cockroach/pkg/sql/schemachanger/scbuild" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/errors" ) type tableDescReferences []descpb.TableDescriptor_Reference @@ -118,17 +119,27 @@ func (f *referenceProviderFactory) NewReferenceProvider( if err := optBld.Build(); err != nil { return nil, err } - // For the time being this is only used for CREATE FUNCTION. We need to handle - // CREATE VIEW when it's needed. - createFnExpr := optFactory.Memo().RootExpr().(*memo.CreateFunctionExpr) - tableReferences, typeReferences, functionReferences, err := toPlanDependencies(createFnExpr.Deps, createFnExpr.TypeDeps, createFnExpr.FuncDeps) + var ( + err error + planDeps planDependencies + typeDeps typeDependencies + funcDeps functionDependencies + ) + switch t := optFactory.Memo().RootExpr().(type) { + case *memo.CreateFunctionExpr: + planDeps, typeDeps, funcDeps, err = toPlanDependencies(t.Deps, t.TypeDeps, t.FuncDeps) + case *memo.CreateTriggerExpr: + planDeps, typeDeps, funcDeps, err = toPlanDependencies(t.Deps, t.TypeDeps, t.FuncDeps) + default: + return nil, errors.AssertionFailedf("unexpected root expression: %s", t.(memo.RelExpr).Op()) + } if err != nil { return nil, err } ret := newReferenceProvider() - for descID, refs := range tableReferences { + for descID, refs := range planDeps { ret.allRelationIDs.Add(descID) if refs.desc.IsView() { ret.viewReferences[descID] = append(ret.viewReferences[descID], refs.deps...) @@ -139,7 +150,7 @@ func (f *referenceProviderFactory) NewReferenceProvider( } } - for typeID := range typeReferences { + for typeID := range typeDeps { desc, err := f.p.descCollection.ByIDWithoutLeased(f.p.txn).WithoutNonPublic().Get().Desc(ctx, typeID) if err != nil { return nil, err @@ -152,7 +163,7 @@ func (f *referenceProviderFactory) NewReferenceProvider( } } - for functionID := range functionReferences { + for functionID := range funcDeps { ret.referencedFunctions.Add(functionID) } return ret, nil