From 97f5318a7bde8e44271156829e1cf092bad18cc5 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Thu, 25 Jun 2026 18:39:21 -0400 Subject: [PATCH] parser: support pg19 null treatment in aggregates --- .../src/generated/syntax_kind.rs | 2 + crates/squawk_parser/src/grammar.rs | 22 +- .../squawk_syntax/src/ast/generated/nodes.rs | 124 +++++++++-- crates/squawk_syntax/src/postgresql.ungram | 14 +- postgres/kwlist.h | 6 +- postgres/regression_suite/aggregates.sql | 21 +- postgres/regression_suite/cluster.sql | 73 +++---- postgres/regression_suite/copyencoding.sql | 19 ++ .../create_property_graph.sql | 31 ++- postgres/regression_suite/create_schema.sql | 7 +- postgres/regression_suite/eager_aggregate.sql | 26 +++ postgres/regression_suite/event_trigger.sql | 15 ++ postgres/regression_suite/fast_default.sql | 51 ----- postgres/regression_suite/for_portion_of.sql | 199 +++++++++++++++++- postgres/regression_suite/foreign_key.sql | 83 ++++++++ .../regression_suite/generated_stored.sql | 19 ++ .../regression_suite/generated_virtual.sql | 21 ++ postgres/regression_suite/graph_table.sql | 20 +- postgres/regression_suite/join.sql | 12 ++ postgres/regression_suite/object_address.sql | 5 +- postgres/regression_suite/opr_sanity.sql | 2 +- postgres/regression_suite/partition_merge.sql | 17 ++ postgres/regression_suite/partition_split.sql | 70 ++++++ postgres/regression_suite/privileges.sql | 10 +- postgres/regression_suite/returning.sql | 11 + postgres/regression_suite/sqljson.sql | 22 ++ postgres/regression_suite/stats_import.sql | 14 ++ postgres/regression_suite/subscription.sql | 49 ++++- postgres/regression_suite/unicode.sql | 20 ++ postgres/regression_suite/window.sql | 3 + postgres/regression_suite/xml.sql | 22 +- 31 files changed, 858 insertions(+), 152 deletions(-) diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index 604e0b464..dd1a9ad18 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -906,6 +906,7 @@ pub enum SyntaxKind { HAVING_CLAUSE, IF_EXISTS, IF_NOT_EXISTS, + IGNORE_NULLS, IMPORT_FOREIGN_SCHEMA, INDEX_EXPR, INHERIT, @@ -1137,6 +1138,7 @@ pub enum SyntaxKind { RESET_FUNC_OPTION, RESET_OPTIONS, RESET_SESSION_AUTH, + RESPECT_NULLS, RESTART, RESTRICT, RETURNING_CLAUSE, diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index fef3d757a..809acf61f 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -2165,6 +2165,7 @@ fn call_expr_args(p: &mut Parser<'_>, lhs: CompletedMarker) -> CompletedMarker { fn opt_agg_clauses(p: &mut Parser<'_>) { opt_within_clause(p); opt_filter_clause(p); + opt_null_treatment(p); opt_over_clause(p); } @@ -2181,13 +2182,10 @@ fn opt_filter_clause(p: &mut Parser<'_>) { } fn opt_over_clause(p: &mut Parser<'_>) { - if p.at(OVER_KW) || p.at(RESPECT_KW) || p.at(IGNORE_KW) { + if p.at(OVER_KW) { // OVER window_name // OVER ( window_definition ) let m = p.start(); - if p.eat(RESPECT_KW) || p.eat(IGNORE_KW) { - p.expect(NULLS_KW); - } p.expect(OVER_KW); if p.eat(L_PAREN) { window_spec(p); @@ -2199,6 +2197,22 @@ fn opt_over_clause(p: &mut Parser<'_>) { } } +fn opt_null_treatment(p: &mut Parser<'_>) { + if !p.at(RESPECT_KW) && !p.at(IGNORE_KW) { + return; + } + + let m = p.start(); + let kind = if p.eat(RESPECT_KW) { + RESPECT_NULLS + } else { + p.bump(IGNORE_KW); + IGNORE_NULLS + }; + p.expect(NULLS_KW); + m.complete(p, kind); +} + fn opt_within_clause(p: &mut Parser<'_>) { if p.at(WITHIN_KW) { let m = p.start(); diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index aff7f473b..41e5cb687 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -3605,6 +3605,10 @@ impl CallExpr { support::child(&self.syntax) } #[inline] + pub fn null_treatment(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn over_clause(&self) -> Option { support::child(&self.syntax) } @@ -11756,6 +11760,21 @@ impl IfNotExists { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct IgnoreNulls { + pub(crate) syntax: SyntaxNode, +} +impl IgnoreNulls { + #[inline] + pub fn ignore_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::IGNORE_KW) + } + #[inline] + pub fn nulls_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::NULLS_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ImportForeignSchema { pub(crate) syntax: SyntaxNode, @@ -15365,21 +15384,9 @@ impl OverClause { support::token(&self.syntax, SyntaxKind::R_PAREN) } #[inline] - pub fn ignore_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::IGNORE_KW) - } - #[inline] - pub fn nulls_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::NULLS_KW) - } - #[inline] pub fn over_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::OVER_KW) } - #[inline] - pub fn respect_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::RESPECT_KW) - } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -17332,6 +17339,21 @@ impl ResetSessionAuth { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct RespectNulls { + pub(crate) syntax: SyntaxNode, +} +impl RespectNulls { + #[inline] + pub fn nulls_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::NULLS_KW) + } + #[inline] + pub fn respect_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::RESPECT_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Restart { pub(crate) syntax: SyntaxNode, @@ -22155,6 +22177,12 @@ pub enum MergeWhenClause { MergeWhenNotMatchedTarget(MergeWhenNotMatchedTarget), } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum NullTreatment { + IgnoreNulls(IgnoreNulls), + RespectNulls(RespectNulls), +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum OnCommitAction { DeleteRows(DeleteRows), @@ -28667,6 +28695,24 @@ impl AstNode for IfNotExists { &self.syntax } } +impl AstNode for IgnoreNulls { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::IGNORE_NULLS + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for ImportForeignSchema { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -32825,6 +32871,24 @@ impl AstNode for ResetSessionAuth { &self.syntax } } +impl AstNode for RespectNulls { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::RESPECT_NULLS + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for Restart { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -38776,6 +38840,42 @@ impl From for MergeWhenClause { MergeWhenClause::MergeWhenNotMatchedTarget(node) } } +impl AstNode for NullTreatment { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + matches!(kind, SyntaxKind::IGNORE_NULLS | SyntaxKind::RESPECT_NULLS) + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + let res = match syntax.kind() { + SyntaxKind::IGNORE_NULLS => NullTreatment::IgnoreNulls(IgnoreNulls { syntax }), + SyntaxKind::RESPECT_NULLS => NullTreatment::RespectNulls(RespectNulls { syntax }), + _ => { + return None; + } + }; + Some(res) + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + match self { + NullTreatment::IgnoreNulls(it) => &it.syntax, + NullTreatment::RespectNulls(it) => &it.syntax, + } + } +} +impl From for NullTreatment { + #[inline] + fn from(node: IgnoreNulls) -> NullTreatment { + NullTreatment::IgnoreNulls(node) + } +} +impl From for NullTreatment { + #[inline] + fn from(node: RespectNulls) -> NullTreatment { + NullTreatment::RespectNulls(node) + } +} impl AstNode for OnCommitAction { #[inline] fn can_cast(kind: SyntaxKind) -> bool { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 392eecf8a..bf1c76d8f 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -65,7 +65,7 @@ ArgList = | '(' ('distinct' | 'all')? args:(Arg (',' Arg)*)? ')' CallExpr = - Expr ArgList WithinClause? FilterClause? OverClause? + Expr ArgList WithinClause? FilterClause? NullTreatment? OverClause? | ExtractFn | GraphTableFn | JsonExistsFn @@ -1728,7 +1728,17 @@ FilterClause = 'filter' '(' 'where' Expr ')' OverClause = - (('respect' | 'ignore') 'nulls')? 'over' (NameRef | '(' WindowSpec? ')') + 'over' (NameRef | '(' WindowSpec? ')') + +NullTreatment = + IgnoreNulls +| RespectNulls + +IgnoreNulls = + 'ignore' 'nulls' + +RespectNulls = + 'respect' 'nulls' WithinClause = 'within' 'group' '(' OrderByClause ')' diff --git a/postgres/kwlist.h b/postgres/kwlist.h index 5ebc18705..4b0b4ad16 100644 --- a/postgres/kwlist.h +++ b/postgres/kwlist.h @@ -1,7 +1,7 @@ // synced from: -// commit: 2c4bd2bf5700db98be0602854a8b7fa2c16b5f4a -// committed at: 2026-05-23T09:39:58+09:00 -// file: https://github.com/postgres/postgres/blob/2c4bd2bf5700db98be0602854a8b7fa2c16b5f4a/src/include/parser/kwlist.h +// commit: 6468f7a853c3c75066410cfb54ecdb3050ec7132 +// committed at: 2026-06-25T14:25:57-07:00 +// file: https://github.com/postgres/postgres/blob/6468f7a853c3c75066410cfb54ecdb3050ec7132/src/include/parser/kwlist.h // // update via: // cargo xtask sync-pg diff --git a/postgres/regression_suite/aggregates.sql b/postgres/regression_suite/aggregates.sql index 968b3ea9b..dbec7a361 100644 --- a/postgres/regression_suite/aggregates.sql +++ b/postgres/regression_suite/aggregates.sql @@ -141,22 +141,33 @@ SELECT covar_pop(1::float8,'inf'::float8), covar_samp(3::float8,'inf'::float8); SELECT covar_pop(1::float8,'nan'::float8), covar_samp(3::float8,'nan'::float8); -- check some cases that formerly had poor roundoff-error behavior +-- note: regr_r2() differs from corr() for a horizontal line, per spec SELECT corr(0.09, g), regr_r2(0.09, g) FROM generate_series(1, 30) g; SELECT corr(g, 0.09), regr_r2(g, 0.09), regr_slope(g, 0.09), regr_intercept(g, 0.09) FROM generate_series(1, 30) g; SELECT corr(1.3 + g * 1e-16, 1.3 + g * 1e-16) FROM generate_series(1, 3) g; -SELECT corr(1e-100 + g * 1e-105, 1e-100 + g * 1e-105) + +-- check some cases that formerly suffered from internal overflow/underflow +SELECT corr(1e-100 + g * 1e-105, 1e-100 + g * 1e-105), + regr_r2(1e-100 + g * 1e-105, 1e-100 + g * 1e-105) FROM generate_series(1, 3) g; -SELECT corr(1e-100 + g * 1e-105, 1e-100 + g * 1e-105) +SELECT corr(1e-100 + g * 1e-105, 1e-100 + g * 1e-105), + regr_r2(1e-100 + g * 1e-105, 1e-100 + g * 1e-105) FROM generate_series(1, 30) g; +SELECT corr(1e100 + g * 1e95, 1e100 + g * 1e95), + regr_r2(1e100 + g * 1e95, 1e100 + g * 1e95) + FROM generate_series(1, 2) g; +SELECT regr_intercept(y, x) FROM (VALUES (-1e150, 0), (2e150, 3e150)) v(x, y); +SELECT regr_intercept(y, x) FROM (VALUES (-1e-131, 0), (2e-131, 3e-131)) v(x, y); -- these examples pose definitional questions for NaN inputs, -- which we resolve by saying that an all-NaN input column is not all equal -SELECT corr(g, 'NaN') FROM generate_series(1, 30) g; -SELECT corr(0.1, 'NaN') FROM generate_series(1, 30) g; -SELECT corr('NaN', 'NaN') FROM generate_series(1, 30) g; +SELECT corr(g, 'NaN'), regr_r2(g, 'NaN') FROM generate_series(1, 30) g; +SELECT corr(0.1, 'NaN'), regr_r2(0.1, 'NaN') FROM generate_series(1, 30) g; +SELECT corr('NaN', 0.1), regr_r2('NaN', 0.1) FROM generate_series(1, 30) g; +SELECT corr('NaN', 'NaN'), regr_r2('NaN', 'NaN') FROM generate_series(1, 30) g; -- test accum and combine functions directly CREATE TABLE regr_test (x float8, y float8); diff --git a/postgres/regression_suite/cluster.sql b/postgres/regression_suite/cluster.sql index ab073600e..781dea12b 100644 --- a/postgres/regression_suite/cluster.sql +++ b/postgres/regression_suite/cluster.sql @@ -328,6 +328,34 @@ EXPLAIN (COSTS OFF) SELECT * FROM clstr_expression WHERE -a = -3 ORDER BY -a, b; SELECT * FROM clstr_expression WHERE -a = -3 ORDER BY -a, b; COMMIT; +-- verify some error cases +CREATE TABLE clstr_table_one (id int, val text); +CREATE TABLE clstr_table_two (id int, val text); +CREATE INDEX clstr_idx_b ON clstr_table_two (id); +CLUSTER clstr_table_one USING clstr_idx_b; +CLUSTER clstr_table_one USING nonexistant; + +CREATE INDEX clstr_hash_idx ON clstr_table_one USING hash (id); +CLUSTER clstr_table_one USING clstr_hash_idx; + +CREATE INDEX clstr_partial_idx ON clstr_table_one (id) WHERE id > 0; +CLUSTER clstr_table_one USING clstr_partial_idx; + +REPACK pg_class USING INDEX pg_class_oid_index; + +DROP TABLE clstr_table_one, clstr_table_two; + +-- verify that CLUSTER/REPACK don't touch a NO DATA matview +CREATE MATERIALIZED VIEW clstr_matview AS + SELECT i FROM generate_series(1, 5) i + WITH NO DATA; +CREATE INDEX clstr_matview_idx ON clstr_matview (i); +SELECT relfilenode FROM pg_class WHERE oid = 'clstr_matview'::regclass /* \gset */; +CLUSTER clstr_matview USING clstr_matview_idx; +REPACK clstr_matview USING INDEX clstr_matview_idx; +SELECT relfilenode = 'relfilenode' FROM pg_class WHERE oid = 'clstr_matview'::regclass; +DROP MATERIALIZED VIEW clstr_matview; + ---------------------------------------------------------------------- -- -- REPACK @@ -383,52 +411,7 @@ JOIN relnodes_new n ON o.relname = n.relname WHERE o.relfilenode <> n.relfilenode ORDER BY o.relname; --- --- Check concurrent mode requirements --- - --- Disallowed in catalogs -REPACK (CONCURRENTLY) pg_class; - --- Doesn't like partitioned tables -REPACK (CONCURRENTLY) clstrpart; - --- Doesn't support catalog tables -REPACK (CONCURRENTLY) pg_class; - --- Only support permanent tables, temp and unlogged tables are not supported -CREATE TEMP TABLE repack_conc_temp (i int PRIMARY KEY); -REPACK (CONCURRENTLY) repack_conc_temp; -DROP TABLE repack_conc_temp; -CREATE UNLOGGED TABLE repack_conc_unlogged (i int PRIMARY KEY); -REPACK (CONCURRENTLY) repack_conc_unlogged; -DROP TABLE repack_conc_unlogged; - --- Doesn't support TOAST tables directly -CREATE TABLE repack_conc_toast (t text); -SELECT reltoastrelid::regclass AS toast_rel -FROM pg_class WHERE oid = 'repack_conc_toast'::regclass /* \gset */; --- \set VERBOSITY sqlstate --- REPACK (CONCURRENTLY) :toast_rel; --- \set VERBOSITY default -DROP TABLE repack_conc_toast; - --- Doesn't support tables with REPLICA IDENTITY NOTHING, even if they have a primary key -CREATE TABLE repack_conc_replident (i int PRIMARY KEY); -ALTER TABLE repack_conc_replident REPLICA IDENTITY NOTHING; -REPACK (CONCURRENTLY) repack_conc_replident; - --- Doesn't support tables without a primary key or replica identity index -ALTER TABLE repack_conc_replident DROP CONSTRAINT repack_conc_replident_pkey; -ALTER TABLE repack_conc_replident REPLICA IDENTITY DEFAULT; -REPACK (CONCURRENTLY) repack_conc_replident; - --- Doesn't support tables with deferrable primary keys -ALTER TABLE repack_conc_replident ADD PRIMARY KEY (i) DEFERRABLE; -REPACK (CONCURRENTLY) repack_conc_replident; - -- clean up -DROP TABLE repack_conc_replident; DROP TABLE clustertest; DROP TABLE clstr_1; DROP TABLE clstr_2; diff --git a/postgres/regression_suite/copyencoding.sql b/postgres/regression_suite/copyencoding.sql index 0e46886d5..83d74a9fd 100644 --- a/postgres/regression_suite/copyencoding.sql +++ b/postgres/regression_suite/copyencoding.sql @@ -57,4 +57,23 @@ SET client_encoding TO EUC_JP; COPY copy_encoding_tab FROM 'utf8_csv' WITH (FORMAT csv); RESET client_encoding; +-- JSON format encoding conversion +-- \set json_latin1 :abs_builddir '/results/copyencoding_json_latin1.json' +COPY (SELECT E'\u00e9' AS c) TO 'json_latin1' WITH (FORMAT json, ENCODING 'LATIN1'); +-- Verify the file contains LATIN1 'é' (single byte 0xe9) and not UTF-8 (0xc3 0xa9). +-- Done as separate position checks to stay independent of the platform's +-- end-of-line convention. +SELECT position('\xe9'::bytea IN pg_read_binary_file('json_latin1')) > 0 AS has_latin1_e9, + position('\xc3a9'::bytea IN pg_read_binary_file('json_latin1')) > 0 AS has_utf8_e9; + +-- Same with implicit encoding inherited from client_encoding (no ENCODING +-- option). Covers the case where a client with a non-UTF8 client_encoding +-- runs COPY ... FORMAT json and would otherwise receive unconverted bytes. +-- \set json_implicit :abs_builddir '/results/copyencoding_json_implicit_latin1.json' +SET client_encoding TO LATIN1; +COPY (SELECT E'\u00e9' AS c) TO 'json_implicit' WITH (FORMAT json); +RESET client_encoding; +SELECT position('\xe9'::bytea IN pg_read_binary_file('json_implicit')) > 0 AS has_latin1_e9, + position('\xc3a9'::bytea IN pg_read_binary_file('json_implicit')) > 0 AS has_utf8_e9; + DROP TABLE copy_encoding_tab; diff --git a/postgres/regression_suite/create_property_graph.sql b/postgres/regression_suite/create_property_graph.sql index 13edc13f0..8a9c5cd98 100644 --- a/postgres/regression_suite/create_property_graph.sql +++ b/postgres/regression_suite/create_property_graph.sql @@ -281,21 +281,30 @@ SELECT * FROM information_schema.pg_property_data_types ORDER BY property_graph_ SELECT * FROM information_schema.pg_property_graph_privileges WHERE grantee LIKE 'regress%' ORDER BY property_graph_name, grantor, grantee, privilege_type; -- test object address functions +CREATE TEMPORARY VIEW deps_tree AS + WITH RECURSIVE deps (classid, objid, objsubid, refclassid, refobjid, refobjsubid) AS ( + SELECT classid, objid, objsubid, + refclassid, refobjid, refobjsubid + FROM pg_depend + WHERE refclassid = 'pg_class'::regclass AND + refobjid = 'create_property_graph_tests.gt'::regclass AND + -- eliminate this view, which is not a real dependency, from the result + classid <> 'pg_rewrite'::regclass + UNION ALL + SELECT d.classid, d.objid, d.objsubid, + d.refclassid, d.refobjid, d.refobjsubid + FROM pg_depend d + JOIN deps dp ON d.refclassid = dp.classid AND d.refobjid = dp.objid AND d.refobjsubid = dp.objsubid + ) SELECT DISTINCT * FROM deps; + SELECT pg_describe_object(classid, objid, objsubid) as obj, - pg_describe_object(refclassid, refobjid, refobjsubid) as reference_graph - FROM pg_depend - WHERE refclassid = 'pg_class'::regclass AND - refobjid = 'create_property_graph_tests.g2'::regclass - ORDER BY 1, 2; + pg_describe_object(refclassid, refobjid, refobjsubid) as refobj + FROM deps_tree ORDER BY 1, 2; SELECT (pg_identify_object_as_address(classid, objid, objsubid)).* - FROM pg_depend - WHERE refclassid = 'pg_class'::regclass AND - refobjid = 'create_property_graph_tests.g2'::regclass + FROM (SELECT DISTINCT classid, objid, objsubid FROM deps_tree) ORDER BY 1, 2, 3; SELECT (pg_identify_object(classid, objid, objsubid)).* - FROM pg_depend - WHERE refclassid = 'pg_class'::regclass AND - refobjid = 'create_property_graph_tests.g2'::regclass + FROM (SELECT DISTINCT classid, objid, objsubid FROM deps_tree) ORDER BY 1, 2, 3, 4; -- \a\t diff --git a/postgres/regression_suite/create_schema.sql b/postgres/regression_suite/create_schema.sql index c27096245..38d9c8881 100644 --- a/postgres/regression_suite/create_schema.sql +++ b/postgres/regression_suite/create_schema.sql @@ -120,7 +120,12 @@ CREATE SCHEMA regress_schema_misc CREATE OPERATOR + (function = cs_add, leftarg = int4, rightarg = int4) CREATE PROCEDURE cs_proc(int4, int4) - BEGIN ATOMIC SELECT cs_add($1,$2); END + BEGIN ATOMIC + SELECT cs_add($1,$2); + END + + -- this checks that psql is not fooled by an irrelevant BEGIN + CREATE VIEW begin AS SELECT 1 AS one CREATE TEXT SEARCH CONFIGURATION cs_ts_conf (copy=english) diff --git a/postgres/regression_suite/eager_aggregate.sql b/postgres/regression_suite/eager_aggregate.sql index 53d9b377a..7bca9c524 100644 --- a/postgres/regression_suite/eager_aggregate.sql +++ b/postgres/regression_suite/eager_aggregate.sql @@ -177,6 +177,32 @@ SELECT t1.a, avg(t2.c) FILTER (WHERE random() > 0.5) JOIN eager_agg_t2 t2 ON t1.b = t2.b GROUP BY t1.a ORDER BY t1.a; +-- Eager aggregation must not push a partial aggregate onto the inner side of a +-- SEMI or ANTI join +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t2.b, count(*) + FROM eager_agg_t2 t2 + WHERE NOT EXISTS (SELECT 1 FROM eager_agg_t3 t3 WHERE t3.a = t2.a) +GROUP BY t2.b ORDER BY t2.b; + +SELECT t2.b, count(*) + FROM eager_agg_t2 t2 + WHERE NOT EXISTS (SELECT 1 FROM eager_agg_t3 t3 WHERE t3.a = t2.a) +GROUP BY t2.b ORDER BY t2.b; + +-- Eager aggregation may still push a partial aggregate onto the outer side of +-- a SEMI or ANTI join +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t2.b, count(*) + FROM eager_agg_t2 t2 + WHERE EXISTS (SELECT 1 FROM eager_agg_t1 t1 WHERE t1.b = t2.b) +GROUP BY t2.b ORDER BY t2.b; + +SELECT t2.b, count(*) + FROM eager_agg_t2 t2 + WHERE EXISTS (SELECT 1 FROM eager_agg_t1 t1 WHERE t1.b = t2.b) +GROUP BY t2.b ORDER BY t2.b; + DROP TABLE eager_agg_t1; DROP TABLE eager_agg_t2; DROP TABLE eager_agg_t3; diff --git a/postgres/regression_suite/event_trigger.sql b/postgres/regression_suite/event_trigger.sql index 32e9bb58c..d0e6ba295 100644 --- a/postgres/regression_suite/event_trigger.sql +++ b/postgres/regression_suite/event_trigger.sql @@ -143,6 +143,21 @@ create user mapping for regress_evt_user server useless_server; alter default privileges for role regress_evt_user revoke delete on tables from regress_evt_user; +-- DROP PROPERTY GRAPH should work with event trigger in place +CREATE TABLE tv1 (a int PRIMARY KEY, b text); +CREATE TABLE tv2 (i int PRIMARY KEY, j text); +CREATE TABLE te1 (p int PRIMARY KEY, a int REFERENCES tv1(a), b int REFERENCES tv2(i), q text); + +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + tv1 LABEL l1 PROPERTIES (b AS p1), + tv2 LABEL l2 PROPERTIES (j AS p1)) + EDGE TABLES (te1 SOURCE tv1 DESTINATION tv2 LABEL e1 PROPERTIES (q as p1)); + +ALTER PROPERTY GRAPH gx ALTER EDGE TABLE te1 ALTER LABEL e1 DROP PROPERTIES (p1); +DROP PROPERTY GRAPH gx; +DROP TABLE tv1, tv2, te1; + -- alter owner to non-superuser should fail alter event trigger regress_event_trigger owner to regress_evt_user; diff --git a/postgres/regression_suite/fast_default.sql b/postgres/regression_suite/fast_default.sql index e5d9a3d2f..ccd138c4e 100644 --- a/postgres/regression_suite/fast_default.sql +++ b/postgres/regression_suite/fast_default.sql @@ -287,62 +287,11 @@ ORDER BY attnum; SELECT a, b, length(c) = 3 as c_ok, d, e >= 10 as e_ok FROM t2; --- test fast default over domains with constraints -CREATE DOMAIN domain5 AS int CHECK(value > 10) DEFAULT 8; -CREATE DOMAIN domain6 as int CHECK(value > 10) DEFAULT random(min=>11, max=>100); -CREATE DOMAIN domain7 as int CHECK((value + random(min=>11::int, max=>11)) > 12); -CREATE DOMAIN domain8 as int NOT NULL; - -CREATE TABLE test_add_domain_col(a int); --- succeeds despite constraint-violating default because table is empty -ALTER TABLE test_add_domain_col ADD COLUMN a1 domain5; -ALTER TABLE test_add_domain_col DROP COLUMN a1; -INSERT INTO test_add_domain_col VALUES(1),(2); - --- tests with non-empty table -ALTER TABLE test_add_domain_col ADD COLUMN b domain5; -- table rewrite, then fail -ALTER TABLE test_add_domain_col ADD COLUMN b domain8; -- table rewrite, then fail -ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 1; -- table rewrite, then fail -ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 12; -- ok, no table rewrite - --- explicit column default expression overrides domain's default --- expression, so no table rewrite -ALTER TABLE test_add_domain_col ADD COLUMN c domain6 DEFAULT 14; - -ALTER TABLE test_add_domain_col ADD COLUMN c1 domain8 DEFAULT 13; -- no table rewrite -SELECT attnum, attname, atthasmissing, atthasdef, attmissingval -FROM pg_attribute -WHERE attnum > 0 AND attrelid = 'test_add_domain_col'::regclass AND attisdropped is false -AND atthasmissing -ORDER BY attnum; - --- We need to rewrite the table whenever domain default contains volatile expression -ALTER TABLE test_add_domain_col ADD COLUMN d domain6; - --- We need to rewrite the table whenever domain constraint expression contains volatile expression -ALTER TABLE test_add_domain_col ADD COLUMN e domain7 default 14; -ALTER TABLE test_add_domain_col ADD COLUMN f domain7; - --- domain with both volatile and non-volatile CHECK constraints: the --- volatile one forces a table rewrite -CREATE DOMAIN domain9 AS int CHECK(value > 10) CHECK((value + random(min=>1::int, max=>1)) > 0); -ALTER TABLE test_add_domain_col ADD COLUMN g domain9 DEFAULT 14; - --- virtual generated columns cannot have domain types -ALTER TABLE test_add_domain_col ADD COLUMN h domain5 - GENERATED ALWAYS AS (a + 20) VIRTUAL; -- error - DROP TABLE t2; -DROP TABLE test_add_domain_col; DROP DOMAIN domain1; DROP DOMAIN domain2; DROP DOMAIN domain3; DROP DOMAIN domain4; -DROP DOMAIN domain5; -DROP DOMAIN domain6; -DROP DOMAIN domain7; -DROP DOMAIN domain8; -DROP DOMAIN domain9; DROP FUNCTION foo(INT); -- Fall back to full rewrite for volatile expressions diff --git a/postgres/regression_suite/for_portion_of.sql b/postgres/regression_suite/for_portion_of.sql index cb59bf5de..71e3d3d3e 100644 --- a/postgres/regression_suite/for_portion_of.sql +++ b/postgres/regression_suite/for_portion_of.sql @@ -913,6 +913,10 @@ CREATE TRIGGER fpo_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_before_stmt1 + BEFORE UPDATE OF valid_at ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + CREATE TRIGGER fpo_after_insert_stmt AFTER INSERT ON for_portion_of_test FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); @@ -931,6 +935,10 @@ CREATE TRIGGER fpo_before_row BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_before_row1 + BEFORE UPDATE OF valid_at ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + CREATE TRIGGER fpo_after_insert_row AFTER INSERT ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); @@ -1169,6 +1177,44 @@ SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; DROP TRIGGER fpo_after_delete_row ON for_portion_of_test; +-- Test that a tuple-modifying BEFORE INSERT ROW trigger acts +-- consistently on both temporal leftovers. +-- When FOR PORTION OF splits a row into two leftovers, both triggers +-- should get the original row's values. + +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int, + valid_at daterange, + name text +); + +CREATE FUNCTION fpo_append_name_suffix() +RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + NEW.name := NEW.name || '+insert'; + RETURN NEW; +END; +$$; + +CREATE TRIGGER fpo_before_insert_row + BEFORE INSERT ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE fpo_append_name_suffix(); + +INSERT INTO for_portion_of_test VALUES (1, '[2020-01-01,2020-12-31)', 'foo'); + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2020-04-01' TO '2020-08-01' + SET name = 'bar' + WHERE id = 1; + +-- Both leftovers should have the same name: 'foo+insert+insert'. +SELECT * FROM for_portion_of_test ORDER BY valid_at; + +DROP FUNCTION fpo_append_name_suffix CASCADE; +DROP TABLE for_portion_of_test; + -- Test with multiranges CREATE TABLE for_portion_of_test2 ( @@ -1292,6 +1338,7 @@ DROP TABLE for_portion_of_test2; DROP TYPE mydaterange; -- Test FOR PORTION OF against a partitioned table. +-- Include a stored generated column to test updatedCols column mapping. -- temporal_partitioned_1 has the same attnums as the root -- temporal_partitioned_3 has the different attnums from the root -- temporal_partitioned_5 has the different attnums too, but reversed @@ -1300,6 +1347,7 @@ CREATE TABLE temporal_partitioned ( id int4range, valid_at daterange, name text, + range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED, CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE temporal_partitioned_1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)'); @@ -1307,13 +1355,15 @@ CREATE TABLE temporal_partitioned_3 PARTITION OF temporal_partitioned FOR VALUES CREATE TABLE temporal_partitioned_5 PARTITION OF temporal_partitioned FOR VALUES IN ('[5,6)', '[6,7)'); ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_3; -ALTER TABLE temporal_partitioned_3 DROP COLUMN id, DROP COLUMN valid_at; +ALTER TABLE temporal_partitioned_3 DROP COLUMN id, DROP COLUMN valid_at CASCADE; ALTER TABLE temporal_partitioned_3 ADD COLUMN id int4range NOT NULL, ADD COLUMN valid_at daterange NOT NULL; +ALTER TABLE temporal_partitioned_3 ADD COLUMN range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED; ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_3 FOR VALUES IN ('[3,4)', '[4,5)'); ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_5; -ALTER TABLE temporal_partitioned_5 DROP COLUMN id, DROP COLUMN valid_at; +ALTER TABLE temporal_partitioned_5 DROP COLUMN id, DROP COLUMN valid_at CASCADE; ALTER TABLE temporal_partitioned_5 ADD COLUMN valid_at daterange NOT NULL, ADD COLUMN id int4range NOT NULL; +ALTER TABLE temporal_partitioned_5 ADD COLUMN range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED; ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_5 FOR VALUES IN ('[5,6)', '[6,7)'); INSERT INTO temporal_partitioned (id, valid_at, name) VALUES @@ -1358,7 +1408,7 @@ UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-0 -- Update all partitions at once (each with leftovers) -SELECT * FROM temporal_partitioned ORDER BY id, valid_at; +SELECT *, upper(valid_at) - lower(valid_at) FROM temporal_partitioned ORDER BY id, valid_at; SELECT * FROM temporal_partitioned_1 ORDER BY id, valid_at; SELECT * FROM temporal_partitioned_3 ORDER BY id, valid_at; SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at; @@ -1398,4 +1448,147 @@ SELECT * FROM fpo_rule ORDER BY f1; DROP TABLE fpo_rule; +-- UPDATE/DELETE FOR PORTION OF with table inheritance +-- Leftover rows must stay in the child table, preserving child-specific columns. +CREATE TABLE fpo_inh_parent ( + id int4range, + valid_at daterange, + name text +); +CREATE TABLE fpo_inh_child ( + description text +) INHERITS (fpo_inh_parent); + +-- Update targets the parent; the matching row lives in the child. +INSERT INTO fpo_inh_child (id, valid_at, name, description) VALUES + ('[1,2)', '[2018-01-01,2019-01-01)', 'one', 'initial'); +UPDATE fpo_inh_parent FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-10-01' + SET name = 'one^1'; +-- All three rows should be in the child, with description preserved. +SELECT tableoid::regclass, * FROM fpo_inh_parent ORDER BY valid_at; +SELECT * FROM fpo_inh_child ORDER BY valid_at; +-- No rows should have leaked into the parent. +SELECT * FROM ONLY fpo_inh_parent ORDER BY valid_at; + +-- Same test for DELETE instead of UPDATE: +TRUNCATE fpo_inh_child, fpo_inh_parent; +INSERT INTO fpo_inh_child (id, valid_at, name, description) VALUES + ('[1,2)', '[2018-01-01,2019-01-01)', 'one', 'initial'); +DELETE FROM fpo_inh_parent FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-10-01'; +-- Both rows should be in the child, with description preserved. +SELECT tableoid::regclass, * FROM fpo_inh_parent ORDER BY valid_at; +SELECT * FROM fpo_inh_child ORDER BY valid_at; +-- No rows should have leaked into the parent. +SELECT * FROM ONLY fpo_inh_parent ORDER BY valid_at; + +DROP TABLE fpo_inh_parent CASCADE; + +-- UPDATE FOR PORTION OF with multiple inheritance +-- Leftover rows must stay in the child table, even if the range column's +-- attnum differs between the target parent and child. +CREATE TABLE temporal_parent ( + id int, + valid_at daterange, + name text +); +CREATE TABLE other_parent ( + prefix text, + note text +); +CREATE TABLE mi_child () INHERITS (other_parent, temporal_parent); + +-- attnum of the range column is different in temporal_parent and mi_child +SELECT attnum, attname + FROM pg_attribute + WHERE attrelid = 'temporal_parent'::regclass + AND attnum > 0 AND NOT attisdropped + ORDER BY attnum; +SELECT attnum, attname + FROM pg_attribute + WHERE attrelid = 'mi_child'::regclass + AND attnum > 0 AND NOT attisdropped + ORDER BY attnum; + +INSERT INTO mi_child (prefix, note, id, valid_at, name) VALUES + ('pfx', 'memo', 1, daterange('2000-01-01', '2010-01-01'), 'old'); + +UPDATE temporal_parent FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' + SET name = 'new' + WHERE id = 1; + +SELECT tableoid::regclass, * FROM temporal_parent ORDER BY valid_at; +SELECT * FROM mi_child ORDER BY valid_at; +SELECT * FROM ONLY temporal_parent ORDER BY valid_at; + +TRUNCATE mi_child, other_parent, temporal_parent; +INSERT INTO mi_child (prefix, note, id, valid_at, name) VALUES + ('pfx', 'memo', 1, daterange('2000-01-01', '2010-01-01'), 'old'); + +DELETE FROM temporal_parent FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' + WHERE id = 1; + +SELECT tableoid::regclass, * FROM temporal_parent ORDER BY valid_at; +SELECT * FROM mi_child ORDER BY valid_at; +SELECT * FROM ONLY temporal_parent ORDER BY valid_at; + +DROP TABLE temporal_parent CASCADE; + +-- UPDATE FOR PORTION OF with generated columns +-- The generated column depends on the range column, so it must be +-- recomputed when FOR PORTION OF narrows the range. +CREATE TABLE fpo_generated ( + id int, + valid_at int4range, + range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED, + range_lenv int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) +); +INSERT INTO fpo_generated (id, valid_at) VALUES (1, '[10,100)'); + +SELECT * FROM fpo_generated ORDER BY valid_at; + +-- After the FOR PORTION OF (FPO) update, all three resulting rows +-- (leftover-before, updated, and leftover-after) must contain the correct +-- values for range_len and range_lenv. +UPDATE fpo_generated + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; + +SELECT * FROM fpo_generated ORDER BY valid_at; + +-- Also test with a generated column that references both a SET column +-- and the range column. +DROP TABLE fpo_generated; +CREATE TABLE fpo_generated ( + id int, + valid_at int4range, + id_plus_len int GENERATED ALWAYS AS (id + upper(valid_at) - lower(valid_at)) STORED, + id_plus_lenv int GENERATED ALWAYS AS (id + upper(valid_at) - lower(valid_at)) +); + +INSERT INTO fpo_generated (id, valid_at) VALUES (1, '[10,100)'); +SELECT * FROM fpo_generated ORDER BY valid_at; + +UPDATE fpo_generated + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +SELECT * FROM fpo_generated ORDER BY valid_at; +DROP TABLE fpo_generated; + +-- Test that UPDATE OF colname triggers fire if colname is valid_at: +CREATE TABLE fpo_update_of_trigger ( + id int, + valid_at int4range +); +INSERT INTO fpo_update_of_trigger (id, valid_at) VALUES (1, '[10,100)'); +CREATE TRIGGER fpo_before_row1 + BEFORE UPDATE OF valid_at ON fpo_update_of_trigger + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_before_row2 + BEFORE UPDATE OF valid_at ON fpo_update_of_trigger + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); +UPDATE fpo_update_of_trigger + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +DROP TABLE fpo_update_of_trigger; + RESET datestyle; diff --git a/postgres/regression_suite/foreign_key.sql b/postgres/regression_suite/foreign_key.sql index 673dc16b3..e55164933 100644 --- a/postgres/regression_suite/foreign_key.sql +++ b/postgres/regression_suite/foreign_key.sql @@ -2673,6 +2673,20 @@ 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; +-- Domain-typed FK column whose base type differs from the PK type: the +-- fast path must look through the domain to its base type when deciding +-- whether the cross-type comparison needs a cast. Otherwise valid rows +-- were wrongly rejected with "no conversion function from ... to ...". +CREATE DOMAIN fp_int8dom AS int8; +CREATE TABLE fp_pk_dom (a int4 PRIMARY KEY); +INSERT INTO fp_pk_dom SELECT generate_series(1, 200); +CREATE TABLE fp_fk_dom (a fp_int8dom REFERENCES fp_pk_dom); +INSERT INTO fp_fk_dom SELECT generate_series(1, 200); +INSERT INTO fp_fk_dom VALUES (999); +INSERT INTO fp_fk_dom VALUES (NULL); +DROP TABLE fp_fk_dom, fp_pk_dom; +DROP DOMAIN fp_int8dom; + -- 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); @@ -2680,3 +2694,72 @@ 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; + +-- Re-entrant FK fast-path: DML on the same FK table from a cast function +-- during a full-batch flush must not corrupt the batch array. +CREATE TABLE fp_reentry_pk (id int PRIMARY KEY); +INSERT INTO fp_reentry_pk VALUES (1), (2); +CREATE TYPE fp_vch AS (v int); +CREATE FUNCTION fp_vcast(fp_vch) RETURNS int LANGUAGE plpgsql AS $$ +BEGIN + IF $1.v = 1 THEN + INSERT INTO fp_reentry_fk VALUES (row(2)::fp_vch); + END IF; + RETURN $1.v; +END$$; +CREATE CAST (fp_vch AS int) WITH FUNCTION fp_vcast(fp_vch) AS IMPLICIT; +CREATE TABLE fp_reentry_fk (a fp_vch + REFERENCES fp_reentry_pk (id)); +-- Fill exactly one batch so the flush fires; the cast re-enters with DML +-- on the same FK and must take the per-row path. +INSERT INTO fp_reentry_fk SELECT row(1)::fp_vch FROM generate_series(1, 64); +SELECT a, count(*) FROM fp_reentry_fk GROUP BY a ORDER BY a; +DROP TABLE fp_reentry_fk, fp_reentry_pk; +DROP CAST (fp_vch AS int); +DROP FUNCTION fp_vcast(fp_vch); +DROP TYPE fp_vch; + +-- Flush error caught by a savepoint must leave the entry empty and reusable. +CREATE TABLE fp_reentry_pk2 (id int PRIMARY KEY); +INSERT INTO fp_reentry_pk2 VALUES (1); +CREATE TABLE fp_reentry_fk2 (a int REFERENCES fp_reentry_pk2 (id)); +DO $$ +BEGIN + -- A batch containing a violating row; the flush reports the violation. + BEGIN + INSERT INTO fp_reentry_fk2 SELECT CASE WHEN g = 32 THEN 999 ELSE 1 END + FROM generate_series(1, 64) g; + EXCEPTION WHEN foreign_key_violation THEN + RAISE NOTICE 'caught fk violation'; + END; + + -- Reuse the same FK with a full batch in the same transaction. The + -- entry must be empty after the caught violation: no stale rows from the + -- rolled-back batch (in particular no 999), and no array overflow. + INSERT INTO fp_reentry_fk2 SELECT 1 FROM generate_series(1, 64); +END$$; +SELECT count(*), max(a) FROM fp_reentry_fk2; -- 64 rows, max 1 +DROP TABLE fp_reentry_fk2, fp_reentry_pk2; + +-- Subtransaction abort during after-trigger firing must not drop FK checks +-- for rows buffered earlier in the same statement. Batching is confined to +-- the top transaction level and the buffered batch is no longer discarded on +-- subxact abort, so the violating rows are detected. +CREATE TABLE fp_subxact_pk (id int PRIMARY KEY); +INSERT INTO fp_subxact_pk SELECT g FROM generate_series(1, 10) g; +CREATE TABLE fp_subxact_fk (a int, tag text); +ALTER TABLE fp_subxact_fk ADD CONSTRAINT fp_subxact_fk_fkey + FOREIGN KEY (a) REFERENCES fp_subxact_pk (id); +CREATE FUNCTION fp_abort_subxact() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF NEW.tag = 'boom' THEN + BEGIN PERFORM 1/0; EXCEPTION WHEN division_by_zero THEN NULL; END; + END IF; + RETURN NEW; +END$$; +CREATE TRIGGER fp_subxact_trg AFTER INSERT ON fp_subxact_fk + FOR EACH ROW EXECUTE FUNCTION fp_abort_subxact(); +INSERT INTO fp_subxact_fk VALUES (999, 'bad'), (0, 'boom'), (1, 'ok'); +DROP TRIGGER fp_subxact_trg ON fp_subxact_fk; +DROP FUNCTION fp_abort_subxact(); +DROP TABLE fp_subxact_fk, fp_subxact_pk; diff --git a/postgres/regression_suite/generated_stored.sql b/postgres/regression_suite/generated_stored.sql index f957905b5..a05daecdb 100644 --- a/postgres/regression_suite/generated_stored.sql +++ b/postgres/regression_suite/generated_stored.sql @@ -368,6 +368,25 @@ INSERT INTO gtest21b (a) VALUES (NULL); -- error ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL; INSERT INTO gtest21b (a) VALUES (0); -- ok now +-- virtual generated columns are not physically stored, even when not null +--CREATE TABLE gtest21c (a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) VIRTUAL NOT NULL, c int NOT NULL); +--INSERT INTO gtest21c (a, c) VALUES (10, 42); +--SELECT a, b, c FROM gtest21c; +--DROP TABLE gtest21c; +-- try adding a virtual generated column to an existing table with tuples, +-- then try adding an atthasmissing column before adding a normal nullable +-- column. +--CREATE TABLE gtest21d (a int NOT NULL); +--INSERT INTO gtest21d (a) VALUES(10); +--ALTER TABLE gtest21d ADD COLUMN b INT GENERATED ALWAYS AS (a * 10) VIRTUAL NOT NULL; +--SELECT * FROM gtest21d ORDER BY a; +--INSERT INTO gtest21d (a) VALUES(20); +--ALTER TABLE gtest21d ADD COLUMN c INT NOT NULL DEFAULT 1234; +--SELECT * FROM gtest21d ORDER BY a; +--ALTER TABLE gtest21d ADD COLUMN d INT; +--INSERT INTO gtest21d (a, c, d) VALUES(30, 12345, 100); +--SELECT * FROM gtest21d ORDER BY a; +--DROP TABLE gtest21d; -- not-null constraint with partitioned table CREATE TABLE gtestnn_parent ( f1 int, diff --git a/postgres/regression_suite/generated_virtual.sql b/postgres/regression_suite/generated_virtual.sql index 5754a956a..4ebe1a1f3 100644 --- a/postgres/regression_suite/generated_virtual.sql +++ b/postgres/regression_suite/generated_virtual.sql @@ -374,6 +374,27 @@ INSERT INTO gtest21b (a) VALUES (NULL); -- error ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL; INSERT INTO gtest21b (a) VALUES (0); -- ok now +-- virtual generated columns are not physically stored, even when not null +CREATE TABLE gtest21c (a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) VIRTUAL NOT NULL, c int NOT NULL); +INSERT INTO gtest21c (a, c) VALUES (10, 42); +SELECT a, b, c FROM gtest21c; +DROP TABLE gtest21c; + +-- try adding a virtual generated column to an existing table with tuples, +-- then try adding an atthasmissing column before adding a normal nullable +-- column. +CREATE TABLE gtest21d (a int NOT NULL); +INSERT INTO gtest21d (a) VALUES(10); +ALTER TABLE gtest21d ADD COLUMN b INT GENERATED ALWAYS AS (a * 10) VIRTUAL NOT NULL; +SELECT * FROM gtest21d ORDER BY a; +INSERT INTO gtest21d (a) VALUES(20); +ALTER TABLE gtest21d ADD COLUMN c INT NOT NULL DEFAULT 1234; +SELECT * FROM gtest21d ORDER BY a; +ALTER TABLE gtest21d ADD COLUMN d INT; +INSERT INTO gtest21d (a, c, d) VALUES(30, 12345, 100); +SELECT * FROM gtest21d ORDER BY a; +DROP TABLE gtest21d; + -- not-null constraint with partitioned table CREATE TABLE gtestnn_parent ( f1 int, diff --git a/postgres/regression_suite/graph_table.sql b/postgres/regression_suite/graph_table.sql index 6da35a44d..df8426c42 100644 --- a/postgres/regression_suite/graph_table.sql +++ b/postgres/regression_suite/graph_table.sql @@ -158,6 +158,12 @@ CREATE TABLE x1 (a int, address text); INSERT INTO x1 VALUES (1, 'one'), (2, 'two'); SELECT * FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US' AND c.customer_id = x1.a)-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name, c.customer_id AS cid)); SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (x1 IS customers WHERE x1.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (x1.name AS customer_name, x1.customer_id AS cid, o.order_id)) g; +-- lateral reference with multi-label pattern, which is rewritten as UNION of +-- path queries +SELECT x1.a, g.* FROM x1, + GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.customer_id = x1.a)-[IS customer_orders | customer_wishlists]->(l IS lists)-[IS list_items]->(p IS products) + COLUMNS (x1.a AS outer_id, c.name AS customer_name, p.name AS product_name, l.list_type)) g + ORDER BY 1, 3, 4, 5; -- non-local property references are not allowed, even if a lateral column -- reference is available SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (x1 IS customers)-[IS customer_orders]->(o IS orders WHERE o.order_id = x1.a) COLUMNS (x1.name AS customer_name, x1.customer_id AS cid, o.order_id)) g; -- error @@ -523,7 +529,8 @@ SELECT * FROM GRAPH_TABLE (g4 MATCH (s IS ptnv)-[e IS ptne]->(d IS ptnv) COLUMNS SELECT * FROM GRAPH_TABLE (g4 MATCH (s)-[e]-(d) WHERE s.id = 3 COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; SELECT * FROM GRAPH_TABLE (g4 MATCH (s WHERE s.id = 3)-[e]-(d) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; --- ruleutils reverse parsing +-- GRAPH_TABLE in views + -- The query in the view definition is intentionally complex to test one view with many -- features like label disjunction, lateral references, WHERE clauses in graph -- patterns. @@ -532,8 +539,17 @@ SELECT g.* FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US' AND c.customer_id = x1.a) -[IS customer_orders | customer_wishlists ]-> (l IS orders | wishlists)-[ IS list_items]->(p IS products) - COLUMNS (c.name AS customer_name, p.name AS product_name, x1.a AS a)) g + COLUMNS (c.name AS customer_name, p.name AS product_name, p.price, x1.a AS a)) g ORDER BY customer_name, product_name; +-- Dropping properties or labels used by a view is not allowed +-- If these DDLs succeed, the pg_get_viewdef call below will throw cache lookup +-- error. +ALTER PROPERTY GRAPH myshop ALTER VERTEX TABLE orders DROP LABEL orders; -- error +ALTER PROPERTY GRAPH myshop ALTER VERTEX TABLE customers + ALTER LABEL customers DROP PROPERTIES (address); -- error +ALTER PROPERTY GRAPH myshop ALTER VERTEX TABLE products + ALTER LABEL products DROP PROPERTIES (price); -- error +-- ruleutils reverse parsing SELECT pg_get_viewdef('customers_us'::regclass); -- test view/graph nesting diff --git a/postgres/regression_suite/join.sql b/postgres/regression_suite/join.sql index fae19113c..78f7b4f54 100644 --- a/postgres/regression_suite/join.sql +++ b/postgres/regression_suite/join.sql @@ -2420,6 +2420,18 @@ CREATE TEMP TABLE parted_b1 partition of parted_b for values from (0) to (10); explain (costs off) select a.* from a left join parted_b pb on a.b_id = pb.id; +-- test that clauses that still embed PHVs are not referencing the removed +-- relation when rebuilt for a partition of the kept relation +explain (costs off) +select 1 from (select t1.id from parted_b t1 left join parted_b t2 on t1.id = t2.id) s +where s.id = 1 group by (); + +explain (costs off) +select 1 from parted_b t1 + join (select t2.id from parted_b t2 left join parted_b t3 on t2.id = t3.id) s + on t1.id = s.id +group by (); + rollback; create temp table parent (k int primary key, pd int); diff --git a/postgres/regression_suite/object_address.sql b/postgres/regression_suite/object_address.sql index a61c6712b..17d6dac7d 100644 --- a/postgres/regression_suite/object_address.sql +++ b/postgres/regression_suite/object_address.sql @@ -67,7 +67,8 @@ DECLARE BEGIN FOR objtype IN VALUES ('toast table'), ('index column'), ('sequence column'), ('toast table column'), ('view column'), ('materialized view column'), - ('property graph element'), ('property graph label'), ('property graph property') + ('property graph element'), ('property graph label'), ('property graph property'), + ('property graph element label'), ('property graph label property') LOOP BEGIN PERFORM pg_get_object_address(objtype, '{one}', '{}'); @@ -284,6 +285,8 @@ WITH objects (classid, objid, objsubid) AS (VALUES ('pg_propgraph_element'::regclass, 0, 0), -- no property graph element ('pg_propgraph_label'::regclass, 0, 0), -- no property graph label ('pg_propgraph_property'::regclass, 0, 0), -- no property graph property + ('pg_propgraph_element_label'::regclass, 0, 0), -- no property graph element label + ('pg_propgraph_label_property'::regclass, 0, 0), -- no property graph label property ('pg_publication'::regclass, 0, 0), -- no publication ('pg_publication_namespace'::regclass, 0, 0), -- no publication namespace ('pg_publication_rel'::regclass, 0, 0), -- no publication relation diff --git a/postgres/regression_suite/opr_sanity.sql b/postgres/regression_suite/opr_sanity.sql index 275adc173..cf29d49d8 100644 --- a/postgres/regression_suite/opr_sanity.sql +++ b/postgres/regression_suite/opr_sanity.sql @@ -360,7 +360,7 @@ AND case proargtypes[array_length(proargtypes, 1)-1] ELSE (SELECT t.oid FROM pg_type t WHERE t.typarray = proargtypes[array_length(proargtypes, 1)-1]) - END != provariadic; + END IS DISTINCT FROM provariadic; -- Check that all and only those functions with a variadic type have -- a variadic argument. diff --git a/postgres/regression_suite/partition_merge.sql b/postgres/regression_suite/partition_merge.sql index bf1fe3570..224a4f493 100644 --- a/postgres/regression_suite/partition_merge.sql +++ b/postgres/regression_suite/partition_merge.sql @@ -781,6 +781,23 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5); DROP TABLE t; +-- A merged partition needs its own TOAST table; otherwise an out-of-line +-- varlena value carried over from one of the merging partitions has +-- nowhere to be stored. SET STORAGE EXTERNAL forces externalization +-- for any value over the TOAST threshold, so a string over that threshold +-- suffices to exercise the toast-table dependency. +CREATE TABLE t (a text) PARTITION BY RANGE(a); +ALTER TABLE t ALTER COLUMN a SET STORAGE EXTERNAL; +CREATE TABLE tp_def PARTITION OF t DEFAULT; +CREATE TABLE tp_2_3 PARTITION OF t FOR VALUES FROM ('2') TO ('3'); +INSERT INTO t SELECT repeat('1', 10000); +ALTER TABLE t MERGE PARTITIONS (tp_def, tp_2_3) INTO tp_merged; +SELECT reltoastrelid <> 0 AS has_toast, + pg_relation_size(reltoastrelid) > 0 AS toast_used + FROM pg_class WHERE relname = 'tp_merged'; +SELECT length(a) FROM t; +DROP TABLE t; + RESET search_path; diff --git a/postgres/regression_suite/partition_split.sql b/postgres/regression_suite/partition_split.sql index e163b7b8f..05157a8fe 100644 --- a/postgres/regression_suite/partition_split.sql +++ b/postgres/regression_suite/partition_split.sql @@ -834,6 +834,57 @@ SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text DROP TABLE sales_range; +-- +-- Test that SPLIT PARTITION rejects the degenerate case where the only +-- non-DEFAULT replacement partition keeps the original bound and the command +-- merely adds a DEFAULT partition. +-- +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_50 PARTITION OF t FOR VALUES FROM (0) TO (50); +INSERT INTO t VALUES (1); + +-- ERROR +ALTER TABLE t SPLIT PARTITION tp_0_50 INTO + (PARTITION tp_0_50 FOR VALUES FROM (0) TO (50), + PARTITION tp_default DEFAULT); + +DROP TABLE t; + +-- +-- Test that a LIST split with DEFAULT is not considered degenerate when +-- only NULL is removed from the explicit replacement partition. +-- +CREATE TABLE t (i int) PARTITION BY LIST (i); +CREATE TABLE tp_null_1 PARTITION OF t FOR VALUES IN (NULL, 1); + +ALTER TABLE t SPLIT PARTITION tp_null_1 INTO + (PARTITION tp_1 FOR VALUES IN (1), + PARTITION tp_default DEFAULT); + +INSERT INTO t VALUES (NULL), (1), (2); +SELECT tableoid::regclass, i FROM t ORDER BY tableoid::regclass::text COLLATE "C", i NULLS FIRST; + +DROP TABLE t; + +-- +-- Test that the same-bound check for LIST partitioning uses the +-- partition operator family, not byte equality. -0.0 and 0.0 have +-- different bit patterns but compare equal under float8, so the +-- replacement bound (-0.0, 1.0) is the same set as the original +-- (0.0, 1.0) and the SPLIT is degenerate. A datumIsEqual()-based +-- check would let this through; the partsupfunc-based check correctly +-- rejects it. +-- +CREATE TABLE t (v float8) PARTITION BY LIST (v); +CREATE TABLE tp_zero_one PARTITION OF t FOR VALUES IN (0.0, 1.0); + +-- ERROR +ALTER TABLE t SPLIT PARTITION tp_zero_one INTO + (PARTITION tp_zero_one FOR VALUES IN (-0.0, 1.0), + PARTITION tp_default DEFAULT); + +DROP TABLE t; + -- -- Test that the explicit partition bound cannot extend outside the split -- partition's bound when a DEFAULT partition is specified. @@ -1133,6 +1184,25 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = DROP TABLE t; +-- Each new partition produced by SPLIT must get its own TOAST table so +-- that out-of-line varlena attributes coming from the source partition +-- can be stored. SET STORAGE EXTERNAL forces externalization for any +-- value over the TOAST threshold, so a string over that threshold +-- suffices to exercise the toast-table dependency. +CREATE TABLE t (a text) PARTITION BY RANGE(a); +ALTER TABLE t ALTER COLUMN a SET STORAGE EXTERNAL; +CREATE TABLE tp_all PARTITION OF t FOR VALUES FROM (MINVALUE) TO (MAXVALUE); +INSERT INTO t SELECT repeat('1', 10000); +ALTER TABLE t SPLIT PARTITION tp_all INTO ( + PARTITION tp_lo FOR VALUES FROM (MINVALUE) TO ('2'), + PARTITION tp_hi FOR VALUES FROM ('2') TO (MAXVALUE) +); +SELECT relname, + reltoastrelid <> 0 AS has_toast, + pg_relation_size(reltoastrelid) > 0 AS toast_used + FROM pg_class WHERE relname IN ('tp_lo', 'tp_hi') ORDER BY relname; +SELECT length(a) FROM t; +DROP TABLE t; RESET search_path; diff --git a/postgres/regression_suite/privileges.sql b/postgres/regression_suite/privileges.sql index edd3dbb88..59e0bf2d7 100644 --- a/postgres/regression_suite/privileges.sql +++ b/postgres/regression_suite/privileges.sql @@ -790,15 +790,23 @@ CREATE TABLE t1 ( valid_at tsrange, CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS) ); --- UPDATE requires select permission on the valid_at column (but not update): +-- UPDATE requires select and update permission on the valid_at column: GRANT SELECT (c1) ON t1 TO regress_priv_user2; GRANT UPDATE (c1) ON t1 TO regress_priv_user2; GRANT SELECT (c1, valid_at) ON t1 TO regress_priv_user3; GRANT UPDATE (c1) ON t1 TO regress_priv_user3; +GRANT SELECT (c1) ON t1 TO regress_priv_user4; +GRANT UPDATE (c1, valid_at) ON t1 TO regress_priv_user4; +GRANT SELECT (c1, valid_at) ON t1 TO regress_priv_user5; +GRANT UPDATE (c1, valid_at) ON t1 TO regress_priv_user5; SET SESSION AUTHORIZATION regress_priv_user2; UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; SET SESSION AUTHORIZATION regress_priv_user3; UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; +SET SESSION AUTHORIZATION regress_priv_user4; +UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; +SET SESSION AUTHORIZATION regress_priv_user5; +UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; SET SESSION AUTHORIZATION regress_priv_user1; -- DELETE requires select permission on the valid_at column: GRANT DELETE ON t1 TO regress_priv_user2; diff --git a/postgres/regression_suite/returning.sql b/postgres/regression_suite/returning.sql index 5c138ba84..f6a8f2c83 100644 --- a/postgres/regression_suite/returning.sql +++ b/postgres/regression_suite/returning.sql @@ -243,6 +243,17 @@ DELETE FROM foo WHERE f1 = 5 RETURNING old.tableoid::regclass, old.ctid, old.*, new.tableoid::regclass, new.ctid, new.*, *; +-- Parenthesized OLD and NEW +INSERT INTO foo VALUES (6, 'paren-test', 60, 600) + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; +UPDATE foo SET f4 = 700 WHERE f1 = 6 + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; +DELETE FROM foo WHERE f1 = 6 + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; + -- RETURNING OLD and NEW from subquery EXPLAIN (verbose, costs off) INSERT INTO foo VALUES (5, 'subquery test') diff --git a/postgres/regression_suite/sqljson.sql b/postgres/regression_suite/sqljson.sql index 88bd0c835..b3c662373 100644 --- a/postgres/regression_suite/sqljson.sql +++ b/postgres/regression_suite/sqljson.sql @@ -559,6 +559,28 @@ SELECT NULL::jd5 IS JSON WITH UNIQUE KEYS; -- error -- domain constraint violation during cast SELECT a::jd2 IS JSON WITH UNIQUE KEYS as col1 FROM (VALUES('{"a": 1, "a": 2}')) s(a); -- error +-- A user-defined string-category type with no implicit cast to text must +-- produce a clean error rather than crash for IS JSON / JSON() input +-- (per bug #19491). +CREATE FUNCTION sqljson_mystr_in(cstring) RETURNS sqljson_mystr + AS 'textin' LANGUAGE internal IMMUTABLE STRICT; +CREATE FUNCTION sqljson_mystr_out(sqljson_mystr) RETURNS cstring + AS 'textout' LANGUAGE internal IMMUTABLE STRICT; +CREATE TYPE sqljson_mystr ( + INPUT = sqljson_mystr_in, + OUTPUT = sqljson_mystr_out, + LIKE = text, + CATEGORY = 'S' +); +SELECT '{"a":1}'::sqljson_mystr IS JSON; -- error +SELECT JSON('{"a":1}'::sqljson_mystr WITH UNIQUE KEYS); -- error +-- An implicit cast to text lets the same query work normally. +CREATE CAST (sqljson_mystr AS text) WITHOUT FUNCTION AS IMPLICIT; +SELECT '{"a":1}'::sqljson_mystr IS JSON; +-- \set VERBOSITY terse +DROP TYPE sqljson_mystr CASCADE; +-- \set VERBOSITY default + -- view creation and deparsing with domain IS JSON CREATE VIEW domain_isjson AS WITH cte(a) AS (VALUES('{"a": 1, "a": 2}')) diff --git a/postgres/regression_suite/stats_import.sql b/postgres/regression_suite/stats_import.sql index 3f023428a..5084bf8a1 100644 --- a/postgres/regression_suite/stats_import.sql +++ b/postgres/regression_suite/stats_import.sql @@ -1612,6 +1612,20 @@ SELECT pg_catalog.pg_restore_extended_stats( 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], 'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]); +-- warn: more MCV items than can be handled. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', (SELECT array_agg(ARRAY[g::text, g::text]) + FROM generate_series(1, 10001) g), + 'most_common_freqs', (SELECT array_agg((1.0 / 10001)::double precision) + FROM generate_series(1, 10001) g), + 'most_common_base_freqs', (SELECT array_agg((1.0 / 10001)::double precision) + FROM generate_series(1, 10001) g)); + -- ok: mcv SELECT pg_catalog.pg_restore_extended_stats( 'schemaname', 'stats_import', diff --git a/postgres/regression_suite/subscription.sql b/postgres/regression_suite/subscription.sql index c9d9e47ab..afe357c83 100644 --- a/postgres/regression_suite/subscription.sql +++ b/postgres/regression_suite/subscription.sql @@ -42,6 +42,17 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB COMMENT ON SUBSCRIPTION regress_testsub IS 'test subscription'; SELECT obj_description(s.oid, 'pg_subscription') FROM pg_subscription s; +-- Check that only subconninfo is not publicly readable in pg_subscription. +SELECT count(*) = 0 AS ok + FROM pg_attribute + WHERE attrelid = 'pg_catalog.pg_subscription'::regclass AND attnum > 0 AND NOT attisdropped + AND ((attname = 'subconninfo' + AND has_column_privilege('regress_subscription_user_dummy', + 'pg_catalog.pg_subscription', attname, 'SELECT')) + OR (attname <> 'subconninfo' + AND NOT has_column_privilege('regress_subscription_user_dummy', + 'pg_catalog.pg_subscription', attname, 'SELECT'))); + -- Check if the subscription stats are created and stats_reset is updated -- by pg_stat_reset_subscription_stats(). SELECT subname, stats_reset IS NULL stats_reset_is_null FROM pg_stat_subscription_stats WHERE subname = 'regress_testsub'; @@ -121,19 +132,45 @@ RESET SESSION AUTHORIZATION; ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; SET SESSION AUTHORIZATION regress_subscription_user3; -CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = 'dummy', connect = false); +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server + PUBLICATION testpub WITH (slot_name = 'dummy', connect = false); -DROP USER MAPPING FOR regress_subscription_user3 SERVER test_server; RESET SESSION AUTHORIZATION; REVOKE USAGE ON FOREIGN SERVER test_server FROM regress_subscription_user3; SET SESSION AUTHORIZATION regress_subscription_user3; --- fail, must connect but lacks USAGE on server, as well as user mapping +-- ok, lacks USAGE on test_server, but replacing connection anyway +BEGIN; +ALTER SUBSCRIPTION regress_testsub6 CONNECTION 'dbname=regress_doesnotexist password=secret'; +ABORT; + +-- fails, cannot drop slot DROP SUBSCRIPTION regress_testsub6; +RESET SESSION AUTHORIZATION; +GRANT USAGE ON FOREIGN SERVER test_server TO regress_subscription_user3; +SET SESSION AUTHORIZATION regress_subscription_user3; + ALTER SUBSCRIPTION regress_testsub6 SET (slot_name = NONE); +DROP SUBSCRIPTION regress_testsub6; --ok + +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server + PUBLICATION testpub WITH (slot_name = 'dummy', connect = false); + +DROP USER MAPPING FOR regress_subscription_user3 SERVER test_server; + +-- ok, test_server lacks user mapping, but replacing connection anyway +BEGIN; +ALTER SUBSCRIPTION regress_testsub6 CONNECTION 'dbname=regress_doesnotexist password=secret'; +ABORT; + +-- fails, cannot drop slot DROP SUBSCRIPTION regress_testsub6; +ALTER SUBSCRIPTION regress_testsub6 DISABLE; +ALTER SUBSCRIPTION regress_testsub6 SET (slot_name = NONE); +DROP SUBSCRIPTION regress_testsub6; --ok + SET SESSION AUTHORIZATION regress_subscription_user; REVOKE CREATE ON DATABASE REGRESSION FROM regress_subscription_user3; @@ -367,11 +404,17 @@ DROP SUBSCRIPTION regress_testsub; -- fail - max_retention_duration must be integer CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = foo); +-- fail - max_retention_duration must be non-negative +CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = -1); + -- ok CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = 1000); -- \dRs+ +-- fail - max_retention_duration must be non-negative +ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = -1); + -- ok ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = 0); diff --git a/postgres/regression_suite/unicode.sql b/postgres/regression_suite/unicode.sql index 7a92781f4..8ec35d52e 100644 --- a/postgres/regression_suite/unicode.sql +++ b/postgres/regression_suite/unicode.sql @@ -36,3 +36,23 @@ FROM ORDER BY num; SELECT is_normalized('abc', 'def'); -- run-time error + +-- Hangul NFC recomposition tests +-- L+V -> LV composition (first and last) +SELECT normalize(U&'\1100\1161', NFC) = U&'\AC00' COLLATE "C" AS hangul_lv_first; +SELECT normalize(U&'\1112\1175', NFC) = U&'\D788' COLLATE "C" AS hangul_lv_last; +-- LV+T -> LVT composition +SELECT normalize(U&'\AC00\11A8', NFC) = U&'\AC01' COLLATE "C" AS hangul_lvt_first_t; +SELECT normalize(U&'\AC00\11C2', NFC) = U&'\AC1B' COLLATE "C" AS hangul_lvt_last_t; +SELECT normalize(U&'\D788\11A8', NFC) = U&'\D789' COLLATE "C" AS hangul_lvt_last_lv; +-- L+V+T -> LVT composition +SELECT normalize(U&'\1100\1161\11A8', NFC) = U&'\AC01' COLLATE "C" AS hangul_full_lvt; +SELECT normalize(U&'\1112\1175\11C2', NFC) = U&'\D7A3' COLLATE "C" AS hangul_full_lvt; +-- TBASE invalid T syllable +SELECT normalize(U&'\AC00\11A7', NFC) = U&'\AC00\11A7' COLLATE "C" AS hangul_tbase_not_combined; +SELECT normalize(U&'\1100\1161\11A7', NFC) = U&'\AC00\11A7' COLLATE "C" AS hangul_lv_tbase_separate; + +-- Hangul NFD decomposition tests +SELECT normalize(U&'\AC00', NFD) = U&'\1100\1161' COLLATE "C" AS hangul_nfd_lv; +SELECT normalize(U&'\AC01', NFD) = U&'\1100\1161\11A8' COLLATE "C" AS hangul_nfd_lvt; +SELECT normalize(U&'\D7A3', NFD) = U&'\1112\1175\11C2' COLLATE "C" AS hangul_nfd_last; diff --git a/postgres/regression_suite/window.sql b/postgres/regression_suite/window.sql index 3dcb29f5d..1fe577035 100644 --- a/postgres/regression_suite/window.sql +++ b/postgres/regression_suite/window.sql @@ -2099,6 +2099,9 @@ WINDOW w AS (ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING EXCLUDE CURR ; -- valid and invalid functions +SELECT abs(1) IGNORE NULLS; -- fails +SELECT no_such_window_func() IGNORE NULLS; -- fails, but not because of null treatment +SELECT sum(orbit) IGNORE NULLS FROM planets; -- fails SELECT sum(orbit) OVER () FROM planets; -- succeeds SELECT sum(orbit) RESPECT NULLS OVER () FROM planets; -- fails SELECT sum(orbit) IGNORE NULLS OVER () FROM planets; -- fails diff --git a/postgres/regression_suite/xml.sql b/postgres/regression_suite/xml.sql index 24615dcfc..7ca0a74c6 100644 --- a/postgres/regression_suite/xml.sql +++ b/postgres/regression_suite/xml.sql @@ -5,7 +5,14 @@ CREATE TABLE xmltest ( INSERT INTO xmltest VALUES (1, 'one'); INSERT INTO xmltest VALUES (2, 'two'); -INSERT INTO xmltest VALUES (3, 'three '); + +-- If no XML data could be inserted, skip the tests as the server has been +-- compiled without libxml support. +SELECT count(*) = 0 AS skip_test FROM xmltest /* \gset */; +-- \if :skip_test +-- \quit +-- \endif SELECT * FROM xmltest; @@ -30,7 +37,7 @@ SELECT xmlconcat(xmlcomment('hello'), SELECT xmlconcat('hello', 'you'); SELECT xmlconcat(1, 2); -SELECT xmlconcat('bad', ' '); SELECT xmlconcat('', NULL, ''); SELECT xmlconcat('', NULL, ''); SELECT xmlconcat(NULL); @@ -75,17 +82,17 @@ SELECT xmlparse(content '&'); SELECT xmlparse(content '&idontexist;'); SELECT xmlparse(content ''); SELECT xmlparse(content ''); -SELECT xmlparse(content '&idontexist;'); +SELECT xmlparse(content '&idontexist; '); SELECT xmlparse(content ''); -SELECT xmlparse(document ' '); +SELECT xmlparse(document '!'); SELECT xmlparse(document 'abc'); SELECT xmlparse(document 'x'); -SELECT xmlparse(document '&'); -SELECT xmlparse(document '&idontexist;'); +SELECT xmlparse(document '& '); +SELECT xmlparse(document '&idontexist; '); SELECT xmlparse(document ''); SELECT xmlparse(document ''); -SELECT xmlparse(document '&idontexist;'); +SELECT xmlparse(document '&idontexist; '); SELECT xmlparse(document ''); @@ -244,6 +251,7 @@ SELECT xpath('count(//*)=3', ''); SELECT xpath('name(/*)', ''); SELECT xpath('/nosuchtag', ''); SELECT xpath('root', ''); +SELECT xpath('//namespace::foo', ''); -- Round-trip non-ASCII data through xpath(). DO $$