diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index 4b1f8121..46b37a91 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -367,6 +367,7 @@ pub enum SyntaxKind { PLAN_KW, PLANS_KW, POLICY_KW, + PORTION_KW, POSITION_KW, PRECEDING_KW, PRECISION_KW, @@ -844,6 +845,7 @@ pub enum SyntaxKind { FILTER_CLAUSE, FORCE_RLS, FOREIGN_KEY_CONSTRAINT, + FOR_PORTION_OF, FOR_PROVIDER, FRAME_CLAUSE, FROM_CLAUSE, @@ -1911,6 +1913,8 @@ impl SyntaxKind { SyntaxKind::PLANS_KW } else if ident.eq_ignore_ascii_case("policy") { SyntaxKind::POLICY_KW + } else if ident.eq_ignore_ascii_case("portion") { + SyntaxKind::PORTION_KW } else if ident.eq_ignore_ascii_case("position") { SyntaxKind::POSITION_KW } else if ident.eq_ignore_ascii_case("preceding") { diff --git a/crates/squawk_parser/src/generated/token_sets.rs b/crates/squawk_parser/src/generated/token_sets.rs index 00559bc5..c393c45f 100644 --- a/crates/squawk_parser/src/generated/token_sets.rs +++ b/crates/squawk_parser/src/generated/token_sets.rs @@ -258,6 +258,7 @@ pub(crate) const COLUMN_OR_TABLE_KEYWORDS: TokenSet = TokenSet::new(&[ SyntaxKind::PLAN_KW, SyntaxKind::PLANS_KW, SyntaxKind::POLICY_KW, + SyntaxKind::PORTION_KW, SyntaxKind::POSITION_KW, SyntaxKind::PRECEDING_KW, SyntaxKind::PRECISION_KW, @@ -689,6 +690,7 @@ pub(crate) const TYPE_KEYWORDS: TokenSet = TokenSet::new(&[ SyntaxKind::PLAN_KW, SyntaxKind::PLANS_KW, SyntaxKind::POLICY_KW, + SyntaxKind::PORTION_KW, SyntaxKind::POSITION_KW, SyntaxKind::PRECEDING_KW, SyntaxKind::PRECISION_KW, @@ -1180,6 +1182,7 @@ pub(crate) const ALL_KEYWORDS: TokenSet = TokenSet::new(&[ SyntaxKind::PLAN_KW, SyntaxKind::PLANS_KW, SyntaxKind::POLICY_KW, + SyntaxKind::PORTION_KW, SyntaxKind::POSITION_KW, SyntaxKind::PRECEDING_KW, SyntaxKind::PRECISION_KW, @@ -1665,6 +1668,7 @@ pub(crate) const BARE_LABEL_KEYWORDS: TokenSet = TokenSet::new(&[ SyntaxKind::PLAN_KW, SyntaxKind::PLANS_KW, SyntaxKind::POLICY_KW, + SyntaxKind::PORTION_KW, SyntaxKind::POSITION_KW, SyntaxKind::PRECEDING_KW, SyntaxKind::PREPARE_KW, @@ -2050,6 +2054,7 @@ pub(crate) const UNRESERVED_KEYWORDS: TokenSet = TokenSet::new(&[ SyntaxKind::PLAN_KW, SyntaxKind::PLANS_KW, SyntaxKind::POLICY_KW, + SyntaxKind::PORTION_KW, SyntaxKind::PRECEDING_KW, SyntaxKind::PREPARE_KW, SyntaxKind::PREPARED_KW, diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index aa0c8881..ea3f65a2 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -6229,6 +6229,14 @@ fn alter_publication(p: &mut Parser<'_>) -> CompletedMarker { SET_KW if p.nth_at(1, L_PAREN) => { set_options(p); } + SET_KW if p.nth_at(1, ALL_KW) => { + p.bump(SET_KW); + publication_all_object(p); + while !p.at(EOF) && p.eat(COMMA) { + publication_all_object(p); + } + opt_except_table_clause(p); + } SET_KW => { p.bump(SET_KW); publication_object(p); @@ -8895,8 +8903,7 @@ fn edge_left(p: &mut Parser<'_>) { let m = p.start(); p.bump(L_ANGLE); p.expect(MINUS); - if p.at(L_BRACK) { - p.bump(L_BRACK); + if p.eat(L_BRACK) { opt_edge_pattern_inner(p); p.expect(R_BRACK); p.expect(MINUS); @@ -9799,15 +9806,17 @@ fn opt_except_table_clause(p: &mut Parser<'_>) { let m = p.start(); p.bump(EXCEPT_KW); - p.expect(TABLE_KW); delimited( p, L_PAREN, R_PAREN, COMMA, || "unexpected comma".to_string(), - RELATION_NAME_FIRST, - |p| opt_relation_name(p).is_some(), + RELATION_NAME_FIRST.union(TokenSet::new(&[TABLE_KW])), + |p| { + p.eat(TABLE_KW); + opt_relation_name(p).is_some() + }, ); m.complete(p, EXCEPT_TABLE_CLAUSE); } @@ -9993,8 +10002,7 @@ fn create_subscription(p: &mut Parser<'_>) -> CompletedMarker { p.bump(CREATE_KW); p.bump(SUBSCRIPTION_KW); name(p); - if p.at(SERVER_KW) { - p.bump(SERVER_KW); + if p.eat(SERVER_KW) { name_ref(p); } else { p.expect(CONNECTION_KW); @@ -12483,7 +12491,7 @@ fn copy_option_list(p: &mut Parser<'_>) { fn opt_copy_option_item(p: &mut Parser<'_>) -> bool { match p.current() { - BINARY_KW | FREEZE_KW | CSV_KW | HEADER_KW => { + BINARY_KW | FREEZE_KW | CSV_KW | HEADER_KW | JSON_KW => { p.bump_any(); } DELIMITER_KW | NULL_KW | QUOTE_KW | ESCAPE_KW => { @@ -13226,6 +13234,7 @@ fn update(p: &mut Parser<'_>, m: Option) -> CompletedMarker { let m = m.unwrap_or_else(|| p.start()); p.bump(UPDATE_KW); relation_name(p); + opt_for_portion_of(p); // postgres parser has the same setup, it assumes the alias can never be // named `SET` if !p.at(SET_KW) { @@ -13242,6 +13251,35 @@ fn update(p: &mut Parser<'_>, m: Option) -> CompletedMarker { m.complete(p, UPDATE) } +// FOR PORTION OF column_name FROM expr TO expr [ [ AS ] alias ] +// FOR PORTION OF column_name ( expr ) [ [ AS ] alias ] +fn opt_for_portion_of(p: &mut Parser<'_>) { + if !p.at(FOR_KW) { + return; + } + let m = p.start(); + p.expect(FOR_KW); + p.expect(PORTION_KW); + p.expect(OF_KW); + name_ref(p); + for_portion_of_target(p); + m.complete(p, FOR_PORTION_OF); +} + +fn for_portion_of_target(p: &mut Parser<'_>) { + if p.eat(L_PAREN) { + expr(p); + p.expect(R_PAREN); + } else { + p.expect(FROM_KW); + // start time + expr(p); + p.expect(TO_KW); + // end time + expr(p); + } +} + fn opt_where_or_current_of(p: &mut Parser<'_>) { if p.at(WHERE_KW) { if p.nth_at(1, CURRENT_KW) { @@ -13291,7 +13329,10 @@ fn delete(p: &mut Parser<'_>, m: Option) -> CompletedMarker { p.bump(DELETE_KW); p.expect(FROM_KW); relation_name(p); - opt_as_alias(p); + opt_for_portion_of(p); + if !p.at(FOR_KW) { + opt_as_alias(p); + } opt_using_clause(p); // [ WHERE condition | WHERE CURRENT OF cursor_name ] opt_where_or_current_of(p); diff --git a/crates/squawk_parser/tests/snapshots/tests__regression_for_portion_of.snap b/crates/squawk_parser/tests/snapshots/tests__regression_for_portion_of.snap new file mode 100644 index 00000000..3c1ef222 --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__regression_for_portion_of.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: postgres/regression_suite/for_portion_of.sql +--- + diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 43484ac8..bd6ddc7a 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -5885,6 +5885,10 @@ impl Delete { support::child(&self.syntax) } #[inline] + pub fn for_portion_of(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn relation_name(&self) -> Option { support::child(&self.syntax) } @@ -8811,6 +8815,53 @@ impl FilterClause { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ForPortionOf { + pub(crate) syntax: SyntaxNode, +} +impl ForPortionOf { + #[inline] + pub fn alias(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn expr(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn name_ref(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn l_paren_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::L_PAREN) + } + #[inline] + pub fn r_paren_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::R_PAREN) + } + #[inline] + pub fn for_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::FOR_KW) + } + #[inline] + pub fn from_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::FROM_KW) + } + #[inline] + pub fn of_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::OF_KW) + } + #[inline] + pub fn portion_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::PORTION_KW) + } + #[inline] + pub fn to_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::TO_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ForProvider { pub(crate) syntax: SyntaxNode, @@ -17040,6 +17091,10 @@ impl Update { support::child(&self.syntax) } #[inline] + pub fn for_portion_of(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn from_clause(&self) -> Option { support::child(&self.syntax) } @@ -23971,6 +24026,24 @@ impl AstNode for FilterClause { &self.syntax } } +impl AstNode for ForPortionOf { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::FOR_PORTION_OF + } + #[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 ForProvider { #[inline] fn can_cast(kind: SyntaxKind) -> bool { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 5ef98e05..2f81179b 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -1359,12 +1359,17 @@ Update = WithClause? 'update' RelationName + ForPortionOf? Alias? SetClause FromClause? WhereClause? ReturningClause? +ForPortionOf = + 'for' 'portion' 'of' NameRef + ('from' Expr 'to' Expr | '(' Expr ')') + ReturningClause = 'returning' ReturningOptionList @@ -1380,7 +1385,9 @@ ReturningOption = Delete = WithClause? - 'delete' 'from' RelationName Alias? + 'delete' 'from' RelationName + ForPortionOf? + Alias? UsingClause? (WhereClause | WhereCurrentOf)? ReturningClause? diff --git a/crates/xtask/src/sync_regression_suite.rs b/crates/xtask/src/sync_regression_suite.rs index e02e30d8..47ec39c0 100644 --- a/crates/xtask/src/sync_regression_suite.rs +++ b/crates/xtask/src/sync_regression_suite.rs @@ -21,6 +21,10 @@ const START_END_MARKERS: &[(&str, &str)] = &[ ("-- Multiple VALUES clause", "\tINSERT VALUES (1,1), (2,2);"), ("-- SELECT query for INSERT", "\tINSERT SELECT (1, 1);"), ("-- UPDATE tablename", "\tUPDATE target SET balance = 0;"), + ( + "-- TO is used for the bound but not the INTERVAL:", + " WHERE id = '[1,2)';", + ), ]; const IGNORED_LINES: &[&str] = &[ diff --git a/postgres/kwlist.h b/postgres/kwlist.h index 7f6ca61a..ab6ce56a 100644 --- a/postgres/kwlist.h +++ b/postgres/kwlist.h @@ -1,7 +1,7 @@ // synced from: -// commit: ab697307dd0f0b4f6c6671421d4dd0bc20f176cb -// committed at: 2026-03-18T02:04:10Z -// file: https://github.com/postgres/postgres/blob/ab697307dd0f0b4f6c6671421d4dd0bc20f176cb/src/include/parser/kwlist.h +// commit: effaa464afd355e8927bf430cfe6a0ddd2ee5695 +// committed at: 2026-04-02T12:39:57Z +// file: https://github.com/postgres/postgres/blob/effaa464afd355e8927bf430cfe6a0ddd2ee5695/src/include/parser/kwlist.h // // update via: // cargo xtask sync-kwlist @@ -361,6 +361,7 @@ PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("portion", PORTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("preceding", PRECEDING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("precision", PRECISION, COL_NAME_KEYWORD, AS_LABEL) diff --git a/postgres/plpgsql/plpgsql_simple.sql b/postgres/plpgsql/plpgsql_simple.sql index 72d8afe4..d64e7918 100644 --- a/postgres/plpgsql/plpgsql_simple.sql +++ b/postgres/plpgsql/plpgsql_simple.sql @@ -114,3 +114,20 @@ begin fetch p_CurData into val; raise notice 'val = %', val; end; $$; + +-- We now optimize "SELECT simple-expr INTO var" using the simple-expression +-- logic. Verify that error reporting works the same as it did before. + +do $$ +declare x bigint := 2^30; y int; +begin + -- overflow during assignment step does not get an extra context line + select x*x into y; +end $$; + +do $$ +declare x bigint := 2^30; y int; +begin + -- overflow during expression evaluation step does get an extra context line + select x*x*x into y; +end $$; diff --git a/postgres/regression_suite/copy.sql b/postgres/regression_suite/copy.sql index c4567853..d676eba4 100644 --- a/postgres/regression_suite/copy.sql +++ b/postgres/regression_suite/copy.sql @@ -82,6 +82,107 @@ copy copytest3 from stdin csv header; copy copytest3 to stdout csv header; +--- test copying in JSON mode with various styles +copy (select 1 union all select 2) to stdout with (format json); +copy (select 1 as foo union all select 2) to stdout with (format json); +copy (values (1), (2)) TO stdout with (format json); +copy (select 1 union all select 2) to stdout with (format json, force_array true); +copy (values (1), (2)) TO stdout with (format json, force_array true); +copy copytest to stdout json; +copy copytest to stdout (format json); +copy (select * from copytest) to stdout (format json); + +-- all of the following should yield error +copy copytest to stdout (format json, delimiter '|'); +copy copytest to stdout (format json, null '\N'); +copy copytest to stdout (format json, default '|'); +copy copytest to stdout (format json, header); +copy copytest to stdout (format json, header 1); +copy copytest to stdout (format json, quote '"'); +copy copytest to stdout (format json, escape '"'); +copy copytest to stdout (format json, force_quote *); +copy copytest to stdout (format json, force_not_null *); +copy copytest to stdout (format json, force_null *); +copy copytest to stdout (format json, on_error ignore); +copy copytest to stdout (format json, reject_limit 1); +copy copytest from stdin(format json); +-- -- all of the above should yield error + +-- column list with json format +copy copytest (style, test, filler) to stdout (format json); + +-- should fail: force_array requires json format +copy copytest to stdout (format csv, force_array true); + +-- force_array variants +copy copytest to stdout (format json, force_array); +copy copytest(style, test) to stdout (format json, force_array true); +copy copytest to stdout (format json, force_array false); + +-- force_array with empty result set +copy (select 1 where false) to stdout (format json, force_array); + +-- column list with diverse data types +create temp table copyjsontest_types ( + id int, + js json, + jsb jsonb, + arr int[], + n numeric(10,2), + b boolean, + ts timestamp, + t text); + +insert into copyjsontest_types values +(1, '{"a":1}', '{"b":2}', '{1,2,3}', 3.14, true, + '2024-01-15 10:30:00', 'hello'), +(2, '[1,null,"x"]', '{"nested":{"k":"v"}}', '{4,5}', -99.99, false, + '2024-06-30 23:59:59', 'world'), +(3, 'null', 'null', '{}', null, null, null, null); + +-- full table +copy copyjsontest_types to stdout (format json); + +-- column subsets exercising each type +copy copyjsontest_types (id, js, jsb) to stdout (format json); +copy copyjsontest_types (id, arr, n, b) to stdout (format json); +copy copyjsontest_types (jsb, t) to stdout (format json); +copy copyjsontest_types (id, ts) to stdout (format json); + +-- single column: json and jsonb +copy copyjsontest_types (js) to stdout (format json); +copy copyjsontest_types (jsb) to stdout (format json); + +drop table copyjsontest_types; + +-- embedded escaped characters +create temp table copyjsontest ( + id bigserial, + f1 text, + f2 timestamptz); + +insert into copyjsontest + select g.i, + CASE WHEN g.i % 2 = 0 THEN + 'line with '' in it: ' || g.i::text + ELSE + 'line with " in it: ' || g.i::text + END, + 'Mon Feb 10 17:32:01 1997 PST' + from generate_series(1,5) as g(i); + +insert into copyjsontest (f1) values +(E'aaa\"bbb'::text), +(E'aaa\\bbb'::text), +(E'aaa\/bbb'::text), +(E'aaa\bbbb'::text), +(E'aaa\fbbb'::text), +(E'aaa\nbbb'::text), +(E'aaa\rbbb'::text), +(E'aaa\tbbb'::text); + +copy copyjsontest to stdout json; + create temp table copytest4 ( c1 int, "colname with tab: " text); diff --git a/postgres/regression_suite/for_portion_of.sql b/postgres/regression_suite/for_portion_of.sql new file mode 100644 index 00000000..cf7bcf3a --- /dev/null +++ b/postgres/regression_suite/for_portion_of.sql @@ -0,0 +1,1368 @@ +-- Tests for UPDATE/DELETE FOR PORTION OF + +SET datestyle TO ISO, YMD; + +-- Works on non-PK columns +CREATE TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text NOT NULL +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-02,2020-01-01)', 'one'); + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + SET name = 'one^1'; +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2019-01-15' TO '2019-01-20'; +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +-- With a table alias with AS + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2019-02-01' TO '2019-02-03' AS t + SET name = 'one^2'; + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2019-02-03' TO '2019-02-04' AS t; + +-- With a table alias without AS + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2019-02-04' TO '2019-02-05' t + SET name = 'one^3'; + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2019-02-05' TO '2019-02-06' t; + +-- UPDATE with FROM + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2019-03-01' to '2019-03-02' + SET name = 'one^4' + FROM (SELECT '[1,2)'::int4range) AS t2(id) + WHERE for_portion_of_test.id = t2.id; + +-- DELETE with USING + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2019-03-02' TO '2019-03-03' + USING (SELECT '[1,2)'::int4range) AS t2(id) + WHERE for_portion_of_test.id = t2.id; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +-- Works on more than one range +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range, + valid1_at daterange, + valid2_at daterange, + name text NOT NULL +); +INSERT INTO for_portion_of_test (id, valid1_at, valid2_at, name) VALUES + ('[1,2)', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', 'one'); + +UPDATE for_portion_of_test + FOR PORTION OF valid1_at FROM '2018-01-15' TO NULL + SET name = 'foo'; +SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; + +UPDATE for_portion_of_test + FOR PORTION OF valid2_at FROM '2018-01-15' TO NULL + SET name = 'bar'; +SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; + +DELETE FROM for_portion_of_test + FOR PORTION OF valid1_at FROM '2018-01-20' TO NULL; +SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; + +DELETE FROM for_portion_of_test + FOR PORTION OF valid2_at FROM '2018-01-20' TO NULL; +SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; + +-- Test with NULLs in the scalar/range key columns. +-- This won't happen if there is a PRIMARY KEY or UNIQUE constraint +-- but FOR PORTION OF shouldn't require that. +DROP TABLE for_portion_of_test; +CREATE UNLOGGED TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', NULL, '1 null'), + ('[1,2)', '(,)', '1 unbounded'), + ('[1,2)', 'empty', '1 empty'), + (NULL, NULL, NULL), + (NULL, daterange('2018-01-01', '2019-01-01'), 'null key'); +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + SET name = 'NULL to NULL'; +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +DROP TABLE for_portion_of_test; + +-- +-- UPDATE tests +-- + +CREATE TABLE for_portion_of_test ( + id int4range NOT NULL, + valid_at daterange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-02,2018-02-03)', 'one'), + ('[1,2)', '[2018-02-03,2018-03-03)', 'one'), + ('[1,2)', '[2018-03-03,2018-04-04)', 'one'), + ('[2,3)', '[2018-01-01,2018-01-05)', 'two'), + ('[3,4)', '[2018-01-01,)', 'three'), + ('[4,5)', '(,2018-04-01)', 'four'), + ('[5,6)', '(,)', 'five') + ; +-- \set QUIET false + +-- Updating with a missing column fails +UPDATE for_portion_of_test + FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL + SET name = 'foo' + WHERE id = '[5,6)'; + +-- Updating the range fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO NULL + SET valid_at = '[1990-01-01,1999-01-01)' + WHERE id = '[5,6)'; + +-- The wrong start type fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM 1 TO '2020-01-01' + SET name = 'nope' + WHERE id = '[3,4)'; + +-- The wrong end type fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2000-01-01' TO 4 + SET name = 'nope' + WHERE id = '[3,4)'; + +-- Updating with timestamps reversed fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' + SET name = 'three^1' + WHERE id = '[3,4)'; + +-- Updating with a subquery fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01' + SET name = 'nope' + WHERE id = '[3,4)'; + +-- Updating with a column fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM lower(valid_at) TO NULL + SET name = 'nope' + WHERE id = '[3,4)'; + +-- Updating with timestamps equal does nothing +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' + SET name = 'three^0' + WHERE id = '[3,4)'; + +-- Updating a finite/open portion with a finite/open target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO NULL + SET name = 'three^1' + WHERE id = '[3,4)'; +SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; + +-- Updating a finite/open portion with an open/finite target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-03-01' + SET name = 'three^2' + WHERE id = '[3,4)'; +SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; + +-- Updating an open/finite portion with an open/finite target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-02-01' + SET name = 'four^1' + WHERE id = '[4,5)'; +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + +-- Updating an open/finite portion with a finite/open target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2017-01-01' TO NULL + SET name = 'four^2' + WHERE id = '[4,5)'; +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + +-- Updating a finite/finite portion with an exact fit +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01' + SET name = 'four^3' + WHERE id = '[4,5)'; +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + +-- Updating an enclosed span +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + SET name = 'two^2' + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at; + +-- Updating an open/open portion with a finite/finite target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01' + SET name = 'five^1' + WHERE id = '[5,6)'; +SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at; + +-- Updating an enclosed span with separate protruding spans +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01' + SET name = 'five^2' + WHERE id = '[5,6)'; +SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at; + +-- Updating multiple enclosed spans +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + SET name = 'one^2' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Updating with a direct target +UPDATE for_portion_of_test + FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-15')) + SET name = 'one^3' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Updating with a direct target, coerced from a string +UPDATE for_portion_of_test + FOR PORTION OF valid_at ('[2018-03-15,2018-03-17)') + SET name = 'one^3' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Updating with a direct target of the wrong range subtype fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at (int4range(1, 4)) + SET name = 'one^3' + WHERE id = '[1,2)'; + +-- Updating with a direct target of a non-rangetype fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at (4) + SET name = 'one^3' + WHERE id = '[1,2)'; + +-- Updating with a direct target of NULL fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at (NULL) + SET name = 'one^3' + WHERE id = '[1,2)'; + +-- Updating with a direct target of empty does nothing +UPDATE for_portion_of_test + FOR PORTION OF valid_at ('empty') + SET name = 'one^3' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Updating the non-range part of the PK: +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-15' TO NULL + SET id = '[6,7)' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id IN ('[1,2)', '[6,7)') ORDER BY id, valid_at; + +-- UPDATE with no WHERE clause +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2030-01-01' TO NULL + SET name = name || '*'; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; +-- \set QUIET true + +-- Updating with a shift/reduce conflict +-- (requires a tsrange column) +CREATE UNLOGGED TABLE for_portion_of_test2 ( + id int4range, + valid_at tsrange, + name text +); +INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES + ('[1,2)', '[2000-01-01,2020-01-01)', 'one'); +-- updates [2011-03-01 01:02:00, 2012-01-01) (note 2 minutes) +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at + FROM '2011-03-01'::timestamp + INTERVAL '1:02:03' HOUR TO MINUTE + TO '2012-01-01' + SET name = 'one^1' + WHERE id = '[1,2)'; + +-- -- TO is used for the bound but not the INTERVAL: +-- -- syntax error +-- UPDATE for_portion_of_test2 +-- FOR PORTION OF valid_at +-- FROM '2013-03-01'::timestamp + INTERVAL '1:02:03' HOUR +-- TO '2014-01-01' +-- SET name = 'one^2' +-- WHERE id = '[1,2)'; + +-- adding parens fixes it +-- updates [2015-03-01 01:00:00, 2016-01-01) (no minutes) +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at + FROM ('2015-03-01'::timestamp + INTERVAL '1:02:03' HOUR) + TO '2016-01-01' + SET name = 'one^3' + WHERE id = '[1,2)'; + +SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at; +DROP TABLE for_portion_of_test2; + +-- UPDATE FOR PORTION OF in a CTE: +-- The outer query sees the table how it was before the updates, +-- and with no leftovers yet, +-- but it also sees the new values via the RETURNING clause. +-- (We test RETURNING more directly, without a CTE, below.) +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[10,11)', '[2018-01-01,2020-01-01)', 'ten'); +WITH update_apr AS ( + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-05-01' + SET name = 'Apr 2018' + WHERE id = '[10,11)' + RETURNING id, valid_at, name +) +SELECT * + FROM for_portion_of_test AS t, update_apr + WHERE t.id = update_apr.id; +SELECT * FROM for_portion_of_test WHERE id = '[10,11)' ORDER BY id, valid_at; + +-- UPDATE FOR PORTION OF with current_date +-- (We take care not to make the expectation depend on the timestamp.) +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[99,100)', '[2000-01-01,)', 'foo'); +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM current_date TO null + SET name = 'bar' + WHERE id = '[99,100)'; +SELECT name, lower(valid_at) FROM for_portion_of_test + WHERE id = '[99,100)' AND valid_at @> current_date - 1; +SELECT name, upper(valid_at) FROM for_portion_of_test + WHERE id = '[99,100)' AND valid_at @> current_date + 1; + +-- UPDATE FOR PORTION OF with clock_timestamp() +-- fails because the function is volatile: +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM clock_timestamp()::date TO null + SET name = 'baz' + WHERE id = '[99,100)'; + +-- clean up: +DELETE FROM for_portion_of_test WHERE id = '[99,100)'; + +-- Not visible to UPDATE: +-- Tuples updated/inserted within the CTE are not visible to the main query yet, +-- but neither are old tuples the CTE changed: +-- (This is the same behavior as without FOR PORTION OF.) +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[11,12)', '[2018-01-01,2020-01-01)', 'eleven'); +WITH update_apr AS ( + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-05-01' + SET name = 'Apr 2018' + WHERE id = '[11,12)' + RETURNING id, valid_at, name +) +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + AS t + SET name = 'May 2018' + FROM update_apr AS j + WHERE t.id = j.id; +SELECT * FROM for_portion_of_test WHERE id = '[11,12)' ORDER BY id, valid_at; +DELETE FROM for_portion_of_test WHERE id IN ('[10,11)', '[11,12)'); + +-- UPDATE FOR PORTION OF in a PL/pgSQL function +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[10,11)', '[2018-01-01,2020-01-01)', 'ten'); +CREATE FUNCTION fpo_update(_id int4range, _target_from date, _target_til date) +RETURNS void LANGUAGE plpgsql AS +$$ +BEGIN + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM $2 TO $3 + SET name = concat(_target_from::text, ' to ', _target_til::text) + WHERE id = $1; +END; +$$; +SELECT fpo_update('[10,11)', '2015-01-01', '2019-01-01'); +SELECT * FROM for_portion_of_test WHERE id = '[10,11)'; + +-- UPDATE FOR PORTION OF in a compiled SQL function +CREATE FUNCTION fpo_update() +RETURNS text +BEGIN ATOMIC + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + SET name = 'one^1' + RETURNING name; +END; +-- \sf+ fpo_update() +CREATE OR REPLACE function fpo_update() +RETURNS text +BEGIN ATOMIC + UPDATE for_portion_of_test + FOR PORTION OF valid_at (daterange('2018-01-15', '2020-01-01') * daterange('2019-01-01', '2022-01-01')) + SET name = 'one^1' + RETURNING name; +END; +-- \sf+ fpo_update() +DROP FUNCTION fpo_update(); + +DROP TABLE for_portion_of_test; + +-- +-- DELETE tests +-- + +CREATE TABLE for_portion_of_test ( + id int4range NOT NULL, + valid_at daterange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-02,2018-02-03)', 'one'), + ('[1,2)', '[2018-02-03,2018-03-03)', 'one'), + ('[1,2)', '[2018-03-03,2018-04-04)', 'one'), + ('[2,3)', '[2018-01-01,2018-01-05)', 'two'), + ('[3,4)', '[2018-01-01,)', 'three'), + ('[4,5)', '(,2018-04-01)', 'four'), + ('[5,6)', '(,)', 'five'), + ('[6,7)', '[2018-01-01,)', 'six'), + ('[7,8)', '(,2018-04-01)', 'seven'), + ('[8,9)', '[2018-01-02,2018-02-03)', 'eight'), + ('[8,9)', '[2018-02-03,2018-03-03)', 'eight'), + ('[8,9)', '[2018-03-03,2018-04-04)', 'eight') + ; +-- \set QUIET false + +-- Deleting with a missing column fails +DELETE FROM for_portion_of_test + FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL + WHERE id = '[5,6)'; + +-- The wrong start type fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM 1 TO '2020-01-01' + WHERE id = '[3,4)'; + +-- The wrong end type fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2000-01-01' TO 4 + WHERE id = '[3,4)'; + +-- Deleting with timestamps reversed fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' + WHERE id = '[3,4)'; + +-- Deleting with a subquery fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01' + WHERE id = '[3,4)'; + +-- Deleting with a column fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM lower(valid_at) TO NULL + WHERE id = '[3,4)'; + +-- Deleting with timestamps equal does nothing +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' + WHERE id = '[3,4)'; + +-- Deleting a finite/open portion with a finite/open target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO NULL + WHERE id = '[3,4)'; +SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; + +-- Deleting a finite/open portion with an open/finite target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-03-01' + WHERE id = '[6,7)'; +SELECT * FROM for_portion_of_test WHERE id = '[6,7)' ORDER BY id, valid_at; + +-- Deleting an open/finite portion with an open/finite target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-02-01' + WHERE id = '[4,5)'; +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + +-- Deleting an open/finite portion with a finite/open target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2017-01-01' TO NULL + WHERE id = '[7,8)'; +SELECT * FROM for_portion_of_test WHERE id = '[7,8)' ORDER BY id, valid_at; + +-- Deleting a finite/finite portion with an exact fit +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-04-01' + WHERE id = '[4,5)'; +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + +-- Deleting an enclosed span +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at; + +-- Deleting an open/open portion with a finite/finite target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01' + WHERE id = '[5,6)'; +SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at; + +-- Deleting an enclosed span with separate protruding spans +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-03' TO '2018-03-03' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Deleting multiple enclosed spans +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + WHERE id = '[8,9)'; +SELECT * FROM for_portion_of_test WHERE id = '[8,9)' ORDER BY id, valid_at; + +-- Deleting with a direct target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-15')) + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Deleting with a direct target, coerced from a string +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at ('[2018-03-15,2018-03-17)') + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Deleting with a direct target of the wrong range subtype fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (int4range(1, 4)) + WHERE id = '[1,2)'; + +-- Deleting with a direct target of a non-rangetype fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (4) + WHERE id = '[1,2)'; + +-- Deleting with a direct target of NULL fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (NULL) + WHERE id = '[1,2)'; + +-- Deleting with a direct target of empty does nothing +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at ('empty') + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- DELETE with no WHERE clause +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2030-01-01' TO NULL; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; +-- \set QUIET true + +-- UPDATE ... RETURNING returns only the updated values +-- (not the inserted side values, which are added by a separate "statement"): +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15' + SET name = 'three^3' + WHERE id = '[3,4)' + RETURNING *; + +-- UPDATE ... RETURNING supports NEW and OLD valid_at +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-10' TO '2018-02-20' + SET name = 'three^4' + WHERE id = '[3,4)' + RETURNING OLD.name, NEW.name, OLD.valid_at, NEW.valid_at; + +-- DELETE FOR PORTION OF with current_date +-- (We take care not to make the expectation depend on the timestamp.) +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[99,100)', '[2000-01-01,)', 'foo'); +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM current_date TO null + WHERE id = '[99,100)'; +SELECT name, lower(valid_at) FROM for_portion_of_test + WHERE id = '[99,100)' AND valid_at @> current_date - 1; +SELECT name, upper(valid_at) FROM for_portion_of_test + WHERE id = '[99,100)' AND valid_at @> current_date + 1; + +-- DELETE FOR PORTION OF with clock_timestamp() +-- fails because the function is volatile: +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM clock_timestamp()::date TO null + WHERE id = '[99,100)'; + +-- clean up: +DELETE FROM for_portion_of_test WHERE id = '[99,100)'; + +-- DELETE ... RETURNING returns the deleted values, regardless of bounds +-- (not the inserted side values, which are added by a separate "statement"): +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-02' TO '2018-02-03' + WHERE id = '[3,4)' + RETURNING *; + +-- DELETE FOR PORTION OF in a PL/pgSQL function +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[10,11)', '[2018-01-01,2020-01-01)', 'ten'); +CREATE FUNCTION fpo_delete(_id int4range, _target_from date, _target_til date) +RETURNS void LANGUAGE plpgsql AS +$$ +BEGIN + DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM $2 TO $3 + WHERE id = $1; +END; +$$; +SELECT fpo_delete('[10,11)', '2015-01-01', '2019-01-01'); +SELECT * FROM for_portion_of_test WHERE id = '[10,11)'; +DELETE FROM for_portion_of_test WHERE id IN ('[10,11)'); + +-- DELETE FOR PORTION OF in a compiled SQL function +CREATE FUNCTION fpo_delete() +RETURNS text +BEGIN ATOMIC + DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + RETURNING name; +END; +-- \sf+ fpo_delete() +CREATE OR REPLACE function fpo_delete() +RETURNS text +BEGIN ATOMIC + DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (daterange('2018-01-15', '2020-01-01') * daterange('2019-01-01', '2022-01-01')) + RETURNING name; +END; +-- \sf+ fpo_delete() +DROP FUNCTION fpo_delete(); + + +-- test domains and CHECK constraints + +-- With a domain on a rangetype +CREATE DOMAIN daterange_d AS daterange CHECK (upper(VALUE) <> '2005-05-05'::date); +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at daterange_d, + name text +); +INSERT INTO for_portion_of_test2 VALUES + (1, '[2000-01-01,2020-01-01)', 'one'), + (2, '[2000-01-01,2020-01-01)', 'two'); +INSERT INTO for_portion_of_test2 VALUES + (1, '[2000-01-01,2005-05-05)', 'nope'); +-- UPDATE works: +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2010-01-01' TO '2010-01-05' + SET name = 'one^1' + WHERE id = 1; +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('[2010-01-07,2010-01-09)') + SET name = 'one^2' + WHERE id = 1; +SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; +-- The target is allowed to violate the domain: +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05' + SET name = 'miss' + WHERE id = -1; +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('[1999-01-01,2005-05-05)') + SET name = 'miss' + WHERE id = -1; +-- test the updated row violating the domain +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05' + SET name = 'one^3' + WHERE id = 1; +-- test inserts violating the domain +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2005-05-05' TO '2010-01-01' + SET name = 'one^3' + WHERE id = 1; +-- test updated row violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (upper(valid_at) <> '2001-01-11'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-11' + SET name = 'one^3' + WHERE id = 1; +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +-- test inserts violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (lower(valid_at) <> '2002-02-02'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-02-02' + SET name = 'one^3' + WHERE id = 1; +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; +-- DELETE works: +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '2010-01-01' TO '2010-01-05' + WHERE id = 2; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('[2010-01-07,2010-01-09)') + WHERE id = 2; +SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; +-- The target is allowed to violate the domain: +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05' + WHERE id = -1; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('[1999-01-01,2005-05-05)') + WHERE id = -1; +-- test inserts violating the domain +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '2005-05-05' TO '2010-01-01' + WHERE id = 2; +-- test inserts violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (lower(valid_at) <> '2002-02-02'); +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-02-02' + WHERE id = 2; +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; +DROP TABLE for_portion_of_test2; + +-- With a domain on a multirangetype +CREATE FUNCTION multirange_lowers(mr anymultirange) RETURNS anyarray LANGUAGE sql AS $$ + SELECT array_agg(lower(r)) FROM UNNEST(mr) u(r); +$$; +CREATE FUNCTION multirange_uppers(mr anymultirange) RETURNS anyarray LANGUAGE sql AS $$ + SELECT array_agg(upper(r)) FROM UNNEST(mr) u(r); +$$; +CREATE DOMAIN datemultirange_d AS datemultirange CHECK (NOT '2005-05-05'::date = ANY (multirange_uppers(VALUE))); +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at datemultirange_d, + name text +); +INSERT INTO for_portion_of_test2 VALUES + (1, '{[2000-01-01,2020-01-01)}', 'one'), + (2, '{[2000-01-01,2020-01-01)}', 'two'); +INSERT INTO for_portion_of_test2 VALUES + (1, '{[2000-01-01,2005-05-05)}', 'nope'); +-- UPDATE works: +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2010-01-07,2010-01-09)}') + SET name = 'one^2' + WHERE id = 1; +SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; +-- The target is allowed to violate the domain: +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}') + SET name = 'miss' + WHERE id = -1; +-- test the updated row violating the domain +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}') + SET name = 'one^3' + WHERE id = 1; +-- test inserts violating the domain +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2005-05-05,2010-01-01)}') + SET name = 'one^3' + WHERE id = 1; +-- test updated row violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (upper(valid_at) <> '2001-01-11'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2000-01-01,2001-01-11)}') + SET name = 'one^3' + WHERE id = 1; +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +-- test inserts violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (NOT '2002-02-02'::date = ANY (multirange_lowers(valid_at))); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2001-01-01,2002-02-02)}') + SET name = 'one^3' + WHERE id = 1; +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; +-- DELETE works: +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[2010-01-07,2010-01-09)}') + WHERE id = 2; +SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; +-- The target is allowed to violate the domain: +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}') + WHERE id = -1; +-- test inserts violating the domain +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[2005-05-05,2010-01-01)}') + WHERE id = 2; +-- test inserts violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (NOT '2002-02-02'::date = ANY (multirange_lowers(valid_at))); +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[2001-01-01,2002-02-02)}') + WHERE id = 2; +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; +DROP TABLE for_portion_of_test2; + +-- test on non-range/multirange columns + +-- With a direct target and a scalar column +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at date, + name text +); +INSERT INTO for_portion_of_test2 VALUES (1, '2020-01-01', 'one'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('2010-01-01') + SET name = 'one^1'; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('2010-01-01'); +DROP TABLE for_portion_of_test2; + +-- With a direct target and a non-{,multi}range gistable column without overlaps +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at point, + name text +); +INSERT INTO for_portion_of_test2 VALUES (1, '0,0', 'one'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('1,1') + SET name = 'one^1'; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('1,1'); +DROP TABLE for_portion_of_test2; + +-- With a direct target and a non-{,multi}range column with overlaps +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at box, + name text +); +INSERT INTO for_portion_of_test2 VALUES (1, '0,0,4,4', 'one'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('1,1,2,2') + SET name = 'one^1'; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('1,1,2,2'); +DROP TABLE for_portion_of_test2; + +-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows + +CREATE FUNCTION dump_trigger() +RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + RAISE NOTICE '%: % % %:', + TG_NAME, TG_WHEN, TG_OP, TG_LEVEL; + + IF TG_ARGV[0] THEN + RAISE NOTICE ' old: %', (SELECT string_agg(old_table::text, '\n ') FROM old_table); + ELSE + RAISE NOTICE ' old: %', OLD.valid_at; + END IF; + IF TG_ARGV[1] THEN + RAISE NOTICE ' new: %', (SELECT string_agg(new_table::text, '\n ') FROM new_table); + ELSE + RAISE NOTICE ' new: %', NEW.valid_at; + END IF; + + IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + RETURN OLD; + END IF; +END; +$$; + +-- statement triggers: + +CREATE TRIGGER fpo_before_stmt + BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_insert_stmt + AFTER INSERT ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_update_stmt + AFTER UPDATE ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_delete_stmt + AFTER DELETE ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + +-- row triggers: + +CREATE TRIGGER fpo_before_row + BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_insert_row + AFTER INSERT ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01' + SET name = 'five^3' + WHERE id = '[5,6)'; + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01' + WHERE id = '[5,6)'; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +-- Triggers with a custom transition table name: + +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO for_portion_of_test VALUES ('[1,2)', '[2018-01-01,2020-01-01)', 'one'); + +-- statement triggers: + +CREATE TRIGGER fpo_before_stmt + BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_insert_stmt + AFTER INSERT ON for_portion_of_test + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, true); + +CREATE TRIGGER fpo_after_update_stmt + AFTER UPDATE ON for_portion_of_test + REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, true); + +CREATE TRIGGER fpo_after_delete_stmt + AFTER DELETE ON for_portion_of_test + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, false); + +-- row triggers: + +CREATE TRIGGER fpo_before_row + BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_insert_row + AFTER INSERT ON for_portion_of_test + REFERENCING NEW TABLE AS new_table + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, true); + +CREATE TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, true); + +CREATE TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + REFERENCING OLD TABLE AS old_table + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, false); + +BEGIN; +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + SET name = '2018-01-15_to_2019-01-01'; +ROLLBACK; + +BEGIN; +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-01-21'; +ROLLBACK; + +BEGIN; +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-01-02' + SET name = 'NULL_to_2018-01-01'; +ROLLBACK; + +-- Deferred triggers +-- (must be CONSTRAINT triggers thus AFTER ROW with no transition tables) + +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-01,2020-01-01)', 'one'); + +CREATE CONSTRAINT TRIGGER fpo_after_insert_row + AFTER INSERT ON for_portion_of_test + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE CONSTRAINT TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE CONSTRAINT TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +BEGIN; +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + SET name = '2018-01-15_to_2019-01-01'; +COMMIT; + +BEGIN; +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-01-21'; +COMMIT; + +BEGIN; +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-01-02' + SET name = 'NULL_to_2018-01-01'; +COMMIT; + +SELECT * FROM for_portion_of_test; + +-- test FOR PORTION OF from triggers during FOR PORTION OF: + +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-01,2020-01-01)', 'one'), + ('[2,3)', '[2018-01-01,2020-01-01)', 'two'), + ('[3,4)', '[2018-01-01,2020-01-01)', 'three'), + ('[4,5)', '[2018-01-01,2020-01-01)', 'four'); + +CREATE FUNCTION trg_fpo_update() +RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + IF pg_trigger_depth() = 1 THEN + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-03-01' + SET name = CONCAT(name, '^') + WHERE id = OLD.id; + END IF; + RETURN CASE WHEN 'TG_OP' = 'DELETE' THEN OLD ELSE NEW END; +END; +$$; + +CREATE FUNCTION trg_fpo_delete() +RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + IF pg_trigger_depth() = 1 THEN + DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-03-01' TO '2018-04-01' + WHERE id = OLD.id; + END IF; + RETURN CASE WHEN 'TG_OP' = 'DELETE' THEN OLD ELSE NEW END; +END; +$$; + +-- UPDATE FOR PORTION OF from a trigger fired by UPDATE FOR PORTION OF + +CREATE TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update(); + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + SET name = CONCAT(name, '*') + WHERE id = '[1,2)'; + +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +DROP TRIGGER fpo_after_update_row ON for_portion_of_test; + +-- UPDATE FOR PORTION OF from a trigger fired by DELETE FOR PORTION OF + +CREATE TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update(); + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + WHERE id = '[2,3)'; + +SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at; + +DROP TRIGGER fpo_after_delete_row ON for_portion_of_test; + +-- DELETE FOR PORTION OF from a trigger fired by UPDATE FOR PORTION OF + +CREATE TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete(); + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + SET name = CONCAT(name, '*') + WHERE id = '[3,4)'; + +SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; + +DROP TRIGGER fpo_after_update_row ON for_portion_of_test; + +-- DELETE FOR PORTION OF from a trigger fired by DELETE FOR PORTION OF + +CREATE TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete(); + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + WHERE id = '[4,5)'; + +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + +DROP TRIGGER fpo_after_delete_row ON for_portion_of_test; + +-- Test with multiranges + +CREATE TABLE for_portion_of_test2 ( + id int4range NOT NULL, + valid_at datemultirange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES + ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03)'), daterange('2018-02-04', '2018-03-03')), 'one'), + ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04)')), 'one'), + ('[2,3)', datemultirange(daterange('2018-01-01', '2018-05-01)')), 'two'), + ('[3,4)', datemultirange(daterange('2018-01-01', null)), 'three'); + ; + +-- Updating with FROM/TO +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01' + SET name = 'one^1' + WHERE id = '[1,2)'; +-- Updating with multirange +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-10', '2018-02-10'), daterange('2018-03-05', '2018-05-01'))) + SET name = 'one^1' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at; +-- Updating with string coercion +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2018-03-05,2018-03-10)}') + SET name = 'one^2' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at; +-- Updating with the wrong range subtype fails +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[1,4)}'::int4multirange) + SET name = 'one^3' + WHERE id = '[1,2)'; +-- Updating with a non-multirangetype fails +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at (4) + SET name = 'one^3' + WHERE id = '[1,2)'; +-- Updating with NULL fails +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at (NULL) + SET name = 'one^3' + WHERE id = '[1,2)'; +-- Updating with empty does nothing +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{}') + SET name = 'one^3' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at; + +-- Deleting with FROM/TO +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01' + SET name = 'one^1' + WHERE id = '[1,2)'; +-- Deleting with multirange +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-15', '2018-02-15'), daterange('2018-03-01', '2018-03-15'))) + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[2,3)' ORDER BY valid_at; +-- Deleting with string coercion +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[2018-03-05,2018-03-20)}') + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[2,3)' ORDER BY valid_at; +-- Deleting with the wrong range subtype fails +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[1,4)}'::int4multirange) + WHERE id = '[2,3)'; +-- Deleting with a non-multirangetype fails +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at (4) + WHERE id = '[2,3)'; +-- Deleting with NULL fails +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at (NULL) + WHERE id = '[2,3)'; +-- Deleting with empty does nothing +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{}') + WHERE id = '[2,3)'; + +SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at; + +DROP TABLE for_portion_of_test2; + +-- Test with a custom range type + +CREATE TYPE mydaterange AS range(subtype=date); + +CREATE TABLE for_portion_of_test2 ( + id int4range NOT NULL, + valid_at mydaterange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-02,2018-02-03)', 'one'), + ('[1,2)', '[2018-02-03,2018-03-03)', 'one'), + ('[1,2)', '[2018-03-03,2018-04-04)', 'one'), + ('[2,3)', '[2018-01-01,2018-05-01)', 'two'), + ('[3,4)', '[2018-01-01,)', 'three'); + ; + +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2018-01-10' TO '2018-02-10' + SET name = 'one^1' + WHERE id = '[1,2)'; + +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '2018-01-15' TO '2018-02-15' + WHERE id = '[2,3)'; + +SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at; + +DROP TABLE for_portion_of_test2; +DROP TYPE mydaterange; + +-- Test FOR PORTION OF against a partitioned table. +-- temporal_partitioned_1 has the same attnums as the root +-- temporal_partitioned_3 has the different attnums from the root +-- temporal_partitioned_5 has the different attnums too, but reversed + +CREATE TABLE temporal_partitioned ( + id int4range, + valid_at daterange, + name text, + CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +) PARTITION BY LIST (id); +CREATE TABLE temporal_partitioned_1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)'); +CREATE TABLE temporal_partitioned_3 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)'); +CREATE TABLE temporal_partitioned_5 PARTITION OF temporal_partitioned FOR VALUES IN ('[5,6)', '[6,7)'); + +ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_3; +ALTER TABLE temporal_partitioned_3 DROP COLUMN id, DROP COLUMN valid_at; +ALTER TABLE temporal_partitioned_3 ADD COLUMN id int4range NOT NULL, ADD COLUMN valid_at daterange NOT NULL; +ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_3 FOR VALUES IN ('[3,4)', '[4,5)'); + +ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_5; +ALTER TABLE temporal_partitioned_5 DROP COLUMN id, DROP COLUMN valid_at; +ALTER TABLE temporal_partitioned_5 ADD COLUMN valid_at daterange NOT NULL, ADD COLUMN id int4range NOT NULL; +ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_5 FOR VALUES IN ('[5,6)', '[6,7)'); + +INSERT INTO temporal_partitioned (id, valid_at, name) VALUES + ('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one'), + ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three'), + ('[5,6)', daterange('2000-01-01', '2010-01-01'), 'five'); + +SELECT * FROM temporal_partitioned; + +-- Update without moving within partition 1 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01' + SET name = 'one^1' + WHERE id = '[1,2)'; + +-- Update without moving within partition 3 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01' + SET name = 'three^1' + WHERE id = '[3,4)'; + +-- Update without moving within partition 5 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01' + SET name = 'five^1' + WHERE id = '[5,6)'; + +-- Move from partition 1 to partition 3 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01' + SET name = 'one^2', + id = '[4,5)' + WHERE id = '[1,2)'; + +-- Move from partition 3 to partition 1 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01' + SET name = 'three^2', + id = '[2,3)' + WHERE id = '[3,4)'; + +-- Move from partition 5 to partition 3 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01' + SET name = 'five^2', + id = '[3,4)' + WHERE id = '[5,6)'; + +-- Update all partitions at once (each with leftovers) + +SELECT * FROM temporal_partitioned ORDER BY id, valid_at; +SELECT * FROM temporal_partitioned_1 ORDER BY id, valid_at; +SELECT * FROM temporal_partitioned_3 ORDER BY id, valid_at; +SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at; + +DROP TABLE temporal_partitioned; + +RESET datestyle; diff --git a/postgres/regression_suite/foreign_data.sql b/postgres/regression_suite/foreign_data.sql index 82832661..53ad67d3 100644 --- a/postgres/regression_suite/foreign_data.sql +++ b/postgres/regression_suite/foreign_data.sql @@ -13,6 +13,11 @@ CREATE FUNCTION test_fdw_handler() AS 'regresslib', 'test_fdw_handler' LANGUAGE C; +CREATE FUNCTION test_fdw_connection(oid, oid, internal) + RETURNS text + AS 'regresslib', 'test_fdw_connection' + LANGUAGE C; + -- Clean up in case a prior regression run failed -- Suppress NOTICE messages when roles don't exist @@ -68,9 +73,11 @@ CREATE FOREIGN DATA WRAPPER test_fdw HANDLER invalid_fdw_handler; -- ERROR CREATE FOREIGN DATA WRAPPER test_fdw HANDLER test_fdw_handler HANDLER invalid_fdw_handler; -- ERROR CREATE FOREIGN DATA WRAPPER test_fdw HANDLER test_fdw_handler; --- should preserve dependency on test_fdw_handler +-- should preserve dependencies on test_fdw_handler and test_fdw_connection +ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; ALTER FOREIGN DATA WRAPPER test_fdw VALIDATOR postgresql_fdw_validator; DROP FUNCTION test_fdw_handler(); -- ERROR +DROP FUNCTION test_fdw_connection(oid, oid, internal); -- ERROR DROP FOREIGN DATA WRAPPER test_fdw; diff --git a/postgres/regression_suite/foreign_key.sql b/postgres/regression_suite/foreign_key.sql index 1ecbb2c1..4318a209 100644 --- a/postgres/regression_suite/foreign_key.sql +++ b/postgres/regression_suite/foreign_key.sql @@ -242,6 +242,70 @@ SELECT * FROM PKTABLE; DROP TABLE FKTABLE; DROP TABLE PKTABLE; +-- +-- Check RLS +-- +CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text ); +CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE, ftest2 int ); + +-- Insert test data into PKTABLE +INSERT INTO PKTABLE VALUES (1, 'Test1'); +INSERT INTO PKTABLE VALUES (2, 'Test2'); +INSERT INTO PKTABLE VALUES (3, 'Test3'); + +-- Grant privileges on PKTABLE/FKTABLE to user regress_foreign_key_user +CREATE USER regress_foreign_key_user NOLOGIN; +GRANT SELECT ON PKTABLE TO regress_foreign_key_user; +GRANT SELECT, INSERT ON FKTABLE TO regress_foreign_key_user; + +-- Enable RLS on PKTABLE and Create policies +ALTER TABLE PKTABLE ENABLE ROW LEVEL SECURITY; +CREATE POLICY pktable_view_odd_policy ON PKTABLE TO regress_foreign_key_user USING (ptest1 % 2 = 1); + +ALTER TABLE PKTABLE OWNER to regress_foreign_key_user; + +SET ROLE regress_foreign_key_user; + +INSERT INTO FKTABLE VALUES (3, 5); +INSERT INTO FKTABLE VALUES (2, 5); -- success, REFERENCES are not subject to row security + +RESET ROLE; + +DROP TABLE FKTABLE; +DROP TABLE PKTABLE; +DROP USER regress_foreign_key_user; + +-- +-- Check ACL +-- +CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text ); +CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE, ftest2 int ); + +-- Insert test data into PKTABLE +INSERT INTO PKTABLE VALUES (1, 'Test1'); +INSERT INTO PKTABLE VALUES (2, 'Test2'); +INSERT INTO PKTABLE VALUES (3, 'Test3'); + +-- Grant usage on PKTABLE to user regress_foreign_key_user +CREATE USER regress_foreign_key_user NOLOGIN; +GRANT SELECT ON PKTABLE TO regress_foreign_key_user; + +ALTER TABLE PKTABLE OWNER to regress_foreign_key_user; + +-- Inserting into FKTABLE should work +INSERT INTO FKTABLE VALUES (3, 5); + +-- Revoke usage on PKTABLE from user regress_foreign_key_user +REVOKE SELECT ON PKTABLE FROM regress_foreign_key_user; + +-- Inserting into FKTABLE should fail +INSERT INTO FKTABLE VALUES (2, 6); + +DROP TABLE FKTABLE; +DROP TABLE PKTABLE; + +DROP USER regress_foreign_key_user; + -- -- Check initial check upon ALTER TABLE -- @@ -784,6 +848,43 @@ INSERT INTO fktable VALUES (500, 1000); COMMIT; +-- Check that the existing FK trigger is both deferrable and initially deferred +SELECT conname, tgrelid::regclass as tgrel, + regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype, + tgdeferrable, tginitdeferred +FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid) +WHERE conrelid = 'fktable'::regclass AND conname = 'fktable_fk_fkey' +ORDER BY tgrelid, tgtype; + +-- Changing the constraint to NOT ENFORCED drops the associated FK triggers +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED; +SELECT conname, tgrelid::regclass as tgrel, + regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype, + tgdeferrable, tginitdeferred +FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid) +WHERE conrelid = 'fktable'::regclass AND conname = 'fktable_fk_fkey' +ORDER BY tgrelid, tgtype; + +-- Changing it back to ENFORCED will recreate the necessary FK triggers +-- that are deferrable and initially deferred +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey ENFORCED; +SELECT conname, tgrelid::regclass as tgrel, + regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype, + tgdeferrable, tginitdeferred +FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid) +WHERE conrelid = 'fktable'::regclass AND conname = 'fktable_fk_fkey' +ORDER BY tgrelid, tgtype; + +-- Verify that a deferrable, initially deferred foreign key still works +-- as expected after being set to NOT ENFORCED and then re-enabled +BEGIN; + +-- doesn't match PK, but no error yet +INSERT INTO fktable VALUES (2, 20); + +-- should catch error from INSERT at commit +COMMIT; + DROP TABLE fktable, pktable; -- tricky behavior: according to SQL99, if a deferred constraint is set diff --git a/postgres/regression_suite/generated_stored.sql b/postgres/regression_suite/generated_stored.sql index 010512c3..5f70d914 100644 --- a/postgres/regression_suite/generated_stored.sql +++ b/postgres/regression_suite/generated_stored.sql @@ -344,13 +344,47 @@ CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0 INSERT INTO gtest21a (a) VALUES (1); -- ok INSERT INTO gtest21a (a) VALUES (0); -- violates constraint -CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED); +-- also check with table constraint syntax +CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED, CONSTRAINT cc NOT NULL b); +INSERT INTO gtest21ax (a) VALUES (0); -- violates constraint +INSERT INTO gtest21ax (a) VALUES (1); --ok +-- SET EXPRESSION supports not null constraint +ALTER TABLE gtest21ax ALTER COLUMN b SET EXPRESSION AS (nullif(a, 1)); --error +DROP TABLE gtest21ax; + +CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED); +ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b; +INSERT INTO gtest21ax (a) VALUES (0); -- violates constraint +DROP TABLE gtest21ax; + +CREATE TABLE gtest21b (a int, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED); ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL; INSERT INTO gtest21b (a) VALUES (1); -- ok -INSERT INTO gtest21b (a) VALUES (0); -- violates constraint +INSERT INTO gtest21b (a) VALUES (2), (0); -- violates constraint +INSERT INTO gtest21b (a) VALUES (NULL); -- error ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL; INSERT INTO gtest21b (a) VALUES (0); -- ok now +-- not-null constraint with partitioned table +CREATE TABLE gtestnn_parent ( + f1 int, + f2 bigint, + f3 bigint GENERATED ALWAYS AS (nullif(f1, 1) + nullif(f2, 10)) STORED NOT NULL +) PARTITION BY RANGE (f1); +CREATE TABLE gtestnn_child PARTITION OF gtestnn_parent FOR VALUES FROM (1) TO (5); +CREATE TABLE gtestnn_childdef PARTITION OF gtestnn_parent default; +-- check the error messages +INSERT INTO gtestnn_parent VALUES (2, 2, default), (3, 5, default), (14, 12, default); -- ok +INSERT INTO gtestnn_parent VALUES (1, 2, default); -- error +INSERT INTO gtestnn_parent VALUES (2, 10, default); -- error +ALTER TABLE gtestnn_parent ALTER COLUMN f3 SET EXPRESSION AS (nullif(f1, 2) + nullif(f2, 11)); -- error +INSERT INTO gtestnn_parent VALUES (10, 11, default); -- ok +SELECT * FROM gtestnn_parent ORDER BY f1, f2, f3; +-- test ALTER TABLE ADD COLUMN +ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 14) + nullif(f2, 10)) STORED; -- error +ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 13) + nullif(f2, 5)) STORED; -- error +ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 4) + nullif(f2, 6)) STORED; -- ok + -- index constraints CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) STORED UNIQUE); INSERT INTO gtest22a VALUES (2); @@ -423,6 +457,11 @@ CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS A INSERT INTO gtest24r (a) VALUES (4); -- ok INSERT INTO gtest24r (a) VALUES (6); -- error +CREATE TABLE gtest24at (a int PRIMARY KEY); +ALTER TABLE gtest24at ADD COLUMN b gtestdomain1 GENERATED ALWAYS AS (a * 2) STORED; -- ok +CREATE TABLE gtest24ata (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED); +ALTER TABLE gtest24ata ALTER COLUMN b TYPE gtestdomain1; -- ok + CREATE DOMAIN gtestdomainnn AS int CHECK (VALUE IS NOT NULL); CREATE TABLE gtest24nn (a int, b gtestdomainnn GENERATED ALWAYS AS (a * 2) STORED); INSERT INTO gtest24nn (a) VALUES (4); -- ok @@ -537,6 +576,14 @@ ALTER TABLE gtest27 ALTER COLUMN x TYPE numeric; SELECT * FROM gtest27; ALTER TABLE gtest27 ALTER COLUMN x TYPE boolean USING x <> 0; -- error ALTER TABLE gtest27 ALTER COLUMN x DROP DEFAULT; -- error +-- test not-null checking during table rewrite +INSERT INTO gtest27 (a, b) VALUES (NULL, NULL); +ALTER TABLE gtest27 + DROP COLUMN x, + ALTER COLUMN a TYPE bigint, + ALTER COLUMN b TYPE bigint, + ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) STORED NOT NULL; -- error +DELETE FROM gtest27 WHERE a IS NULL AND b IS NULL; -- It's possible to alter the column types this way: ALTER TABLE gtest27 DROP COLUMN x, diff --git a/postgres/regression_suite/generated_virtual.sql b/postgres/regression_suite/generated_virtual.sql index 4c61ed8b..f9e2c7c8 100644 --- a/postgres/regression_suite/generated_virtual.sql +++ b/postgres/regression_suite/generated_virtual.sql @@ -256,7 +256,7 @@ CREATE TYPE double_int as (a int, b int); CREATE TABLE gtest4 ( a int, b double_int GENERATED ALWAYS AS ((a * 2, a * 3)) VIRTUAL -); +); -- fails, user-defined type --INSERT INTO gtest4 VALUES (1), (6); --SELECT * FROM gtest4; @@ -473,9 +473,6 @@ CREATE TABLE gtest24nn (a int, b gtestdomainnn GENERATED ALWAYS AS (a * 2) VIRTU --INSERT INTO gtest24nn (a) VALUES (4); -- ok --INSERT INTO gtest24nn (a) VALUES (NULL); -- error --- using user-defined type not yet supported -CREATE TABLE gtest24xxx (a gtestdomain1, b gtestdomain1, c int GENERATED ALWAYS AS (greatest(a, b)) VIRTUAL); -- error - -- typed tables (currently not supported) CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint); CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) VIRTUAL); @@ -825,6 +822,9 @@ SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT -- these tests are specific to generated_virtual.sql -- +-- using user-defined type not yet supported +CREATE TABLE gtest24xxx (a gtestdomain1, b gtestdomain1, c int GENERATED ALWAYS AS (greatest(a, b)) VIRTUAL); -- error + create table gtest32 ( a int primary key, b int generated always as (a * 2), diff --git a/postgres/regression_suite/graph_table.sql b/postgres/regression_suite/graph_table.sql index de9319cc..5254c79f 100644 --- a/postgres/regression_suite/graph_table.sql +++ b/postgres/regression_suite/graph_table.sql @@ -151,9 +151,17 @@ SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[IS customer_orders | c SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)->(o IS orders) COLUMNS (c.name, o.ordered_when)) ORDER BY 1; -- lateral test -CREATE TABLE x1 (a int, b text); +-- Use table with a column name same as a property in the property graph so as +-- to test resolution preferences. Property references are preferred over +-- lateral table references. +CREATE TABLE x1 (a int, address text); INSERT INTO x1 VALUES (1, 'one'), (2, 'two'); SELECT * FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US' AND c.customer_id = x1.a)-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name, c.customer_id AS cid)); +SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (x1 IS customers WHERE x1.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (x1.name AS customer_name, x1.customer_id AS cid, o.order_id)) g; +-- non-local property references are not allowed, even if a lateral column +-- reference is available +SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (x1 IS customers)-[IS customer_orders]->(o IS orders WHERE o.order_id = x1.a) COLUMNS (x1.name AS customer_name, x1.customer_id AS cid, o.order_id)) g; -- error +SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US' AND c.customer_id = x1.a)-[IS customer_orders]->(x1 IS orders) COLUMNS (c.name AS customer_name, c.customer_id AS cid, x1.order_id)) g; -- error DROP TABLE x1; CREATE TABLE v1 ( @@ -289,6 +297,16 @@ SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS el1 | vl1)-[conn]->(dest) COLUMNS (c SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.*)); -- star anywhere else is not allowed as a property reference SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.* IS NOT NULL)-[IS customer_orders]->(o IS orders) COLUMNS (c.name)); +-- consecutive element patterns with same kind +SELECT * FROM GRAPH_TABLE (g1 MATCH ()() COLUMNS (1 as one)); +SELECT * FROM GRAPH_TABLE (g1 MATCH -> COLUMNS (1 AS one)); +SELECT * FROM GRAPH_TABLE (g1 MATCH ()-[]- COLUMNS (1 AS one)); +SELECT * FROM GRAPH_TABLE (g1 MATCH ()-> ->() COLUMNS (1 AS one)); +-- non-local element variable reference with element patterns without variable +-- names +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[WHERE a.vprop1 = 10]->(c) COLUMNS (a.vname AS aname, c.vname AS cname)); +SELECT * FROM GRAPH_TABLE (g1 MATCH (WHERE b.eprop1 = 10001)-[b]->(c) COLUMNS (b.ename AS bname, c.vname AS cname)); + -- select all the properties across all the labels associated with a given type -- of graph element diff --git a/postgres/regression_suite/graph_table_rls.sql b/postgres/regression_suite/graph_table_rls.sql index 044bc27c..5837eac4 100644 --- a/postgres/regression_suite/graph_table_rls.sql +++ b/postgres/regression_suite/graph_table_rls.sql @@ -360,4 +360,19 @@ EXECUTE graph_rls_query; -- error -- Clean up DEALLOCATE graph_rls_query; --- leave objects behind for pg_upgrade/pg_dump tests +-- leave as many objects behind for pg_upgrade/pg_dump tests as possible. The +-- pg_dump test only dumps the regression database, not the global objects like +-- users and roles. Reassign ownership of all objects to superuser and drop +-- users and roles created in this test. Policies can not be reassigned, so drop +-- them explicitly. +RESET SESSION AUTHORIZATION; +REASSIGN OWNED BY regress_graph_rls_alice TO current_user; +DROP USER regress_graph_rls_alice; +DROP USER regress_graph_rls_bob; +DROP USER regress_graph_rls_carol; +DROP USER regress_graph_rls_dave; +DROP USER regress_graph_rls_exempt_user; +DROP POLICY p3 ON document_people; +DROP POLICY p4 ON document_people; +DROP ROLE regress_graph_rls_group1; +DROP ROLE regress_graph_rls_group2; diff --git a/postgres/regression_suite/hash_index.sql b/postgres/regression_suite/hash_index.sql index 769fbebb..8855b481 100644 --- a/postgres/regression_suite/hash_index.sql +++ b/postgres/regression_suite/hash_index.sql @@ -314,6 +314,23 @@ INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 50) as i; CHECKPOINT; VACUUM hash_cleanup_heap; +-- Test cleanup of dead index tuples on single page with INSERT. +TRUNCATE hash_cleanup_heap; +INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 1000) as i; +-- This relies on a rollbacked INSERT instead of a DELETE to make the creation +-- of the dead tuples concurrent-safe. +BEGIN; +INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 500) as i; +ROLLBACK; +SET enable_seqscan = off; +SET enable_bitmapscan = off; +SELECT count(*) FROM hash_cleanup_heap WHERE keycol = 1; +-- This query checks the hash index pages for dead tuples where the data +-- is inserted, and performs a local VACUUM on a single page. +INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 200) as i; +RESET enable_seqscan; +RESET enable_bitmapscan; + -- Clean up. DROP TABLE hash_cleanup_heap; diff --git a/postgres/regression_suite/indexing.sql b/postgres/regression_suite/indexing.sql index 466655f5..0258e900 100644 --- a/postgres/regression_suite/indexing.sql +++ b/postgres/regression_suite/indexing.sql @@ -934,3 +934,15 @@ reindex index test_pg_index_toast_index; drop index test_pg_index_toast_index; drop function test_pg_index_toast_func; drop table test_pg_index_toast_table; + +-- test creation of an index involving a whole-row expression +create table test_pg_wholerow_index (a int, b text, c numeric); +create or replace function row_image(test_pg_wholerow_index) + returns test_pg_wholerow_index as $$select $1$$ language sql immutable; +insert into test_pg_wholerow_index values (1, 'multiplication', 1.0); +create index row_image_index + on test_pg_wholerow_index ((row_image(test_pg_wholerow_index))); +insert into test_pg_wholerow_index values (2, 'addition', 0); +drop index row_image_index; +drop function row_image(test_pg_wholerow_index); +drop table test_pg_wholerow_index; diff --git a/postgres/regression_suite/join.sql b/postgres/regression_suite/join.sql index 7f3449e2..30b479dd 100644 --- a/postgres/regression_suite/join.sql +++ b/postgres/regression_suite/join.sql @@ -3156,6 +3156,12 @@ SELECT * FROM (SELECT n2.a FROM sj n1, sj n2 WHERE n1.a <> n2.a) q0, sl WHERE q0.a = 1; +-- Do not forget to replace relid in bare Var join clause (bug #19435) +ALTER TABLE sl ADD COLUMN bool_col boolean; +EXPLAIN (COSTS OFF) +SELECT 1 AS c1 FROM sl sl1 LEFT JOIN (sl AS sl2 NATURAL JOIN sl AS sl3) + ON sl2.bool_col LEFT JOIN sl AS sl4 ON sl2.bool_col; + -- Check optimization disabling if it will violate special join conditions. -- Two identical joined relations satisfies self join removal conditions but -- stay in different special join infos. @@ -3717,14 +3723,16 @@ set enable_nestloop to 0; set enable_hashjoin to 0; set enable_sort to 0; +-- we need additional data to get the partial indexes to be preferred +insert into j1 select 2, i from generate_series(1, 100) i; +insert into j2 select 1, i from generate_series(2, 100) i; +analyze j1; +analyze j2; + -- create indexes that will be preferred over the PKs to perform the join create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1; create index j2_id1_idx on j2 (id1) where id1 % 1000 = 1; --- need an additional row in j2, if we want j2_id1_idx to be preferred -insert into j2 values(1,2); -analyze j2; - explain (costs off) select * from j1 inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2 where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1; diff --git a/postgres/regression_suite/memoize.sql b/postgres/regression_suite/memoize.sql index 8d1cdd69..e39bbb65 100644 --- a/postgres/regression_suite/memoize.sql +++ b/postgres/regression_suite/memoize.sql @@ -139,6 +139,7 @@ INSERT INTO flt VALUES('-0.0'::float),('+0.0'::float); ANALYZE flt; SET enable_seqscan TO off; +SET enable_material TO off; -- Ensure memoize operates in logical mode SELECT explain_memoize(' @@ -218,6 +219,7 @@ WHERE unique1 < 3 WHERE t0.ten = t1.twenty AND t0.two <> t2.four OFFSET 0); RESET enable_seqscan; +RESET enable_material; RESET enable_mergejoin; RESET work_mem; RESET hash_mem_multiplier; diff --git a/postgres/regression_suite/privileges.sql b/postgres/regression_suite/privileges.sql index b3ed9cfe..edd3dbb8 100644 --- a/postgres/regression_suite/privileges.sql +++ b/postgres/regression_suite/privileges.sql @@ -783,6 +783,33 @@ UPDATE errtst SET a = 'aaaa', b = NULL WHERE a = 'aaa'; SET SESSION AUTHORIZATION regress_priv_user1; DROP TABLE errtst; +-- test column-level privileges on the range used in FOR PORTION OF +SET SESSION AUTHORIZATION regress_priv_user1; +CREATE TABLE t1 ( + c1 int4range, + valid_at tsrange, + CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS) +); +-- UPDATE requires select permission on the valid_at column (but not update): +GRANT SELECT (c1) ON t1 TO regress_priv_user2; +GRANT UPDATE (c1) ON t1 TO regress_priv_user2; +GRANT SELECT (c1, valid_at) ON t1 TO regress_priv_user3; +GRANT UPDATE (c1) ON t1 TO regress_priv_user3; +SET SESSION AUTHORIZATION regress_priv_user2; +UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; +SET SESSION AUTHORIZATION regress_priv_user3; +UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; +SET SESSION AUTHORIZATION regress_priv_user1; +-- DELETE requires select permission on the valid_at column: +GRANT DELETE ON t1 TO regress_priv_user2; +GRANT DELETE ON t1 TO regress_priv_user3; +SET SESSION AUTHORIZATION regress_priv_user2; +DELETE FROM t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01'; +SET SESSION AUTHORIZATION regress_priv_user3; +DELETE FROM t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01'; +SET SESSION AUTHORIZATION regress_priv_user1; +DROP TABLE t1; + -- test column-level privileges when involved with DELETE SET SESSION AUTHORIZATION regress_priv_user1; ALTER TABLE atest6 ADD COLUMN three integer; diff --git a/postgres/regression_suite/publication.sql b/postgres/regression_suite/publication.sql index 19414e50..1b183865 100644 --- a/postgres/regression_suite/publication.sql +++ b/postgres/regression_suite/publication.sql @@ -106,26 +106,75 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall -- \dRp+ testpub_foralltables --------------------------------------------- --- EXCEPT TABLE tests for normal tables +-- EXCEPT clause tests for normal tables --------------------------------------------- SET client_min_messages = 'ERROR'; --- Specify table list in the EXCEPT TABLE clause of a FOR ALL TABLES publication -CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2); +CREATE TABLE testpub_tbl3 (id serial primary key, data text); +-- Specify table list in the EXCEPT clause of a FOR ALL TABLES publication +CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT (TABLE testpub_tbl1, testpub_tbl2, TABLE testpub_tbl3); -- \dRp+ testpub_foralltables_excepttable --- Specify table in the EXCEPT TABLE clause of a FOR ALL TABLES publication -CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT TABLE (testpub_tbl1); +-- Specify table in the EXCEPT clause of a FOR ALL TABLES publication +CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (TABLE testpub_tbl1); -- \dRp+ testpub_foralltables_excepttable1 -- Check that the table description shows the publications where it is listed --- in the EXCEPT TABLE clause +-- in the EXCEPT clause -- \d testpub_tbl1 +-- fail - first table in the EXCEPT list should use TABLE keyword +CREATE PUBLICATION testpub_foralltables_excepttable2 FOR ALL TABLES EXCEPT (testpub_tbl1, testpub_tbl2); + +--------------------------------------------- +-- SET ALL TABLES/SEQUENCES +--------------------------------------------- +-- Replace the existing table list in the EXCEPT clause (testpub_tbl1, +-- testpub_tbl2, testpub_tbl3) with table (testpub_tbl2). +ALTER PUBLICATION testpub_foralltables_excepttable SET ALL TABLES EXCEPT (TABLE testpub_tbl2); +-- \dRp+ testpub_foralltables_excepttable + +-- Replace the existing table list in the EXCEPT clause (testpub_tbl2) with a +-- table list containing (testpub_tbl1, testpub_tbl2, testpub_tbl3). +ALTER PUBLICATION testpub_foralltables_excepttable SET ALL TABLES EXCEPT (TABLE testpub_tbl1, testpub_tbl2, TABLE testpub_tbl3); +-- \dRp+ testpub_foralltables_excepttable + +-- Clear the table list in the EXCEPT clause, making the publication include all +-- tables. +ALTER PUBLICATION testpub_foralltables_excepttable SET ALL TABLES; +-- \dRp+ testpub_foralltables_excepttable + +-- Create an empty publication for subsequent tests. +CREATE PUBLICATION testpub_forall_tbls_seqs; + +-- Enable both puballtables and puballsequences +ALTER PUBLICATION testpub_forall_tbls_seqs SET ALL TABLES, ALL SEQUENCES; +-- \dRp+ testpub_forall_tbls_seqs + +-- Explicitly test that SET ALL TABLES resets puballsequences to false +-- Result should be: puballtables = true, puballsequences = false +ALTER PUBLICATION testpub_forall_tbls_seqs SET ALL TABLES; +-- \dRp+ testpub_forall_tbls_seqs + +-- Explicitly test that SET ALL SEQUENCES resets puballtables to false +-- Result should be: puballtables = false, puballsequences = true +ALTER PUBLICATION testpub_forall_tbls_seqs SET ALL SEQUENCES; +-- \dRp+ testpub_forall_tbls_seqs + +-- fail - SET ALL TABLES/SEQUENCES is not allowed for a 'FOR TABLE' publication +ALTER PUBLICATION testpub_fortable SET ALL TABLES EXCEPT (TABLE testpub_tbl1); +ALTER PUBLICATION testpub_fortable SET ALL TABLES; +ALTER PUBLICATION testpub_fortable SET ALL SEQUENCES; + +-- fail - SET ALL TABLES/SEQUENCES is not allowed for a schema publication +ALTER PUBLICATION testpub_forschema SET ALL TABLES EXCEPT (TABLE pub_test.testpub_nopk); +ALTER PUBLICATION testpub_forschema SET ALL TABLES; +ALTER PUBLICATION testpub_forschema SET ALL SEQUENCES; RESET client_min_messages; -DROP TABLE testpub_tbl2; -DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1; +DROP TABLE testpub_tbl2, testpub_tbl3; +DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema; +DROP PUBLICATION testpub_forall_tbls_seqs, testpub_foralltables_excepttable, testpub_foralltables_excepttable1; --------------------------------------------- -- Tests for inherited tables, and --- EXCEPT TABLE tests for inherited tables +-- EXCEPT clause tests for inherited tables --------------------------------------------- SET client_min_messages = 'ERROR'; CREATE TABLE testpub_tbl_parent (a int); @@ -134,14 +183,14 @@ CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent; -- \dRp+ testpub3 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent; -- \dRp+ testpub4 --- List the parent table in the EXCEPT TABLE clause (without ONLY or '*') -CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent); +-- List the parent table in the EXCEPT clause (without ONLY or '*') +CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT (TABLE testpub_tbl_parent); -- \dRp+ testpub5 --- EXCEPT with '*': list the table and all its descendants in the EXCEPT TABLE clause -CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *); +-- EXCEPT with '*': list the table and all its descendants in the EXCEPT clause +CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT (TABLE testpub_tbl_parent *); -- \dRp+ testpub6 --- EXCEPT with ONLY: list the table in the EXCEPT TABLE clause, but not its descendants -CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent); +-- EXCEPT with ONLY: list the table in the EXCEPT clause, but not its descendants +CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT (TABLE ONLY testpub_tbl_parent); -- \dRp+ testpub7 RESET client_min_messages; @@ -149,20 +198,20 @@ DROP TABLE testpub_tbl_parent, testpub_tbl_child; DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7; --------------------------------------------- --- EXCEPT TABLE tests for partitioned tables +-- EXCEPT clause tests for partitioned tables --------------------------------------------- SET client_min_messages = 'ERROR'; CREATE TABLE testpub_root(a int) PARTITION BY RANGE(a); CREATE TABLE testpub_part1 PARTITION OF testpub_root FOR VALUES FROM (0) TO (100); -CREATE PUBLICATION testpub8 FOR ALL TABLES EXCEPT TABLE (testpub_root); +CREATE PUBLICATION testpub8 FOR ALL TABLES EXCEPT (TABLE testpub_root); -- \dRp+ testpub8; -- \d testpub_part1 -- \d testpub_root -CREATE PUBLICATION testpub9 FOR ALL TABLES EXCEPT TABLE (testpub_part1); +CREATE PUBLICATION testpub9 FOR ALL TABLES EXCEPT (TABLE testpub_part1); CREATE TABLE tab_main (a int) PARTITION BY RANGE(a); -- Attaching a partition is not allowed if the partitioned table appears in a --- publication's EXCEPT TABLE clause. +-- publication's EXCEPT clause. ALTER TABLE tab_main ATTACH PARTITION testpub_root FOR VALUES FROM (0) TO (200); RESET client_min_messages; @@ -991,7 +1040,18 @@ ALTER PUBLICATION testpub4 owner to regress_publication_user2; -- fail ALTER PUBLICATION testpub4 owner to regress_publication_user; -- ok SET ROLE regress_publication_user; -DROP PUBLICATION testpub4; +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION testpub5 FOR ALL TABLES; +RESET client_min_messages; +ALTER PUBLICATION testpub5 OWNER TO regress_publication_user3; +SET ROLE regress_publication_user3; +-- fail - SET ALL TABLES/SEQUENCES on a publication requires superuser privileges +ALTER PUBLICATION testpub5 SET ALL TABLES EXCEPT (TABLE testpub_tbl1); -- fail +ALTER PUBLICATION testpub5 SET ALL TABLES; -- fail +ALTER PUBLICATION testpub5 SET ALL SEQUENCES; -- fail + +SET ROLE regress_publication_user; +DROP PUBLICATION testpub4, testpub5; DROP ROLE regress_publication_user3; REVOKE CREATE ON DATABASE regression FROM regress_publication_user2; diff --git a/postgres/regression_suite/select.sql b/postgres/regression_suite/select.sql index 1d1bf2b9..2dfe88d2 100644 --- a/postgres/regression_suite/select.sql +++ b/postgres/regression_suite/select.sql @@ -221,7 +221,6 @@ SET enable_indexscan TO off; explain (costs off) select unique2 from onek2 where unique2 = 11 and stringu1 < 'B'; select unique2 from onek2 where unique2 = 11 and stringu1 < 'B'; -RESET enable_indexscan; -- check multi-index cases too explain (costs off) select unique1, unique2 from onek2 @@ -233,6 +232,16 @@ select unique1, unique2 from onek2 where (unique2 = 11 and stringu1 < 'B') or unique1 = 0; select unique1, unique2 from onek2 where (unique2 = 11 and stringu1 < 'B') or unique1 = 0; +RESET enable_indexscan; + +-- onek2_u2_prtl should be preferred over this index, but we have to +-- discount the metapage to arrive at that answer +begin; +create index onek2_index_full on onek2 (stringu1, unique2); +explain (costs off) +select unique2 from onek2 + where stringu1 < 'B'::name; +rollback; -- -- Test some corner cases that have been known to confuse the planner diff --git a/postgres/regression_suite/stats.sql b/postgres/regression_suite/stats.sql index 7dbd01c1..7701ae58 100644 --- a/postgres/regression_suite/stats.sql +++ b/postgres/regression_suite/stats.sql @@ -964,4 +964,40 @@ SELECT * FROM check_estimated_rows('SELECT * FROM table_fillfactor'); DROP TABLE table_fillfactor; +-- Test fastpath_exceeded stat +CREATE TABLE part_test (id int) PARTITION BY RANGE (id); + +SELECT pg_stat_reset_shared('lock'); + +-- Create partitions (exceeds number of slots) +DO $$ +DECLARE + max_locks int; +BEGIN + SELECT setting::int INTO max_locks + FROM pg_settings + WHERE name = 'max_locks_per_transaction'; + + FOR i IN 1..(max_locks + 10) LOOP + EXECUTE format( + 'CREATE TABLE part_test_%s PARTITION OF part_test + FOR VALUES FROM (%s) TO (%s)', + i, (i-1)*1000, i*1000 + ); + END LOOP; +END; +$$; + +SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' /* \gset */; + +-- Needs a lock on each partition +SELECT count(*) FROM part_test; + +-- Ensure pending stats are flushed +SELECT pg_stat_force_next_flush(); + +SELECT fastpath_exceeded > 'fastpath_exceeded_before' FROM pg_stat_lock WHERE locktype = 'relation'; + +DROP TABLE part_test; + -- End of Stats Test diff --git a/postgres/regression_suite/stats_ext.sql b/postgres/regression_suite/stats_ext.sql index a9e9e9ed..9bf0d2a2 100644 --- a/postgres/regression_suite/stats_ext.sql +++ b/postgres/regression_suite/stats_ext.sql @@ -28,7 +28,7 @@ end; $$; -- Verify failures -CREATE TABLE ext_stats_test (x text, y int, z int); +CREATE TABLE ext_stats_test (x text, y int, z int, w xid); -- CREATE STATISTICS tst; -- CREATE STATISTICS tst ON a, b; CREATE STATISTICS tst FROM sometab; @@ -56,21 +56,16 @@ DROP FUNCTION tftest; CREATE STATISTICS tst ON (y) FROM ext_stats_test; -- single column reference CREATE STATISTICS tst ON y + z FROM ext_stats_test; -- missing parentheses CREATE STATISTICS tst ON (x, y) FROM ext_stats_test; -- tuple expression -DROP TABLE ext_stats_test; --- statistics on virtual generated column not allowed -CREATE TABLE ext_stats_test1 (x int, y int, z int GENERATED ALWAYS AS (x+y) VIRTUAL, w xid); -CREATE STATISTICS tst on z from ext_stats_test1; -CREATE STATISTICS tst on (z) from ext_stats_test1; -CREATE STATISTICS tst on (z+1) from ext_stats_test1; -CREATE STATISTICS tst (ndistinct) ON z from ext_stats_test1; -- statistics on system column not allowed -CREATE STATISTICS tst on tableoid from ext_stats_test1; -CREATE STATISTICS tst on (tableoid) from ext_stats_test1; -CREATE STATISTICS tst on (tableoid::int+1) from ext_stats_test1; -CREATE STATISTICS tst (ndistinct) ON xmin from ext_stats_test1; --- statistics without a less-than operator not supported -CREATE STATISTICS tst (ndistinct) ON w from ext_stats_test1; -DROP TABLE ext_stats_test1; +CREATE STATISTICS tst on tableoid from ext_stats_test; +CREATE STATISTICS tst on (tableoid) from ext_stats_test; +CREATE STATISTICS tst on (tableoid::int+1) from ext_stats_test; +CREATE STATISTICS tst (ndistinct) ON xmin from ext_stats_test; +-- statistics kinds are not allowed with univariate statistics +CREATE STATISTICS tst (ndistinct) ON (y + z) FROM ext_stats_test; +-- multivariate statistics without a less-than operator not supported +CREATE STATISTICS tst (ndistinct) ON x, w from ext_stats_test; +DROP TABLE ext_stats_test; -- Ensure stats are dropped sanely, and test IF NOT EXISTS while at it CREATE TABLE ab1 (a INTEGER, b INTEGER, c INTEGER); @@ -1584,6 +1579,35 @@ SELECT c0 FROM ONLY expr_stats_incompatible_test WHERE DROP TABLE expr_stats_incompatible_test; +-- multivariate statistics on virtual generated columns +CREATE TABLE virtual_gen_stats (a int, b int, c int GENERATED ALWAYS AS (2*a), d int GENERATED ALWAYS AS (a+b), w xid GENERATED ALWAYS AS (a::text::xid)); +INSERT INTO virtual_gen_stats SELECT mod(i,10), mod(i,10) FROM generate_series(1,100) s(i); +ANALYZE virtual_gen_stats; + +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0 AND (3*b) = 0'); +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE d = 0 AND (d-2*a) = 0'); + +CREATE STATISTICS virtual_gen_stats_1 (mcv) ON c, (3*b), d, (d-2*a) FROM virtual_gen_stats; +ANALYZE virtual_gen_stats; + +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0 AND (3*b) = 0'); +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE d = 0 AND (d-2*a) = 0'); + +-- univariate statistics on individual virtual generated columns +DROP STATISTICS virtual_gen_stats_1; + +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0'); +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE w = 0'); + +CREATE STATISTICS virtual_gen_stats_single ON c FROM virtual_gen_stats; +CREATE STATISTICS virtual_gen_stats_single_without_less_than ON w FROM virtual_gen_stats; +ANALYZE virtual_gen_stats; + +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0'); +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE w = 0'); + +DROP TABLE virtual_gen_stats; + -- Permission tests. Users should not be able to see specific data values in -- the extended statistics, if they lack permission to see those values in -- the underlying table. diff --git a/postgres/regression_suite/strings.sql b/postgres/regression_suite/strings.sql index 13a07d72..01163ddc 100644 --- a/postgres/regression_suite/strings.sql +++ b/postgres/regression_suite/strings.sql @@ -835,10 +835,65 @@ SELECT encode('\x01'::bytea, 'invalid'); -- error SELECT decode('00', 'invalid'); -- error -- --- base64url encoding/decoding +-- base32hex encoding/decoding -- SET bytea_output TO hex; +SELECT encode('', 'base32hex'); -- '' +SELECT encode('\x11', 'base32hex'); -- '24======' +SELECT encode('\x1122', 'base32hex'); -- '24H0====' +SELECT encode('\x112233', 'base32hex'); -- '24H36===' +SELECT encode('\x11223344', 'base32hex'); -- '24H36H0=' +SELECT encode('\x1122334455', 'base32hex'); -- '24H36H2L' +SELECT encode('\x112233445566', 'base32hex'); -- '24H36H2LCO======' + +SELECT decode('', 'base32hex'); -- '' +SELECT decode('24======', 'base32hex'); -- \x11 +SELECT decode('24H0====', 'base32hex'); -- \x1122 +SELECT decode('24H36===', 'base32hex'); -- \x112233 +SELECT decode('24H36H0=', 'base32hex'); -- \x11223344 +SELECT decode('24H36H2L', 'base32hex'); -- \x1122334455 +SELECT decode('24H36H2LCO======', 'base32hex'); -- \x112233445566 + +SELECT decode('24h36h2lco', 'base32hex'); -- OK, the encoding is case-insensitive + +-- Tests for decoding unpadded base32hex strings. Padding '=' are optional. +SELECT decode('24', 'base32hex'); +SELECT decode('24H', 'base32hex'); +SELECT decode('24H36', 'base32hex'); +SELECT decode('24H36H0', 'base32hex'); + +SELECT decode('2', 'base32hex'); -- \x, 5 bits isn't enough for a byte, so nothing is emitted +SELECT decode('11=', 'base32hex'); -- OK, non-zero padding bits are accepted (consistent with base64) + +SELECT decode('2=', 'base32hex'); -- error +SELECT decode('=', 'base32hex'); -- error +SELECT decode('W', 'base32hex'); -- error +SELECT decode('24H36H0=24', 'base32hex'); -- error + +-- Check round-trip capability of base32hex encoding for multiple random UUIDs. +DO $$ +DECLARE + v1 uuid; + v2 uuid; +BEGIN + FOR i IN 1..10 LOOP + v1 := gen_random_uuid(); + v2 := decode(encode(v1::bytea, 'base32hex'), 'base32hex')::uuid; + + IF v1 != v2 THEN + RAISE EXCEPTION 'base32hex encoding round-trip failed, expected % got %', v1, v2; + END IF; + END LOOP; + RAISE NOTICE 'OK'; +END; +$$; + + +-- +-- base64url encoding/decoding +-- + -- Simple encoding/decoding SELECT encode('\x69b73eff', 'base64url'); -- abc-_w SELECT decode('abc-_w', 'base64url'); -- \x69b73eff diff --git a/postgres/regression_suite/subscription.sql b/postgres/regression_suite/subscription.sql index 5e766ef5..c9d9e47a 100644 --- a/postgres/regression_suite/subscription.sql +++ b/postgres/regression_suite/subscription.sql @@ -138,6 +138,14 @@ SET SESSION AUTHORIZATION regress_subscription_user; REVOKE CREATE ON DATABASE REGRESSION FROM regress_subscription_user3; DROP SERVER test_server; + +-- fail, FDW is dependent +DROP FUNCTION test_fdw_connection(oid, oid, internal); +-- warn +ALTER FOREIGN DATA WRAPPER test_fdw NO CONNECTION; + +DROP FUNCTION test_fdw_connection(oid, oid, internal); + DROP FOREIGN DATA WRAPPER test_fdw; -- fail - invalid connection string during ALTER diff --git a/postgres/regression_suite/updatable_views.sql b/postgres/regression_suite/updatable_views.sql index 2cf9a584..0259f4fe 100644 --- a/postgres/regression_suite/updatable_views.sql +++ b/postgres/regression_suite/updatable_views.sql @@ -1889,6 +1889,20 @@ select * from uv_iocu_tab; drop view uv_iocu_view; drop table uv_iocu_tab; +-- Check UPDATE FOR PORTION OF works correctly +create table uv_fpo_tab (id int4range, valid_at tsrange, b float, + constraint pk_uv_fpo_tab primary key (id, valid_at without overlaps)); +insert into uv_fpo_tab values ('[1,1]', '[2020-01-01, 2030-01-01)', 0); +create view uv_fpo_view as + select b, b+1 as c, valid_at, id, '2.0'::text as two from uv_fpo_tab; + +insert into uv_fpo_view (id, valid_at, b) values ('[1,1]', '[2010-01-01, 2020-01-01)', 1); +select * from uv_fpo_view order by id, valid_at; +update uv_fpo_view for portion of valid_at from '2015-01-01' to '2020-01-01' set b = 2 where id = '[1,1]'; +select * from uv_fpo_view order by id, valid_at; +delete from uv_fpo_view for portion of valid_at from '2017-01-01' to '2022-01-01' where id = '[1,1]'; +select * from uv_fpo_view order by id, valid_at; + -- Test whole-row references to the view create table uv_iocu_tab (a int unique, b text); create view uv_iocu_view as diff --git a/postgres/regression_suite/uuid.sql b/postgres/regression_suite/uuid.sql index f512f4de..8cc2ad40 100644 --- a/postgres/regression_suite/uuid.sql +++ b/postgres/regression_suite/uuid.sql @@ -13,7 +13,8 @@ CREATE TABLE guid2 CREATE TABLE guid3 ( id SERIAL, - guid_field UUID + guid_field UUID, + guid_encoded text GENERATED ALWAYS AS (encode(guid_field::bytea, 'base32hex')) STORED ); -- inserting invalid data tests @@ -116,9 +117,17 @@ INSERT INTO guid1 (guid_field) VALUES (uuidv7(INTERVAL '1 day')); SELECT count(DISTINCT guid_field) FROM guid1; -- test sortability of v7 +INSERT INTO guid3 (guid_field) VALUES ('00000000-0000-0000-0000-000000000000'::uuid); INSERT INTO guid3 (guid_field) SELECT uuidv7() FROM generate_series(1, 10); +INSERT INTO guid3 (guid_field) VALUES ('ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid); SELECT array_agg(id ORDER BY guid_field) FROM guid3; +-- Test base32hex encoding of UUIDs and its lexicographical sorting property. +-- COLLATE "C" is required to prevent buildfarm failures in non-C locales +-- where natural language collations (such as cs_CZ) would break strict +-- byte-wise ordering. +SELECT array_agg(id ORDER BY guid_encoded COLLATE "C") FROM guid3; + -- Check the timestamp offsets for v7. -- -- generate UUIDv7 values with timestamps ranging from 1970 (the Unix epoch year) diff --git a/postgres/regression_suite/without_overlaps.sql b/postgres/regression_suite/without_overlaps.sql index ecea1522..a7c15ea0 100644 --- a/postgres/regression_suite/without_overlaps.sql +++ b/postgres/regression_suite/without_overlaps.sql @@ -2,7 +2,7 @@ -- -- We leave behind several tables to test pg_dump etc: -- temporal_rng, temporal_rng2, --- temporal_fk_rng2rng. +-- temporal_fk_rng2rng, temporal_fk2_rng2rng. SET datestyle TO ISO, YMD; @@ -632,6 +632,20 @@ INSERT INTO temporal3 (id, valid_at, id2, name) ('[1,2)', daterange('2000-01-01', '2010-01-01'), '[7,8)', 'foo'), ('[2,3)', daterange('2000-01-01', '2010-01-01'), '[9,10)', 'bar') ; +UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-05-01' TO '2000-07-01' + SET name = name || '1'; +UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-04-01' TO '2000-06-01' + SET name = name || '2' + WHERE id = '[2,3)'; +SELECT * FROM temporal3 ORDER BY id, valid_at; +-- conflicting id only: +INSERT INTO temporal3 (id, valid_at, id2, name) + VALUES + ('[1,2)', daterange('2005-01-01', '2006-01-01'), '[8,9)', 'foo3'); +-- conflicting id2 only: +INSERT INTO temporal3 (id, valid_at, id2, name) + VALUES + ('[3,4)', daterange('2005-01-01', '2010-01-01'), '[9,10)', 'bar3'); DROP TABLE temporal3; -- @@ -667,9 +681,23 @@ INSERT INTO temporal_partitioned (id, valid_at, name) VALUES ('[1,2)', daterange('2000-01-01', '2000-02-01'), 'one'), ('[1,2)', daterange('2000-02-01', '2000-03-01'), 'one'), ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three'); -SELECT * FROM temporal_partitioned ORDER BY id, valid_at; -SELECT * FROM tp1 ORDER BY id, valid_at; -SELECT * FROM tp2 ORDER BY id, valid_at; +SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15' + SET name = 'one2' + WHERE id = '[1,2)'; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25' + SET id = '[4,5)' + WHERE name = 'one'; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' + SET id = '[2,3)' + WHERE name = 'three'; +DELETE FROM temporal_partitioned + FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15' + WHERE id = '[3,4)'; +SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at; DROP TABLE temporal_partitioned; -- temporal UNIQUE: @@ -685,9 +713,23 @@ INSERT INTO temporal_partitioned (id, valid_at, name) VALUES ('[1,2)', daterange('2000-01-01', '2000-02-01'), 'one'), ('[1,2)', daterange('2000-02-01', '2000-03-01'), 'one'), ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three'); -SELECT * FROM temporal_partitioned ORDER BY id, valid_at; -SELECT * FROM tp1 ORDER BY id, valid_at; -SELECT * FROM tp2 ORDER BY id, valid_at; +SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15' + SET name = 'one2' + WHERE id = '[1,2)'; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25' + SET id = '[4,5)' + WHERE name = 'one'; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' + SET id = '[2,3)' + WHERE name = 'three'; +DELETE FROM temporal_partitioned + FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15' + WHERE id = '[3,4)'; +SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at; DROP TABLE temporal_partitioned; -- ALTER TABLE REPLICA IDENTITY @@ -1291,6 +1333,18 @@ COMMIT; -- changing the scalar part fails: UPDATE temporal_rng SET id = '[7,8)' WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); +-- changing an unreferenced part is okay: +UPDATE temporal_rng + FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03' + SET id = '[7,8)' + WHERE id = '[5,6)'; +-- changing just a part fails: +UPDATE temporal_rng + FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10' + SET id = '[7,8)' + WHERE id = '[5,6)'; +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; -- then delete the objecting FK record and the same PK update succeeds: DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)'; UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') @@ -1338,6 +1392,18 @@ BEGIN; DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); COMMIT; +-- deleting an unreferenced part is okay: +DELETE FROM temporal_rng +FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03' +WHERE id = '[5,6)'; +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; +-- deleting just a part fails: +DELETE FROM temporal_rng +FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10' +WHERE id = '[5,6)'; +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; -- then delete the objecting FK record and the same PK delete succeeds: DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)'; DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); @@ -1356,12 +1422,13 @@ ALTER TABLE temporal_fk_rng2rng ON DELETE RESTRICT; -- --- test ON UPDATE/DELETE options +-- rng2rng test ON UPDATE/DELETE options -- -- test FK referenced updates CASCADE +TRUNCATE temporal_rng, temporal_fk_rng2rng; INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01')); -INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[4,5)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); ALTER TABLE temporal_fk_rng2rng ADD CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at) @@ -1369,8 +1436,9 @@ ALTER TABLE temporal_fk_rng2rng ON DELETE CASCADE ON UPDATE CASCADE; -- test FK referenced updates SET NULL -INSERT INTO temporal_rng (id, valid_at) VALUES ('[9,10)', daterange('2018-01-01', '2021-01-01')); -INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'), '[9,10)'); +TRUNCATE temporal_rng, temporal_fk_rng2rng; +INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); ALTER TABLE temporal_fk_rng2rng ADD CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at) @@ -1378,9 +1446,10 @@ ALTER TABLE temporal_fk_rng2rng ON DELETE SET NULL ON UPDATE SET NULL; -- test FK referenced updates SET DEFAULT +TRUNCATE temporal_rng, temporal_fk_rng2rng; INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', daterange(null, null)); -INSERT INTO temporal_rng (id, valid_at) VALUES ('[12,13)', daterange('2018-01-01', '2021-01-01')); -INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[8,9)', daterange('2018-01-01', '2021-01-01'), '[12,13)'); +INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); ALTER TABLE temporal_fk_rng2rng ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', ADD CONSTRAINT temporal_fk_rng2rng_fk @@ -1716,6 +1785,20 @@ BEGIN; WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); COMMIT; -- changing the scalar part fails: +UPDATE temporal_mltrng SET id = '[7,8)' + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +-- changing an unreferenced part is okay: +UPDATE temporal_mltrng + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03'))) + SET id = '[7,8)' + WHERE id = '[5,6)'; +-- changing just a part fails: +UPDATE temporal_mltrng + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10'))) + SET id = '[7,8)' + WHERE id = '[5,6)'; +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)'; UPDATE temporal_mltrng SET id = '[7,8)' WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); @@ -1760,6 +1843,17 @@ BEGIN; DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); COMMIT; +-- deleting an unreferenced part is okay: +DELETE FROM temporal_mltrng +FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03'))) +WHERE id = '[5,6)'; +-- deleting just a part fails: +DELETE FROM temporal_mltrng +FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10'))) +WHERE id = '[5,6)'; +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)'; +DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); -- -- FK between partitioned tables: ranges