From db890b090b00fc60b09b1034be7a2c200da52eef Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Tue, 14 Apr 2026 22:09:41 -0400 Subject: [PATCH] parser: sync latest pg regression suite & update create schema parsing --- crates/squawk_parser/src/grammar.rs | 46 +++++- .../tests__regression_compression_pglz.snap | 5 + .../tests__regression_database_ddl.snap | 5 + .../snapshots/tests__regression_role_ddl.snap | 5 + .../tests__regression_tablespace_ddl.snap | 5 + postgres/regression_suite/cluster.sql | 6 + .../regression_suite/collate.icu.utf8.sql | 45 ++++++ .../regression_suite/compression_pglz.sql | 53 +++++++ postgres/regression_suite/conversion.sql | 27 ---- postgres/regression_suite/copy2.sql | 2 + postgres/regression_suite/create_schema.sql | 108 ++++++++++++- postgres/regression_suite/database_ddl.sql | 66 ++++++++ postgres/regression_suite/eager_aggregate.sql | 14 ++ postgres/regression_suite/encoding.sql | 11 +- postgres/regression_suite/explain.sql | 7 +- postgres/regression_suite/foreign_key.sql | 145 ++++++++++++++++++ postgres/regression_suite/jsonb_jsonpath.sql | 106 +++++++++++++ postgres/regression_suite/jsonpath.sql | 41 +++++ postgres/regression_suite/misc_functions.sql | 7 + postgres/regression_suite/namespace.sql | 11 +- postgres/regression_suite/object_address.sql | 2 +- postgres/regression_suite/partition_prune.sql | 71 +++++++++ postgres/regression_suite/partition_split.sql | 14 +- postgres/regression_suite/publication.sql | 107 +++++++++++++ postgres/regression_suite/returning.sql | 20 +++ postgres/regression_suite/role_ddl.sql | 96 ++++++++++++ .../regression_suite/sqljson_queryfuncs.sql | 11 +- postgres/regression_suite/stats.sql | 20 ++- postgres/regression_suite/subselect.sql | 14 ++ postgres/regression_suite/tablespace_ddl.sql | 58 +++++++ postgres/regression_suite/tid.sql | 21 +++ postgres/regression_suite/union.sql | 4 + postgres/regression_suite/window.sql | 26 ++++ .../regression_suite/without_overlaps.sql | 31 ++++ postgres/regression_suite/xml.sql | 4 +- 35 files changed, 1145 insertions(+), 69 deletions(-) create mode 100644 crates/squawk_parser/tests/snapshots/tests__regression_compression_pglz.snap create mode 100644 crates/squawk_parser/tests/snapshots/tests__regression_database_ddl.snap create mode 100644 crates/squawk_parser/tests/snapshots/tests__regression_role_ddl.snap create mode 100644 crates/squawk_parser/tests/snapshots/tests__regression_tablespace_ddl.snap create mode 100644 postgres/regression_suite/compression_pglz.sql create mode 100644 postgres/regression_suite/database_ddl.sql create mode 100644 postgres/regression_suite/role_ddl.sql create mode 100644 postgres/regression_suite/tablespace_ddl.sql diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index 654eb56b6..db9450a65 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -12839,11 +12839,6 @@ fn drop_schema(p: &mut Parser<'_>) -> CompletedMarker { } // An SQL statement defining an object to be created within the schema. -// -// Currently, only CREATE TABLE, CREATE VIEW, CREATE INDEX, CREATE SEQUENCE, -// CREATE TRIGGER and GRANT are accepted as clauses within CREATE SCHEMA. Other -// kinds of objects may be created in separate commands after the schema is -// created. fn opt_schema_elements(p: &mut Parser<'_>) { while !p.at(EOF) { match (p.current(), p.nth(1)) { @@ -12864,9 +12859,11 @@ fn opt_schema_elements(p: &mut Parser<'_>) { } (CREATE_KW, OR_KW) => { match p.nth(3) { + AGGREGATE_KW => create_aggregate(p), CONSTRAINT_KW | TRIGGER_KW => create_trigger(p), + PROCEDURE_KW => create_procedure(p), RECURSIVE_KW | TEMP_KW | TEMPORARY_KW | VIEW_KW => create_view(p), - _ => return, + _ => create_function(p), }; } (CREATE_KW, RECURSIVE_KW | VIEW_KW) => { @@ -12884,6 +12881,43 @@ fn opt_schema_elements(p: &mut Parser<'_>) { (CREATE_KW, INDEX_KW | UNIQUE_KW) => { create_index(p); } + (CREATE_KW, AGGREGATE_KW) => { + create_aggregate(p); + } + (CREATE_KW, COLLATION_KW) => { + create_collation(p); + } + (CREATE_KW, DOMAIN_KW) => { + create_domain(p); + } + (CREATE_KW, FUNCTION_KW) => { + create_function(p); + } + (CREATE_KW, OPERATOR_KW) => { + match p.nth(2) { + CLASS_KW => create_operator_class(p), + FAMILY_KW => create_operator_family(p), + _ => create_operator(p), + }; + } + (CREATE_KW, PROCEDURE_KW) => { + create_procedure(p); + } + (CREATE_KW, TEXT_KW) if p.nth_at(2, SEARCH_KW) => { + match p.nth(3) { + CONFIGURATION_KW => create_text_search_config(p), + DICTIONARY_KW => create_text_search_dict(p), + PARSER_KW => create_text_search_parser(p), + TEMPLATE_KW => create_text_search_template(p), + _ => return, + }; + } + (CREATE_KW, TYPE_KW) => { + create_type(p); + } + (GRANT_KW, _) => { + grant(p); + } _ => return, }; } diff --git a/crates/squawk_parser/tests/snapshots/tests__regression_compression_pglz.snap b/crates/squawk_parser/tests/snapshots/tests__regression_compression_pglz.snap new file mode 100644 index 000000000..e353a0819 --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__regression_compression_pglz.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: postgres/regression_suite/compression_pglz.sql +--- + diff --git a/crates/squawk_parser/tests/snapshots/tests__regression_database_ddl.snap b/crates/squawk_parser/tests/snapshots/tests__regression_database_ddl.snap new file mode 100644 index 000000000..467a60403 --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__regression_database_ddl.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: postgres/regression_suite/database_ddl.sql +--- + diff --git a/crates/squawk_parser/tests/snapshots/tests__regression_role_ddl.snap b/crates/squawk_parser/tests/snapshots/tests__regression_role_ddl.snap new file mode 100644 index 000000000..025d9730b --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__regression_role_ddl.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: postgres/regression_suite/role_ddl.sql +--- + diff --git a/crates/squawk_parser/tests/snapshots/tests__regression_tablespace_ddl.snap b/crates/squawk_parser/tests/snapshots/tests__regression_tablespace_ddl.snap new file mode 100644 index 000000000..1e92e1b88 --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__regression_tablespace_ddl.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: postgres/regression_suite/tablespace_ddl.sql +--- + diff --git a/postgres/regression_suite/cluster.sql b/postgres/regression_suite/cluster.sql index eb987f36a..51e6bb241 100644 --- a/postgres/regression_suite/cluster.sql +++ b/postgres/regression_suite/cluster.sql @@ -248,6 +248,9 @@ REPACK clstrpart; CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname) ORDER BY relname COLLATE "C"; +-- CONCURRENTLY doesn't like partitioned tables +REPACK (CONCURRENTLY) clstrpart; + DROP TABLE clstrpart; -- Ownership of partitions is checked @@ -383,6 +386,9 @@ JOIN relnodes_new n ON o.relname = n.relname WHERE o.relfilenode <> n.relfilenode ORDER BY o.relname; +-- concurrently +REPACK (CONCURRENTLY) pg_class; + -- clean up DROP TABLE clustertest; DROP TABLE clstr_1; diff --git a/postgres/regression_suite/collate.icu.utf8.sql b/postgres/regression_suite/collate.icu.utf8.sql index d0688a4d9..947fae123 100644 --- a/postgres/regression_suite/collate.icu.utf8.sql +++ b/postgres/regression_suite/collate.icu.utf8.sql @@ -990,6 +990,51 @@ RESET enable_partitionwise_aggregate; RESET max_parallel_workers_per_gather; RESET enable_incremental_sort; +-- +-- Test for eager aggregation non-deterministic collation bug +-- + +CREATE TABLE eager_agg_t1 (id int, val text COLLATE case_insensitive); +CREATE TABLE eager_agg_t2 (val text COLLATE case_insensitive); + +INSERT INTO eager_agg_t1 SELECT 1, 'a' FROM generate_series(1, 50); +INSERT INTO eager_agg_t1 SELECT 1, 'A' FROM generate_series(1, 50); +INSERT INTO eager_agg_t2 VALUES ('A'); + +ANALYZE eager_agg_t1; +ANALYZE eager_agg_t2; + +-- Ensure that eager aggregation is not used for t1.val due to the +-- non-deterministic collation. +EXPLAIN (COSTS OFF) +SELECT t1.id, count(t1.val) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.val = t2.val COLLATE "C" +GROUP BY t1.id; + +-- Ensure it returns 1 row with count = 50 +SELECT t1.id, count(t1.val) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.val = t2.val COLLATE "C" +GROUP BY t1.id; + +-- Ensure that eager aggregation is not used when grouping by a column with +-- non-deterministic collation. +EXPLAIN (COSTS OFF) +SELECT t1.id, t1.val, count(t1.val) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.val = t2.val COLLATE "C" +GROUP BY t1.id, t1.val; + +-- Ensure it returns 1 row with count = 50 +SELECT t1.id, t1.val, count(t1.val) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.val = t2.val COLLATE "C" +GROUP BY t1.id, t1.val; + +DROP TABLE eager_agg_t1; +DROP TABLE eager_agg_t2; + -- virtual generated columns CREATE TABLE t5 ( a int, diff --git a/postgres/regression_suite/compression_pglz.sql b/postgres/regression_suite/compression_pglz.sql new file mode 100644 index 000000000..981301764 --- /dev/null +++ b/postgres/regression_suite/compression_pglz.sql @@ -0,0 +1,53 @@ +-- +-- Tests for PGLZ compression +-- + +-- directory paths and dlsuffix are passed to us in environment variables +-- \getenv libdir PG_LIBDIR +-- \getenv dlsuffix PG_DLSUFFIX + +-- \set regresslib :libdir '/regress' :dlsuffix + +CREATE FUNCTION test_pglz_compress(bytea) + RETURNS bytea + AS 'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_pglz_decompress(bytea, int4, bool) + RETURNS bytea + AS 'regresslib' LANGUAGE C STRICT; + +-- Round-trip with pglz: compress then decompress. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, false) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, true) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; + +-- Decompression with rawsize too large, fails to fill the destination +-- buffer. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 500, true); + +-- Decompression with rawsize too small, fails with source not fully +-- consumed. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 100, true); + +-- Corrupted compressed data. Set control bit with read of a match tag, +-- no data follows. +SELECT length(test_pglz_decompress('\x01'::bytea, 1024, false)) AS ctrl_only_len; +SELECT test_pglz_decompress('\x01'::bytea, 1024, true); + +-- Corrupted compressed data. Set control bit with read of a match tag, +-- 1 byte follows. +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, false); +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, true); + +-- Corrupted compressed data. Set control bit with match tag where length +-- nibble is 3 bytes (extended length), no data follows. +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, false); +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, true); + +-- Clean up +DROP FUNCTION test_pglz_compress; +DROP FUNCTION test_pglz_decompress; diff --git a/postgres/regression_suite/conversion.sql b/postgres/regression_suite/conversion.sql index 70b6e4987..9069c52fa 100644 --- a/postgres/regression_suite/conversion.sql +++ b/postgres/regression_suite/conversion.sql @@ -329,7 +329,6 @@ select description, inbytes, (test_conv(inbytes, 'iso8859-5', 'iso8859-5')).* fr -- Test conversions from ISO-8859-5 select description, inbytes, (test_conv(inbytes, 'iso8859-5', 'utf8')).* from iso8859_5_inputs; select description, inbytes, (test_conv(inbytes, 'iso8859-5', 'koi8r')).* from iso8859_5_inputs; -select description, inbytes, (test_conv(inbytes, 'iso8859_5', 'mule_internal')).* from iso8859_5_inputs; -- -- Big5 @@ -346,29 +345,3 @@ insert into big5_inputs values select description, inbytes, (test_conv(inbytes, 'big5', 'big5')).* from big5_inputs; -- Test conversions from Big5 select description, inbytes, (test_conv(inbytes, 'big5', 'utf8')).* from big5_inputs; -select description, inbytes, (test_conv(inbytes, 'big5', 'mule_internal')).* from big5_inputs; - --- --- MULE_INTERNAL --- -CREATE TABLE mic_inputs (inbytes bytea, description text); -insert into mic_inputs values - ('\x666f6f', 'valid, pure ASCII'), - ('\x8bc68bcf8bcf', 'valid (in KOI8R)'), - ('\x8bc68bcf8b', 'invalid,incomplete char'), - ('\x92bedd', 'valid (in SHIFT_JIS)'), - ('\x92be', 'invalid, incomplete char)'), - ('\x666f6f95a3c1', 'valid (in Big5)'), - ('\x666f6f95a3', 'invalid, incomplete char'), - ('\x9200bedd', 'invalid, NUL byte'), - ('\x92bedd00', 'invalid, NUL byte'), - ('\x8b00c68bcf8bcf', 'invalid, NUL byte'); - --- Test MULE_INTERNAL verification -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'mule_internal')).* from mic_inputs; --- Test conversions from MULE_INTERNAL -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'koi8r')).* from mic_inputs; -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'iso8859-5')).* from mic_inputs; -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'sjis')).* from mic_inputs; -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'big5')).* from mic_inputs; -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'euc_jp')).* from mic_inputs; diff --git a/postgres/regression_suite/copy2.sql b/postgres/regression_suite/copy2.sql index 81e3b0b7d..936ad0217 100644 --- a/postgres/regression_suite/copy2.sql +++ b/postgres/regression_suite/copy2.sql @@ -168,6 +168,8 @@ COPY x from stdin WHERE a IN (generate_series(1,5)); -- COPY x from stdin WHERE a = row_number() over(b); -- +COPY x from stdin WHERE tableoid = 'x'::regclass; +-- -- -- -- check results of copy in SELECT * FROM x; diff --git a/postgres/regression_suite/create_schema.sql b/postgres/regression_suite/create_schema.sql index 140d143b5..c27096245 100644 --- a/postgres/regression_suite/create_schema.sql +++ b/postgres/regression_suite/create_schema.sql @@ -8,7 +8,7 @@ CREATE ROLE regress_create_schema_role SUPERUSER; -- Cases where schema creation fails as objects are qualified with a schema -- that does not match with what's expected. --- This checks all the object types that include schema qualifications. +-- This checks most object types that include schema qualifications. CREATE SCHEMA AUTHORIZATION regress_create_schema_role CREATE SEQUENCE schema_not_existing.seq; CREATE SCHEMA AUTHORIZATION regress_create_schema_role @@ -20,6 +20,9 @@ CREATE SCHEMA AUTHORIZATION regress_create_schema_role CREATE SCHEMA AUTHORIZATION regress_create_schema_role CREATE TRIGGER schema_trig BEFORE INSERT ON schema_not_existing.tab EXECUTE FUNCTION schema_trig.no_func(); +CREATE SCHEMA AUTHORIZATION regress_create_schema_role + CREATE FUNCTION schema_not_existing.func(int) RETURNS int + AS 'SELECT $1' LANGUAGE sql; -- Again, with a role specification and no schema names. SET ROLE regress_create_schema_role; CREATE SCHEMA AUTHORIZATION CURRENT_ROLE @@ -47,6 +50,11 @@ CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE EXECUTE FUNCTION schema_trig.no_func(); RESET ROLE; +-- Forward references no longer work in general. +CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE + CREATE VIEW abcd_view AS SELECT a FROM abcd + CREATE TABLE abcd (a int); + -- Cases where the schema creation succeeds. -- The schema created matches the role name. CREATE SCHEMA AUTHORIZATION regress_create_schema_role @@ -66,5 +74,103 @@ CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE DROP SCHEMA regress_schema_1 CASCADE; RESET ROLE; +-- Test forward-referencing foreign key clauses. +CREATE SCHEMA regress_schema_fk + CREATE TABLE regress_schema_fk.t2 ( + b int, + a int REFERENCES t1 DEFERRABLE INITIALLY DEFERRED NOT ENFORCED + REFERENCES t3 DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t1 NOT DEFERRABLE) + + CREATE TABLE regress_schema_fk.t1 (a int PRIMARY KEY) + + CREATE TABLE t3 (a int PRIMARY KEY) + + CREATE TABLE t4 ( + b int, + a int REFERENCES t5 NOT DEFERRABLE ENFORCED + REFERENCES t6 DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t6 DEFERRABLE INITIALLY DEFERRED) + + CREATE TABLE t5 (a int, b int, PRIMARY KEY (a)) + + CREATE TABLE t6 (a int, b int, PRIMARY KEY (a)); + +-- \d regress_schema_fk.t2 +-- \d regress_schema_fk.t4 + +DROP SCHEMA regress_schema_fk CASCADE; + +-- Test miscellaneous object types within CREATE SCHEMA. +CREATE SCHEMA regress_schema_misc + CREATE AGGREGATE cs_sum(int4) + ( + SFUNC = int4_sum(int8, int4), + STYPE = int8, + INITCOND = '0' + ) + + CREATE COLLATION cs_builtin_c ( PROVIDER = builtin, LOCALE = "C" ) + + CREATE DOMAIN cs_positive AS integer CHECK (VALUE > 0) + + CREATE FUNCTION cs_add(int4, int4) returns int4 language sql + as 'select $1 + $2' + + CREATE OPERATOR + (function = cs_add, leftarg = int4, rightarg = int4) + + CREATE PROCEDURE cs_proc(int4, int4) + BEGIN ATOMIC SELECT cs_add($1,$2); END + + CREATE TEXT SEARCH CONFIGURATION cs_ts_conf (copy=english) + + CREATE TEXT SEARCH DICTIONARY cs_ts_dict (template=simple) + + CREATE TEXT SEARCH PARSER cs_ts_prs + (start = prsd_start, gettoken = prsd_nexttoken, end = prsd_end, + lextypes = prsd_lextype) + + CREATE TEXT SEARCH TEMPLATE cs_ts_temp (lexize=dsimple_lexize) + + CREATE TYPE regress_schema_misc.cs_enum AS ENUM ('red', 'orange') + + CREATE TYPE cs_composite AS (a int, b float8) + + CREATE TYPE cs_range AS RANGE (subtype = float8, subtype_diff = float8mi) + + -- demonstrate creation of a base type with its I/O functions + + CREATE TYPE cs_type + + CREATE FUNCTION cs_type_in(cstring) + RETURNS cs_type LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT + AS 'int4in' + + CREATE FUNCTION cs_type_out(cs_type) + RETURNS cstring LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT + AS 'int4out' + + CREATE TYPE cs_type ( + INPUT = cs_type_in, + OUTPUT = cs_type_out, + LIKE = int4 + ) + + GRANT USAGE ON TYPE cs_type TO public +; + +-- \df regress_schema_misc.cs_add +-- \df regress_schema_misc.cs_proc +-- \da regress_schema_misc.cs_sum +-- \do regress_schema_misc.+ +-- \dO regress_schema_misc.* +-- \dT regress_schema_misc.* +-- \dF regress_schema_misc.* +-- \dFd regress_schema_misc.* +-- \dFp regress_schema_misc.* +-- \dFt regress_schema_misc.* + +DROP SCHEMA regress_schema_misc CASCADE; + -- Clean up DROP ROLE regress_create_schema_role; diff --git a/postgres/regression_suite/database_ddl.sql b/postgres/regression_suite/database_ddl.sql new file mode 100644 index 000000000..e06a5c404 --- /dev/null +++ b/postgres/regression_suite/database_ddl.sql @@ -0,0 +1,66 @@ +-- +-- Tests for pg_get_database_ddl() +-- + +-- To produce stable regression test output, strip locale/collation details +-- from the DDL output. Uses a plain SQL function to avoid a PL/pgSQL +-- dependency. + +CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT) +RETURNS TEXT LANGUAGE sql AS $$ +SELECT regexp_replace( + regexp_replace( + regexp_replace( + regexp_replace( + regexp_replace( + ddl_input, + '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)', '', 'gi'), + '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1', '', 'gi'), + '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1', '', 'gi'), + '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1', '', 'gi'), + '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1', '', 'gi') +$$; + +CREATE ROLE regress_datdba; +CREATE DATABASE regression_database_ddl + ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0 + OWNER regress_datdba; +ALTER DATABASE regression_database_ddl CONNECTION_LIMIT 123; +ALTER DATABASE regression_database_ddl SET random_page_cost = 2.0; +ALTER ROLE regress_datdba IN DATABASE regression_database_ddl SET random_page_cost = 1.1; + +-- Database doesn't exist +SELECT * FROM pg_get_database_ddl('regression_database'); + +-- NULL value +SELECT * FROM pg_get_database_ddl(NULL); + +-- Invalid option value (should error) +SELECT * FROM pg_get_database_ddl('regression_database_ddl', 'owner', 'invalid'); + +-- Duplicate option (should error) +SELECT * FROM pg_get_database_ddl('regression_database_ddl', 'owner', 'false', 'owner', 'true'); + +-- Without options +SELECT ddl_filter(pg_get_database_ddl) FROM pg_get_database_ddl('regression_database_ddl'); + +-- With owner +SELECT ddl_filter(pg_get_database_ddl) FROM pg_get_database_ddl('regression_database_ddl', 'owner', 'true'); + +-- Pretty-printed output +-- \pset format unaligned +SELECT ddl_filter(pg_get_database_ddl) FROM pg_get_database_ddl('regression_database_ddl', 'pretty', 'true', 'tablespace', 'false'); +-- \pset format aligned + +-- Permission check: revoke CONNECT on database +CREATE ROLE regress_db_ddl_noaccess; +REVOKE CONNECT ON DATABASE regression_database_ddl FROM PUBLIC; +SET ROLE regress_db_ddl_noaccess; +SELECT * FROM pg_get_database_ddl('regression_database_ddl'); -- should fail +RESET ROLE; +GRANT CONNECT ON DATABASE regression_database_ddl TO PUBLIC; +DROP ROLE regress_db_ddl_noaccess; + +DROP DATABASE regression_database_ddl; +DROP FUNCTION ddl_filter(text); +DROP ROLE regress_datdba; diff --git a/postgres/regression_suite/eager_aggregate.sql b/postgres/regression_suite/eager_aggregate.sql index abe6d6ae0..53d9b377a 100644 --- a/postgres/regression_suite/eager_aggregate.sql +++ b/postgres/regression_suite/eager_aggregate.sql @@ -163,6 +163,20 @@ GROUP BY t1.a ORDER BY t1.a; RESET geqo; RESET geqo_threshold; +-- Ensure eager aggregation is not applied because random() is a volatile +-- function +EXPLAIN (COSTS OFF) +SELECT t1.a, avg(t2.c + random()) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +EXPLAIN (COSTS OFF) +SELECT t1.a, avg(t2.c) FILTER (WHERE random() > 0.5) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + DROP TABLE eager_agg_t1; DROP TABLE eager_agg_t2; DROP TABLE eager_agg_t3; diff --git a/postgres/regression_suite/encoding.sql b/postgres/regression_suite/encoding.sql index 2071729d5..047ef03d9 100644 --- a/postgres/regression_suite/encoding.sql +++ b/postgres/regression_suite/encoding.sql @@ -199,16 +199,7 @@ INSERT INTO encoding_tests VALUES ('UTF8', '6 byte, unsupported', '\xfd8283'), ('UTF8', '6 byte, unsupported', '\xfd828384'), ('UTF8', '6 byte, unsupported', '\xfd82838485'), - ('UTF8', '6 byte, unsupported', '\xfd8283848586'), - -- MULE_INTERNAL - -- 2 81..8d LC1 - -- 3 90..99 LC2 - ('MULE_INTERNAL', 'ASCII', 'a'), - ('MULE_INTERNAL', 'LC1, short', '\x81'), - ('MULE_INTERNAL', 'LC1', '\x8182'), - ('MULE_INTERNAL', 'LC2, short', '\x90'), - ('MULE_INTERNAL', 'LC2, short', '\x9082'), - ('MULE_INTERNAL', 'LC2', '\x908283'); + ('UTF8', '6 byte, unsupported', '\xfd8283848586'); SELECT COUNT(test_encoding(encoding, description, input)) > 0 FROM encoding_tests; diff --git a/postgres/regression_suite/explain.sql b/postgres/regression_suite/explain.sql index ebdab4260..c32fa47b7 100644 --- a/postgres/regression_suite/explain.sql +++ b/postgres/regression_suite/explain.sql @@ -66,10 +66,13 @@ select explain_filter('explain select * from int8_tbl i8'); select explain_filter('explain (analyze, buffers off) select * from int8_tbl i8'); select explain_filter('explain (analyze, buffers off, verbose) select * from int8_tbl i8'); select explain_filter('explain (analyze, buffers, format text) select * from int8_tbl i8'); -select explain_filter('explain (analyze, buffers, format xml) select * from int8_tbl i8'); -select explain_filter('explain (analyze, serialize, buffers, format yaml) select * from int8_tbl i8'); select explain_filter('explain (buffers, format text) select * from int8_tbl i8'); + +-- \a +select explain_filter('explain (analyze, buffers, io, format xml) select * from int8_tbl i8'); +select explain_filter('explain (analyze, serialize, buffers, io, format yaml) select * from int8_tbl i8'); select explain_filter('explain (buffers, format json) select * from int8_tbl i8'); +-- \a -- Check expansion of window definitions diff --git a/postgres/regression_suite/foreign_key.sql b/postgres/regression_suite/foreign_key.sql index 4318a2093..673dc16b3 100644 --- a/postgres/regression_suite/foreign_key.sql +++ b/postgres/regression_suite/foreign_key.sql @@ -2535,3 +2535,148 @@ WITH cte AS ( DROP SCHEMA fkpart13 CASCADE; RESET search_path; + +-- Tests foreign key check fast-path no-cache path. +CREATE TABLE fp_pk_alter (a int PRIMARY KEY); +INSERT INTO fp_pk_alter SELECT generate_series(1, 100); +CREATE TABLE fp_fk_alter (a int); +INSERT INTO fp_fk_alter SELECT generate_series(1, 100); +-- Validation path: should succeed +ALTER TABLE fp_fk_alter ADD FOREIGN KEY (a) REFERENCES fp_pk_alter; +INSERT INTO fp_fk_alter VALUES (101); -- should fail (constraint active) +DROP TABLE fp_fk_alter, fp_pk_alter; + +-- Separate test: validation catches existing violation +CREATE TABLE fp_pk_alter2 (a int PRIMARY KEY); +INSERT INTO fp_pk_alter2 VALUES (1); +CREATE TABLE fp_fk_alter2 (a int); +INSERT INTO fp_fk_alter2 VALUES (1), (200); -- 200 has no PK match +ALTER TABLE fp_fk_alter2 ADD FOREIGN KEY (a) REFERENCES fp_pk_alter2; -- should fail +DROP TABLE fp_fk_alter2, fp_pk_alter2; + +-- Tests that the fast-path handles caching for multiple constraints +CREATE TABLE fp_pk1 (a int PRIMARY KEY); +CREATE TABLE fp_pk2 (b int PRIMARY KEY); +INSERT INTO fp_pk1 VALUES (1); +INSERT INTO fp_pk2 VALUES (1); +CREATE TABLE fp_multi_fk ( + a int REFERENCES fp_pk1, + b int REFERENCES fp_pk2 +); +INSERT INTO fp_multi_fk VALUES (1, 1); -- two constraints, one batch +INSERT INTO fp_multi_fk VALUES (1, 2); -- second constraint fails +DROP TABLE fp_multi_fk, fp_pk1, fp_pk2; + +-- Test that fast-path cache handles deferred constraints and SET CONSTRAINTS IMMEDIATE +CREATE TABLE fp_pk_defer (a int PRIMARY KEY); +CREATE TABLE fp_fk_defer (a int REFERENCES fp_pk_defer DEFERRABLE INITIALLY DEFERRED); +INSERT INTO fp_pk_defer VALUES (1), (2); + +BEGIN; +INSERT INTO fp_fk_defer VALUES (1); +INSERT INTO fp_fk_defer VALUES (2); +SET CONSTRAINTS ALL IMMEDIATE; -- fires batch callback here +INSERT INTO fp_fk_defer VALUES (3); -- should fail, also tests that cache was cleaned up +COMMIT; +DROP TABLE fp_pk_defer, fp_fk_defer; + +-- Subtransaction abort: cached state must be invalidated on ROLLBACK TO +CREATE TABLE fp_pk_subxact (a int PRIMARY KEY); +CREATE TABLE fp_fk_subxact (a int REFERENCES fp_pk_subxact); +INSERT INTO fp_pk_subxact VALUES (1), (2); +BEGIN; +INSERT INTO fp_fk_subxact VALUES (1); +SAVEPOINT sp1; +INSERT INTO fp_fk_subxact VALUES (2); +ROLLBACK TO sp1; +INSERT INTO fp_fk_subxact VALUES (1); +COMMIT; +SELECT * FROM fp_fk_subxact; +DROP TABLE fp_fk_subxact, fp_pk_subxact; + +-- FK check must see PK rows inserted by earlier AFTER triggers +-- firing on the same statement +CREATE TABLE fp_pk_cci (a int PRIMARY KEY); +CREATE TABLE fp_fk_cci (a int REFERENCES fp_pk_cci); + +CREATE FUNCTION fp_auto_pk() RETURNS trigger AS $$ +BEGIN + RAISE NOTICE 'fp_auto_pk called'; + INSERT INTO fp_pk_cci VALUES (NEW.a); + RETURN NEW; +END $$ LANGUAGE plpgsql; + +-- Name sorts before the RI trigger, so fires first per row +CREATE TRIGGER "AAA_auto" AFTER INSERT ON fp_fk_cci + FOR EACH ROW EXECUTE FUNCTION fp_auto_pk(); + +-- Should succeed: AAA_auto provisions the PK row before RI check +INSERT INTO fp_fk_cci VALUES (1), (2), (3); + +DROP TABLE fp_fk_cci, fp_pk_cci; +DROP FUNCTION fp_auto_pk; + +-- Multi-column FK: exercises batched per-row probing with composite keys +CREATE TABLE fp_pk_multi (a int, b int, PRIMARY KEY (a, b)); +INSERT INTO fp_pk_multi SELECT i, i FROM generate_series(1, 100) i; +CREATE TABLE fp_fk_multi (x int, a int, b int, + FOREIGN KEY (a, b) REFERENCES fp_pk_multi); +INSERT INTO fp_fk_multi SELECT i, i, i FROM generate_series(1, 100) i; +INSERT INTO fp_fk_multi VALUES (1, 999, 999); +DROP TABLE fp_fk_multi, fp_pk_multi; + +-- Multi-column FK with columns in different order than PK index. +-- The FK references columns in a different order than they appear in the +-- PK's primary key, which requires mapping constraint key positions to +-- index column positions when building scan keys. +CREATE TABLE fp_pk_order (a int, b text, c int, PRIMARY KEY (a, b, c)); +INSERT INTO fp_pk_order VALUES (1, 'one', 10), (2, 'two', 20); +CREATE TABLE fp_fk_order ( + x int, + c int, + b text, + a int, + FOREIGN KEY (a, c, b) REFERENCES fp_pk_order (a, c, b) -- c and b swapped +); +INSERT INTO fp_fk_order VALUES (1, 10, 'one', 1); -- should succeed +INSERT INTO fp_fk_order VALUES (2, 20, 'two', 2); -- should succeed +INSERT INTO fp_fk_order VALUES (3, 99, 'none', 9); -- should fail +DROP TABLE fp_fk_order, fp_pk_order; + +-- Same-type columns in different order: +CREATE TABLE fp_pk_same (c1 int, c2 int, PRIMARY KEY (c1, c2)); +INSERT INTO fp_pk_same VALUES (1, 2); +CREATE TABLE fp_fk_same (c1 int, c2 int, + FOREIGN KEY (c2, c1) REFERENCES fp_pk_same (c2, c1)); +INSERT INTO fp_fk_same VALUES (1, 2); -- should succeed +INSERT INTO fp_fk_same VALUES (9, 9); -- should fail +DROP TABLE fp_fk_same, fp_pk_same; + +-- Deferred constraint: batch flushed at COMMIT, not at statement end +CREATE TABLE fp_pk_commit (a int PRIMARY KEY); +CREATE TABLE fp_fk_commit (a int REFERENCES fp_pk_commit + DEFERRABLE INITIALLY DEFERRED); +INSERT INTO fp_pk_commit VALUES (1); +BEGIN; +INSERT INTO fp_fk_commit VALUES (1); +INSERT INTO fp_fk_commit VALUES (1); +INSERT INTO fp_fk_commit VALUES (999); +COMMIT; +DROP TABLE fp_fk_commit, fp_pk_commit; + +-- Cross-type FK with bulk insert: int8 FK referencing int4 PK, +-- values cast during array construction +CREATE TABLE fp_pk_cross (a int4 PRIMARY KEY); +INSERT INTO fp_pk_cross SELECT generate_series(1, 200); +CREATE TABLE fp_fk_cross (a int8 REFERENCES fp_pk_cross); +INSERT INTO fp_fk_cross SELECT generate_series(1, 200); +INSERT INTO fp_fk_cross VALUES (999); +DROP TABLE fp_fk_cross, fp_pk_cross; + +-- Duplicate FK values: when using the batched SAOP path, every +-- row must be recognized as satisfied, not just the first match +CREATE TABLE fp_pk_dup (a int PRIMARY KEY); +INSERT INTO fp_pk_dup VALUES (1); +CREATE TABLE fp_fk_dup (a int REFERENCES fp_pk_dup); +INSERT INTO fp_fk_dup SELECT 1 FROM generate_series(1, 100); +DROP TABLE fp_fk_dup, fp_pk_dup; diff --git a/postgres/regression_suite/jsonb_jsonpath.sql b/postgres/regression_suite/jsonb_jsonpath.sql index 3e8929a52..d3a38c577 100644 --- a/postgres/regression_suite/jsonb_jsonpath.sql +++ b/postgres/regression_suite/jsonb_jsonpath.sql @@ -623,6 +623,112 @@ select jsonb_path_query('"2023-08-15 12:34:56 +5:30"', '$.timestamp_tz().string( select jsonb_path_query('"2023-08-15 12:34:56"', '$.timestamp().string()'); rollback; +-- test .ltrim() +select jsonb_path_query('" hello "', '$.ltrim(" ")'); +select jsonb_path_query('" hello "', '$.ltrim()'); +select jsonb_path_query('"zzzytest"', '$.ltrim("xyz")'); +select jsonb_path_query('null', '$.ltrim()'); +select jsonb_path_query('null', '$.ltrim()', silent => true); +select jsonb_path_query('[]', '$.ltrim()'); +select jsonb_path_query('[]', 'strict $.ltrim()'); +select jsonb_path_query('{}', '$.ltrim()'); +select jsonb_path_query('[]', 'strict $.ltrim()', silent => true); +select jsonb_path_query('{}', '$.ltrim()', silent => true); +select jsonb_path_query('1.23', '$.ltrim()'); +select jsonb_path_query('"1.23"', '$.ltrim()'); +select jsonb_path_query('"1.23aaa"', '$.ltrim()'); +select jsonb_path_query('1234', '$.ltrim()'); +select jsonb_path_query('true', '$.ltrim()'); +select jsonb_path_query('1234', '$.ltrim().type()'); +select jsonb_path_query('[2, true]', '$.ltrim()'); +select jsonb_path_query_array('[" maybe ", " yes", " no"]', '$[*].ltrim()'); +select jsonb_path_query_array('[" maybe ", " yes", " no"]', '$[*].ltrim().type()'); + +-- test .rtrim() +select jsonb_path_query('" hello "', '$.rtrim(" ")'); +select jsonb_path_query('"testxxzx"', '$.rtrim("xyz")'); +select jsonb_path_query('" hello "', '$.rtrim()'); + +-- test .btrim() +select jsonb_path_query('" hello "', '$.btrim(" ")'); +select jsonb_path_query('"xyxtrimyyx"', '$.btrim("xyz")'); +select jsonb_path_query('" hello "', '$.btrim()'); + +-- test .lower() +select jsonb_path_query('null', '$.lower()'); +select jsonb_path_query('null', '$.lower()', silent => true); +select jsonb_path_query('[]', '$.lower()'); +select jsonb_path_query('[]', 'strict $.lower()'); +select jsonb_path_query('{}', '$.lower()'); +select jsonb_path_query('[]', 'strict $.lower()', silent => true); +select jsonb_path_query('{}', '$.lower()', silent => true); +select jsonb_path_query('1.23', '$.lower()'); +select jsonb_path_query('"1.23"', '$.lower()'); +select jsonb_path_query('"1.23aaa"', '$.lower()'); +select jsonb_path_query('1234', '$.lower()'); +select jsonb_path_query('true', '$.lower()'); +select jsonb_path_query('1234', '$.lower().type()'); +select jsonb_path_query('[2, true]', '$.lower()'); +select jsonb_path_query_array('["maybe", "yes", "no"]', '$[*].lower()'); +select jsonb_path_query_array('["maybe", "yes", "no"]', '$[*].lower().type()'); + +-- test .upper() +select jsonb_path_query('null', '$.upper()'); +select jsonb_path_query('null', '$.upper()', silent => true); +select jsonb_path_query('[]', '$.upper()'); +select jsonb_path_query('[]', 'strict $.upper()'); +select jsonb_path_query('{}', '$.upper()'); +select jsonb_path_query('[]', 'strict $.upper()', silent => true); +select jsonb_path_query('{}', '$.upper()', silent => true); +select jsonb_path_query('1.23', '$.upper()'); +select jsonb_path_query('"1.23"', '$.upper()'); +select jsonb_path_query('"1.23aaa"', '$.upper()'); +select jsonb_path_query('1234', '$.upper()'); +select jsonb_path_query('true', '$.upper()'); +select jsonb_path_query('1234', '$.upper().type()'); +select jsonb_path_query('[2, true]', '$.upper()'); +select jsonb_path_query_array('["maybe", "yes", "no"]', '$[*].upper()'); +select jsonb_path_query_array('["maybe", "yes", "no"]', '$[*].upper().type()'); + +-- test .initcap() +select jsonb_path_query('null', '$.initcap()'); +select jsonb_path_query('null', '$.initcap()', silent => true); +select jsonb_path_query('[]', '$.initcap()'); +select jsonb_path_query('[]', 'strict $.initcap()'); +select jsonb_path_query('{}', '$.initcap()'); +select jsonb_path_query('[]', 'strict $.initcap()', silent => true); +select jsonb_path_query('{}', '$.initcap()', silent => true); +select jsonb_path_query('1.23', '$.initcap()'); +select jsonb_path_query('"1.23"', '$.initcap()'); +select jsonb_path_query('"1.23aaa"', '$.initcap()'); +select jsonb_path_query('1234', '$.initcap()'); +select jsonb_path_query('true', '$.initcap()'); +select jsonb_path_query('1234', '$.initcap().type()'); +select jsonb_path_query('[2, true]', '$.initcap()'); +select jsonb_path_query('["maybe yes", "probably no"]', '$.initcap()'); + +-- Test .replace() +select jsonb_path_query('null', '$.replace("x", "bye")'); +select jsonb_path_query('null', '$.replace("x", "bye")', silent => true); +select jsonb_path_query('["x", "y", "z"]', '$.replace("x", "bye")'); +select jsonb_path_query('{}', '$.replace("x", "bye")'); +select jsonb_path_query('[]', 'strict $.replace("x", "bye")', silent => true); +select jsonb_path_query('{}', '$.replace("x", "bye")', silent => true); +select jsonb_path_query('1.23', '$.replace("x", "bye")'); +select jsonb_path_query('"hello world"', '$.replace("hello","bye")'); +select jsonb_path_query('"hello world"', '$.replace("hello","bye") starts with "bye"'); + +-- Test .split_part() +select jsonb_path_query('"abc~@~def~@~ghi"', '$.split_part("~@~", 2)'); +select jsonb_path_query('"abc,def,ghi,jkl"', '$.split_part(",", -2)'); + +-- Test string methods play nicely together +select jsonb_path_query('"hello world"', '$.replace("hello","bye").upper()'); +select jsonb_path_query('"hElLo WorlD"', '$.lower().upper().lower().replace("hello","bye")'); +select jsonb_path_query('"hElLo WorlD"', '$.upper().lower().upper().replace("HELLO", "BYE")'); +select jsonb_path_query('"hElLo WorlD"', '$.lower().upper().lower().replace("hello","bye") starts with "bye"'); +select jsonb_path_query('" hElLo WorlD "', '$.btrim().lower().upper().lower().replace("hello","bye") starts with "bye"'); + -- Test .time() select jsonb_path_query('null', '$.time()'); select jsonb_path_query('true', '$.time()'); diff --git a/postgres/regression_suite/jsonpath.sql b/postgres/regression_suite/jsonpath.sql index 61a5270d4..44178d8b4 100644 --- a/postgres/regression_suite/jsonpath.sql +++ b/postgres/regression_suite/jsonpath.sql @@ -78,6 +78,47 @@ select '$.boolean()'::jsonpath; select '$.date()'::jsonpath; select '$.decimal(4,2)'::jsonpath; select '$.string()'::jsonpath; +select '$.replace("hello","bye")'::jsonpath; +select '$.lower()'::jsonpath; +select '$.upper()'::jsonpath; +select '$.lower().upper().lower().replace("hello","bye")'::jsonpath; +select '$.ltrim()'::jsonpath; +select '$.ltrim("xyz")'::jsonpath; +select '$.rtrim()'::jsonpath; +select '$.rtrim("xyz")'::jsonpath; +select '$.btrim()'::jsonpath; +select '$.btrim("xyz")'::jsonpath; +select '$.initcap()'::jsonpath; +select '$.split_part("~@~", 2)'::jsonpath; + +-- Parse errors +select '$.replace("hello")'::jsonpath; +select '$.replace()'::jsonpath; +select '$.replace("hello","bye","extra")'::jsonpath; +select '$.split_part("~@~")'::jsonpath; +select '$.split_part()'::jsonpath; +select '$.split_part("~@~", "hi")'::jsonpath; +select '$.split_part("~@~", 2, "extra")'::jsonpath; +select '$.lower("hi")'::jsonpath; +select '$.upper("hi")'::jsonpath; +select '$.initcap("hi")'::jsonpath; +select '$.ltrim(42)'::jsonpath; +select '$.ltrim("x", "y")'::jsonpath; +select '$.rtrim(42)'::jsonpath; +select '$.rtrim("x", "y")'::jsonpath; +select '$.trim(42)'::jsonpath; +select '$.trim("x", "y")'::jsonpath; + +-- Verify method keywords work as object key names +select '$.lower'::jsonpath; +select '$.upper'::jsonpath; +select '$.initcap'::jsonpath; +select '$.replace'::jsonpath; +select '$.split_part'::jsonpath; +select '$.ltrim'::jsonpath; +select '$.rtrim'::jsonpath; +select '$.btrim'::jsonpath; + select '$.time()'::jsonpath; select '$.time(6)'::jsonpath; select '$.time_tz()'::jsonpath; diff --git a/postgres/regression_suite/misc_functions.sql b/postgres/regression_suite/misc_functions.sql index e85e469fe..8d416e25d 100644 --- a/postgres/regression_suite/misc_functions.sql +++ b/postgres/regression_suite/misc_functions.sql @@ -349,3 +349,10 @@ SET ROLE regress_multixact_funcs; SELECT oldest_multixact IS NULL AS null_result FROM pg_get_multixact_stats(); RESET ROLE; DROP ROLE regress_multixact_funcs; + +-- test instr_time nanosecond<->ticks conversion +CREATE FUNCTION test_instr_time() + RETURNS bool + AS 'regresslib' + LANGUAGE C; +SELECT test_instr_time(); diff --git a/postgres/regression_suite/namespace.sql b/postgres/regression_suite/namespace.sql index d6cb370ba..d5605aad6 100644 --- a/postgres/regression_suite/namespace.sql +++ b/postgres/regression_suite/namespace.sql @@ -7,15 +7,16 @@ SELECT pg_catalog.set_config('search_path', ' ', false); CREATE SCHEMA test_ns_schema_1 + CREATE TABLE abc ( + a serial, + b int UNIQUE + ) + CREATE UNIQUE INDEX abc_a_idx ON abc (a) CREATE VIEW abc_view AS SELECT a+1 AS a, b+1 AS b FROM abc - - CREATE TABLE abc ( - a serial, - b int UNIQUE - ); +; -- verify that the correct search_path restored on abort SET search_path to public; diff --git a/postgres/regression_suite/object_address.sql b/postgres/regression_suite/object_address.sql index b3f86fd33..a61c6712b 100644 --- a/postgres/regression_suite/object_address.sql +++ b/postgres/regression_suite/object_address.sql @@ -179,7 +179,7 @@ WITH objects (type, name, args) AS (VALUES ('collation', '{default}', '{}'), ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'), ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'), - ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'), + ('conversion', '{pg_catalog, koi8_r_to_utf8}', '{}'), ('default value', '{addr_nsp, gentable, b}', '{}'), ('language', '{plpgsql}', '{}'), -- large object diff --git a/postgres/regression_suite/partition_prune.sql b/postgres/regression_suite/partition_prune.sql index e812fb8f2..361391be5 100644 --- a/postgres/regression_suite/partition_prune.sql +++ b/postgres/regression_suite/partition_prune.sql @@ -1447,3 +1447,74 @@ select min(a) over (partition by a order by a) from part_abc where a >= stable_o drop view part_abc_view; drop table part_abc; + +-- +-- Check that operands wrapped in PlaceHolderVars are matched to partition +-- keys, allowing partition pruning to occur. PlaceHolderVars can be +-- introduced when a subquery's output is used with grouping sets. +-- +create table phv_part (a int, b text) partition by list (a); +create table phv_part_1 partition of phv_part for values in (1); +create table phv_part_2 partition of phv_part for values in (2); +create table phv_part_null partition of phv_part for values in (null); +insert into phv_part values (1, 'one'), (2, 'two'), (null, 'null'); + +-- OpExpr: PHV-wrapped operand matched via equal() +explain (costs off) +select * from (select a, b from phv_part) t + where a = 1 + group by grouping sets (a, b); + +select * from (select a, b from phv_part) t + where a = 1 + group by grouping sets (a, b); + +-- OpExpr with RelabelType: PHV wrapped around a casted column +explain (costs off) +select * from (select a::oid as x, b from phv_part) t + where x::int = 1 + group by grouping sets (x, b); + +select * from (select a::oid as x, b from phv_part) t + where x::int = 1 + group by grouping sets (x, b); + +-- ScalarArrayOpExpr: IN clause with PHV-wrapped operand +explain (costs off) +select * from (select a, b from phv_part) t + where a in (1, null) + group by grouping sets (a, b); + +select * from (select a, b from phv_part) t + where a in (1, null) + group by grouping sets (a, b); + +-- NullTest: IS NULL with PHV-wrapped operand +explain (costs off) +select * from (select a, b from phv_part) t + where a is null + group by grouping sets (a, b); + +select * from (select a, b from phv_part) t + where a is null + group by grouping sets (a, b); + +drop table phv_part; + +-- BooleanTest: IS TRUE with PHV-wrapped boolean partition key +create table phv_boolpart (a bool, b text) partition by list (a); +create table phv_boolpart_t partition of phv_boolpart for values in (true); +create table phv_boolpart_f partition of phv_boolpart for values in (false); +create table phv_boolpart_null partition of phv_boolpart default; +insert into phv_boolpart values (true, 'yes'), (false, 'no'), (null, 'unknown'); + +explain (costs off) +select * from (select a, b from phv_boolpart) t + where a is true + group by grouping sets (a, b); + +select * from (select a, b from phv_boolpart) t + where a is true + group by grouping sets (a, b); + +drop table phv_boolpart; diff --git a/postgres/regression_suite/partition_split.sql b/postgres/regression_suite/partition_split.sql index 9f213ce54..e3e99d213 100644 --- a/postgres/regression_suite/partition_split.sql +++ b/postgres/regression_suite/partition_split.sql @@ -48,7 +48,7 @@ ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-10-01')); -- ERROR: lower bound of partition "sales_feb2022" is not equal to lower bound of split partition "sales_feb_mar_apr2022" --- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-01-01') TO ('2022-03-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), @@ -81,7 +81,7 @@ ALTER TABLE sales_feb_mar_apr2022 SPLIT PARTITION sales_feb_mar_apr2022 INTO PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); -- ERROR: upper bound of partition "sales_apr2022" is not equal to upper bound of split partition "sales_feb_mar_apr2022" --- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), @@ -97,7 +97,7 @@ ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO ALTER TABLE sales_range DETACH PARTITION sales_others; -- ERROR: lower bound of partition "sales_feb2022" is not equal to lower bound of split partition "sales_feb_mar_apr2022" --- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-02') TO ('2022-03-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), @@ -122,7 +122,7 @@ CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01 CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); -- ERROR: upper bound of partition "sales_apr2022" is not equal to upper bound of split partition "sales_feb_mar_apr2022" --- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), @@ -331,7 +331,7 @@ ALTER TABLE sales_range SPLIT PARTITION sales_others INTO PARTITION sales_others DEFAULT); -- ERROR: can not split DEFAULT partition "sales_others" --- HINT: To split DEFAULT partition one of the new partition msut be DEFAULT +-- HINT: To split DEFAULT partition one of the new partition must be DEFAULT. ALTER TABLE sales_range SPLIT PARTITION sales_others INTO (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), PARTITION sales_jan2022 FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'), @@ -661,14 +661,14 @@ CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Helsinki', 'St. CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Beijing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok', NULL); -- ERROR: new partitions combined partition bounds do not contain value (NULL) but split partition "sales_all" does --- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); -- ERROR: new partitions combined partition bounds do not contain value ('Kyiv'::character varying(20)) but split partition "sales_all" does --- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), diff --git a/postgres/regression_suite/publication.sql b/postgres/regression_suite/publication.sql index 1b183865e..7f83bed4c 100644 --- a/postgres/regression_suite/publication.sql +++ b/postgres/regression_suite/publication.sql @@ -1438,6 +1438,113 @@ RESET SESSION AUTHORIZATION; DROP ROLE regress_publication_user, regress_publication_user2; DROP ROLE regress_publication_user_dummy; +-- Test pg_get_publication_tables(text[], oid) function +CREATE SCHEMA gpt_test_sch; +CREATE TABLE gpt_test_sch.tbl_sch (id int); +CREATE TABLE tbl_normal (id int); +CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1); +CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10); +CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal; + +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true); +CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false); +CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true); +CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false); +CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch; +CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10); +CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false); +CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true); +CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false); +CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true); +RESET client_min_messages; + +CREATE FUNCTION test_gpt(pubnames text[], relname text) +RETURNS TABLE ( + pubname text, + relname name, + attrs text, + qual text +) +BEGIN ATOMIC + SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid) + FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt + JOIN pg_publication p ON p.oid = gpt.pubid + JOIN pg_class c ON c.oid = gpt.relid + ORDER BY p.pubname, c.relname; +END; + +SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal'); +SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch'); +SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent'); +SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1'); +SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1'); +SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent'); +SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1'); +SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent'); +SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result + +-- test for the EXCEPT clause +SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal'); +SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded) +SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded) +SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result +SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal'); +SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded) +SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded) +SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result + +-- two rows with different row filter +SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal'); + +-- one row with 'pub_part_parent' +SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent'); + +-- no result, tbl_parent is the effective published OID due to pubviaroot +SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1'); + +-- no result, non-existent publication +SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal'); + +-- no result, non-table object +SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view'); + +-- no result, empty publication array +SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal'); + +-- no result, OID 0 as target_relid +SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid); + +-- Clean up +DROP FUNCTION test_gpt(text[], text); +DROP PUBLICATION pub_all; +DROP PUBLICATION pub_all_no_viaroot; +DROP PUBLICATION pub_all_except; +DROP PUBLICATION pub_all_except_no_viaroot; +DROP PUBLICATION pub_schema; +DROP PUBLICATION pub_normal; +DROP PUBLICATION pub_part_leaf; +DROP PUBLICATION pub_part_parent; +DROP PUBLICATION pub_part_parent_no_viaroot; +DROP PUBLICATION pub_part_parent_child; +DROP VIEW gpt_test_view; +DROP TABLE tbl_normal, tbl_parent, tbl_part1; +DROP SCHEMA gpt_test_sch CASCADE; + -- stage objects for pg_dump tests CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int); CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int); diff --git a/postgres/regression_suite/returning.sql b/postgres/regression_suite/returning.sql index 7f5d87ab5..5c138ba84 100644 --- a/postgres/regression_suite/returning.sql +++ b/postgres/regression_suite/returning.sql @@ -408,3 +408,23 @@ END; -- \sf foo_update DROP FUNCTION foo_update; + +-- Test that the planner does not fold OLD/NEW IS NULL tests to constants +-- based on NOT NULL constraints, since OLD is NULL for INSERT and NEW is +-- NULL for DELETE. +CREATE TEMP TABLE ret_nn (a int NOT NULL); + +-- INSERT has no OLD row, should return true +INSERT INTO ret_nn VALUES (1) RETURNING old.a IS NULL; + +-- DELETE has no NEW row, should return true +DELETE FROM ret_nn WHERE a = 1 RETURNING new.a IS NULL; + +-- MERGE: DELETE should have new.a IS NULL, INSERT should have old.a IS NULL +INSERT INTO ret_nn VALUES (2); +MERGE INTO ret_nn USING (VALUES (2), (3)) AS src(a) ON ret_nn.a = src.a + WHEN MATCHED THEN DELETE + WHEN NOT MATCHED THEN INSERT VALUES (src.a) + RETURNING merge_action(), old.a IS NULL, new.a IS NULL; + +DROP TABLE ret_nn; diff --git a/postgres/regression_suite/role_ddl.sql b/postgres/regression_suite/role_ddl.sql new file mode 100644 index 000000000..2fb952197 --- /dev/null +++ b/postgres/regression_suite/role_ddl.sql @@ -0,0 +1,96 @@ +-- Consistent test results +SET timezone TO 'UTC'; +SET DateStyle TO 'ISO, YMD'; + +-- Create test database +CREATE DATABASE regression_role_ddl_test; + +-- Basic role +CREATE ROLE regress_role_ddl_test1; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test1'); + +-- Role with LOGIN +CREATE ROLE regress_role_ddl_test2 LOGIN; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test2'); + +-- Role with multiple privileges +CREATE ROLE regress_role_ddl_test3 + LOGIN + SUPERUSER + CREATEDB + CREATEROLE + CONNECTION LIMIT 5 + VALID UNTIL '2030-12-31 23:59:59+00'; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test3'); + +-- Role with configuration parameters +CREATE ROLE regress_role_ddl_test4; +ALTER ROLE regress_role_ddl_test4 SET work_mem TO '256MB'; +ALTER ROLE regress_role_ddl_test4 SET search_path TO myschema, public; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test4'); + +-- Role with database-specific configuration +CREATE ROLE regress_role_ddl_test5; +ALTER ROLE regress_role_ddl_test5 IN DATABASE regression_role_ddl_test SET work_mem TO '128MB'; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test5'); + +-- Role with special characters (requires quoting) +CREATE ROLE "regress_role-with-dash"; +SELECT * FROM pg_get_role_ddl('regress_role-with-dash'); + +-- Pretty-printed output +-- \pset format unaligned +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test3', 'pretty', 'true'); +-- \pset format aligned + +-- Role with memberships +CREATE ROLE regress_role_ddl_grantor CREATEROLE; +CREATE ROLE regress_role_ddl_group1; +CREATE ROLE regress_role_ddl_group2; +CREATE ROLE regress_role_ddl_member; +GRANT regress_role_ddl_group1 TO regress_role_ddl_grantor WITH ADMIN TRUE; +GRANT regress_role_ddl_group2 TO regress_role_ddl_grantor WITH ADMIN TRUE; +SET ROLE regress_role_ddl_grantor; +GRANT regress_role_ddl_group1 TO regress_role_ddl_member WITH INHERIT TRUE, SET FALSE; +GRANT regress_role_ddl_group2 TO regress_role_ddl_member WITH ADMIN TRUE; +RESET ROLE; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_member'); + +-- Role with memberships suppressed +SELECT * FROM pg_get_role_ddl('regress_role_ddl_member', 'memberships', 'false'); + +-- Non-existent role (should error) +SELECT * FROM pg_get_role_ddl(9999999::oid); + +-- NULL input (should return no rows) +SELECT * FROM pg_get_role_ddl(NULL); + +-- Permission check: revoke SELECT on pg_authid +CREATE ROLE regress_role_ddl_noaccess; +REVOKE SELECT ON pg_authid FROM PUBLIC; +SET ROLE regress_role_ddl_noaccess; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test1'); -- should fail +RESET ROLE; +GRANT SELECT ON pg_authid TO PUBLIC; +DROP ROLE regress_role_ddl_noaccess; + +-- Cleanup +DROP ROLE regress_role_ddl_test1; +DROP ROLE regress_role_ddl_test2; +DROP ROLE regress_role_ddl_test3; +DROP ROLE regress_role_ddl_test4; +DROP ROLE regress_role_ddl_test5; +DROP ROLE "regress_role-with-dash"; +SET ROLE regress_role_ddl_grantor; +REVOKE regress_role_ddl_group1 FROM regress_role_ddl_member; +REVOKE regress_role_ddl_group2 FROM regress_role_ddl_member; +RESET ROLE; +DROP ROLE regress_role_ddl_member; +DROP ROLE regress_role_ddl_group1; +DROP ROLE regress_role_ddl_group2; +DROP ROLE regress_role_ddl_grantor; + +DROP DATABASE regression_role_ddl_test; + +-- Reset timezone to default +RESET timezone; diff --git a/postgres/regression_suite/sqljson_queryfuncs.sql b/postgres/regression_suite/sqljson_queryfuncs.sql index 373eb1cc0..9568b2e24 100644 --- a/postgres/regression_suite/sqljson_queryfuncs.sql +++ b/postgres/regression_suite/sqljson_queryfuncs.sql @@ -357,7 +357,7 @@ INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); DROP TABLE test_jsonb_constraints; --- Test mutabilily of query functions +-- Test mutability of query functions CREATE TABLE test_jsonb_mutability(js jsonb, b int); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); @@ -402,6 +402,15 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_VALUE(js, '$' DEFAULT random()::int ON ERROR)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.rtrim()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.ltrim()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.btrim()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.lower()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.upper()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.initcap()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.replace("hello", "bye")')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.split_part(",", 2)')); + -- DEFAULT expression CREATE OR REPLACE FUNCTION ret_setint() RETURNS SETOF integer AS $$ diff --git a/postgres/regression_suite/stats.sql b/postgres/regression_suite/stats.sql index 7701ae58f..0dd17cc7e 100644 --- a/postgres/regression_suite/stats.sql +++ b/postgres/regression_suite/stats.sql @@ -416,9 +416,17 @@ COMMIT; -- check that the stats are reset. SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables WHERE relid = 'pg_shdescription'::regclass; +-- stats_reset may not be set for datid=0 and shared objects in +-- pg_stat_database, so reset once. SELECT pg_stat_reset_single_table_counters('pg_shdescription'::regclass); SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables WHERE relid = 'pg_shdescription'::regclass; +SELECT stats_reset AS shared_db_reset_before + FROM pg_stat_database WHERE datid = 0 /* \gset */; +-- Second reset for comparison. +SELECT pg_stat_reset_single_table_counters('pg_shdescription'::regclass); +SELECT stats_reset > 'shared_db_reset_before'::timestamptz AS has_updated + FROM pg_stat_database WHERE datid = 0; -- set back comment -- \if :{?description_before} @@ -432,10 +440,10 @@ SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables ----- -- Test that sessions is incremented when a new session is started in pg_stat_database -SELECT sessions AS db_stat_sessions FROM pg_stat_database WHERE datname = (SELECT current_database()) /* \gset */; +SELECT sessions AS db_stat_sessions FROM pg_stat_database WHERE datname = current_database() /* \gset */; -- \c SELECT pg_stat_force_next_flush(); -SELECT sessions > 'db_stat_sessions' FROM pg_stat_database WHERE datname = (SELECT current_database()); +SELECT sessions > 'db_stat_sessions' FROM pg_stat_database WHERE datname = current_database(); -- Test pg_stat_checkpointer checkpointer-related stats, together with pg_stat_wal SELECT num_requested AS rqst_ckpts_before FROM pg_stat_checkpointer /* \gset */; @@ -529,12 +537,12 @@ SELECT pg_stat_reset_shared('unknown'); -- have a baseline for comparison. The same for pg_stat_database_conflicts as it shares -- the same stats_reset as pg_stat_database. SELECT pg_stat_reset(); -SELECT stats_reset AS db_reset_ts FROM pg_stat_database WHERE datname = (SELECT current_database()) /* \gset */; -SELECT stats_reset AS dbc_reset_ts FROM pg_stat_database_conflicts WHERE datname = (SELECT current_database()) /* \gset */; +SELECT stats_reset AS db_reset_ts FROM pg_stat_database WHERE datname = current_database() /* \gset */; +SELECT stats_reset AS dbc_reset_ts FROM pg_stat_database_conflicts WHERE datname = current_database() /* \gset */; SELECT 'db_reset_ts'::timestamptz = 'dbc_reset_ts'::timestamptz; SELECT pg_stat_reset(); -SELECT stats_reset > 'db_reset_ts'::timestamptz FROM pg_stat_database WHERE datname = (SELECT current_database()); -SELECT stats_reset > 'dbc_reset_ts'::timestamptz FROM pg_stat_database_conflicts WHERE datname = (SELECT current_database()); +SELECT stats_reset > 'db_reset_ts'::timestamptz FROM pg_stat_database WHERE datname = current_database(); +SELECT stats_reset > 'dbc_reset_ts'::timestamptz FROM pg_stat_database_conflicts WHERE datname = current_database(); -- Test that reset works for pg_statio_all_sequences diff --git a/postgres/regression_suite/subselect.sql b/postgres/regression_suite/subselect.sql index a4b705172..70de57eb3 100644 --- a/postgres/regression_suite/subselect.sql +++ b/postgres/regression_suite/subselect.sql @@ -1632,4 +1632,18 @@ EXPLAIN (COSTS OFF) SELECT * FROM not_null_tab WHERE NOT id ?= ANY (SELECT id FROM not_null_tab); +-- No ANTI JOIN: the inner side has an unvalidated NOT NULL constraint, so +-- the column might contain NULLs. +CREATE TEMP TABLE notnull_notvalid_tab (id int); +INSERT INTO notnull_notvalid_tab VALUES (NULL); +ALTER TABLE notnull_notvalid_tab ADD CONSTRAINT nn NOT NULL id NOT VALID; + +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM notnull_notvalid_tab); + +-- NOT IN with NULL on inner side should return no rows +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM notnull_notvalid_tab); + ROLLBACK; diff --git a/postgres/regression_suite/tablespace_ddl.sql b/postgres/regression_suite/tablespace_ddl.sql new file mode 100644 index 000000000..62a963df6 --- /dev/null +++ b/postgres/regression_suite/tablespace_ddl.sql @@ -0,0 +1,58 @@ +-- +-- Tests for pg_get_tablespace_ddl() +-- + +SET allow_in_place_tablespaces = true; +CREATE ROLE regress_tblspc_ddl_user; + +-- error: non-existent tablespace by name +SELECT * FROM pg_get_tablespace_ddl('regress_nonexistent_tblsp'); + +-- error: non-existent tablespace by OID +SELECT * FROM pg_get_tablespace_ddl(0::oid); + +-- NULL input returns no rows (name variant) +SELECT * FROM pg_get_tablespace_ddl(NULL::name); + +-- NULL input returns no rows (OID variant) +SELECT * FROM pg_get_tablespace_ddl(NULL::oid); + +-- tablespace name requiring quoting +CREATE TABLESPACE "regress_ tblsp" OWNER regress_tblspc_ddl_user LOCATION ''; +SELECT * FROM pg_get_tablespace_ddl('regress_ tblsp'); +DROP TABLESPACE "regress_ tblsp"; + +-- tablespace with multiple options +CREATE TABLESPACE regress_allopt_tblsp OWNER regress_tblspc_ddl_user LOCATION '' + WITH (seq_page_cost = '1.5', random_page_cost = '1.1234567890', + effective_io_concurrency = '17', maintenance_io_concurrency = '18'); +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp'); + +-- pretty-printed output +-- \pset format unaligned +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp', 'pretty', 'true'); +-- \pset format aligned + +-- tablespace with owner suppressed +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp', 'owner', 'false'); + +DROP TABLESPACE regress_allopt_tblsp; + +-- test by OID +CREATE TABLESPACE regress_oid_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; +SELECT oid AS tsid FROM pg_tablespace WHERE spcname = 'regress_oid_tblsp' /* \gset */; +SELECT * FROM pg_get_tablespace_ddl('tsid'); +DROP TABLESPACE regress_oid_tblsp; + +-- Permission check: revoke SELECT on pg_tablespace +CREATE TABLESPACE regress_acl_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; +CREATE ROLE regress_tblspc_ddl_noaccess; +REVOKE SELECT ON pg_tablespace FROM PUBLIC; +SET ROLE regress_tblspc_ddl_noaccess; +SELECT * FROM pg_get_tablespace_ddl('regress_acl_tblsp'); -- should fail +RESET ROLE; +GRANT SELECT ON pg_tablespace TO PUBLIC; +DROP TABLESPACE regress_acl_tblsp; +DROP ROLE regress_tblspc_ddl_noaccess; + +DROP ROLE regress_tblspc_ddl_user; diff --git a/postgres/regression_suite/tid.sql b/postgres/regression_suite/tid.sql index 2602e20eb..c0a70be5c 100644 --- a/postgres/regression_suite/tid.sql +++ b/postgres/regression_suite/tid.sql @@ -16,6 +16,22 @@ SELECT pg_input_is_valid('(0,-1)', 'tid'); SELECT * FROM pg_input_error_info('(0,-1)', 'tid'); +-- tests for tid_block() and tid_offset() +SELECT tid_block('(0,0)'::tid), tid_offset('(0,0)'::tid); +SELECT tid_block('(0,1)'::tid), tid_offset('(0,1)'::tid); +SELECT tid_block('(42,7)'::tid), tid_offset('(42,7)'::tid); +-- max values: blockno uint32 max, offset uint16 max +SELECT tid_block('(4294967295,65535)'::tid), tid_offset('(4294967295,65535)'::tid); +-- (-1,0) wraps to blockno 4294967295 +SELECT tid_block('(-1,0)'::tid); +-- NULL handling (strict functions) +SELECT tid_block(NULL::tid), tid_offset(NULL::tid); +-- round-trip: blockno + offset reconstruct the original TID +SELECT t, tid_block(t), tid_offset(t), + format('(%s,%s)', tid_block(t), tid_offset(t))::tid = t AS roundtrip_ok +FROM (VALUES ('(0,0)'::tid), ('(1,42)'::tid), ('(4294967295,65535)'::tid)) AS v(t); + + -- tests for functions related to TID handling CREATE TABLE tid_tab (a int); @@ -24,6 +40,11 @@ CREATE TABLE tid_tab (a int); INSERT INTO tid_tab VALUES (1), (2); SELECT min(ctid) FROM tid_tab; SELECT max(ctid) FROM tid_tab; + +-- tid_block() and tid_offset() with real table ctid +SELECT ctid, tid_block(ctid), tid_offset(ctid) FROM tid_tab; +-- use in WHERE clause +SELECT ctid FROM tid_tab WHERE tid_block(ctid) = 0 AND tid_offset(ctid) = 1; TRUNCATE tid_tab; -- Tests for currtid2() with various relation kinds diff --git a/postgres/regression_suite/union.sql b/postgres/regression_suite/union.sql index d0c70fafb..078c85842 100644 --- a/postgres/regression_suite/union.sql +++ b/postgres/regression_suite/union.sql @@ -664,3 +664,7 @@ select * from tenk1 t1 left join lateral (select t1.tenthous from tenk2 t2 union all (values(1))) on true limit 1; + +-- Test handling of Vars with varno 0 in estimate_array_length +explain (verbose, costs off) +select null::int[] union all select null::int[] union all select null::bigint[]; diff --git a/postgres/regression_suite/window.sql b/postgres/regression_suite/window.sql index f088264e7..999c7aecc 100644 --- a/postgres/regression_suite/window.sql +++ b/postgres/regression_suite/window.sql @@ -330,6 +330,32 @@ CREATE TEMP VIEW v_window AS SELECT pg_get_viewdef('v_window'); +-- test overflow frame specifications +SELECT sum(unique1) over (rows between current row and 9223372036854775807 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (rows between 9223372036854775807 following and 1 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT last_value(unique1) over (ORDER BY four rows between current row and 9223372036854775807 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +-- These test GROUPS mode with an offset large enough to cause overflow when +-- added to currentgroup. Although the overflow doesn't produce visibly wrong +-- results (due to the incremental nature of group pointer advancement), we +-- still need to protect against it as signed integer overflow is undefined +-- behavior in C. +SELECT sum(unique1) over (ORDER BY four groups between current row and 9223372036854775807 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (ORDER BY four groups between 9223372036854775807 following and unbounded following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + -- RANGE offset PRECEDING/FOLLOWING tests SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding), diff --git a/postgres/regression_suite/without_overlaps.sql b/postgres/regression_suite/without_overlaps.sql index a7c15ea0f..14684576c 100644 --- a/postgres/regression_suite/without_overlaps.sql +++ b/postgres/regression_suite/without_overlaps.sql @@ -180,6 +180,37 @@ ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq; DROP TABLE temporal_rng3; DROP TYPE textrange2; +-- +-- test PRIMARY KEY and UNIQUE constraints' interaction with domains +-- + +-- range over domain: +CREATE DOMAIN int4_d as integer check (value <> 10); +CREATE TYPE int4_d_range as range (subtype = int4_d); +CREATE TABLE temporal_rng4 ( + id int4range, + valid_at int4_d_range, + CONSTRAINT temporal_rng4_pk PRIMARY KEY(id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO temporal_rng4 VALUES ('[1,11)', '[9,10)'); -- start bound violates domain +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[10,11)'); -- end bound violates domain +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[2,5)'); -- overlaps +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[20,23)'); -- okay +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[30,)'); -- null bound is okay +DROP TABLE temporal_rng4; + +-- domain over range: +CREATE DOMAIN int4range_d AS int4range CHECK (VALUE <> '[10,11)'); +CREATE TABLE temporal_rng4 ( + id int4range, + valid_at int4range_d, + CONSTRAINT temporal_rng4_pk UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[10,11)'); -- violates domain +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[2,13)'); -- overlaps +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[20,23)'); -- okay +DROP TABLE temporal_rng4; + -- -- test ALTER TABLE ADD CONSTRAINT -- diff --git a/postgres/regression_suite/xml.sql b/postgres/regression_suite/xml.sql index d459c8175..24615dcfc 100644 --- a/postgres/regression_suite/xml.sql +++ b/postgres/regression_suite/xml.sql @@ -272,9 +272,7 @@ BEGIN END IF; EXCEPTION -- character with byte sequence 0xc2 0xb0 in encoding "UTF8" has no equivalent in encoding "LATIN8" - WHEN untranslatable_character - -- default conversion function for encoding "UTF8" to "MULE_INTERNAL" does not exist - OR undefined_function + WHEN undefined_function -- unsupported XML feature OR feature_not_supported THEN RAISE LOG 'skip: %', SQLERRM;