diff --git a/pkg/ccl/backupccl/testdata/backup-restore/plpgsql_procedures b/pkg/ccl/backupccl/testdata/backup-restore/plpgsql_procedures new file mode 100644 index 000000000000..bb8f95f30875 --- /dev/null +++ b/pkg/ccl/backupccl/testdata/backup-restore/plpgsql_procedures @@ -0,0 +1,433 @@ +# Test backing up and restoring a database with PL/pgSQL procedures. +new-cluster name=s +---- + +exec-sql +CREATE DATABASE db1; +USE db1; +CREATE SCHEMA sc1; +CREATE TABLE sc1.tbl1(a INT PRIMARY KEY); +CREATE TYPE sc1.enum1 AS ENUM('Good'); +CREATE SEQUENCE sc1.sq1; +CREATE PROCEDURE sc1.p1(a sc1.enum1) LANGUAGE PLpgSQL AS $$ + BEGIN + SELECT a FROM sc1.tbl1; + SELECT nextval('sc1.sq1'); + END +$$; +CREATE SCHEMA sc2; +CREATE TABLE sc2.tbl2(a INT PRIMARY KEY); +CREATE PROCEDURE sc2.p2(i INT) LANGUAGE PLpgSQL AS $$ + BEGIN + INSERT INTO sc2.tbl2 VALUES (i); + END +$$; +---- + +exec-sql +CALL sc1.p1('Good'::sc1.enum1) +---- + +exec-sql +CALL sc2.p2(123) +---- + +query-sql +SELECT * FROM sc2.tbl2 +---- +123 + +exec-sql +BACKUP DATABASE db1 INTO 'nodelocal://1/test/' +---- + +query-sql +WITH descs AS ( + SHOW BACKUP LATEST IN 'nodelocal://1/test/' +) +SELECT database_name, parent_schema_name, object_name, object_type, is_full_cluster FROM descs +---- + db1 database false +db1 public schema false +db1 sc1 schema false +db1 sc1 tbl1 table false +db1 sc1 enum1 type false +db1 sc1 _enum1 type false +db1 sc1 sq1 table false +db1 sc1 p1 function false +db1 sc2 schema false +db1 sc2 tbl2 table false +db1 sc2 p2 function false + +query-sql +SELECT create_statement FROM [SHOW CREATE PROCEDURE sc1.p1] +---- +---- +CREATE PROCEDURE sc1.p1(IN a db1.sc1.enum1) + LANGUAGE plpgsql + AS $$ + BEGIN + SELECT a FROM db1.sc1.tbl1; + SELECT nextval('sc1.sq1'::REGCLASS); + END + +$$ +---- +---- + +exec-sql +CALL sc1.p1('Good'::sc1.enum1) +---- + +query-sql +SELECT currval('sc1.sq1') +---- +2 + +exec-sql +DROP DATABASE db1 +---- + +exec-sql +RESTORE DATABASE db1 FROM LATEST IN 'nodelocal://1/test/' WITH new_db_name = db1_new +---- + +exec-sql +USE db1_new +---- + +# Make sure ids in signature and body are rewritten. +# 1. argument type id is rewritten so that type name is deserialized correctly. +# 2. db name in qualified name is rewritten. +# 3. sequence id is rewritten so that sequence name is deserialized correctly. +query-sql +SELECT create_statement FROM [SHOW CREATE PROCEDURE sc1.p1] +---- +---- +CREATE PROCEDURE sc1.p1(IN a db1_new.sc1.enum1) + LANGUAGE plpgsql + AS $$ + BEGIN + SELECT a FROM db1_new.sc1.tbl1; + SELECT nextval('sc1.sq1'::REGCLASS); + END + +$$ +---- +---- + +# Make sure procedure signature is rewritten in schema descriptor so that +# procedure can be resolved and executed. +exec-sql +CALL sc1.p1('Good'::sc1.enum1) +---- + +query-sql +SELECT currval('sc1.sq1') +---- +2 + +# Make sure procedure still inserts into the correct table. +exec-sql +CALL sc2.p2(456) +---- + +query-sql +SELECT * FROM sc2.tbl2 +---- +123 +456 + +# Make sure dependency IDs are rewritten. +# Note that technically this only tests forward-reference IDs in depended-on +# objects are rewritten. But since we have cross-references validation, so this +# also means back-references in the function descriptor are good. +exec-sql +DROP SEQUENCE sc1.sq1 +---- +pq: cannot drop sequence sq1 because other objects depend on it + +exec-sql +DROP TABLE sc1.tbl1 +---- +pq: cannot drop table tbl1 because other objects depend on it + +# TODO(mgartner): The error message should say "procedure". +exec-sql +ALTER TABLE sc1.tbl1 RENAME TO tbl1_new +---- +pq: cannot rename relation "sc1.tbl1" because function "p1" depends on it +HINT: consider dropping "p1" first. + +# TODO(mgartner): The error message should say "procedure". +exec-sql +ALTER TABLE sc1.tbl1 SET SCHEMA sc2; +---- +pq: cannot set schema on relation "tbl1" because function "p1" depends on it +HINT: consider dropping "p1" first. + +exec-sql +DROP TYPE sc1.enum1 +---- +pq: cannot drop type "enum1" because other objects ([db1_new.sc1.p1]) still depend on it + +# Test backing up and restoring a full cluster with procedures. +new-cluster name=s1 +---- + +exec-sql cluster=s1 +CREATE DATABASE db1; +USE db1; +CREATE SCHEMA sc1; +CREATE TABLE sc1.tbl1(a INT PRIMARY KEY); +CREATE TYPE sc1.enum1 AS ENUM('Good'); +CREATE SEQUENCE sc1.sq1; +CREATE PROCEDURE sc1.p1(a sc1.enum1) LANGUAGE PLpgSQL AS $$ + BEGIN + SELECT a FROM sc1.tbl1; + SELECT nextval('sc1.sq1'); + END +$$; +CREATE SCHEMA sc2; +CREATE TABLE sc2.tbl2(a INT PRIMARY KEY); +CREATE PROCEDURE sc2.p2(i INT) LANGUAGE PLpgSQL AS $$ + BEGIN + INSERT INTO sc2.tbl2 VALUES (i); + END +$$; +---- + +exec-sql +CALL sc1.p1('Good'::sc1.enum1) +---- + +exec-sql +CALL sc2.p2(123) +---- + +query-sql +SELECT * FROM sc2.tbl2 +---- +123 + +exec-sql +BACKUP INTO 'nodelocal://1/test/' +---- + +query-sql +WITH descs AS ( + SHOW BACKUP LATEST IN 'nodelocal://1/test/' +) +SELECT + database_name, parent_schema_name, object_name, object_type, is_full_cluster +FROM + descs +WHERE + database_name = 'db1' + +---- +db1 public schema true +db1 sc1 schema true +db1 sc1 tbl1 table true +db1 sc1 enum1 type true +db1 sc1 _enum1 type true +db1 sc1 sq1 table true +db1 sc1 p1 function true +db1 sc2 schema true +db1 sc2 tbl2 table true +db1 sc2 p2 function true + +query-sql +SELECT create_statement FROM [SHOW CREATE PROCEDURE sc1.p1] +---- +---- +CREATE PROCEDURE sc1.p1(IN a db1.sc1.enum1) + LANGUAGE plpgsql + AS $$ + BEGIN + SELECT a FROM db1.sc1.tbl1; + SELECT nextval('sc1.sq1'::REGCLASS); + END + +$$ +---- +---- + +query-sql +CALL sc1.p1('Good'::sc1.enum1) +---- + +query-sql +SELECT currval('sc1.sq1') +---- +2 + +# Start a new cluster with the same IO dir. +new-cluster name=s2 share-io-dir=s1 +---- + +# Restore into the new cluster. +exec-sql cluster=s2 +RESTORE FROM LATEST IN 'nodelocal://1/test/' +---- + +exec-sql +USE db1 +---- + +# Make sure ids in signature and body are rewritten. +# 1. argument type id is rewritten so that type name is deserialized correctly. +# 2. db name in qualified name is rewritten. +# 3. sequence id is rewritten so that sequence name is deserialized correctly. +query-sql +SELECT create_statement FROM [SHOW CREATE PROCEDURE sc1.p1] +---- +---- +CREATE PROCEDURE sc1.p1(IN a db1.sc1.enum1) + LANGUAGE plpgsql + AS $$ + BEGIN + SELECT a FROM db1.sc1.tbl1; + SELECT nextval('sc1.sq1'::REGCLASS); + END + +$$ +---- +---- + +# Make sure procedure signature is rewritten in schema descriptor so that +# procedure can be resolved and executed. +exec-sql +CALL sc2.p2(456) +---- + +query-sql +SELECT * FROM sc2.tbl2 +---- +123 +456 + +# Make sure dependency IDs are rewritten. +# Note that technically this only tests forward-reference IDs in depended-on +# objects are rewritten. But since we have cross-references validation, so this +# also means back-references in the function descriptor are good. +exec-sql +DROP SEQUENCE sc1.sq1 +---- +pq: cannot drop sequence sq1 because other objects depend on it + +exec-sql +DROP TABLE sc1.tbl1 +---- +pq: cannot drop table tbl1 because other objects depend on it + +exec-sql +ALTER TABLE sc1.tbl1 RENAME TO tbl1_new +---- +pq: cannot rename relation "sc1.tbl1" because function "p1" depends on it +HINT: consider dropping "p1" first. + +exec-sql +ALTER TABLE sc1.tbl1 SET SCHEMA sc2; +---- +pq: cannot set schema on relation "tbl1" because function "p1" depends on it +HINT: consider dropping "p1" first. + +exec-sql +DROP TYPE sc1.enum1 +---- +pq: cannot drop type "enum1" because other objects ([db1.sc1.p1]) still depend on it + +# Make sure that backup and restore individual tables from schema with procedure +# does not crash. +new-cluster name=s3 +---- + +exec-sql cluster=s3 +CREATE DATABASE db1; +CREATE SCHEMA sc1; +CREATE TABLE sc1.t(a INT PRIMARY KEY); +CREATE PROCEDURE sc1.p() LANGUAGE PLpgSQL AS $$ BEGIN SELECT 1; END $$; +---- + +# Make sure the original schema has procedure signatures +query-sql +WITH db_id AS ( + SELECT id FROM system.namespace WHERE name = 'defaultdb' +), +schema_id AS ( + SELECT ns.id + FROM system.namespace AS ns + JOIN db_id ON ns."parentID" = db_id.id + WHERE ns.name = 'sc1' +) +SELECT id FROM schema_id; +---- +109 + +query-sql +WITH to_json AS ( + SELECT + id, + crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', + descriptor, + false + ) AS d + FROM + system.descriptor + WHERE id = 109 +) +SELECT d->'schema'->>'functions'::string FROM to_json; +---- +{"p": {"signatures": [{"id": 111, "isProcedure": true, "returnType": {"family": "VoidFamily", "oid": 2278}}]}} + +exec-sql +BACKUP TABLE sc1.t INTO 'nodelocal://1/test/' +---- + +exec-sql +RESTORE TABLE sc1.t FROM LATEST IN 'nodelocal://1/test/' WITH into_db = 'db1'; +---- + +exec-sql +USE db1; +---- + +query-sql +WITH db_id AS ( + SELECT id FROM system.namespace WHERE name = 'db1' +), +schema_id AS ( + SELECT ns.id + FROM system.namespace AS ns + JOIN db_id ON ns."parentID" = db_id.id + WHERE ns.name = 'sc1' +) +SELECT id FROM schema_id; +---- +112 + +query-sql +WITH to_json AS ( + SELECT + id, + crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', + descriptor, + false + ) AS d + FROM + system.descriptor + WHERE id = 112 +) +SELECT d->'schema'->>'functions'::string FROM to_json; +---- + + +# Make sure proper error message is returned when trying to resolve the +# procedure from the restore target db. +query-sql +CALL p() +---- +pq: procedure p does not exist diff --git a/pkg/ccl/backupccl/testdata/backup-restore/plpgsql_user_defined_functions b/pkg/ccl/backupccl/testdata/backup-restore/plpgsql_user_defined_functions new file mode 100644 index 000000000000..a3c80a7272bc --- /dev/null +++ b/pkg/ccl/backupccl/testdata/backup-restore/plpgsql_user_defined_functions @@ -0,0 +1,571 @@ +# Test backing up and restoring a database with PL/pgSQL user defined functions. +new-cluster name=s +---- + +exec-sql +CREATE DATABASE db1; +USE db1; +CREATE SCHEMA sc1; +CREATE TABLE sc1.tbl1(a INT PRIMARY KEY); +CREATE TYPE sc1.enum1 AS ENUM('Good'); +CREATE SEQUENCE sc1.sq1; +CREATE FUNCTION sc1.f1(a sc1.enum1) RETURNS INT LANGUAGE PLpgSQL AS $$ + DECLARE + x INT := 0; + BEGIN + SELECT a FROM sc1.tbl1; + RETURN nextval('sc1.sq1'); + END +$$; +CREATE SCHEMA sc2; +CREATE TABLE sc2.tbl2(a INT PRIMARY KEY); +CREATE FUNCTION sc2.f2() RETURNS INT LANGUAGE PLpgSQL AS $$ + DECLARE + x INT; + BEGIN + SELECT a INTO x FROM sc2.tbl2 LIMIT 1; + RETURN x; + END +$$; +---- + +exec-sql +INSERT INTO sc2.tbl2 VALUES (123) +---- + +query-sql +SELECT sc1.f1('Good'::sc1.enum1) +---- +1 + +query-sql +SELECT sc2.f2() +---- +123 + +exec-sql +BACKUP DATABASE db1 INTO 'nodelocal://1/test/' +---- + +query-sql +WITH descs AS ( + SHOW BACKUP LATEST IN 'nodelocal://1/test/' +) +SELECT database_name, parent_schema_name, object_name, object_type, is_full_cluster FROM descs +---- + db1 database false +db1 public schema false +db1 sc1 schema false +db1 sc1 tbl1 table false +db1 sc1 enum1 type false +db1 sc1 _enum1 type false +db1 sc1 sq1 table false +db1 sc1 f1 function false +db1 sc2 schema false +db1 sc2 tbl2 table false +db1 sc2 f2 function false + +query-sql +SELECT create_statement FROM [SHOW CREATE FUNCTION sc1.f1] +---- +---- +CREATE FUNCTION sc1.f1(IN a db1.sc1.enum1) + RETURNS INT8 + VOLATILE + NOT LEAKPROOF + CALLED ON NULL INPUT + LANGUAGE plpgsql + AS $$ + DECLARE + x INT8 := 0; + BEGIN + SELECT a FROM db1.sc1.tbl1; + RETURN nextval('sc1.sq1'::REGCLASS); + END + +$$ +---- +---- + +query-sql +SELECT sc1.f1('Good'::sc1.enum1) +---- +2 + +exec-sql +DROP DATABASE db1 +---- + +exec-sql +RESTORE DATABASE db1 FROM LATEST IN 'nodelocal://1/test/' WITH new_db_name = db1_new +---- + +exec-sql +USE db1_new +---- + +# Make sure ids in signature and body are rewritten. +# 1. argument type id is rewritten so that type name is deserialized correctly. +# 2. db name in qualified name is rewritten. +# 3. sequence id is rewritten so that sequence name is deserialized correctly. +query-sql +SELECT create_statement FROM [SHOW CREATE FUNCTION sc1.f1] +---- +---- +CREATE FUNCTION sc1.f1(IN a db1_new.sc1.enum1) + RETURNS INT8 + VOLATILE + NOT LEAKPROOF + CALLED ON NULL INPUT + LANGUAGE plpgsql + AS $$ + DECLARE + x INT8 := 0; + BEGIN + SELECT a FROM db1_new.sc1.tbl1; + RETURN nextval('sc1.sq1'::REGCLASS); + END + +$$ +---- +---- + +# Make sure function signature is rewritten in schema descriptor so that +# function can be resolved and executed. +query-sql +SELECT sc1.f1('Good'::db1_new.sc1.enum1) +---- +2 + +# Make sure function still queries from correct table. +query-sql +SELECT db1_new.sc2.f2() +---- +123 + +# Make sure dependency IDs are rewritten. +# Note that technically this only tests forward-reference IDs in depended-on +# objects are rewritten. But since we have cross-references validation, so this +# also means back-references in UDF descriptor are good. +exec-sql +DROP SEQUENCE sc1.sq1 +---- +pq: cannot drop sequence sq1 because other objects depend on it + +exec-sql +DROP TABLE sc1.tbl1 +---- +pq: cannot drop table tbl1 because other objects depend on it + +exec-sql +ALTER TABLE sc1.tbl1 RENAME TO tbl1_new +---- +pq: cannot rename relation "sc1.tbl1" because function "f1" depends on it +HINT: consider dropping "f1" first. + +exec-sql +ALTER TABLE sc1.tbl1 SET SCHEMA sc2; +---- +pq: cannot set schema on relation "tbl1" because function "f1" depends on it +HINT: consider dropping "f1" first. + +exec-sql +DROP TYPE sc1.enum1 +---- +pq: cannot drop type "enum1" because other objects ([db1_new.sc1.f1]) still depend on it + +# Test backing up and restoring a full cluster with user defined function. +new-cluster name=s1 +---- + +exec-sql cluster=s1 +CREATE DATABASE db1; +USE db1; +CREATE SCHEMA sc1; +CREATE TABLE sc1.tbl1(a INT PRIMARY KEY); +CREATE TYPE sc1.enum1 AS ENUM('Good'); +CREATE SEQUENCE sc1.sq1; +CREATE FUNCTION sc1.f1(a sc1.enum1) RETURNS INT LANGUAGE PLpgSQL AS $$ + DECLARE + x INT; + BEGIN + SELECT a FROM sc1.tbl1; + SELECT nextval('sc1.sq1') INTO x; + RETURN x; + END +$$; +CREATE SCHEMA sc2; +CREATE TABLE sc2.tbl2(a INT PRIMARY KEY); +CREATE FUNCTION sc2.f2() RETURNS INT LANGUAGE PLpgSQL AS $$ + BEGIN + RETURN (SELECT a FROM sc2.tbl2 LIMIT 1); + END +$$; +---- + +exec-sql +INSERT INTO sc2.tbl2 VALUES (123) +---- + +query-sql +SELECT sc1.f1('Good'::sc1.enum1) +---- +1 + +query-sql +SELECT sc2.f2() +---- +123 + +exec-sql +BACKUP INTO 'nodelocal://1/test/' +---- + +query-sql +WITH descs AS ( + SHOW BACKUP LATEST IN 'nodelocal://1/test/' +) +SELECT + database_name, parent_schema_name, object_name, object_type, is_full_cluster +FROM + descs +WHERE + database_name = 'db1' + +---- +db1 public schema true +db1 sc1 schema true +db1 sc1 tbl1 table true +db1 sc1 enum1 type true +db1 sc1 _enum1 type true +db1 sc1 sq1 table true +db1 sc1 f1 function true +db1 sc2 schema true +db1 sc2 tbl2 table true +db1 sc2 f2 function true + +query-sql +SELECT create_statement FROM [SHOW CREATE FUNCTION sc1.f1] +---- +---- +CREATE FUNCTION sc1.f1(IN a db1.sc1.enum1) + RETURNS INT8 + VOLATILE + NOT LEAKPROOF + CALLED ON NULL INPUT + LANGUAGE plpgsql + AS $$ + DECLARE + x INT8; + BEGIN + SELECT a FROM db1.sc1.tbl1; + SELECT nextval('sc1.sq1'::REGCLASS) INTO x; + RETURN x; + END + +$$ +---- +---- + +query-sql +SELECT sc1.f1('Good'::sc1.enum1) +---- +2 + +# Start a new cluster with the same IO dir. +new-cluster name=s2 share-io-dir=s1 +---- + +# Restore into the new cluster. +exec-sql cluster=s2 +RESTORE FROM LATEST IN 'nodelocal://1/test/' +---- + +exec-sql +USE db1 +---- + +# Make sure ids in signature and body are rewritten. +# 1. argument type id is rewritten so that type name is deserialized correctly. +# 2. db name in qualified name is rewritten. +# 3. sequence id is rewritten so that sequence name is deserialized correctly. +query-sql +SELECT create_statement FROM [SHOW CREATE FUNCTION sc1.f1] +---- +---- +CREATE FUNCTION sc1.f1(IN a db1.sc1.enum1) + RETURNS INT8 + VOLATILE + NOT LEAKPROOF + CALLED ON NULL INPUT + LANGUAGE plpgsql + AS $$ + DECLARE + x INT8; + BEGIN + SELECT a FROM db1.sc1.tbl1; + SELECT nextval('sc1.sq1'::REGCLASS) INTO x; + RETURN x; + END + +$$ +---- +---- + +# Make sure function signature is rewritten in schema descriptor so that +# function can be resolved and executed. +query-sql +SELECT sc1.f1('Good'::sc1.enum1) +---- +2 + +# Make sure function still queries from correct table. +query-sql +SELECT sc2.f2() +---- +123 + +# Make sure dependency IDs are rewritten. +# Note that technically this only tests forward-reference IDs in depended-on +# objects are rewritten. But since we have cross-references validation, so this +# also means back-references in UDF descriptor are good. +exec-sql +DROP SEQUENCE sc1.sq1 +---- +pq: cannot drop sequence sq1 because other objects depend on it + +exec-sql +DROP TABLE sc1.tbl1 +---- +pq: cannot drop table tbl1 because other objects depend on it + +exec-sql +ALTER TABLE sc1.tbl1 RENAME TO tbl1_new +---- +pq: cannot rename relation "sc1.tbl1" because function "f1" depends on it +HINT: consider dropping "f1" first. + +exec-sql +ALTER TABLE sc1.tbl1 SET SCHEMA sc2; +---- +pq: cannot set schema on relation "tbl1" because function "f1" depends on it +HINT: consider dropping "f1" first. + +exec-sql +DROP TYPE sc1.enum1 +---- +pq: cannot drop type "enum1" because other objects ([db1.sc1.f1]) still depend on it + +# Make sure that backup and restore individual tables from schema with UDF does +# not crash. +new-cluster name=s3 +---- + +exec-sql cluster=s3 +CREATE DATABASE db1; +CREATE SCHEMA sc1; +CREATE TABLE sc1.t(a INT PRIMARY KEY); +CREATE FUNCTION sc1.f() RETURNS INT LANGUAGE PLpgSQL AS $$ BEGIN RETURN 1; END $$; +---- + +# Make sure the original schema has function signatures +query-sql +WITH db_id AS ( + SELECT id FROM system.namespace WHERE name = 'defaultdb' +), +schema_id AS ( + SELECT ns.id + FROM system.namespace AS ns + JOIN db_id ON ns."parentID" = db_id.id + WHERE ns.name = 'sc1' +) +SELECT id FROM schema_id; +---- +109 + +query-sql +WITH to_json AS ( + SELECT + id, + crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', + descriptor, + false + ) AS d + FROM + system.descriptor + WHERE id = 109 +) +SELECT d->'schema'->>'functions'::string FROM to_json; +---- +{"f": {"signatures": [{"id": 111, "returnType": {"family": "IntFamily", "oid": 20, "width": 64}}]}} + +exec-sql +BACKUP TABLE sc1.t INTO 'nodelocal://1/test/' +---- + +exec-sql +RESTORE TABLE sc1.t FROM LATEST IN 'nodelocal://1/test/' WITH into_db = 'db1'; +---- + +exec-sql +USE db1; +---- + +query-sql +WITH db_id AS ( + SELECT id FROM system.namespace WHERE name = 'db1' +), +schema_id AS ( + SELECT ns.id + FROM system.namespace AS ns + JOIN db_id ON ns."parentID" = db_id.id + WHERE ns.name = 'sc1' +) +SELECT id FROM schema_id; +---- +112 + +query-sql +WITH to_json AS ( + SELECT + id, + crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', + descriptor, + false + ) AS d + FROM + system.descriptor + WHERE id = 112 +) +SELECT d->'schema'->>'functions'::string FROM to_json; +---- + + +# Make sure proper error message is returned when trying to resolve the +# function from the restore target db. +query-sql +SELECT f() +---- +pq: unknown function: f() + +# Test that backing up and restoring a cluster with a function on 23.2 does not +# grant EXECUTE privileges on the public role for functions where that privilege +# has been revoked. +new-cluster name=s4 +---- + +exec-sql cluster=s4 +CREATE DATABASE db1; +USE db1; +CREATE USER u1; +CREATE FUNCTION add(x INT, y INT) RETURNS INT LANGUAGE PLpgSQL AS 'BEGIN RETURN x + y; END'; +REVOKE EXECUTE ON FUNCTION ADD FROM public; +SET ROLE = u1; +---- + +query-sql +SELECT add(1, 2) +---- +pq: user u1 does not have EXECUTE privilege on function add + +query-sql +SELECT database_name, schema_name, routine_signature, grantee, privilege_type, is_grantable +FROM [SHOW GRANTS ON FUNCTION add] +---- +db1 public add(int8, int8) admin ALL true +db1 public add(int8, int8) root ALL true + +exec-sql +SET ROLE = root +---- + +exec-sql cluster=s4 +BACKUP INTO 'nodelocal://1/test/' +---- + +# Start a new cluster with the same IO dir. +new-cluster name=s5 share-io-dir=s4 +---- + +# Restore into the new cluster. +exec-sql cluster=s5 +RESTORE FROM LATEST IN 'nodelocal://1/test/' +---- + +exec-sql cluster=s5 +USE db1; +SET ROLE = u1; +---- + +query-sql cluster=s5 +SELECT add(1, 2) +---- +pq: user u1 does not have EXECUTE privilege on function add + +query-sql cluster=s5 +SELECT database_name, schema_name, routine_signature, grantee, privilege_type, is_grantable +FROM [SHOW GRANTS ON FUNCTION add] +---- +db1 public add(int8, int8) admin ALL true +db1 public add(int8, int8) root ALL true + +# Backing up and restoring a database with a function resets privileges on +# functions to the default privileges because we cannot be sure if the same +# users are present in the new cluster. +new-cluster name=s6 +---- + +exec-sql cluster=s6 +CREATE DATABASE db1; +USE db1; +CREATE USER u1; +CREATE FUNCTION add(x INT, y INT) RETURNS INT LANGUAGE PLpgSQL AS 'BEGIN RETURN x + y; END'; +REVOKE EXECUTE ON FUNCTION ADD FROM public; +SET ROLE = u1; +---- + +query-sql +SELECT add(1, 2) +---- +pq: user u1 does not have EXECUTE privilege on function add + +query-sql +SELECT database_name, schema_name, routine_signature, grantee, privilege_type, is_grantable +FROM [SHOW GRANTS ON FUNCTION add] +---- +db1 public add(int8, int8) admin ALL true +db1 public add(int8, int8) root ALL true + +exec-sql +SET ROLE = root +---- + +exec-sql cluster=s6 +BACKUP DATABASE db1 INTO 'nodelocal://1/test/' +---- + +# Restore into the new cluster. +exec-sql cluster=s6 +RESTORE DATABASE db1 FROM LATEST IN 'nodelocal://1/test/' WITH new_db_name = db1_new +---- + +exec-sql cluster=s6 +USE db1_new; +SET ROLE = u1; +---- + +# The user now has EXECUTE privilege via the public role. +query-sql cluster=s6 +SELECT add(1, 2) +---- +3 + +query-sql cluster=s6 +SELECT database_name, schema_name, routine_signature, grantee, privilege_type, is_grantable +FROM [SHOW GRANTS ON FUNCTION add] +---- +db1_new public add(int8, int8) admin ALL true +db1_new public add(int8, int8) public EXECUTE false +db1_new public add(int8, int8) root ALL true diff --git a/pkg/ccl/backupccl/testdata/backup-restore/procedures b/pkg/ccl/backupccl/testdata/backup-restore/procedures index d570adc2ad4e..58583a635ef6 100644 --- a/pkg/ccl/backupccl/testdata/backup-restore/procedures +++ b/pkg/ccl/backupccl/testdata/backup-restore/procedures @@ -20,6 +20,10 @@ CREATE PROCEDURE sc2.p2(i INT) LANGUAGE SQL AS $$ $$; ---- +exec-sql +CALL sc1.p1('Good'::sc1.enum1) +---- + exec-sql CALL sc2.p2(123) ---- @@ -68,7 +72,7 @@ CALL sc1.p1('Good'::sc1.enum1) query-sql SELECT currval('sc1.sq1') ---- -1 +2 exec-sql DROP DATABASE db1 @@ -82,7 +86,7 @@ exec-sql USE db1_new ---- -# Make sure procedure ids in signature and body are rewritten. +# Make sure ids in the signature and body are rewritten. # 1. argument type id is rewritten so that type name is deserialized correctly. # 2. db name in qualified name is rewritten. # 3. sequence id is rewritten so that sequence name is deserialized correctly. @@ -105,7 +109,7 @@ CALL sc1.p1('Good'::sc1.enum1) query-sql SELECT currval('sc1.sq1') ---- -1 +2 # Make sure procedure still inserts into the correct table. exec-sql @@ -173,6 +177,10 @@ CREATE PROCEDURE sc2.p2(i INT) LANGUAGE SQL AS $$ $$; ---- +exec-sql +CALL sc1.p1('Good'::sc1.enum1) +---- + exec-sql CALL sc2.p2(123) ---- @@ -226,7 +234,7 @@ CALL sc1.p1('Good'::sc1.enum1) query-sql SELECT currval('sc1.sq1') ---- -1 +2 # Start a new cluster with the same IO dir. new-cluster name=s2 share-io-dir=s1 @@ -241,7 +249,7 @@ exec-sql USE db1 ---- -# Make sure procedure ids in signature and body are rewritten. +# Make sure ids in signature and body are rewritten. # 1. argument type id is rewritten so that type name is deserialized correctly. # 2. db name in qualified name is rewritten. # 3. sequence id is rewritten so that sequence name is deserialized correctly. diff --git a/pkg/ccl/backupccl/testdata/backup-restore/user-defined-functions b/pkg/ccl/backupccl/testdata/backup-restore/user-defined-functions index bd77f22e60ca..8f6fd2562218 100644 --- a/pkg/ccl/backupccl/testdata/backup-restore/user-defined-functions +++ b/pkg/ccl/backupccl/testdata/backup-restore/user-defined-functions @@ -22,6 +22,11 @@ exec-sql INSERT INTO sc2.tbl2 VALUES (123) ---- +query-sql +SELECT sc1.f1('Good'::sc1.enum1) +---- +1 + query-sql SELECT sc2.f2() ---- @@ -66,7 +71,7 @@ $$ query-sql SELECT sc1.f1('Good'::sc1.enum1) ---- -1 +2 exec-sql DROP DATABASE db1 @@ -80,7 +85,7 @@ exec-sql USE db1_new ---- -# Make sure function ids in signature and body are rewritten. +# Make sure ids in signature and body are rewritten. # 1. argument type id is rewritten so that type name is deserialized correctly. # 2. db name in qualified name is rewritten. # 3. sequence id is rewritten so that sequence name is deserialized correctly. @@ -103,7 +108,7 @@ $$ query-sql SELECT sc1.f1('Good'::db1_new.sc1.enum1) ---- -1 +2 # Make sure function still queries from correct table. query-sql @@ -166,6 +171,11 @@ exec-sql INSERT INTO sc2.tbl2 VALUES (123) ---- +query-sql +SELECT sc1.f1('Good'::sc1.enum1) +---- +1 + query-sql SELECT sc2.f2() ---- @@ -215,7 +225,7 @@ $$ query-sql SELECT sc1.f1('Good'::sc1.enum1) ---- -1 +2 # Start a new cluster with the same IO dir. new-cluster name=s2 share-io-dir=s1 @@ -230,7 +240,7 @@ exec-sql USE db1 ---- -# Make sure function ids in signature and body are rewritten. +# Make sure ids in signature and body are rewritten. # 1. argument type id is rewritten so that type name is deserialized correctly. # 2. db name in qualified name is rewritten. # 3. sequence id is rewritten so that sequence name is deserialized correctly. @@ -253,7 +263,7 @@ $$ query-sql SELECT sc1.f1('Good'::sc1.enum1) ---- -1 +2 # Make sure function still queries from correct table. query-sql diff --git a/pkg/sql/catalog/rewrite/BUILD.bazel b/pkg/sql/catalog/rewrite/BUILD.bazel index 96328501312d..9a2c5b311966 100644 --- a/pkg/sql/catalog/rewrite/BUILD.bazel +++ b/pkg/sql/catalog/rewrite/BUILD.bazel @@ -19,9 +19,12 @@ go_library( "//pkg/sql/parser", "//pkg/sql/pgwire/pgcode", "//pkg/sql/pgwire/pgerror", + "//pkg/sql/plpgsql/parser:plpgparser", "//pkg/sql/schemachanger/scpb", "//pkg/sql/schemachanger/screl", "//pkg/sql/sem/catid", + "//pkg/sql/sem/plpgsqltree", + "//pkg/sql/sem/plpgsqltree/utils", "//pkg/sql/sem/tree", "//pkg/sql/types", "//pkg/util/hlc", diff --git a/pkg/sql/catalog/rewrite/rewrite.go b/pkg/sql/catalog/rewrite/rewrite.go index 96c0b3ad92a0..c3f3fa89e55d 100644 --- a/pkg/sql/catalog/rewrite/rewrite.go +++ b/pkg/sql/catalog/rewrite/rewrite.go @@ -27,9 +27,12 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/parser" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" + plpgsqlparser "github.com/cockroachdb/cockroach/pkg/sql/plpgsql/parser" "github.com/cockroachdb/cockroach/pkg/sql/schemachanger/scpb" "github.com/cockroachdb/cockroach/pkg/sql/schemachanger/screl" "github.com/cockroachdb/cockroach/pkg/sql/sem/catid" + "github.com/cockroachdb/cockroach/pkg/sql/sem/plpgsqltree" + "github.com/cockroachdb/cockroach/pkg/sql/sem/plpgsqltree/utils" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/sql/types" "github.com/cockroachdb/cockroach/pkg/util/hlc" @@ -323,27 +326,46 @@ func rewriteViewQueryDBNames(table *tabledesc.Mutable, newDB string) error { return nil } -func rewriteFunctionBodyDBNames(fnBody string, newDB string) (string, error) { - stmts, err := parser.Parse(fnBody) - if err != nil { - return "", err - } +func rewriteFunctionBodyDBNames( + fnBody string, newDB string, lang catpb.Function_Language, +) (string, error) { replaceFunc := makeDBNameReplaceFunc(newDB) + switch lang { + case catpb.Function_SQL: + fmtCtx := tree.NewFmtCtx(tree.FmtSimple) + stmts, err := parser.Parse(fnBody) + if err != nil { + return "", err + } + for i, stmt := range stmts { + if i > 0 { + fmtCtx.WriteString("\n") + } + f := tree.NewFmtCtx( + tree.FmtParsable, + tree.FmtReformatTableNames(replaceFunc), + ) + f.FormatNode(stmt.AST) + fmtCtx.WriteString(f.CloseAndGetString()) + fmtCtx.WriteString(";") + } + return fmtCtx.CloseAndGetString(), nil - fmtCtx := tree.NewFmtCtx(tree.FmtSimple) - for i, stmt := range stmts { - if i > 0 { - fmtCtx.WriteString("\n") + case catpb.Function_PLPGSQL: + stmt, err := plpgsqlparser.Parse(fnBody) + if err != nil { + return "", err } - f := tree.NewFmtCtx( + fmtCtx := tree.NewFmtCtx( tree.FmtParsable, tree.FmtReformatTableNames(replaceFunc), ) - f.FormatNode(stmt.AST) - fmtCtx.WriteString(f.CloseAndGetString()) - fmtCtx.WriteString(";") + fmtCtx.FormatNode(stmt.AST) + return fmtCtx.CloseAndGetString(), nil + + default: + return "", errors.AssertionFailedf("unexpected function language %s", lang) } - return fmtCtx.CloseAndGetString(), nil } // rewriteTypesInExpr rewrites all explicit ID type references in the input @@ -457,23 +479,40 @@ func rewriteSequencesInView(viewQuery string, rewrites jobspb.DescRewriteMap) (s return newStmt.String(), nil } -func rewriteSequencesInFunction(fnBody string, rewrites jobspb.DescRewriteMap) (string, error) { - stmts, err := parser.Parse(fnBody) - if err != nil { - return "", err - } - +func rewriteSequencesInFunction( + fnBody string, rewrites jobspb.DescRewriteMap, lang catpb.Function_Language, +) (string, error) { fmtCtx := tree.NewFmtCtx(tree.FmtSimple) - for i, stmt := range stmts { - newStmt, err := tree.SimpleStmtVisit(stmt.AST, makeSequenceReplaceFunc(rewrites)) + replaceSeqFunc := makeSequenceReplaceFunc(rewrites) + switch lang { + case catpb.Function_SQL: + stmts, err := parser.Parse(fnBody) if err != nil { return "", err } - if i > 0 { - fmtCtx.WriteString("\n") + for i, stmt := range stmts { + newStmt, err := tree.SimpleStmtVisit(stmt.AST, replaceSeqFunc) + if err != nil { + return "", err + } + if i > 0 { + fmtCtx.WriteString("\n") + } + fmtCtx.FormatNode(newStmt) + fmtCtx.WriteString(";") + } + + case catpb.Function_PLPGSQL: + stmt, err := plpgsqlparser.Parse(fnBody) + if err != nil { + return "", err } + v := utils.SQLStmtVisitor{Fn: replaceSeqFunc} + newStmt := plpgsqltree.Walk(&v, stmt.AST) fmtCtx.FormatNode(newStmt) - fmtCtx.WriteString(";") + + default: + return "", errors.AssertionFailedf("unexpected function language %s", lang) } return fmtCtx.CloseAndGetString(), nil } @@ -913,13 +952,13 @@ func FunctionDescs( // Rewrite function body. fnBody := fnDesc.FunctionBody if overrideDB != "" { - dbNameReplaced, err := rewriteFunctionBodyDBNames(fnDesc.FunctionBody, overrideDB) + dbNameReplaced, err := rewriteFunctionBodyDBNames(fnBody, overrideDB, fnDesc.Lang) if err != nil { return err } fnBody = dbNameReplaced } - fnBody, err := rewriteSequencesInFunction(fnBody, descriptorRewrites) + fnBody, err := rewriteSequencesInFunction(fnBody, descriptorRewrites, fnDesc.Lang) if err != nil { return err }