From 2b4a31272a2bf6dd82f6c5ec78ec507e78d68b2d Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 14 Feb 2026 13:57:04 -0500 Subject: [PATCH] parser: support on conflict do select & sync regression suite --- .../src/generated/syntax_kind.rs | 1 + crates/squawk_parser/src/grammar.rs | 4 + .../tests/data/ok/insert_pg18.sql | 3 + .../snapshots/tests__insert_pg18_ok.snap | 77 +++ .../snapshots/tests__regression_encoding.snap | 5 + .../snapshots/tests__regression_euc_kr.snap | 5 + .../snapshots/tests__regression_oid8.snap | 5 + .../squawk_syntax/src/ast/generated/nodes.rs | 56 +- crates/squawk_syntax/src/postgresql.ungram | 4 + crates/xtask/src/sync_regression_suite.rs | 8 +- postgres/kwlist.h | 8 +- postgres/regression_suite/arrays.sql | 11 +- postgres/regression_suite/constraints.sql | 7 +- postgres/regression_suite/copy.sql | 15 + postgres/regression_suite/copy2.sql | 3 + postgres/regression_suite/copyencoding.sql | 7 + postgres/regression_suite/encoding.sql | 228 +++++++ postgres/regression_suite/euc_kr.sql | 12 + postgres/regression_suite/foreign_key.sql | 2 +- .../regression_suite/generated_virtual.sql | 11 +- postgres/regression_suite/groupingsets.sql | 69 ++ postgres/regression_suite/guc.sql | 22 + postgres/regression_suite/hash_func.sql | 7 + postgres/regression_suite/insert_conflict.sql | 74 +- postgres/regression_suite/join.sql | 37 + postgres/regression_suite/join_hash.sql | 1 + postgres/regression_suite/jsonb.sql | 21 +- postgres/regression_suite/lock.sql | 2 +- postgres/regression_suite/misc_functions.sql | 15 + postgres/regression_suite/oid8.sql | 87 +++ postgres/regression_suite/partition_join.sql | 3 +- postgres/regression_suite/partition_merge.sql | 6 +- postgres/regression_suite/partition_split.sql | 26 +- postgres/regression_suite/plpgsql.sql | 22 - postgres/regression_suite/predicate.sql | 208 ++++++ postgres/regression_suite/privileges.sql | 18 + postgres/regression_suite/publication.sql | 6 +- postgres/regression_suite/regex.sql | 3 - postgres/regression_suite/reloptions.sql | 11 + postgres/regression_suite/rowsecurity.sql | 62 +- postgres/regression_suite/rules.sql | 26 + postgres/regression_suite/stats_ext.sql | 18 + postgres/regression_suite/stats_import.sql | 644 +++++++++++++++++- postgres/regression_suite/strings.sql | 74 +- postgres/regression_suite/subselect.sql | 51 ++ postgres/regression_suite/sysviews.sql | 2 +- postgres/regression_suite/triggers.sql | 7 +- postgres/regression_suite/tsearch.sql | 1 + postgres/regression_suite/type_sanity.sql | 48 +- postgres/regression_suite/updatable_views.sql | 31 +- .../regression_suite/without_overlaps.sql | 8 +- 51 files changed, 1954 insertions(+), 128 deletions(-) create mode 100644 crates/squawk_parser/tests/data/ok/insert_pg18.sql create mode 100644 crates/squawk_parser/tests/snapshots/tests__insert_pg18_ok.snap create mode 100644 crates/squawk_parser/tests/snapshots/tests__regression_encoding.snap create mode 100644 crates/squawk_parser/tests/snapshots/tests__regression_euc_kr.snap create mode 100644 crates/squawk_parser/tests/snapshots/tests__regression_oid8.snap create mode 100644 postgres/regression_suite/encoding.sql create mode 100644 postgres/regression_suite/euc_kr.sql create mode 100644 postgres/regression_suite/oid8.sql diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index b90468af..9896bbd0 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -647,6 +647,7 @@ pub enum SyntaxKind { COMPOUND_SELECT, COMPRESSION_METHOD, CONFLICT_DO_NOTHING, + CONFLICT_DO_SELECT, CONFLICT_DO_UPDATE_SET, CONFLICT_INDEX_ITEM, CONFLICT_INDEX_ITEM_LIST, diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index f749e7fb..a6901be6 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -12420,6 +12420,10 @@ fn conflict_action(p: &mut Parser<'_>) { p.expect(DO_KW); if p.eat(NOTHING_KW) { m.complete(p, CONFLICT_DO_NOTHING); + } else if p.eat(SELECT_KW) { + opt_locking_clause(p); + opt_where_clause(p); + m.complete(p, CONFLICT_DO_SELECT); } else { p.expect(UPDATE_KW); set_clause(p); diff --git a/crates/squawk_parser/tests/data/ok/insert_pg18.sql b/crates/squawk_parser/tests/data/ok/insert_pg18.sql new file mode 100644 index 00000000..7c3f7bc8 --- /dev/null +++ b/crates/squawk_parser/tests/data/ok/insert_pg18.sql @@ -0,0 +1,3 @@ +-- on conflict do select +INSERT INTO t (a) VALUES (1) + ON CONFLICT (a) DO SELECT FOR UPDATE WHERE a > 0 RETURNING *; diff --git a/crates/squawk_parser/tests/snapshots/tests__insert_pg18_ok.snap b/crates/squawk_parser/tests/snapshots/tests__insert_pg18_ok.snap new file mode 100644 index 00000000..8e8c68ef --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__insert_pg18_ok.snap @@ -0,0 +1,77 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: crates/squawk_parser/tests/data/ok/insert_pg18.sql +--- +SOURCE_FILE + COMMENT "-- on conflict do select" + WHITESPACE "\n" + INSERT + INSERT_KW "INSERT" + WHITESPACE " " + INTO_KW "INTO" + WHITESPACE " " + PATH + PATH_SEGMENT + NAME_REF + IDENT "t" + WHITESPACE " " + COLUMN_LIST + L_PAREN "(" + COLUMN + NAME_REF + IDENT "a" + R_PAREN ")" + WHITESPACE " " + VALUES + VALUES_KW "VALUES" + WHITESPACE " " + ROW_LIST + ROW + L_PAREN "(" + LITERAL + INT_NUMBER "1" + R_PAREN ")" + WHITESPACE "\n " + ON_CONFLICT_CLAUSE + ON_KW "ON" + WHITESPACE " " + CONFLICT_KW "CONFLICT" + WHITESPACE " " + CONFLICT_ON_INDEX + CONFLICT_INDEX_ITEM_LIST + L_PAREN "(" + CONFLICT_INDEX_ITEM + NAME_REF + IDENT "a" + R_PAREN ")" + WHITESPACE " " + CONFLICT_DO_SELECT + DO_KW "DO" + WHITESPACE " " + SELECT_KW "SELECT" + WHITESPACE " " + LOCKING_CLAUSE + FOR_KW "FOR" + WHITESPACE " " + UPDATE_KW "UPDATE" + WHITESPACE " " + WHERE_CLAUSE + WHERE_KW "WHERE" + WHITESPACE " " + BIN_EXPR + NAME_REF + IDENT "a" + WHITESPACE " " + R_ANGLE ">" + WHITESPACE " " + LITERAL + INT_NUMBER "0" + WHITESPACE " " + RETURNING_CLAUSE + RETURNING_KW "RETURNING" + WHITESPACE " " + TARGET_LIST + TARGET + STAR "*" + SEMICOLON ";" + WHITESPACE "\n" diff --git a/crates/squawk_parser/tests/snapshots/tests__regression_encoding.snap b/crates/squawk_parser/tests/snapshots/tests__regression_encoding.snap new file mode 100644 index 00000000..fc652012 --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__regression_encoding.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: postgres/regression_suite/encoding.sql +--- + diff --git a/crates/squawk_parser/tests/snapshots/tests__regression_euc_kr.snap b/crates/squawk_parser/tests/snapshots/tests__regression_euc_kr.snap new file mode 100644 index 00000000..4769527b --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__regression_euc_kr.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: postgres/regression_suite/euc_kr.sql +--- + diff --git a/crates/squawk_parser/tests/snapshots/tests__regression_oid8.snap b/crates/squawk_parser/tests/snapshots/tests__regression_oid8.snap new file mode 100644 index 00000000..c28473c3 --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__regression_oid8.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: postgres/regression_suite/oid8.sql +--- + diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index f4b522dd..2b4b8726 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -3027,6 +3027,29 @@ impl ConflictDoNothing { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ConflictDoSelect { + pub(crate) syntax: SyntaxNode, +} +impl ConflictDoSelect { + #[inline] + pub fn locking_clause(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn where_clause(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn do_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::DO_KW) + } + #[inline] + pub fn select_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SELECT_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ConflictDoUpdateSet { pub(crate) syntax: SyntaxNode, @@ -16957,6 +16980,7 @@ pub enum ConfigValue { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ConflictAction { ConflictDoNothing(ConflictDoNothing), + ConflictDoSelect(ConflictDoSelect), ConflictDoUpdateSet(ConflictDoUpdateSet), } @@ -19129,6 +19153,24 @@ impl AstNode for ConflictDoNothing { &self.syntax } } +impl AstNode for ConflictDoSelect { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::CONFLICT_DO_SELECT + } + #[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 ConflictDoUpdateSet { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -30005,7 +30047,9 @@ impl AstNode for ConflictAction { fn can_cast(kind: SyntaxKind) -> bool { matches!( kind, - SyntaxKind::CONFLICT_DO_NOTHING | SyntaxKind::CONFLICT_DO_UPDATE_SET + SyntaxKind::CONFLICT_DO_NOTHING + | SyntaxKind::CONFLICT_DO_SELECT + | SyntaxKind::CONFLICT_DO_UPDATE_SET ) } #[inline] @@ -30014,6 +30058,9 @@ impl AstNode for ConflictAction { SyntaxKind::CONFLICT_DO_NOTHING => { ConflictAction::ConflictDoNothing(ConflictDoNothing { syntax }) } + SyntaxKind::CONFLICT_DO_SELECT => { + ConflictAction::ConflictDoSelect(ConflictDoSelect { syntax }) + } SyntaxKind::CONFLICT_DO_UPDATE_SET => { ConflictAction::ConflictDoUpdateSet(ConflictDoUpdateSet { syntax }) } @@ -30027,6 +30074,7 @@ impl AstNode for ConflictAction { fn syntax(&self) -> &SyntaxNode { match self { ConflictAction::ConflictDoNothing(it) => &it.syntax, + ConflictAction::ConflictDoSelect(it) => &it.syntax, ConflictAction::ConflictDoUpdateSet(it) => &it.syntax, } } @@ -30037,6 +30085,12 @@ impl From for ConflictAction { ConflictAction::ConflictDoNothing(node) } } +impl From for ConflictAction { + #[inline] + fn from(node: ConflictDoSelect) -> ConflictAction { + ConflictAction::ConflictDoSelect(node) + } +} impl From for ConflictAction { #[inline] fn from(node: ConflictDoUpdateSet) -> ConflictAction { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 93b57edc..dab98d99 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -1305,6 +1305,7 @@ ConflictIndexItem = ConflictAction = ConflictDoNothing | ConflictDoUpdateSet +| ConflictDoSelect ConflictDoUpdateSet = 'do' 'update' SetClause WhereClause? @@ -1312,6 +1313,9 @@ ConflictDoUpdateSet = ConflictDoNothing = 'do' 'nothing' +ConflictDoSelect = + 'do' 'select' LockingClause? WhereClause? + SetClause = 'set' SetColumnList diff --git a/crates/xtask/src/sync_regression_suite.rs b/crates/xtask/src/sync_regression_suite.rs index 2ce815fd..a7080301 100644 --- a/crates/xtask/src/sync_regression_suite.rs +++ b/crates/xtask/src/sync_regression_suite.rs @@ -282,7 +282,9 @@ pub(crate) fn preprocess_sql(source: R, mut dest: W) -> Re in_copy_stdin = false; } should_comment = true; - } else if (line.trim_start().starts_with('\\') && !line.contains("\\gset")) + } else if (line.trim_start().starts_with('\\') + && !line.contains("\\gset") + && !line.contains("\\gx")) || line.starts_with("'show_data'") || line.starts_with(':') { @@ -318,8 +320,8 @@ pub(crate) fn preprocess_sql(source: R, mut dest: W) -> Re line = line.replace(from, to); } - if line.contains("\\gset") { - if let Some(start) = line.find("\\gset") { + for pattern in ["\\gx", "\\gset"] { + if let Some(start) = line.find(pattern) { let end = line[start..] .find('\n') .map(|i| start + i) diff --git a/postgres/kwlist.h b/postgres/kwlist.h index 8b780fa7..2492dc72 100644 --- a/postgres/kwlist.h +++ b/postgres/kwlist.h @@ -1,7 +1,7 @@ // synced from: -// commit: e5f3839af685c303d8ebcc1ea0d407c124372931 -// committed at: 2025-12-22T22:41:34Z -// file: https://github.com/postgres/postgres/blob/e5f3839af685c303d8ebcc1ea0d407c124372931/src/include/parser/kwlist.h +// commit: ef3c3cf6d021ff9884c513afd850a9fe85cad736 +// committed at: 2026-02-14T06:50:06Z +// file: https://github.com/postgres/postgres/blob/ef3c3cf6d021ff9884c513afd850a9fe85cad736/src/include/parser/kwlist.h // // update via: // cargo xtask sync-kwlist @@ -15,7 +15,7 @@ * by the PG_KEYWORD macro, which is not defined in this file; it can * be defined by the caller for special purposes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/postgres/regression_suite/arrays.sql b/postgres/regression_suite/arrays.sql index eff45a76..55470f5d 100644 --- a/postgres/regression_suite/arrays.sql +++ b/postgres/regression_suite/arrays.sql @@ -528,6 +528,10 @@ select '[2147483646:2147483646]={1}'::int[]; select '[-2147483648:-2147483647]={1,2}'::int[]; -- all of the above should be accepted +-- some day we might allow these cases, but for now they're errors: +select array[]::oidvector; +select array[]::int2vector; + -- tests for array aggregates CREATE TEMP TABLE arraggtest ( f1 INT[], f2 TEXT[][], f3 FLOAT[]); @@ -555,19 +559,22 @@ SELECT max(f1), min(f1), max(f2), min(f2), max(f3), min(f3) FROM arraggtest; -- A few simple tests for arrays of composite types -create type comptype as (f1 int, f2 text); +create type comptype as (f1 int, f2 text, f3 int[]); create table comptable (c1 comptype, c2 comptype[]); -- XXX would like to not have to specify row() construct types here ... insert into comptable - values (row(1,'foo'), array[row(2,'bar')::comptype, row(3,'baz')::comptype]); + values (row(1,'foo',array[10,20]), array[row(2,'bar',array[30,40])::comptype, row(3,'baz',array[50,60])::comptype]); -- check that implicitly named array type _comptype isn't a problem create type _comptype as enum('fooey'); select * from comptable; select c2[2].f2 from comptable; +select c2[2].f3 from comptable; +select c2[2].f3[1:2] from comptable; +select c2[1:2].f3[1:2] from comptable; drop type _comptype; drop table comptable; diff --git a/postgres/regression_suite/constraints.sql b/postgres/regression_suite/constraints.sql index cf34b584..f1746042 100644 --- a/postgres/regression_suite/constraints.sql +++ b/postgres/regression_suite/constraints.sql @@ -568,6 +568,9 @@ INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>') -- fail, because DO UPDATE variant requires unique index INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>') ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO UPDATE SET c2 = EXCLUDED.c2; +-- fail, because DO SELECT variant requires unique index +INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>') + ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO SELECT RETURNING *; -- succeed because c1 doesn't overlap INSERT INTO circles VALUES('<(20,20), 1>', '<(0,0), 5>'); -- succeed because c2 doesn't overlap @@ -623,7 +626,9 @@ DROP TABLE deferred_excl; -- verify constraints created for NOT NULL clauses CREATE TABLE notnull_tbl1 (a INTEGER NOT NULL NOT NULL); -- \d+ notnull_tbl1 --- no-op +-- specifying an existing constraint is a no-op +ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_tbl1_a_not_null NOT NULL a; +-- but using a different constraint name is not allowed ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; -- \d+ notnull_tbl1 -- duplicate name diff --git a/postgres/regression_suite/copy.sql b/postgres/regression_suite/copy.sql index 5eb639c0..c4567853 100644 --- a/postgres/regression_suite/copy.sql +++ b/postgres/regression_suite/copy.sql @@ -124,6 +124,21 @@ copy copytest5 from stdin (format csv, header 5); -- \. select count(*) from copytest5; +-- test header line feature (given as strings) +truncate copytest5; +copy copytest5 from stdin (format csv, header '0'); +-- 1 +-- 2 +-- \. +select * from copytest5 order by c1; + +truncate copytest5; +copy copytest5 from stdin (format csv, header '1'); +-- 1 +-- 2 +-- \. +select * from copytest5 order by c1; + -- test copy from with a partitioned table create table parted_copytest ( a int, diff --git a/postgres/regression_suite/copy2.sql b/postgres/regression_suite/copy2.sql index 5cbe243d..38afd408 100644 --- a/postgres/regression_suite/copy2.sql +++ b/postgres/regression_suite/copy2.sql @@ -93,6 +93,9 @@ COPY x from stdin with (on_error ignore, reject_limit 0); COPY x from stdin with (header -1); COPY x from stdin with (header 2.5); -- COPY x to stdout with (header 2); +COPY x to stdout with (header '-1'); +COPY x from stdin with (header '2.5'); +-- COPY x to stdout with (header '2'); -- too many columns in column list: should fail COPY x (a, b, c, d, e, d, c) from stdin; diff --git a/postgres/regression_suite/copyencoding.sql b/postgres/regression_suite/copyencoding.sql index c8a9f290..0e46886d 100644 --- a/postgres/regression_suite/copyencoding.sql +++ b/postgres/regression_suite/copyencoding.sql @@ -23,6 +23,13 @@ CREATE TABLE copy_encoding_tab (t text); COPY (SELECT E'\u3042') TO 'utf8_csv' WITH (FORMAT csv, ENCODING 'UTF8'); -- Read UTF8 data as LATIN1: no error COPY copy_encoding_tab FROM 'utf8_csv' WITH (FORMAT csv, ENCODING 'LATIN1'); +-- Non-server encodings have distinct code paths. +-- \set fname :abs_builddir '/results/copyencoding_gb18030.csv' +COPY (SELECT E'\u3042,') TO 'fname' WITH (FORMAT csv, ENCODING 'GB18030'); +COPY copy_encoding_tab FROM 'fname' WITH (FORMAT csv, ENCODING 'GB18030'); +-- \set fname :abs_builddir '/results/copyencoding_gb18030.data' +COPY (SELECT E'\u3042,') TO 'fname' WITH (FORMAT text, ENCODING 'GB18030'); +COPY copy_encoding_tab FROM 'fname' WITH (FORMAT text, ENCODING 'GB18030'); -- Use client_encoding SET client_encoding TO UTF8; diff --git a/postgres/regression_suite/encoding.sql b/postgres/regression_suite/encoding.sql new file mode 100644 index 00000000..b5c85c0f --- /dev/null +++ b/postgres/regression_suite/encoding.sql @@ -0,0 +1,228 @@ +/* skip test if not UTF8 server encoding */ +SELECT getdatabaseencoding() <> 'UTF8' AS skip_test /* \gset */; +-- \if :skip_test +-- \quit +-- \endif + +-- \getenv libdir PG_LIBDIR +-- \getenv dlsuffix PG_DLSUFFIX + +-- \set regresslib :libdir '/regress' :dlsuffix + +CREATE FUNCTION test_bytea_to_text(bytea) RETURNS text + AS 'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_text_to_bytea(text) RETURNS bytea + AS 'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_mblen_func(text, text, text, int) RETURNS int + AS 'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_text_to_wchars(text, text) RETURNS int[] + AS 'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_wchars_to_text(text, int[]) RETURNS text + AS 'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_valid_server_encoding(text) RETURNS boolean + AS 'regresslib' LANGUAGE C STRICT; + + +CREATE TABLE regress_encoding(good text, truncated text, with_nul text, truncated_with_nul text); +INSERT INTO regress_encoding +VALUES ('café', + 'caf' || test_bytea_to_text('\xc3'), + 'café' || test_bytea_to_text('\x00') || 'dcba', + 'caf' || test_bytea_to_text('\xc300') || 'dcba'); + +SELECT good, truncated, with_nul FROM regress_encoding; + +SELECT length(good) FROM regress_encoding; +SELECT substring(good, 3, 1) FROM regress_encoding; +SELECT substring(good, 4, 1) FROM regress_encoding; +SELECT regexp_replace(good, '^caf(.)$', '\1') FROM regress_encoding; +SELECT reverse(good) FROM regress_encoding; + +-- invalid short mb character = error +SELECT length(truncated) FROM regress_encoding; +SELECT substring(truncated, 1, 1) FROM regress_encoding; +SELECT reverse(truncated) FROM regress_encoding; +-- invalid short mb character = silently dropped +SELECT regexp_replace(truncated, '^caf(.)$', '\1') FROM regress_encoding; + +-- PostgreSQL doesn't allow strings to contain NUL. If a corrupted string +-- contains NUL at a character boundary position, some functions treat it as a +-- character while others treat it as a terminator, as implementation details. + +-- NUL = terminator +SELECT length(with_nul) FROM regress_encoding; +SELECT substring(with_nul, 3, 1) FROM regress_encoding; +SELECT substring(with_nul, 4, 1) FROM regress_encoding; +SELECT substring(with_nul, 5, 1) FROM regress_encoding; +SELECT convert_to(substring(with_nul, 5, 1), 'UTF8') FROM regress_encoding; +SELECT regexp_replace(with_nul, '^caf(.)$', '\1') FROM regress_encoding; +-- NUL = character +SELECT with_nul, reverse(with_nul), reverse(reverse(with_nul)) FROM regress_encoding; + +-- If a corrupted string contains NUL in the tail bytes of a multibyte +-- character (invalid in all encodings), it is considered part of the +-- character for length purposes. An error will only be raised in code paths +-- that convert or verify encodings. + +SELECT length(truncated_with_nul) FROM regress_encoding; +SELECT substring(truncated_with_nul, 3, 1) FROM regress_encoding; +SELECT substring(truncated_with_nul, 4, 1) FROM regress_encoding; +SELECT convert_to(substring(truncated_with_nul, 4, 1), 'UTF8') FROM regress_encoding; +SELECT substring(truncated_with_nul, 5, 1) FROM regress_encoding; +SELECT regexp_replace(truncated_with_nul, '^caf(.)dcba$', '\1') = test_bytea_to_text('\xc300') FROM regress_encoding; +SELECT reverse(truncated_with_nul) FROM regress_encoding; + +-- unbounded: sequence would overrun the string! +SELECT test_mblen_func('pg_mblen_unbounded', 'UTF8', truncated, 3) +FROM regress_encoding; + +-- condition detected when using the length/range variants +SELECT test_mblen_func('pg_mblen_with_len', 'UTF8', truncated, 3) +FROM regress_encoding; +SELECT test_mblen_func('pg_mblen_range', 'UTF8', truncated, 3) +FROM regress_encoding; + +-- unbounded: sequence would overrun the string, if the terminator were really +-- the end of it +SELECT test_mblen_func('pg_mblen_unbounded', 'UTF8', truncated_with_nul, 3) +FROM regress_encoding; +SELECT test_mblen_func('pg_encoding_mblen', 'GB18030', truncated_with_nul, 3) +FROM regress_encoding; + +-- condition detected when using the cstr variants +SELECT test_mblen_func('pg_mblen_cstr', 'UTF8', truncated_with_nul, 3) +FROM regress_encoding; + +DROP TABLE regress_encoding; + +-- mb<->wchar conversions +CREATE FUNCTION test_encoding(encoding text, description text, input bytea) +RETURNS VOID LANGUAGE plpgsql AS +$$ +DECLARE + prefix text; + len int; + wchars int[]; + round_trip bytea; + result text; +BEGIN + prefix := rpad(encoding || ' ' || description || ':', 28); + + -- XXX could also test validation, length functions and include client + -- only encodings with these test cases + + IF test_valid_server_encoding(encoding) THEN + wchars := test_text_to_wchars(encoding, test_bytea_to_text(input)); + round_trip = test_text_to_bytea(test_wchars_to_text(encoding, wchars)); + if input = round_trip then + result := 'OK'; + elsif length(input) > length(round_trip) and round_trip = substr(input, 1, length(round_trip)) then + result := 'truncated'; + else + result := 'failed'; + end if; + RAISE NOTICE '% % -> % -> % = %', prefix, input, wchars, round_trip, result; + END IF; +END; +$$; +-- No validation is done on the encoding itself, just the length to avoid +-- overruns, so some of the byte sequences below are bogus. They cover +-- all code branches, server encodings only for now. +CREATE TABLE encoding_tests (encoding text, description text, input bytea); +INSERT INTO encoding_tests VALUES + -- LATIN1, other single-byte encodings + ('LATIN1', 'ASCII', 'a'), + ('LATIN1', 'extended', '\xe9'), + -- EUC_JP, EUC_JIS_2004, EUR_KR (for the purposes of wchar conversion): + -- 2 8e (CS2, not used by EUR_KR but arbitrarily considered to have EUC_JP length) + -- 3 8f (CS3, not used by EUR_KR but arbitrarily considered to have EUC_JP length) + -- 2 80..ff (CS1) + ('EUC_JP', 'ASCII', 'a'), + ('EUC_JP', 'CS1, short', '\x80'), + ('EUC_JP', 'CS1', '\x8002'), + ('EUC_JP', 'CS2, short', '\x8e'), + ('EUC_JP', 'CS2', '\x8e02'), + ('EUC_JP', 'CS3, short', '\x8f'), + ('EUC_JP', 'CS3, short', '\x8f02'), + ('EUC_JP', 'CS3', '\x8f0203'), + -- EUC_CN + -- 3 8e (CS2, not used but arbitrarily considered to have length 3) + -- 3 8f (CS3, not used but arbitrarily considered to have length 3) + -- 2 80..ff (CS1) + ('EUC_CN', 'ASCII', 'a'), + ('EUC_CN', 'CS1, short', '\x80'), + ('EUC_CN', 'CS1', '\x8002'), + ('EUC_CN', 'CS2, short', '\x8e'), + ('EUC_CN', 'CS2, short', '\x8e02'), + ('EUC_CN', 'CS2', '\x8e0203'), + ('EUC_CN', 'CS3, short', '\x8f'), + ('EUC_CN', 'CS3, short', '\x8f02'), + ('EUC_CN', 'CS3', '\x8f0203'), + -- EUC_TW: + -- 4 8e (CS2) + -- 3 8f (CS3, not used but arbitrarily considered to have length 3) + -- 2 80..ff (CS1) + ('EUC_TW', 'ASCII', 'a'), + ('EUC_TW', 'CS1, short', '\x80'), + ('EUC_TW', 'CS1', '\x8002'), + ('EUC_TW', 'CS2, short', '\x8e'), + ('EUC_TW', 'CS2, short', '\x8e02'), + ('EUC_TW', 'CS2, short', '\x8e0203'), + ('EUC_TW', 'CS2', '\x8e020304'), + ('EUC_TW', 'CS3, short', '\x8f'), + ('EUC_TW', 'CS3, short', '\x8f02'), + ('EUC_TW', 'CS3', '\x8f0203'), + -- UTF8 + -- 2 c0..df + -- 3 e0..ef + -- 4 f0..f7 (but maximum real codepoint U+10ffff has f4) + -- 5 f8..fb (not supported) + -- 6 fc..fd (not supported) + ('UTF8', 'ASCII', 'a'), + ('UTF8', '2 byte, short', '\xdf'), + ('UTF8', '2 byte', '\xdf82'), + ('UTF8', '3 byte, short', '\xef'), + ('UTF8', '3 byte, short', '\xef82'), + ('UTF8', '3 byte', '\xef8283'), + ('UTF8', '4 byte, short', '\xf7'), + ('UTF8', '4 byte, short', '\xf782'), + ('UTF8', '4 byte, short', '\xf78283'), + ('UTF8', '4 byte', '\xf7828384'), + ('UTF8', '5 byte, unsupported', '\xfb'), + ('UTF8', '5 byte, unsupported', '\xfb82'), + ('UTF8', '5 byte, unsupported', '\xfb8283'), + ('UTF8', '5 byte, unsupported', '\xfb828384'), + ('UTF8', '5 byte, unsupported', '\xfb82838485'), + ('UTF8', '6 byte, unsupported', '\xfd'), + ('UTF8', '6 byte, unsupported', '\xfd82'), + ('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'); + +SELECT COUNT(test_encoding(encoding, description, input)) > 0 +FROM encoding_tests; + +DROP TABLE encoding_tests; +DROP FUNCTION test_encoding; +DROP FUNCTION test_text_to_wchars; +DROP FUNCTION test_mblen_func; +DROP FUNCTION test_bytea_to_text; +DROP FUNCTION test_text_to_bytea; + + +-- substring slow path: multi-byte escape char vs. multi-byte pattern char. +SELECT SUBSTRING('a' SIMILAR U&'\00AC' ESCAPE U&'\00A7'); +-- Levenshtein distance metric: exercise character length cache. +SELECT U&"real\00A7_name" FROM (select 1) AS x(real_name); +-- JSON errcontext: truncate long data. +SELECT repeat(U&'\00A7', 30)::json; diff --git a/postgres/regression_suite/euc_kr.sql b/postgres/regression_suite/euc_kr.sql new file mode 100644 index 00000000..2b4e56bf --- /dev/null +++ b/postgres/regression_suite/euc_kr.sql @@ -0,0 +1,12 @@ +-- This test is about EUC_KR encoding, chosen as perhaps the most prevalent +-- non-UTF8, multibyte encoding as of 2026-01. Since UTF8 can represent all +-- of EUC_KR, also run the test in UTF8. +SELECT getdatabaseencoding() NOT IN ('EUC_KR', 'UTF8') AS skip_test /* \gset */; +-- \if :skip_test +-- \quit +-- \endif + +-- Exercise is_multibyte_char_in_char (non-UTF8) slow path. +SELECT POSITION( + convert_from('\xbcf6c7d0', 'EUC_KR') IN + convert_from('\xb0fac7d02c20bcf6c7d02c20b1e2bcfa2c20bbee', 'EUC_KR')); diff --git a/postgres/regression_suite/foreign_key.sql b/postgres/regression_suite/foreign_key.sql index 535c7de0..1ecbb2c1 100644 --- a/postgres/regression_suite/foreign_key.sql +++ b/postgres/regression_suite/foreign_key.sql @@ -2421,7 +2421,7 @@ INSERT INTO fkpart13_t1 (a) VALUES (1); INSERT INTO fkpart13_t2 (part_id) VALUES (1); INSERT INTO fkpart13_t3 (a) VALUES (1); --- Test a cascading update works correctly with with the dropped column +-- Test that a cascading update works correctly with the dropped column UPDATE fkpart13_t1 SET a = 2 WHERE a = 1; SELECT tableoid::regclass,* FROM fkpart13_t2; SELECT tableoid::regclass,* FROM fkpart13_t3; diff --git a/postgres/regression_suite/generated_virtual.sql b/postgres/regression_suite/generated_virtual.sql index 79bc9fb8..36acc5ff 100644 --- a/postgres/regression_suite/generated_virtual.sql +++ b/postgres/regression_suite/generated_virtual.sql @@ -817,11 +817,12 @@ create table gtest32 ( a int primary key, b int generated always as (a * 2), c int generated always as (10 + 10), - d int generated always as (coalesce(a, 100)), - e int + d int generated always as (coalesce(f, 100)), + e int, + f int ); -insert into gtest32 values (1), (2); +insert into gtest32 (a, f) values (1, 1), (2, 2); analyze gtest32; -- Ensure that nullingrel bits are propagated into the generation expressions @@ -859,8 +860,8 @@ select t2.* from gtest32 t1 left join gtest32 t2 on false; select t2.* from gtest32 t1 left join gtest32 t2 on false; explain (verbose, costs off) -select * from gtest32 t group by grouping sets (a, b, c, d, e) having c = 20; -select * from gtest32 t group by grouping sets (a, b, c, d, e) having c = 20; +select * from gtest32 t group by grouping sets (a, b, c, d, e, f) having c = 20; +select * from gtest32 t group by grouping sets (a, b, c, d, e, f) having c = 20; -- Ensure that the virtual generated columns in ALTER COLUMN TYPE USING expression are expanded alter table gtest32 alter column e type bigint using b; diff --git a/postgres/regression_suite/groupingsets.sql b/postgres/regression_suite/groupingsets.sql index 3f87e47d..363f8b4e 100644 --- a/postgres/regression_suite/groupingsets.sql +++ b/postgres/regression_suite/groupingsets.sql @@ -183,6 +183,52 @@ select x, y || 'y' group by grouping sets (x, y) order by 1, 2; +-- check that operands wrapped in PlaceHolderVars are capable of index matching +begin; + +set local enable_bitmapscan = off; + +explain (costs off) +select x, y + from (select unique1 as x, unique2 as y from tenk1) as t + where x = 1 + group by grouping sets (x, y) + order by 1, 2; + +select x, y + from (select unique1 as x, unique2 as y from tenk1) as t + where x = 1 + group by grouping sets (x, y) + order by 1, 2; + +explain (costs off) +select x, y + from (select unique1::oid as x, unique2 as y from tenk1) as t + where x::integer = 1 + group by grouping sets (x, y) + order by 1, 2; + +select x, y + from (select unique1::oid as x, unique2 as y from tenk1) as t + where x::integer = 1 + group by grouping sets (x, y) + order by 1, 2; + +explain (costs off) +select x, y + from (select t1.unique1 as x, t1.unique2 as y from tenk1 t1, tenk1 t2) as t + where x = 1 + group by grouping sets (x, y) + order by 1, 2; + +select x, y + from (select t1.unique1 as x, t1.unique2 as y from tenk1 t1, tenk1 t2) as t + where x = 1 + group by grouping sets (x, y) + order by 1, 2; + +rollback; + -- check qual push-down rules for a subquery with grouping sets explain (verbose, costs off) select * from ( @@ -721,4 +767,27 @@ select a, b, row_number() over (order by a, b nulls first) from (values (1, 1), (2, 2)) as t (a, b) where a = b group by grouping sets((a, b), (a)); +-- test handling of SRFs with grouping sets +explain (verbose, costs off) +select generate_series(1, a) as g +from (values (1, 1), (2, 2)) as t (a, b) +group by rollup(g) +order by 1; + +select generate_series(1, a) as g +from (values (1, 1), (2, 2)) as t (a, b) +group by rollup(g) +order by 1; + +explain (verbose, costs off) +select generate_series(1, a) as g, a+b as ab +from (values (1, 1), (2, 2)) as t (a, b) +group by rollup(a, ab) +order by 1, 2; + +select generate_series(1, a) as g, a+b as ab +from (values (1, 1), (2, 2)) as t (a, b) +group by rollup(a, ab) +order by 1, 2; + -- end diff --git a/postgres/regression_suite/guc.sql b/postgres/regression_suite/guc.sql index bafaf067..dfb843fd 100644 --- a/postgres/regression_suite/guc.sql +++ b/postgres/regression_suite/guc.sql @@ -232,6 +232,28 @@ drop schema not_there_initially; select current_schemas(false); reset search_path; +-- +-- Test parsing of log_min_messages +-- + +SET log_min_messages TO foo; -- fail +SET log_min_messages TO fatal; +SHOW log_min_messages; +SET log_min_messages TO 'fatal'; +SHOW log_min_messages; +SET log_min_messages TO 'checkpointer:debug2, autovacuum:debug1'; -- fail +SET log_min_messages TO 'debug1, backend:error, fatal'; -- fail +SET log_min_messages TO 'backend:error, debug1, backend:warning'; -- fail +SET log_min_messages TO 'backend:error, foo:fatal, archiver:debug1'; -- fail +SET log_min_messages TO 'backend:error, checkpointer:bar, archiver:debug1'; -- fail +SET log_min_messages TO 'backend:error, checkpointer:debug3, fatal, archiver:debug2, autovacuum:debug1, walsender:debug3'; +SHOW log_min_messages; +SET log_min_messages TO 'warning, autovacuum:debug1'; +SHOW log_min_messages; +SET log_min_messages TO 'autovacuum:debug1, warning'; +SHOW log_min_messages; +RESET log_min_messages; + -- -- Tests for function-local GUC settings -- diff --git a/postgres/regression_suite/hash_func.sql b/postgres/regression_suite/hash_func.sql index 33756bd2..ce38da72 100644 --- a/postgres/regression_suite/hash_func.sql +++ b/postgres/regression_suite/hash_func.sql @@ -48,6 +48,13 @@ FROM (VALUES (0), (1), (17), (42), (550273), (207112489)) x(v) WHERE hashoid(v)::bit(32) != hashoidextended(v, 0)::bit(32) OR hashoid(v)::bit(32) = hashoidextended(v, 1)::bit(32); +SELECT v as value, hashoid8(v)::bit(32) as standard, + hashoid8extended(v, 0)::bit(32) as extended0, + hashoid8extended(v, 1)::bit(32) as extended1 +FROM (VALUES (0), (1), (17), (42), (550273), (207112489)) x(v) +WHERE hashoid8(v)::bit(32) != hashoid8extended(v, 0)::bit(32) + OR hashoid8(v)::bit(32) = hashoid8extended(v, 1)::bit(32); + SELECT v as value, hashchar(v)::bit(32) as standard, hashcharextended(v, 0)::bit(32) as extended0, hashcharextended(v, 1)::bit(32) as extended1 diff --git a/postgres/regression_suite/insert_conflict.sql b/postgres/regression_suite/insert_conflict.sql index 2699dcc3..2d3adf54 100644 --- a/postgres/regression_suite/insert_conflict.sql +++ b/postgres/regression_suite/insert_conflict.sql @@ -93,6 +93,9 @@ explain (costs off) insert into insertconflicttest values (0, 'Bilberry') on con explain (costs off) insert into insertconflicttest values(0, 'Crowberry') on conflict (key) do update set fruit = excluded.fruit where excluded.fruit != 'Elderberry'; -- Does the same, but JSON format shows "Conflict Arbiter Index" as JSON array: explain (costs off, format json) insert into insertconflicttest values (0, 'Bilberry') on conflict (key) do update set fruit = excluded.fruit where insertconflicttest.fruit != 'Lime' returning *; +-- Should display lock strength, if specified +explain (costs off) insert into insertconflicttest values (1, 'Apple') on conflict (key) do select returning *; +explain (costs off) insert into insertconflicttest values (1, 'Apple') on conflict (key) do select for key share returning *; -- Fails (no unique index inference specification, required for do update variant): insert into insertconflicttest values (1, 'Apple') on conflict do update set fruit = excluded.fruit; @@ -130,6 +133,18 @@ insert into insertconflicttest AS ict values (6, 'Passionfruit') on conflict (ke -- Check helpful hint when qualifying set column with target table insert into insertconflicttest values (3, 'Kiwi') on conflict (key, fruit) do update set insertconflicttest.fruit = 'Mango'; +-- +-- DO SELECT tests +-- +delete from insertconflicttest where fruit = 'Apple'; +insert into insertconflicttest values (1, 'Apple') on conflict (key) do select; -- fails +insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select returning old, new, i; +insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select returning old, new, i; +insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select where i.fruit = 'Apple' returning *; +insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select where i.fruit = 'Orange' returning *; +insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select where excluded.fruit = 'Apple' returning *; +insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select where excluded.fruit = 'Orange' returning *; + drop index key_index; -- @@ -459,6 +474,30 @@ begin transaction isolation level serializable; insert into selfconflict values (6,1), (6,2) on conflict(f1) do update set f2 = 0; commit; +begin transaction isolation level read committed; +insert into selfconflict values (7,1), (7,2) on conflict(f1) do select returning *; +commit; + +begin transaction isolation level repeatable read; +insert into selfconflict values (8,1), (8,2) on conflict(f1) do select returning *; +commit; + +begin transaction isolation level serializable; +insert into selfconflict values (9,1), (9,2) on conflict(f1) do select returning *; +commit; + +begin transaction isolation level read committed; +insert into selfconflict values (10,1), (10,2) on conflict(f1) do select for update returning *; +commit; + +begin transaction isolation level repeatable read; +insert into selfconflict values (11,1), (11,2) on conflict(f1) do select for update returning *; +commit; + +begin transaction isolation level serializable; +insert into selfconflict values (12,1), (12,2) on conflict(f1) do select for update returning *; +commit; + select * from selfconflict; drop table selfconflict; @@ -473,13 +512,17 @@ insert into parted_conflict_test values (1, 'a') on conflict do nothing; -- index on a required, which does exist in parent insert into parted_conflict_test values (1, 'a') on conflict (a) do nothing; insert into parted_conflict_test values (1, 'a') on conflict (a) do update set b = excluded.b; +insert into parted_conflict_test values (1, 'a') on conflict (a) do select returning *; +insert into parted_conflict_test values (1, 'a') on conflict (a) do select for update returning *; -- targeting partition directly will work insert into parted_conflict_test_1 values (1, 'a') on conflict (a) do nothing; insert into parted_conflict_test_1 values (1, 'b') on conflict (a) do update set b = excluded.b; +insert into parted_conflict_test_1 values (1, 'b') on conflict (a) do select returning b; -- index on b required, which doesn't exist in parent -insert into parted_conflict_test values (2, 'b') on conflict (b) do update set a = excluded.a; +insert into parted_conflict_test values (2, 'b') on conflict (b) do update set a = excluded.a; -- fail +insert into parted_conflict_test values (2, 'b') on conflict (b) do select returning b; -- fail -- targeting partition directly will work insert into parted_conflict_test_1 values (2, 'b') on conflict (b) do update set a = excluded.a; @@ -487,13 +530,16 @@ insert into parted_conflict_test_1 values (2, 'b') on conflict (b) do update set -- should see (2, 'b') select * from parted_conflict_test order by a; --- now check that DO UPDATE works correctly for target partition with --- different attribute numbers +-- now check that DO UPDATE and DO SELECT work correctly for target partition +-- with different attribute numbers create table parted_conflict_test_2 (b char, a int unique); alter table parted_conflict_test attach partition parted_conflict_test_2 for values in (3); truncate parted_conflict_test; insert into parted_conflict_test values (3, 'a') on conflict (a) do update set b = excluded.b; insert into parted_conflict_test values (3, 'b') on conflict (a) do update set b = excluded.b; +insert into parted_conflict_test values (3, 'a') on conflict (a) do select returning b; +insert into parted_conflict_test values (3, 'a') on conflict (a) do select where excluded.b = 'a' returning parted_conflict_test; +insert into parted_conflict_test values (3, 'a') on conflict (a) do select where parted_conflict_test.b = 'b' returning b; -- should see (3, 'b') select * from parted_conflict_test order by a; @@ -504,6 +550,7 @@ create table parted_conflict_test_3 partition of parted_conflict_test for values truncate parted_conflict_test; insert into parted_conflict_test (a, b) values (4, 'a') on conflict (a) do update set b = excluded.b; insert into parted_conflict_test (a, b) values (4, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a'; +insert into parted_conflict_test (a, b) values (4, 'b') on conflict (a) do select returning b; -- should see (4, 'b') select * from parted_conflict_test order by a; @@ -514,6 +561,7 @@ create table parted_conflict_test_4_1 partition of parted_conflict_test_4 for va truncate parted_conflict_test; insert into parted_conflict_test (a, b) values (5, 'a') on conflict (a) do update set b = excluded.b; insert into parted_conflict_test (a, b) values (5, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a'; +insert into parted_conflict_test (a, b) values (5, 'b') on conflict (a) do select where parted_conflict_test.b = 'a' returning b; -- should see (5, 'b') select * from parted_conflict_test order by a; @@ -526,6 +574,26 @@ insert into parted_conflict_test (a, b) values (1, 'b'), (2, 'c'), (4, 'b') on c -- should see (1, 'b'), (2, 'a'), (4, 'b') select * from parted_conflict_test order by a; +-- test DO SELECT with multiple rows hitting different partitions +truncate parted_conflict_test; +insert into parted_conflict_test (a, b) values (1, 'a'), (2, 'b'), (4, 'c'); +insert into parted_conflict_test (a, b) values (1, 'x'), (2, 'y'), (4, 'z') + on conflict (a) do select returning *, tableoid::regclass; + +-- should see original values (1, 'a'), (2, 'b'), (4, 'c') +select * from parted_conflict_test order by a; + +-- test DO SELECT with WHERE filtering across partitions +insert into parted_conflict_test (a, b) values (1, 'n') on conflict (a) do select where parted_conflict_test.b = 'a' returning *; +insert into parted_conflict_test (a, b) values (2, 'n') on conflict (a) do select where parted_conflict_test.b = 'x' returning *; + +-- test DO SELECT with EXCLUDED in WHERE across partitions with different layouts +insert into parted_conflict_test (a, b) values (3, 't') on conflict (a) do select where excluded.b = 't' returning *; + +-- test DO SELECT FOR UPDATE across different partition layouts +insert into parted_conflict_test (a, b) values (1, 'l') on conflict (a) do select for update returning *; +insert into parted_conflict_test (a, b) values (3, 'l') on conflict (a) do select for update returning *; + drop table parted_conflict_test; -- test behavior of inserting a conflicting tuple into an intermediate diff --git a/postgres/regression_suite/join.sql b/postgres/regression_suite/join.sql index 7ec84f3b..14cbec28 100644 --- a/postgres/regression_suite/join.sql +++ b/postgres/regression_suite/join.sql @@ -866,6 +866,33 @@ select 1 from tenk1 where (hundred, thousand) in (select twothousand, twothousand from onek); reset enable_memoize; +-- +-- more antijoin recognition tests using NOT NULL constraints +-- + +begin; + +create temp table tbl_anti(a int not null, b int, c int); + +-- this is an antijoin, as t2.a is non-null for any matching row +explain (costs off) +select * from tenk1 t1 left join tbl_anti t2 on t1.unique1 = t2.b +where t2.a is null; + +-- this is an antijoin, as t2.a is non-null for any matching row +explain (costs off) +select * from tenk1 t1 left join + (tbl_anti t2 left join tbl_anti t3 on t2.c = t3.c) on t1.unique1 = t2.b +where t2.a is null; + +-- this is not an antijoin, as t3.a can be nulled by t2/t3 join +explain (costs off) +select * from tenk1 t1 left join + (tbl_anti t2 left join tbl_anti t3 on t2.c = t3.c) on t1.unique1 = t2.b +where t3.a is null; + +rollback; + -- -- regression test for bogus RTE_GROUP entries -- @@ -3764,6 +3791,16 @@ GROUP BY s.c1, s.c2; DROP TABLE group_tbl; +-- Test that we ignore PlaceHolderVars when looking up statistics +EXPLAIN (COSTS OFF) +SELECT t1.unique1 FROM tenk1 t1 LEFT JOIN + (SELECT *, 42 AS phv FROM tenk1 t2) ss ON t1.unique2 = ss.unique2 +WHERE ss.unique1 = ss.phv AND t1.unique1 < 100; + +SELECT t1.unique1 FROM tenk1 t1 LEFT JOIN + (SELECT *, 42 AS phv FROM tenk1 t2) ss ON t1.unique2 = ss.unique2 +WHERE ss.unique1 = ss.phv AND t1.unique1 < 100; + -- -- Test for a nested loop join involving index scan, transforming OR-clauses -- to SAOP. diff --git a/postgres/regression_suite/join_hash.sql b/postgres/regression_suite/join_hash.sql index 6b0688ab..49d3fd61 100644 --- a/postgres/regression_suite/join_hash.sql +++ b/postgres/regression_suite/join_hash.sql @@ -314,6 +314,7 @@ create table join_foo as select generate_series(1, 3) as id, 'xxxxx'::text as t; alter table join_foo set (parallel_workers = 0); create table join_bar as select generate_series(1, 10000) as id, 'xxxxx'::text as t; alter table join_bar set (parallel_workers = 2); +analyze join_foo, join_bar; -- multi-batch with rescan, parallel-oblivious savepoint settings; diff --git a/postgres/regression_suite/jsonb.sql b/postgres/regression_suite/jsonb.sql index 5afeb598..10359b16 100644 --- a/postgres/regression_suite/jsonb.sql +++ b/postgres/regression_suite/jsonb.sql @@ -857,6 +857,7 @@ SELECT count(*) FROM testjsonb WHERE j @? '$'; SELECT count(*) FROM testjsonb WHERE j @? '$.public'; SELECT count(*) FROM testjsonb WHERE j @? '$.bar'; +ALTER TABLE testjsonb SET (parallel_workers = 2); CREATE INDEX jidx ON testjsonb USING gin (j); SET enable_seqscan = off; @@ -945,7 +946,7 @@ SELECT count(*) FROM testjsonb WHERE j = '{"pos":98, "line":371, "node":"CBA", " --gin path opclass DROP INDEX jidx; -CREATE INDEX jidx ON testjsonb USING gin (j jsonb_path_ops); +CREATE INDEX CONCURRENTLY jidx ON testjsonb USING gin (j jsonb_path_ops); SET enable_seqscan = off; SELECT count(*) FROM testjsonb WHERE j @> '{"wait":null}'; @@ -1596,3 +1597,21 @@ select '12345.0000000000000000000000000000000000000000000005'::jsonb::float8; select '12345.0000000000000000000000000000000000000000000005'::jsonb::int2; select '12345.0000000000000000000000000000000000000000000005'::jsonb::int4; select '12345.0000000000000000000000000000000000000000000005'::jsonb::int8; + +-- single argument jsonb functions as jsonb_function(jsonb) and jsonb.jsonb_function +select jsonb_typeof('{"a":1}'::jsonb); +select ('{"a":1}'::jsonb).jsonb_typeof; +select jsonb_array_length('["a", "b", "c"]'::jsonb); +select ('["a", "b", "c"]'::jsonb).jsonb_array_length; +select jsonb_object_keys('{"a":1, "b":2}'::jsonb); +select ('{"a":1, "b":2}'::jsonb).jsonb_object_keys; + +-- cast jsonb to other types as (jsonb)::type and (jsonb).type +select ('123.45'::jsonb)::numeric; +select ('123.45'::jsonb).numeric; +select ('[{"name": "alice"}, {"name": "bob"}]'::jsonb)::name; +select ('[{"name": "alice"}, {"name": "bob"}]'::jsonb).name; +select ('true'::jsonb)::bool; +select ('true'::jsonb).bool; +select ('{"text": "hello"}'::jsonb)::text; +select ('{"text": "hello"}'::jsonb).text; diff --git a/postgres/regression_suite/lock.sql b/postgres/regression_suite/lock.sql index 747f74f6..09c8beac 100644 --- a/postgres/regression_suite/lock.sql +++ b/postgres/regression_suite/lock.sql @@ -85,7 +85,7 @@ select relname from pg_locks l, pg_class c ROLLBACK; BEGIN TRANSACTION; LOCK TABLE lock_view6 IN EXCLUSIVE MODE; --- lock_view6 an lock_tbl1 are locked. +-- lock_view6 and lock_tbl1 are locked. select relname from pg_locks l, pg_class c where l.relation = c.oid and relname like '%lock_%' and mode = 'ExclusiveLock' order by relname; diff --git a/postgres/regression_suite/misc_functions.sql b/postgres/regression_suite/misc_functions.sql index 93be4c7a..d40c5ccc 100644 --- a/postgres/regression_suite/misc_functions.sql +++ b/postgres/regression_suite/misc_functions.sql @@ -459,3 +459,18 @@ SELECT test_relpath(); -- pg_replication_origin.roname limit SELECT pg_replication_origin_create('regress_' || repeat('a', 505)); + +-- pg_get_multixact_stats tests +CREATE ROLE regress_multixact_funcs; +-- Access granted for superusers. +SELECT oldest_multixact IS NULL AS null_result FROM pg_get_multixact_stats(); +-- Access revoked. +SET ROLE regress_multixact_funcs; +SELECT oldest_multixact IS NULL AS null_result FROM pg_get_multixact_stats(); +RESET ROLE; +-- Access granted for users with pg_monitor rights. +GRANT pg_monitor TO regress_multixact_funcs; +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; diff --git a/postgres/regression_suite/oid8.sql b/postgres/regression_suite/oid8.sql new file mode 100644 index 00000000..44cb79dd --- /dev/null +++ b/postgres/regression_suite/oid8.sql @@ -0,0 +1,87 @@ +-- +-- OID8 +-- + +CREATE TABLE OID8_TBL(f1 oid8); + +INSERT INTO OID8_TBL(f1) VALUES ('1234'); +INSERT INTO OID8_TBL(f1) VALUES ('1235'); +INSERT INTO OID8_TBL(f1) VALUES ('987'); +INSERT INTO OID8_TBL(f1) VALUES ('-1040'); +INSERT INTO OID8_TBL(f1) VALUES ('99999999'); +INSERT INTO OID8_TBL(f1) VALUES ('5 '); +INSERT INTO OID8_TBL(f1) VALUES (' 10 '); +INSERT INTO OID8_TBL(f1) VALUES ('123456789012345678'); +-- UINT64_MAX +INSERT INTO OID8_TBL(f1) VALUES ('18446744073709551615'); +-- leading/trailing hard tab is also allowed +INSERT INTO OID8_TBL(f1) VALUES (' 15 '); + +-- bad inputs +INSERT INTO OID8_TBL(f1) VALUES (''); +INSERT INTO OID8_TBL(f1) VALUES (' '); +INSERT INTO OID8_TBL(f1) VALUES ('asdfasd'); +INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd'); +INSERT INTO OID8_TBL(f1) VALUES ('5 d'); +INSERT INTO OID8_TBL(f1) VALUES (' 5d'); +INSERT INTO OID8_TBL(f1) VALUES ('5 5'); +INSERT INTO OID8_TBL(f1) VALUES (' - 500'); +INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485'); +INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934'); +-- UINT64_MAX + 1 +INSERT INTO OID8_TBL(f1) VALUES ('18446744073709551616'); + +SELECT * FROM OID8_TBL; + +-- Also try it with non-error-throwing API +SELECT pg_input_is_valid('1234', 'oid8'); +SELECT pg_input_is_valid('01XYZ', 'oid8'); +SELECT * FROM pg_input_error_info('01XYZ', 'oid8'); +SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8'); +SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8'); + +-- Operators +SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234; +SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234'; +SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234'; +SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234'; +SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234'; +SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234'; + +-- Casts +SELECT 1::int2::oid8; +SELECT 1::int4::oid8; +SELECT 1::int8::oid8; +SELECT 1::oid8::int8; +SELECT 1::oid::oid8; -- ok +SELECT 1::oid8::oid; -- not ok + +-- Aggregates +SELECT min(f1), max(f1) FROM OID8_TBL; + +-- Check btree and hash opclasses +EXPLAIN (COSTS OFF) +SELECT DISTINCT (i || '000000000000' || j)::oid8 f + FROM generate_series(1, 10) i, + generate_series(1, 10) j, + generate_series(1, 5) k + WHERE i <= 10 AND j > 0 AND j <= 10 + ORDER BY f; + +SELECT DISTINCT (i || '000000000000' || j)::oid8 f + FROM generate_series(1, 10) i, + generate_series(1, 10) j, + generate_series(1, 5) k + WHERE i <= 10 AND j > 0 AND j <= 10 + ORDER BY f; + +-- 3-way compare for btrees +SELECT btoid8cmp(1::oid8, 2::oid8) < 0 AS val_lower; +SELECT btoid8cmp(2::oid8, 2::oid8) = 0 AS val_equal; +SELECT btoid8cmp(2::oid8, 1::oid8) > 0 AS val_higher; + +-- oid8 has btree and hash opclasses +CREATE INDEX on OID8_TBL USING btree(f1); +CREATE INDEX ON OID8_TBL USING hash(f1); + +DROP TABLE OID8_TBL; diff --git a/postgres/regression_suite/partition_join.sql b/postgres/regression_suite/partition_join.sql index d153297a..c4549fc1 100644 --- a/postgres/regression_suite/partition_join.sql +++ b/postgres/regression_suite/partition_join.sql @@ -35,7 +35,7 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.b = SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.b = 0 ORDER BY t1.a, t2.b; -- inner join with partially-redundant join clauses --- (avoid a mergejoin, because the planner thinks that an non-partitionwise +-- (avoid a mergejoin, because the planner thinks that a non-partitionwise -- merge join is the cheapest plan, and we want to test a partitionwise join) BEGIN; SET LOCAL enable_mergejoin = false; @@ -1225,7 +1225,6 @@ ANALYZE fract_t; -- (avoid merge joins, because the costs of partitionwise and non-partitionwise -- merge joins tend to be almost equal, and we want this test to be stable) SET max_parallel_workers_per_gather = 0; -SET enable_partitionwise_join = on; SET enable_mergejoin = off; EXPLAIN (COSTS OFF) diff --git a/postgres/regression_suite/partition_merge.sql b/postgres/regression_suite/partition_merge.sql index 50213edf..e0e26f18 100644 --- a/postgres/regression_suite/partition_merge.sql +++ b/postgres/regression_suite/partition_merge.sql @@ -344,7 +344,7 @@ CREATE TABLE sales_list PARTITION BY LIST (sales_state); CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki'); CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Lisbon', 'New York', 'Madrid'); -CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'); +CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'); CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'); CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; @@ -385,12 +385,12 @@ CREATE INDEX sales_list_sales_state_idx ON sales_list USING btree (sales_state); CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki'); CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Lisbon', 'New York', 'Madrid'); -CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'); +CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'); CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'); CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES - ('Trump', 'Bejing', 1000, '2022-03-01'), + ('Trump', 'Beijing', 1000, '2022-03-01'), ('Smirnoff', 'New York', 500, '2022-03-03'), ('Ford', 'St. Petersburg', 2000, '2022-03-05'), ('Ivanov', 'Warsaw', 750, '2022-03-04'), diff --git a/postgres/regression_suite/partition_split.sql b/postgres/regression_suite/partition_split.sql index 5140ca4d..9f213ce5 100644 --- a/postgres/regression_suite/partition_split.sql +++ b/postgres/regression_suite/partition_split.sql @@ -605,37 +605,37 @@ DROP TABLE sales_range; CREATE TABLE sales_list (sales_state VARCHAR(20)) PARTITION BY LIST (sales_state); CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki'); -CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Bejing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok'); +CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Beijing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok'); CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; -- ERROR: new partition "sales_east" would overlap with another (not split) partition "sales_nord" 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 ('Bejing', 'Delhi', 'Vladivostok', 'Helsinki'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok', 'Helsinki'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); -- ERROR: new partition "sales_west" would overlap with another new partition "sales_central" 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 ('Bejing', 'Delhi', 'Vladivostok'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Lisbon', 'Kyiv')); -- ERROR: new partition "sales_west" cannot have NULL value because split partition "sales_all" does not have ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', NULL), - PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); -- ERROR: new partition "sales_west" cannot have this value because split partition "sales_all" does not have ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne'), - PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); -- ERROR: new partition cannot be DEFAULT because DEFAULT partition "sales_others" already exists ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne'), - PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'), PARTITION sales_others2 DEFAULT); @@ -658,26 +658,26 @@ DROP TABLE t; CREATE TABLE sales_list(sales_state VARCHAR(20)) PARTITION BY LIST (sales_state); CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Helsinki', 'St. Petersburg', 'Oslo'); -CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Bejing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok', NULL); +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 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 ('Bejing', 'Delhi', 'Vladivostok'), + 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 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 ('Bejing', 'Delhi', 'Vladivostok'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', NULL)); -- ERROR DEFAULT partition should be one 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 ('Bejing', 'Delhi', 'Vladivostok'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'), PARTITION sales_others DEFAULT, PARTITION sales_others2 DEFAULT); @@ -699,11 +699,11 @@ CREATE INDEX sales_list_salesperson_name_idx ON sales_list USING btree (salesper CREATE INDEX sales_list_sales_state_idx ON sales_list USING btree (sales_state); CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Helsinki', 'St. Petersburg', 'Oslo'); -CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Bejing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok'); +CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Beijing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok'); CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES - ('Trump', 'Bejing', 1000, '2022-03-01'), + ('Trump', 'Beijing', 1000, '2022-03-01'), ('Smirnoff', 'New York', 500, '2022-03-03'), ('Ford', 'St. Petersburg', 2000, '2022-03-05'), ('Ivanov', 'Warsaw', 750, '2022-03-04'), @@ -720,7 +720,7 @@ INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) 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 ('Bejing', 'Delhi', 'Vladivostok'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); SELECT tableoid::regclass, * FROM sales_list ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; diff --git a/postgres/regression_suite/plpgsql.sql b/postgres/regression_suite/plpgsql.sql index 0fae1b82..efcb47d2 100644 --- a/postgres/regression_suite/plpgsql.sql +++ b/postgres/regression_suite/plpgsql.sql @@ -3773,28 +3773,6 @@ drop function fail(); -- Test handling of string literals. -set standard_conforming_strings = off; - -create or replace function strtest() returns text as $$ -begin - raise notice 'foo\\bar\041baz'; - return 'foo\\bar\041baz'; -end -$$ language plpgsql; - -select strtest(); - -create or replace function strtest() returns text as $$ -begin - raise notice E'foo\\bar\041baz'; - return E'foo\\bar\041baz'; -end -$$ language plpgsql; - -select strtest(); - -set standard_conforming_strings = on; - create or replace function strtest() returns text as $$ begin raise notice 'foo\\bar\041baz\'; diff --git a/postgres/regression_suite/predicate.sql b/postgres/regression_suite/predicate.sql index 7d4fda1b..0f92bb52 100644 --- a/postgres/regression_suite/predicate.sql +++ b/postgres/regression_suite/predicate.sql @@ -240,3 +240,211 @@ SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL; SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL; DROP TABLE pred_tab; + +-- +-- Test that COALESCE expressions in predicates are simplified using +-- non-nullable arguments. +-- +CREATE TABLE pred_tab (a int NOT NULL, b int, c int); + +-- Ensure that constant NULL arguments are dropped +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(NULL, b, NULL, a) > 1; + +-- Ensure that argument "b*a" is dropped +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(b, a, b*a) > 1; + +-- Ensure that the entire COALESCE expression is replaced by "a" +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(a, b) > 1; + +-- +-- Test detection of non-nullable expressions in predicates +-- + +-- CoalesceExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(b, a) IS NULL; + +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(b, c) IS NULL; + +-- MinMaxExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE GREATEST(b, a) IS NULL; + +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE GREATEST(b, c) IS NULL; + +-- CaseExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a ELSE a END) IS NULL; + +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN b ELSE a END) IS NULL; + +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a END) IS NULL; + +-- ArrayExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE ARRAY[b] IS NULL; + +-- NullTest +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (b IS NULL) IS NULL; + +-- BooleanTest +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE ((a > 1) IS TRUE) IS NULL; + +-- DistinctExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (a IS DISTINCT FROM b) IS NULL; + +-- RelabelType +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (a::oid) IS NULL; + +DROP TABLE pred_tab; + +-- +-- Test optimization of IS [NOT] DISTINCT FROM +-- + +CREATE TYPE dist_row_t AS (a int, b int); +CREATE TABLE dist_tab (id int, val_nn int NOT NULL, val_null int, row_nn dist_row_t NOT NULL); + +INSERT INTO dist_tab VALUES (1, 10, 10, ROW(1, 1)); +INSERT INTO dist_tab VALUES (2, 20, NULL, ROW(2, 2)); +INSERT INTO dist_tab VALUES (3, 30, 30, ROW(1, NULL)); + +CREATE INDEX dist_tab_nn_idx ON dist_tab (val_nn); + +ANALYZE dist_tab; + +-- Ensure that the predicate folds to constant TRUE +EXPLAIN(COSTS OFF) +SELECT id FROM dist_tab WHERE val_nn IS DISTINCT FROM NULL::INT; +SELECT id FROM dist_tab WHERE val_nn IS DISTINCT FROM NULL::INT; + +-- Ensure that the predicate folds to constant FALSE +EXPLAIN(COSTS OFF) +SELECT id FROM dist_tab WHERE val_nn IS NOT DISTINCT FROM NULL::INT; +SELECT id FROM dist_tab WHERE val_nn IS NOT DISTINCT FROM NULL::INT; + +-- Ensure that the predicate is converted to an inequality operator +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_nn IS DISTINCT FROM 10; +SELECT id FROM dist_tab WHERE val_nn IS DISTINCT FROM 10; + +-- Ensure that the predicate is converted to an equality operator, and thus can +-- use index scan +SET enable_seqscan TO off; +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_nn IS NOT DISTINCT FROM 10; +SELECT id FROM dist_tab WHERE val_nn IS NOT DISTINCT FROM 10; +RESET enable_seqscan; + +-- Ensure that the predicate is preserved as "IS DISTINCT FROM" +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM 20; +SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM 20; + +-- Safety check for rowtypes +-- Ensure that the predicate is converted to an inequality operator +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE row_nn IS DISTINCT FROM ROW(1, 5)::dist_row_t; +-- ... and that all 3 rows are returned +SELECT id FROM dist_tab WHERE row_nn IS DISTINCT FROM ROW(1, 5)::dist_row_t; + +-- Ensure that the predicate is converted to an equality operator, and thus +-- mergejoinable or hashjoinable +SET enable_nestloop TO off; +EXPLAIN (COSTS OFF) +SELECT * FROM dist_tab t1 JOIN dist_tab t2 ON t1.val_nn IS NOT DISTINCT FROM t2.val_nn; +SELECT * FROM dist_tab t1 JOIN dist_tab t2 ON t1.val_nn IS NOT DISTINCT FROM t2.val_nn; +RESET enable_nestloop; + +-- Ensure that the predicate is converted to IS NOT NULL +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM NULL::INT; +SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM NULL::INT; + +-- Ensure that the predicate is converted to IS NULL +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_null IS NOT DISTINCT FROM NULL::INT; +SELECT id FROM dist_tab WHERE val_null IS NOT DISTINCT FROM NULL::INT; + +-- Safety check for rowtypes +-- The predicate is converted to IS NOT NULL, and get_rule_expr prints it as IS +-- DISTINCT FROM because argisrow is false, indicating that we're applying a +-- scalar test +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE (val_null, val_null) IS DISTINCT FROM NULL::RECORD; +SELECT id FROM dist_tab WHERE (val_null, val_null) IS DISTINCT FROM NULL::RECORD; + +-- The predicate is converted to IS NULL, and get_rule_expr prints it as IS NOT +-- DISTINCT FROM because argisrow is false, indicating that we're applying a +-- scalar test +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE (val_null, val_null) IS NOT DISTINCT FROM NULL::RECORD; +SELECT id FROM dist_tab WHERE (val_null, val_null) IS NOT DISTINCT FROM NULL::RECORD; + +DROP TABLE dist_tab; +DROP TYPE dist_row_t; + +-- +-- Test optimization of BooleanTest (IS [NOT] TRUE/FALSE/UNKNOWN) on +-- non-nullable input +-- +CREATE TABLE bool_tab (id int, flag_nn boolean NOT NULL, flag_null boolean); + +INSERT INTO bool_tab VALUES (1, true, true); +INSERT INTO bool_tab VALUES (2, false, NULL); + +CREATE INDEX bool_tab_nn_idx ON bool_tab (flag_nn); + +ANALYZE bool_tab; + +-- Ensure that the predicate folds to constant FALSE +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS UNKNOWN; +SELECT id FROM bool_tab WHERE flag_nn IS UNKNOWN; + +-- Ensure that the predicate folds to constant TRUE +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS NOT UNKNOWN; +SELECT id FROM bool_tab WHERE flag_nn IS NOT UNKNOWN; + +-- Ensure that the predicate folds to flag_nn +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS TRUE; +SELECT id FROM bool_tab WHERE flag_nn IS TRUE; + +-- Ensure that the predicate folds to flag_nn, and thus can use index scan +SET enable_seqscan TO off; +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS NOT FALSE; +SELECT id FROM bool_tab WHERE flag_nn IS NOT FALSE; +RESET enable_seqscan; + +-- Ensure that the predicate folds to not flag_nn +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS FALSE; +SELECT id FROM bool_tab WHERE flag_nn IS FALSE; + +-- Ensure that the predicate folds to not flag_nn, and thus can use index scan +SET enable_seqscan TO off; +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS NOT TRUE; +SELECT id FROM bool_tab WHERE flag_nn IS NOT TRUE; +RESET enable_seqscan; + +-- Ensure that the predicate is preserved as a BooleanTest +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_null IS UNKNOWN; +SELECT id FROM bool_tab WHERE flag_null IS UNKNOWN; + +DROP TABLE bool_tab; diff --git a/postgres/regression_suite/privileges.sql b/postgres/regression_suite/privileges.sql index 30633a24..15c6b735 100644 --- a/postgres/regression_suite/privileges.sql +++ b/postgres/regression_suite/privileges.sql @@ -565,6 +565,24 @@ INSERT INTO atest5(two) VALUES (6) ON CONFLICT (two) DO UPDATE set three = EXCLU INSERT INTO atest5(two) VALUES (6) ON CONFLICT (two) DO UPDATE set three = EXCLUDED.three; INSERT INTO atest5(two) VALUES (6) ON CONFLICT (two) DO UPDATE set one = 8; -- fails (due to UPDATE) INSERT INTO atest5(three) VALUES (4) ON CONFLICT (two) DO UPDATE set three = 10; -- fails (due to INSERT) +-- Check that column level privileges are enforced for ON CONFLICT ... WHERE +-- Ok. we may select one +INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT WHERE atest5.one = 1 RETURNING atest5.two; +-- Error. No select rights on three +INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT WHERE atest5.three = 1 RETURNING atest5.two; + +-- Check that ON CONFLICT ... SELECT FOR UPDATE/SHARE requires an updatable column +SET SESSION AUTHORIZATION regress_priv_user1; +REVOKE UPDATE (three) ON atest5 FROM regress_priv_user4; +SET SESSION AUTHORIZATION regress_priv_user4; + +INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT FOR UPDATE RETURNING atest5.two; -- fails + +SET SESSION AUTHORIZATION regress_priv_user1; +GRANT UPDATE (three) ON atest5 TO regress_priv_user4; +SET SESSION AUTHORIZATION regress_priv_user4; + +INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT FOR UPDATE RETURNING atest5.two; -- ok -- Check that the columns in the inference require select privileges INSERT INTO atest5(four) VALUES (4); -- fail diff --git a/postgres/regression_suite/publication.sql b/postgres/regression_suite/publication.sql index 12223dea..38a76ca9 100644 --- a/postgres/regression_suite/publication.sql +++ b/postgres/regression_suite/publication.sql @@ -146,8 +146,8 @@ CREATE PUBLICATION regress_pub_for_allsequences_alltables FOR ALL SEQUENCES, ALL -- Specifying WITH clause in an ALL SEQUENCES publication will emit a NOTICE. SET client_min_messages = 'NOTICE'; -CREATE PUBLICATION regress_pub_for_allsequences_alltables_withclause FOR ALL SEQUENCES, ALL TABLES WITH (publish = 'insert'); -CREATE PUBLICATION regress_pub_for_allsequences_withclause FOR ALL SEQUENCES WITH (publish_generated_columns = 'stored'); +ALTER PUBLICATION regress_pub_for_allsequences_alltables SET (publish = 'insert'); +ALTER PUBLICATION regress_pub_for_allsequences_alltables SET (publish_generated_columns = 'stored'); RESET client_min_messages; SELECT pubname, puballtables, puballsequences FROM pg_publication WHERE pubname = 'regress_pub_for_allsequences_alltables'; @@ -157,8 +157,6 @@ DROP SEQUENCE regress_pub_seq0, pub_test.regress_pub_seq1; DROP PUBLICATION regress_pub_forallsequences1; DROP PUBLICATION regress_pub_forallsequences2; DROP PUBLICATION regress_pub_for_allsequences_alltables; -DROP PUBLICATION regress_pub_for_allsequences_alltables_withclause; -DROP PUBLICATION regress_pub_for_allsequences_withclause; -- fail - Specifying ALL TABLES more than once CREATE PUBLICATION regress_pub_for_allsequences_alltables FOR ALL SEQUENCES, ALL TABLES, ALL TABLES; diff --git a/postgres/regression_suite/regex.sql b/postgres/regression_suite/regex.sql index 56217104..a4f99c1f 100644 --- a/postgres/regression_suite/regex.sql +++ b/postgres/regression_suite/regex.sql @@ -2,9 +2,6 @@ -- Regular expression tests -- --- Don't want to have to double backslashes in regexes -set standard_conforming_strings = on; - -- Test simple quantified backrefs select 'bbbbb' ~ '^([bc])\1*$' as t; select 'ccc' ~ '^([bc])\1*$' as t; diff --git a/postgres/regression_suite/reloptions.sql b/postgres/regression_suite/reloptions.sql index 8946b465..bda7a80b 100644 --- a/postgres/regression_suite/reloptions.sql +++ b/postgres/regression_suite/reloptions.sql @@ -59,6 +59,17 @@ UPDATE pg_class ALTER TABLE reloptions_test RESET (illegal_option); SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; +-- Tests for ternary options + +-- behave as boolean option: accept unassigned name and truncated value +DROP TABLE reloptions_test; +CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate); +SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; + +DROP TABLE reloptions_test; +CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS); +SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; + -- Test vacuum_truncate option DROP TABLE reloptions_test; diff --git a/postgres/regression_suite/rowsecurity.sql b/postgres/regression_suite/rowsecurity.sql index f19eb4e5..abe96d6e 100644 --- a/postgres/regression_suite/rowsecurity.sql +++ b/postgres/regression_suite/rowsecurity.sql @@ -121,8 +121,9 @@ BEGIN; DELETE FROM rls_test_tgt; ROLLBACK; BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK; DELETE FROM rls_test_tgt RETURNING *; --- INSERT ... ON CONFLICT DO NOTHING should apply INSERT CHECK and SELECT USING --- policy clauses (to new value, whether it conflicts or not) +-- INSERT ... ON CONFLICT DO NOTHING with an arbiter clause should apply +-- INSERT CHECK and SELECT USING policy clauses (to new value, whether it +-- conflicts or not) INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING; INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING; @@ -141,6 +142,21 @@ INSERT INTO rls_test_tgt VALUES (3, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = ' INSERT INTO rls_test_tgt VALUES (3, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d' RETURNING *; ROLLBACK; +-- INSERT ... ON CONFLICT DO SELECT should apply INSERT CHECK and SELECT USING +-- policy clauses to values proposed for insert. In the event of a conflict it +-- should also apply SELECT USING policy clauses to the existing values. +BEGIN; +INSERT INTO rls_test_tgt VALUES (4, 'tgt a') ON CONFLICT (a) DO SELECT RETURNING *; +INSERT INTO rls_test_tgt VALUES (4, 'tgt b') ON CONFLICT (a) DO SELECT RETURNING *; +ROLLBACK; + +-- INSERT ... ON CONFLICT DO SELECT FOR UPDATE should also apply UPDATE USING +-- policy clauses to the existing values, in the event of a conflict. +BEGIN; +INSERT INTO rls_test_tgt VALUES (5, 'tgt a') ON CONFLICT (a) DO SELECT FOR UPDATE RETURNING *; +INSERT INTO rls_test_tgt VALUES (5, 'tgt b') ON CONFLICT (a) DO SELECT FOR UPDATE RETURNING *; +ROLLBACK; + -- MERGE should always apply SELECT USING policy clauses to both source and -- target rows MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a @@ -952,11 +968,51 @@ INSERT INTO document VALUES (4, (SELECT cid from category WHERE cname = 'novel') INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'my first novel') ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol'; +-- +-- INSERT ... ON CONFLICT DO SELECT and Row-level security +-- +SET SESSION AUTHORIZATION regress_rls_alice; +DROP POLICY p3_with_all ON document; + +CREATE POLICY p1_select_novels ON document FOR SELECT + USING (cid = (SELECT cid from category WHERE cname = 'novel')); +CREATE POLICY p2_insert_own ON document FOR INSERT + WITH CHECK (dauthor = current_user); +CREATE POLICY p3_update_novels ON document FOR UPDATE + USING (cid = (SELECT cid from category WHERE cname = 'novel') AND dlevel = 1) + WITH CHECK (dauthor = current_user); + +SET SESSION AUTHORIZATION regress_rls_bob; + +-- DO SELECT requires SELECT rights, should succeed for novel +INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel') + ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle; + +-- DO SELECT requires SELECT rights, should fail for non-novel +INSERT INTO document VALUES (33, (SELECT cid from category WHERE cname = 'science fiction'), 1, 'regress_rls_bob', 'another sci-fi') + ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle; + +-- DO SELECT with WHERE and EXCLUDED reference +INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel') + ON CONFLICT (did) DO SELECT WHERE excluded.dlevel = 1 RETURNING did, dauthor, dtitle; + +-- DO SELECT FOR UPDATE requires both SELECT and UPDATE rights, should succeed for novel and dlevel = 1 +INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel') + ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle; + +-- should fail UPDATE USING policy for novel with dlevel = 2 +INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel') + ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle; + +SET SESSION AUTHORIZATION regress_rls_alice; +DROP POLICY p1_select_novels ON document; +DROP POLICY p2_insert_own ON document; +DROP POLICY p3_update_novels ON document; + -- -- MERGE -- RESET SESSION AUTHORIZATION; -DROP POLICY p3_with_all ON document; ALTER TABLE document ADD COLUMN dnotes text DEFAULT ''; -- all documents are readable diff --git a/postgres/regression_suite/rules.sql b/postgres/regression_suite/rules.sql index 000f77ab..162a7c6d 100644 --- a/postgres/regression_suite/rules.sql +++ b/postgres/regression_suite/rules.sql @@ -1205,6 +1205,32 @@ SELECT * FROM hat_data WHERE hat_name IN ('h8', 'h9', 'h7') ORDER BY hat_name; DROP RULE hat_upsert ON hats; +-- DO SELECT with a WHERE clause +CREATE RULE hat_confsel AS ON INSERT TO hats + DO INSTEAD + INSERT INTO hat_data VALUES ( + NEW.hat_name, + NEW.hat_color) + ON CONFLICT (hat_name) + DO SELECT FOR UPDATE + WHERE excluded.hat_color <> 'forbidden' AND hat_data.* != excluded.* + RETURNING *; +SELECT definition FROM pg_rules WHERE tablename = 'hats' ORDER BY rulename; + +-- fails without RETURNING +INSERT INTO hats VALUES ('h7', 'blue'); + +-- works (returns conflicts) +EXPLAIN (costs off) +INSERT INTO hats VALUES ('h7', 'blue') RETURNING *; +INSERT INTO hats VALUES ('h7', 'blue') RETURNING *; + +-- conflicts excluded by WHERE clause +INSERT INTO hats VALUES ('h7', 'forbidden') RETURNING *; +INSERT INTO hats VALUES ('h7', 'black') RETURNING *; + +DROP RULE hat_confsel ON hats; + drop table hats; drop table hat_data; diff --git a/postgres/regression_suite/stats_ext.sql b/postgres/regression_suite/stats_ext.sql index f42c6bec..a9e9e9ed 100644 --- a/postgres/regression_suite/stats_ext.sql +++ b/postgres/regression_suite/stats_ext.sql @@ -1866,3 +1866,21 @@ SELECT * FROM check_estimated_rows('SELECT * FROM sb_2 WHERE numeric_lt(y, 1.0)' -- Tidy up DROP TABLE sb_1, sb_2 CASCADE; + +-- Check statistics generated for range type and expressions. +CREATE TABLE stats_ext_tbl_range(name text, irange int4range); +INSERT INTO stats_ext_tbl_range VALUES + ('red', '[1,7)'::int4range), + ('blue', '[2,8]'::int4range), + ('green', '[3,9)'::int4range); +CREATE STATISTICS stats_ext_range (mcv) + ON irange, (irange + '[4,10)'::int4range) + FROM stats_ext_tbl_range; +ANALYZE stats_ext_tbl_range; +SELECT attnames, most_common_vals + FROM pg_stats_ext + WHERE statistics_name = 'stats_ext_range'; +SELECT range_length_histogram, range_empty_frac, range_bounds_histogram + FROM pg_stats_ext_exprs + WHERE statistics_name = 'stats_ext_range'; +DROP TABLE stats_ext_tbl_range; diff --git a/postgres/regression_suite/stats_import.sql b/postgres/regression_suite/stats_import.sql index d140733a..39dbee72 100644 --- a/postgres/regression_suite/stats_import.sql +++ b/postgres/regression_suite/stats_import.sql @@ -15,6 +15,12 @@ CREATE TABLE stats_import.test( tags text[] ) WITH (autovacuum_enabled = false); +CREATE TABLE stats_import.test_mr( + id INTEGER PRIMARY KEY, + name text, + mrange int4multirange +) WITH (autovacuum_enabled = false); + SELECT pg_catalog.pg_restore_relation_stats( 'schemaname', 'stats_import', @@ -22,7 +28,7 @@ SELECT 'relpages', 18::integer, 'reltuples', 21::real, 'relallvisible', 24::integer, - 'relallfrozen', 27::integer); + 'relallfrozen', 27::integer); -- CREATE INDEX on a table with autovac disabled should not overwrite -- stats @@ -108,10 +114,27 @@ CREATE TABLE stats_import.part_child_1 FOR VALUES FROM (0) TO (10) WITH (autovacuum_enabled = false); +-- This ensures the presence of extended statistics marked with +-- inherited = true. +CREATE STATISTICS stats_import.part_parent_stat + ON i, (i % 2) + FROM stats_import.part_parent; + CREATE INDEX part_parent_i ON stats_import.part_parent(i); +INSERT INTO stats_import.part_parent +SELECT g.g +FROM generate_series(0,9) AS g(g); + +SELECT COUNT(*) FROM stats_import.part_parent; +SELECT COUNT(*) FROM stats_import.part_child_1; + ANALYZE stats_import.part_parent; +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'part_parent_stat' GROUP BY e.inherited; + SELECT relpages FROM pg_class WHERE oid = 'stats_import.part_parent'::regclass; @@ -747,6 +770,24 @@ AND tablename = 'test' AND inherited = false AND attname = 'id'; +-- test for multiranges +INSERT INTO stats_import.test_mr +VALUES + (1, 'red', '{[1,3),[5,9),[20,30)}'::int4multirange), + (2, 'red', '{[11,13),[15,19),[20,30)}'::int4multirange), + (3, 'red', '{[21,23),[25,29),[120,130)}'::int4multirange); + +-- ensure that we set attribute stats for a multirange +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'attname', 'mrange', + 'inherited', false, + 'range_length_histogram', '{19,29,109}'::text, + 'range_empty_frac', '0'::real, + 'range_bounds_histogram', '{"[1,30)","[11,30)","[21,130)"}'::text +); + -- -- Test the ability to exactly copy data from one table to an identical table, -- correctly reconstructing the stakind order as well as the staopN and @@ -766,6 +807,34 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL; CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1)); +CREATE STATISTICS stats_import.test_stat + ON name, comp, lower(arange), array_length(tags,1) + FROM stats_import.test; + +CREATE STATISTICS stats_import.test_stat_ndistinct (ndistinct) + ON name, comp + FROM stats_import.test; + +CREATE STATISTICS stats_import.test_stat_dependencies (dependencies) + ON name, comp + FROM stats_import.test; + +CREATE STATISTICS stats_import.test_stat_mcv (mcv) + ON name, comp + FROM stats_import.test; + +CREATE STATISTICS stats_import.test_stat_ndistinct_exprs (ndistinct) + ON lower(name), upper(name) + FROM stats_import.test; + +CREATE STATISTICS stats_import.test_stat_dependencies_exprs (dependencies) + ON lower(name), upper(name) + FROM stats_import.test; + +CREATE STATISTICS stats_import.test_stat_mcv_exprs (mcv) + ON lower(name), upper(name) + FROM stats_import.test; + -- Generate statistics on table with data ANALYZE stats_import.test; @@ -774,6 +843,10 @@ CREATE TABLE stats_import.test_clone ( LIKE stats_import.test ) CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1)); +CREATE STATISTICS stats_import.test_stat_clone + ON name, comp, lower(arange), array_length(tags,1) + FROM stats_import.test_clone; + -- -- Copy stats from test to test_clone, and is_odd to is_odd_clone -- @@ -970,4 +1043,573 @@ AND tablename = 'stats_temp' AND inherited = false AND attname = 'i'; DROP TABLE stats_temp; + +-- Tests for pg_clear_extended_stats(). +-- Invalid argument values. +SELECT pg_clear_extended_stats(schemaname => NULL, + relname => 'rel_foo', + statistics_schemaname => 'schema_foo', + statistics_name => 'stat_bar', + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'schema_foo', + relname => NULL, + statistics_schemaname => 'schema_foo', + statistics_name => 'stat_bar', + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'schema_foo', + relname => 'rel_foo', + statistics_schemaname => NULL, + statistics_name => 'stat_bar', + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'schema_foo', + relname => 'rel_foo', + statistics_schemaname => 'schema_foo', + statistics_name => NULL, + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'schema_foo', + relname => 'rel_foo', + statistics_schemaname => 'schema_foo', + statistics_name => 'stat_bar', + inherited => NULL); +-- Missing objects +SELECT pg_clear_extended_stats(schemaname => 'schema_not_exist', + relname => 'test', + statistics_schemaname => 'schema_not_exist', + statistics_name => 'test_stat', + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'stats_import', + relname => 'table_not_exist', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat', + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'schema_not_exist', + statistics_name => 'test_stat', + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'ext_stats_not_exist', + inherited => false); +-- Incorrect relation/extended stats combination +SELECT pg_clear_extended_stats(schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat_clone', + inherited => false); + +-- Check that records are removed after a valid clear call. +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat' GROUP BY e.inherited; +SELECT COUNT(*), e.inherited FROM pg_stats_ext_exprs AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat' GROUP BY e.inherited; +BEGIN; +SELECT pg_catalog.pg_clear_extended_stats( + schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat', + inherited => false); +SELECT mode FROM pg_locks WHERE locktype = 'relation' AND + relation = 'stats_import.test'::regclass AND + pid = pg_backend_pid(); +COMMIT; +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat' GROUP BY e.inherited; +SELECT COUNT(*), e.inherited FROM pg_stats_ext_exprs AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat' GROUP BY e.inherited; +-- And before/after on inherited stats +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'part_parent_stat' GROUP BY e.inherited; +SELECT pg_catalog.pg_clear_extended_stats( + schemaname => 'stats_import', + relname => 'part_parent', + statistics_schemaname => 'stats_import', + statistics_name => 'part_parent_stat', + inherited => true); +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'part_parent_stat' GROUP BY e.inherited; + +-- Check that MAINTAIN is required when clearing statistics. +CREATE ROLE regress_test_extstat_clear; +GRANT ALL ON SCHEMA stats_import TO regress_test_extstat_clear; +SET ROLE regress_test_extstat_clear; +SELECT pg_catalog.pg_clear_extended_stats( + schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat', + inherited => false); +RESET ROLE; +GRANT MAINTAIN ON stats_import.test TO regress_test_extstat_clear; +SET ROLE regress_test_extstat_clear; +SELECT pg_catalog.pg_clear_extended_stats( + schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat', + inherited => false); +RESET ROLE; +REVOKE MAINTAIN ON stats_import.test FROM regress_test_extstat_clear; +REVOKE ALL ON SCHEMA stats_import FROM regress_test_extstat_clear; +DROP ROLE regress_test_extstat_clear; + +-- Tests for pg_restore_extended_stats(). +-- Invalid argument values. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', NULL, + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', NULL, + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', NULL, + 'statistics_name', 'test_stat_clone', + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', NULL, + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', NULL); +-- Missing objects +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'schema_not_exist', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'table_not_exist', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'schema_not_exist', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'ext_stats_not_exist', + 'inherited', false); +-- Incorrect relation/extended stats combination +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); + +-- Check that MAINTAIN is required when restoring statistics. +CREATE ROLE regress_test_extstat_restore; +GRANT ALL ON SCHEMA stats_import TO regress_test_extstat_restore; +SET ROLE regress_test_extstat_restore; +-- No data to restore; this fails on a permission failure. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +RESET ROLE; +GRANT MAINTAIN ON stats_import.test_clone TO regress_test_extstat_restore; +SET ROLE regress_test_extstat_restore; +-- This works, check the lock on the relation while on it. +BEGIN; +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct); +SELECT mode FROM pg_locks WHERE locktype = 'relation' AND + relation = 'stats_import.test_clone'::regclass AND + pid = pg_backend_pid(); +COMMIT; +RESET ROLE; +REVOKE MAINTAIN ON stats_import.test_clone FROM regress_test_extstat_restore; +REVOKE ALL ON SCHEMA stats_import FROM regress_test_extstat_restore; +DROP ROLE regress_test_extstat_restore; + +-- ndistinct value doesn't match object definition +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct', + 'inherited', false, + 'n_distinct', '[{"attributes" : [1,3], "ndistinct" : 4}]'::pg_ndistinct); +-- Incorrect extended stats kind, ndistinct not supported +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies', + 'inherited', false, + 'n_distinct', '[{"attributes" : [1,3], "ndistinct" : 4}]'::pg_ndistinct); +-- Incorrect extended stats kind, dependencies not supported +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct', + 'inherited', false, + 'dependencies', '[{"attributes": [2], "dependency": 3, "degree": 1.000000}, + {"attributes": [3], "dependency": 2, "degree": 1.000000}]'::pg_dependencies); + +-- ok: ndistinct +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct', + 'inherited', false, + 'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct); + +-- dependencies value doesn't match definition +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies', + 'inherited', false, + 'dependencies', '[{"attributes": [1], "dependency": 3, "degree": 1.000000}, + {"attributes": [3], "dependency": 1, "degree": 1.000000}]'::pg_dependencies); + +-- ok: dependencies +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies', + 'inherited', false, + 'dependencies', '[{"attributes": [2], "dependency": 3, "degree": 1.000000}, + {"attributes": [3], "dependency": 2, "degree": 1.000000}]'::pg_dependencies); + +-- ndistinct with expressions, invalid attributes. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct_exprs', + 'inherited', false, + 'n_distinct', '[{"attributes" : [1,-1], "ndistinct" : 4}]'::pg_ndistinct); + +-- ok: ndistinct with expressions. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct_exprs', + 'inherited', false, + 'n_distinct', '[{"attributes" : [-1,-2], "ndistinct" : 4}]'::pg_ndistinct); + +-- dependencies with expressions, invalid attributes. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies_exprs', + 'inherited', false, + 'dependencies', '[{"attributes": [-1], "dependency": 1, "degree": 1.000000}, + {"attributes": [1], "dependency": -1, "degree": 1.000000}]'::pg_dependencies); + +-- ok: dependencies with expressions +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies_exprs', + 'inherited', false, + 'dependencies', '[{"attributes": [-1], "dependency": -2, "degree": 1.000000}, + {"attributes": [-2], "dependency": -1, "degree": 1.000000}]'::pg_dependencies); + +-- ok: MCV with expressions +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv_exprs', + 'inherited', false, + 'most_common_vals', '{{four,FOUR},{one,NULL},{NULL,TRE},{two,TWO}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.99}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.023,0.087}'::double precision[]); + +-- Check the presence of the restored stats, for each object. +SELECT replace(e.n_distinct, '}, ', E'},\n') AS n_distinct +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_ndistinct' AND + e.inherited = false; + +SELECT replace(e.dependencies, '}, ', E'},\n') AS dependencies +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_dependencies' AND + e.inherited = false; + +SELECT replace(e.n_distinct, '}, ', E'},\n') AS n_distinct +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_ndistinct_exprs' AND + e.inherited = false; + +SELECT replace(e.dependencies, '}, ', E'},\n') AS dependencies +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_dependencies_exprs' AND + e.inherited = false; + +SELECT e.most_common_vals, e.most_common_val_nulls, + e.most_common_freqs, e.most_common_base_freqs +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_mcv_exprs' AND + e.inherited = false /* \gx */; + +-- Incorrect extended stats kind, mcv not supported +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); + +-- MCV requires all three parameters +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_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); +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', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); +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', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[]); + +-- most_common_vals that is not 2-D +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', '{four,NULL}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); + +-- most_common_freqs with length not matching with most_common_vals. +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', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); + +-- most_common_base_freqs with length not matching most_common_vals. +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', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625}'::double precision[]); + +-- mcv attributes not matching object definition +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', '{{four,NULL,0,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2}, + {tre,"(3,3.3,TRE,03-03-2003,)",-1,3}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[], + '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[]); + +-- ok: mcv +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', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); + +SELECT replace(e.most_common_vals::text, '},', E'},\n ') AS mcvs, + e.most_common_val_nulls, + e.most_common_freqs, e.most_common_base_freqs +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_mcv' AND + e.inherited = false +/* \gx */; + +-- Check import of all kinds for multirange. +CREATE STATISTICS stats_import.test_mr_stat + ON name, mrange, ( mrange + '{[10000,10200)}'::int4multirange) + FROM stats_import.test_mr; + +-- ok: multirange stats +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'n_distinct', '[{"attributes": [2, 3], "ndistinct": 3}, + {"attributes": [2, -1], "ndistinct": 3}, + {"attributes": [3, -1], "ndistinct": 3}, + {"attributes": [2, 3, -1], "ndistinct": 3}]'::pg_catalog.pg_ndistinct, + 'dependencies', '[{"attributes": [3], "dependency": 2, "degree": 1.000000}, + {"attributes": [3], "dependency": -1, "degree": 1.000000}, + {"attributes": [-1], "dependency": 2, "degree": 1.000000}, + {"attributes": [-1], "dependency": 3, "degree": 1.000000}, + {"attributes": [2, 3], "dependency": -1, "degree": 1.000000}, + {"attributes": [2, -1], "dependency": 3, "degree": 1.000000}, + {"attributes": [3, -1], "dependency": 2, "degree": 1.000000}]'::pg_catalog.pg_dependencies, + 'most_common_vals', '{{red,"{[1,3),[5,9),[20,30)}","{[1,3),[5,9),[20,30),[10000,10200)}"}, + {red,"{[11,13),[15,19),[20,30)}","{[11,13),[15,19),[20,30),[10000,10200)}"}, + {red,"{[21,23),[25,29),[120,130)}","{[21,23),[25,29),[120,130),[10000,10200)}"}}'::text[], + 'most_common_freqs', '{0.3333333333333333,0.3333333333333333,0.3333333333333333}'::double precision[], + 'most_common_base_freqs', '{0.1111111111111111,0.1111111111111111,0.1111111111111111}'::double precision[] +); + +SELECT replace(e.n_distinct, '}, ', E'},\n') AS n_distinct, + replace(e.dependencies, '}, ', E'},\n') AS dependencies, + replace(e.most_common_vals::text, '},', E'},\n ') AS mcvs, + e.most_common_val_nulls, + e.most_common_freqs, e.most_common_base_freqs +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_mr_stat' AND + e.inherited = false +/* \gx */; + +-- Test the ability of pg_restore_extended_stats() to import all of the +-- statistic values from an extended statistic object that has been +-- populated via a regular ANALYZE. This checks after the statistics +-- kinds supported by pg_restore_extended_stats(). +-- +-- Note: Keep this test at the bottom of the file, so as the amount of +-- statistics data handled is maximized. +ANALYZE stats_import.test; + +-- Copy stats from test_stat to test_stat_clone +SELECT e.statistics_name, + pg_catalog.pg_restore_extended_stats( + 'schemaname', e.statistics_schemaname::text, + 'relname', 'test_clone', + 'statistics_schemaname', e.statistics_schemaname::text, + 'statistics_name', 'test_stat_clone', + 'inherited', e.inherited, + 'n_distinct', e.n_distinct, + 'dependencies', e.dependencies, + 'most_common_vals', e.most_common_vals, + 'most_common_freqs', e.most_common_freqs, + 'most_common_base_freqs', e.most_common_base_freqs) +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' +AND e.statistics_name = 'test_stat'; + +-- Set difference old MINUS new. +SELECT o.inherited, + o.n_distinct, o.dependencies, o.most_common_vals, + o.most_common_freqs, o.most_common_base_freqs + FROM pg_stats_ext AS o + WHERE o.statistics_schemaname = 'stats_import' AND + o.statistics_name = 'test_stat' +EXCEPT +SELECT n.inherited, + n.n_distinct, n.dependencies, n.most_common_vals, + n.most_common_freqs, n.most_common_base_freqs + FROM pg_stats_ext AS n + WHERE n.statistics_schemaname = 'stats_import' AND + n.statistics_name = 'test_stat_clone'; +-- Set difference new MINUS old. +SELECT n.inherited, + n.n_distinct, n.dependencies, n.most_common_vals, + n.most_common_freqs, n.most_common_base_freqs + FROM pg_stats_ext AS n + WHERE n.statistics_schemaname = 'stats_import' AND + n.statistics_name = 'test_stat_clone' +EXCEPT +SELECT o.inherited, + o.n_distinct, o.dependencies, o.most_common_vals, + o.most_common_freqs, o.most_common_base_freqs + FROM pg_stats_ext AS o + WHERE o.statistics_schemaname = 'stats_import' AND + o.statistics_name = 'test_stat'; + DROP SCHEMA stats_import CASCADE; diff --git a/postgres/regression_suite/strings.sql b/postgres/regression_suite/strings.sql index b86dc6eb..13a07d72 100644 --- a/postgres/regression_suite/strings.sql +++ b/postgres/regression_suite/strings.sql @@ -17,8 +17,6 @@ SELECT 'first line' AS "Illegal comment within continuation"; -- Unicode escapes -SET standard_conforming_strings TO on; - SELECT U&'d\0061t\+000061' AS U&"d\0061t\+000061"; SELECT U&'d!0061t\+000061' UESCAPE '!' AS U&"d*0061t\+000061" UESCAPE '*'; SELECT U&'a\\b' AS "a\b"; @@ -50,18 +48,11 @@ SELECT E'wrong: \udb99\u0061'; SELECT E'wrong: \U0000db99\U00000061'; SELECT E'wrong: \U002FFFFF'; +-- this is no longer allowed: SET standard_conforming_strings TO off; - -SELECT U&'d\0061t\+000061' AS U&"d\0061t\+000061"; -SELECT U&'d!0061t\+000061' UESCAPE '!' AS U&"d*0061t\+000061" UESCAPE '*'; - -SELECT U&' \' UESCAPE '!' AS "tricky"; -SELECT 'tricky' AS U&"\" UESCAPE '!'; - -SELECT U&'wrong: \061'; -SELECT U&'wrong: \+0061'; -SELECT U&'wrong: +0061' UESCAPE '+'; - +-- but this should be acceptable: +SET standard_conforming_strings TO on; +-- or this: RESET standard_conforming_strings; -- bytea @@ -687,6 +678,30 @@ SELECT substr(f1, 5, 10) AS f1_data, substr(f2, 5, 10) AS f2_data FROM toasttest; SELECT pg_column_compression(f1) AS f1_comp, pg_column_compression(f2) AS f2_comp FROM toasttest; +TRUNCATE toasttest; +-- test with inline compressible varlenas. +SET default_toast_compression = 'pglz'; +ALTER TABLE toasttest ALTER COLUMN f1 SET STORAGE MAIN; +ALTER TABLE toasttest ALTER COLUMN f2 SET STORAGE MAIN; +INSERT INTO toasttest values(repeat('1234', 1024), repeat('5678', 1024)); +-- There should be no values in the toast relation. +SELECT substr(f1, 5, 10) AS f1_data, substr(f2, 5, 10) AS f2_data + FROM toasttest; +SELECT pg_column_compression(f1) AS f1_comp, pg_column_compression(f2) AS f2_comp + FROM toasttest; +SELECT count(*) FROM reltoastname; +TRUNCATE toasttest; +-- test with external compressed data (default). +ALTER TABLE toasttest ALTER COLUMN f1 SET STORAGE EXTENDED; +ALTER TABLE toasttest ALTER COLUMN f2 SET STORAGE EXTENDED; +INSERT INTO toasttest values(repeat('1234', 10240), NULL); +-- There should be one value in the toast relation. +SELECT substr(f1, 5, 10) AS f1_data, substr(f2, 5, 10) AS f2_data + FROM toasttest; +SELECT pg_column_compression(f1) AS f1_comp, pg_column_compression(f2) AS f2_comp + FROM toasttest; +SELECT count(*) FROM reltoastname WHERE chunk_seq = 0; +RESET default_toast_compression; DROP TABLE toasttest; -- @@ -914,39 +929,6 @@ SELECT '\x80000000'::bytea::int4 AS "-2147483648", '\x7FFFFFFF'::bytea::int4 AS SELECT '\x8000000000000000'::bytea::int8 AS "-9223372036854775808", '\x7FFFFFFFFFFFFFFF'::bytea::int8 AS "9223372036854775807"; --- --- test behavior of escape_string_warning and standard_conforming_strings options --- -set escape_string_warning = off; -set standard_conforming_strings = off; - -show escape_string_warning; -show standard_conforming_strings; - -set escape_string_warning = on; -set standard_conforming_strings = on; - -show escape_string_warning; -show standard_conforming_strings; - -select 'a\bcd' as f1, 'a\b''cd' as f2, 'a\b''''cd' as f3, 'abcd\' as f4, 'ab\''cd' as f5, '\\' as f6; - -set standard_conforming_strings = off; - --- select 'a\\bcd' as f1, 'a\\b\'cd' as f2, 'a\\b\'''cd' as f3, 'abcd\\' as f4, 'ab\\\'cd' as f5, '\\\\' as f6; - -set escape_string_warning = off; -set standard_conforming_strings = on; - -select 'a\bcd' as f1, 'a\b''cd' as f2, 'a\b''''cd' as f3, 'abcd\' as f4, 'ab\''cd' as f5, '\\' as f6; - -set standard_conforming_strings = off; - --- select 'a\\bcd' as f1, 'a\\b\'cd' as f2, 'a\\b\'''cd' as f3, 'abcd\\' as f4, 'ab\\\'cd' as f5, '\\\\' as f6; - -reset standard_conforming_strings; - - -- -- Additional string functions -- diff --git a/postgres/regression_suite/subselect.sql b/postgres/regression_suite/subselect.sql index 881c88b6..e2e89d31 100644 --- a/postgres/regression_suite/subselect.sql +++ b/postgres/regression_suite/subselect.sql @@ -340,6 +340,17 @@ select * from ( where not exists (select 1 from tenk1 as b where b.unique2 = 10000) ) ss; + +-- +-- Test cases for interactions between PARAM_EXEC, subplans and array +-- subscripts +-- + +-- check that array subscription doesn't conflict with PARAM_EXEC (see #19370) +SELECT (array[1,2])[(SELECT g.i)] FROM generate_series(1, 1) g(i); +SELECT (array[1,2])[(SELECT g.i):(SELECT g.i + 1)] FROM generate_series(1, 1) g(i); + + -- -- Test that an IN implemented using a UniquePath does unique-ification -- with the right semantics, as per bug #4113. (Unfortunately we have @@ -941,6 +952,46 @@ select * from (select 9 as x, unnest(array[1,2,3,11,12,13]) as u) ss where tattle(x, u); +-- +-- check that an upper-level qual is not pushed down if it references a grouped +-- Var whose underlying expression contains SRFs +-- +explain (verbose, costs off) +select * from + (select generate_series(1, ten) as g, count(*) from tenk1 group by 1) ss + where ss.g = 1; + +select * from + (select generate_series(1, ten) as g, count(*) from tenk1 group by 1) ss + where ss.g = 1; + +-- +-- check that an upper-level qual is not pushed down if it references a grouped +-- Var whose underlying expression contains volatile functions +-- +alter function tattle(x int, y int) volatile; + +explain (verbose, costs off) +select * from + (select tattle(3, ten) as v, count(*) from tenk1 where unique1 < 3 group by 1) ss + where ss.v; + +select * from + (select tattle(3, ten) as v, count(*) from tenk1 where unique1 < 3 group by 1) ss + where ss.v; + +-- if we pretend it's stable, we get different results: +alter function tattle(x int, y int) stable; + +explain (verbose, costs off) +select * from + (select tattle(3, ten) as v, count(*) from tenk1 where unique1 < 3 group by 1) ss + where ss.v; + +select * from + (select tattle(3, ten) as v, count(*) from tenk1 where unique1 < 3 group by 1) ss + where ss.v; + drop function tattle(x int, y int); -- diff --git a/postgres/regression_suite/sysviews.sql b/postgres/regression_suite/sysviews.sql index 66179f02..004f9a70 100644 --- a/postgres/regression_suite/sysviews.sql +++ b/postgres/regression_suite/sysviews.sql @@ -18,7 +18,7 @@ select type, name, ident, level, total_bytes >= free_bytes from pg_backend_memory_contexts where level = 1; -- We can exercise some MemoryContext type stats functions. Most of the --- column values are too platform-dependant to display. +-- column values are too platform-dependent to display. -- Ensure stats from the bump allocator look sane. Bump isn't a commonly -- used context, but it is used in tuplesort.c, so open a cursor to keep diff --git a/postgres/regression_suite/triggers.sql b/postgres/regression_suite/triggers.sql index 165356db..e86f65cd 100644 --- a/postgres/regression_suite/triggers.sql +++ b/postgres/regression_suite/triggers.sql @@ -1148,7 +1148,7 @@ drop function trigger_ddl_func(); -- -- Verify behavior of before and after triggers with INSERT...ON CONFLICT --- DO UPDATE +-- DO UPDATE and DO SELECT -- create table upsert (key int4 primary key, color text); @@ -1197,6 +1197,7 @@ insert into upsert values(5, 'purple') on conflict (key) do update set color = ' insert into upsert values(6, 'white') on conflict (key) do update set color = 'updated ' || upsert.color; insert into upsert values(7, 'pink') on conflict (key) do update set color = 'updated ' || upsert.color; insert into upsert values(8, 'yellow') on conflict (key) do update set color = 'updated ' || upsert.color; +insert into upsert values(8, 'blue') on conflict (key) do select for update where upsert.color = 'yellow trig modified' returning old.*, new.*, upsert.*; select * from upsert; @@ -2228,6 +2229,10 @@ with wcte as (insert into table1 values (42)) with wcte as (insert into table1 values (43)) insert into table1 values (44); +with wcte as (insert into table1 values (45)) + merge into table1 using (values (46)) as v(a) on table1.a = v.a + when not matched then insert values (v.a); + select * from table1; select * from table2; diff --git a/postgres/regression_suite/tsearch.sql b/postgres/regression_suite/tsearch.sql index a603db25..abfd60fe 100644 --- a/postgres/regression_suite/tsearch.sql +++ b/postgres/regression_suite/tsearch.sql @@ -222,6 +222,7 @@ RESET enable_bitmapscan; DROP INDEX wowidx; +ALTER TABLE test_tsvector SET (parallel_workers = 2); CREATE INDEX wowidx ON test_tsvector USING gin (a); SET enable_seqscan=OFF; diff --git a/postgres/regression_suite/type_sanity.sql b/postgres/regression_suite/type_sanity.sql index 54d86712..664295b3 100644 --- a/postgres/regression_suite/type_sanity.sql +++ b/postgres/regression_suite/type_sanity.sql @@ -451,7 +451,9 @@ WHERE (is_catalog_text_unique_index_oid(indexrelid) <> SELECT r.rngtypid, r.rngsubtype FROM pg_range as r -WHERE r.rngtypid = 0 OR r.rngsubtype = 0 OR r.rngsubopc = 0; +WHERE r.rngtypid = 0 OR r.rngsubtype = 0 OR r.rngsubopc = 0 + OR r.rngconstruct2 = 0 OR r.rngconstruct3 = 0 + OR r.rngmltconstruct0 = 0 OR r.rngmltconstruct1 = 0 OR r.rngmltconstruct2 = 0; -- rngcollation should be specified iff subtype is collatable @@ -491,6 +493,49 @@ SELECT r.rngtypid, r.rngsubtype, r.rngmultitypid FROM pg_range r WHERE r.rngmultitypid IS NULL OR r.rngmultitypid = 0; +-- check constructor function arguments and return types +-- +-- proname and prosrc are not required to have these particular +-- values, but this matches what DefineRange() produces and serves to +-- sanity-check the catalog entries for built-in types. + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngconstruct2 JOIN pg_type t ON r.rngtypid = t.oid +WHERE p.pronargs != 2 + OR p.proargtypes[0] != r.rngsubtype OR p.proargtypes[1] != r.rngsubtype + OR p.prorettype != r.rngtypid + OR p.proname != t.typname OR p.prosrc != 'range_constructor2'; + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngconstruct3 JOIN pg_type t ON r.rngtypid = t.oid +WHERE p.pronargs != 3 + OR p.proargtypes[0] != r.rngsubtype OR p.proargtypes[1] != r.rngsubtype OR p.proargtypes[2] != 'pg_catalog.text'::regtype + OR p.prorettype != r.rngtypid + OR p.proname != t.typname OR p.prosrc != 'range_constructor3'; + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngmltconstruct0 JOIN pg_type t ON r.rngmultitypid = t.oid +WHERE p.pronargs != 0 + OR p.prorettype != r.rngmultitypid + OR p.proname != t.typname OR p.prosrc != 'multirange_constructor0'; + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngmltconstruct1 JOIN pg_type t ON r.rngmultitypid = t.oid +WHERE p.pronargs != 1 + OR p.proargtypes[0] != r.rngtypid + OR p.prorettype != r.rngmultitypid + OR p.proname != t.typname OR p.prosrc != 'multirange_constructor1'; + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngmltconstruct2 JOIN pg_type t ON r.rngmultitypid = t.oid JOIN pg_type t2 ON r.rngtypid = t2.oid +WHERE p.pronargs != 1 + OR p.proargtypes[0] != t2.typarray + OR p.prorettype != r.rngmultitypid + OR p.proname != t.typname OR p.prosrc != 'multirange_constructor2'; + + +-- ****************************************** + -- Create a table that holds all the known in-core data types and leave it -- around so as pg_upgrade is able to test their binary compatibility. CREATE TABLE tab_core_types AS SELECT @@ -530,6 +575,7 @@ CREATE TABLE tab_core_types AS SELECT 'abc'::refcursor, '1 2'::int2vector, '1 2'::oidvector, + '1234'::oid8, format('%I=UC/%I', USER, USER)::aclitem AS aclitem, 'a fat cat sat on a mat and ate a fat rat'::tsvector, 'fat & rat'::tsquery, diff --git a/postgres/regression_suite/updatable_views.sql b/postgres/regression_suite/updatable_views.sql index 0ed98137..2cf9a584 100644 --- a/postgres/regression_suite/updatable_views.sql +++ b/postgres/regression_suite/updatable_views.sql @@ -106,6 +106,12 @@ INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO UPDATE set a = excluded. SELECT * FROM rw_view15; INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO UPDATE set upper = 'blarg'; -- fails SELECT * FROM rw_view15; +INSERT INTO rw_view15 (a) VALUES (3) + ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.upper = 'UNSPECIFIED' + RETURNING old, new; +INSERT INTO rw_view15 (a) VALUES (3) + ON CONFLICT (a) DO SELECT WHERE excluded.upper = 'UNSPECIFIED' RETURNING old, new; + SELECT * FROM rw_view15; ALTER VIEW rw_view15 ALTER COLUMN upper SET DEFAULT 'NOT SET'; INSERT INTO rw_view15 (a) VALUES (4); -- should fail @@ -1850,7 +1856,7 @@ insert into wcowrtest_v2 values (2, 'no such row in sometable'); drop view wcowrtest_v, wcowrtest_v2; drop table wcowrtest, sometable; --- Check INSERT .. ON CONFLICT DO UPDATE works correctly when the view's +-- Check INSERT .. ON CONFLICT DO SELECT/UPDATE works correctly when the view's -- columns are named and ordered differently than the underlying table's. create table uv_iocu_tab (a text unique, b float); insert into uv_iocu_tab values ('xyxyxy', 0); @@ -1863,6 +1869,8 @@ select * from uv_iocu_tab; insert into uv_iocu_view (a, b) values ('xyxyxy', 1) on conflict (a) do update set b = excluded.b; select * from uv_iocu_tab; +insert into uv_iocu_view (a, b) values ('xyxyxy', 1) + on conflict (a) do select where uv_iocu_view.c = 2 and excluded.c = 2 returning *; -- OK to access view columns that are not present in underlying base -- relation in the ON CONFLICT portion of the query @@ -1899,6 +1907,11 @@ insert into uv_iocu_view (aa,bb) values (1,'y') and excluded.bb != '' and excluded.cc is not null; select * from uv_iocu_view; +explain (costs off) +insert into uv_iocu_view (aa,bb) values (1,'Rejected: (y,1,"(1,y)")') + on conflict (aa) do select where uv_iocu_view.* = excluded.* returning *; +insert into uv_iocu_view (aa,bb) values (1,'Rejected: (y,1,"(1,y)")') + on conflict (aa) do select where uv_iocu_view.* = excluded.* returning *; -- Test omitting a column of the base relation delete from uv_iocu_view; @@ -1911,11 +1924,15 @@ alter table uv_iocu_tab alter column b set default 'table default'; insert into uv_iocu_view (aa) values (1) on conflict (aa) do update set bb = 'Rejected: '||excluded.*; select * from uv_iocu_view; +insert into uv_iocu_view (aa) values (1) + on conflict (aa) do select returning *; alter view uv_iocu_view alter column bb set default 'view default'; insert into uv_iocu_view (aa) values (1) on conflict (aa) do update set bb = 'Rejected: '||excluded.*; select * from uv_iocu_view; +insert into uv_iocu_view (aa) values (1) + on conflict (aa) do select returning *; -- Should fail to update non-updatable columns insert into uv_iocu_view (aa) values (1) @@ -1924,7 +1941,7 @@ insert into uv_iocu_view (aa) values (1) drop view uv_iocu_view; drop table uv_iocu_tab; --- ON CONFLICT DO UPDATE permissions checks +-- ON CONFLICT DO SELECT/UPDATE permissions checks create user regress_view_user1; create user regress_view_user2; @@ -1948,6 +1965,10 @@ insert into rw_view1 values ('zzz',2.0,1) on conflict (aa) do update set bb = rw_view1.bb||'xxx'; -- OK insert into rw_view1 values ('zzz',2.0,1) on conflict (aa) do update set cc = 3.0; -- Not allowed +insert into rw_view1 values ('yyy',2.0,1) + on conflict (aa) do select for update returning cc; -- Not allowed +insert into rw_view1 values ('yyy',2.0,1) + on conflict (aa) do select for update returning aa, bb; reset session authorization; select * from base_tbl; @@ -1960,9 +1981,13 @@ set session authorization regress_view_user2; create view rw_view2 as select b as bb, c as cc, a as aa from base_tbl; insert into rw_view2 (aa,bb) values (1,'xxx') on conflict (aa) do update set bb = excluded.bb; -- Not allowed +insert into rw_view2 (aa,bb) values (1,'xxx') + on conflict (aa) do select returning 1; -- Not allowed create view rw_view3 as select b as bb, a as aa from base_tbl; insert into rw_view3 (aa,bb) values (1,'xxx') on conflict (aa) do update set bb = excluded.bb; -- OK +insert into rw_view3 (aa,bb) values (1,'xxx') + on conflict (aa) do select returning aa, bb; -- OK reset session authorization; select * from base_tbl; @@ -1970,6 +1995,8 @@ set session authorization regress_view_user2; create view rw_view4 as select aa, bb, cc FROM rw_view1; insert into rw_view4 (aa,bb) values (1,'yyy') on conflict (aa) do update set bb = excluded.bb; -- Not allowed +insert into rw_view4 (aa,bb) values (1,'yyy') + on conflict (aa) do select returning 1; -- Not allowed create view rw_view5 as select aa, bb FROM rw_view1; insert into rw_view5 (aa,bb) values (1,'yyy') on conflict (aa) do update set bb = excluded.bb; -- OK diff --git a/postgres/regression_suite/without_overlaps.sql b/postgres/regression_suite/without_overlaps.sql index 8a06646d..ecea1522 100644 --- a/postgres/regression_suite/without_overlaps.sql +++ b/postgres/regression_suite/without_overlaps.sql @@ -659,7 +659,7 @@ CREATE TABLE temporal_partitioned ( id int4range, valid_at daterange, name text, - CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) + CONSTRAINT temporal_partitioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)'); CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)'); @@ -677,7 +677,7 @@ CREATE TABLE temporal_partitioned ( id int4range, valid_at daterange, name text, - CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) + CONSTRAINT temporal_partitioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)'); CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)'); @@ -1769,7 +1769,7 @@ CREATE TABLE temporal_partitioned_rng ( id int4range, valid_at daterange, name text, - CONSTRAINT temporal_paritioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) + CONSTRAINT temporal_partitioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE tp1 partition OF temporal_partitioned_rng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)'); CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)'); @@ -1899,7 +1899,7 @@ CREATE TABLE temporal_partitioned_mltrng ( id int4range, valid_at datemultirange, name text, - CONSTRAINT temporal_paritioned_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) + CONSTRAINT temporal_partitioned_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE tp1 PARTITION OF temporal_partitioned_mltrng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)', '[13,14)', '[15,16)', '[17,18)', '[19,20)', '[21,22)', '[23,24)'); CREATE TABLE tp2 PARTITION OF temporal_partitioned_mltrng FOR VALUES IN ('[0,1)', '[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)', '[14,15)', '[16,17)', '[18,19)', '[20,21)', '[22,23)', '[24,25)');