citus/src/test/regress/expected/pg18.out

3020 lines
109 KiB
Plaintext

--
-- PG18
--
SHOW server_version \gset
SELECT substring(:'server_version', '\d+')::int >= 18 AS server_version_ge_18
\gset
-- test invalid statistics
-- behavior is same among PG versions, error message differs
-- relevant PG18 commit: 3eea4dc2c7, 38883916e
CREATE STATISTICS tst ON a FROM (VALUES (x)) AS foo;
ERROR: CREATE STATISTICS only supports relation names in the FROM clause
CREATE FUNCTION tftest(int) returns table(a int, b int) as $$
SELECT $1, $1+i FROM generate_series(1,5) g(i);
$$ LANGUAGE sql IMMUTABLE STRICT;
CREATE STATISTICS alt_stat2 ON a FROM tftest(1);
ERROR: CREATE STATISTICS only supports relation names in the FROM clause
DROP FUNCTION tftest;
\if :server_version_ge_18
\else
\q
\endif
-- PG18-specific tests go here.
--
-- Purpose: Verify PG18 behavior that NOT NULL constraints are materialized
-- as pg_constraint rows with contype = 'n' on both coordinator and
-- worker shards. Also confirm our helper view (table_checks) does
-- NOT surface NOT NULL entries.
-- https://github.com/postgres/postgres/commit/14e87ffa5c543b5f30ead7413084c25f7735039f
CREATE SCHEMA pg18_nn;
SET search_path TO pg18_nn;
-- Local control table
DROP TABLE IF EXISTS nn_local CASCADE;
NOTICE: table "nn_local" does not exist, skipping
CREATE TABLE nn_local(
a int NOT NULL,
b int,
c text NOT NULL
);
-- Distributed table
DROP TABLE IF EXISTS nn_dist CASCADE;
NOTICE: table "nn_dist" does not exist, skipping
CREATE TABLE nn_dist(
a int NOT NULL,
b int,
c text NOT NULL
);
SELECT create_distributed_table('nn_dist', 'a');
create_distributed_table
---------------------------------------------------------------------
(1 row)
-- Coordinator: count NOT NULL constraint rows
SELECT 'local_n_count' AS label, contype, count(*)
FROM pg_constraint
WHERE conrelid = 'pg18_nn.nn_local'::regclass
GROUP BY contype
ORDER BY contype;
label | contype | count
---------------------------------------------------------------------
local_n_count | n | 2
(1 row)
SELECT 'dist_n_count' AS label, contype, count(*)
FROM pg_constraint
WHERE conrelid = 'pg18_nn.nn_dist'::regclass
GROUP BY contype
ORDER BY contype;
label | contype | count
---------------------------------------------------------------------
dist_n_count | n | 2
(1 row)
-- Our helper view should exclude NOT NULL
SELECT 'table_checks_local_count' AS label, count(*)
FROM public.table_checks
WHERE relid = 'pg18_nn.nn_local'::regclass;
label | count
---------------------------------------------------------------------
table_checks_local_count | 0
(1 row)
SELECT 'table_checks_dist_count' AS label, count(*)
FROM public.table_checks
WHERE relid = 'pg18_nn.nn_dist'::regclass;
label | count
---------------------------------------------------------------------
table_checks_dist_count | 0
(1 row)
-- Add a real CHECK to ensure table_checks still reports real checks
ALTER TABLE nn_dist ADD CONSTRAINT nn_dist_check CHECK (b IS DISTINCT FROM 42);
SELECT 'table_checks_dist_with_real_check' AS label, count(*)
FROM public.table_checks
WHERE relid = 'pg18_nn.nn_dist'::regclass;
label | count
---------------------------------------------------------------------
table_checks_dist_with_real_check | 1
(1 row)
-- === Worker checks ===
\c - - - :worker_1_port
SET client_min_messages TO WARNING;
SET search_path TO pg18_nn;
-- Pick one heap shard of nn_dist in our schema
SELECT format('%I.%I', n.nspname, c.relname) AS shard_regclass
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'pg18_nn'
AND c.relname LIKE 'nn_dist_%'
AND c.relkind = 'r'
ORDER BY c.relname
LIMIT 1
\gset
-- Expect: 2 NOT NULL rows (a,c) + 1 CHECK row on the shard
SELECT 'worker_shard_n_count' AS label, contype, count(*)
FROM pg_constraint
WHERE conrelid = :'shard_regclass'::regclass
GROUP BY contype
ORDER BY contype;
label | contype | count
---------------------------------------------------------------------
worker_shard_n_count | c | 1
worker_shard_n_count | n | 2
(2 rows)
-- table_checks on shard should hide NOT NULL
SELECT 'table_checks_worker_shard_count' AS label, count(*)
FROM public.table_checks
WHERE relid = :'shard_regclass'::regclass;
label | count
---------------------------------------------------------------------
table_checks_worker_shard_count | 1
(1 row)
-- Drop one NOT NULL on coordinator; verify propagation
\c - - - :master_port
SET search_path TO pg18_nn;
ALTER TABLE nn_dist ALTER COLUMN c DROP NOT NULL;
-- Re-check on worker: NOT NULL count should drop to 1
\c - - - :worker_1_port
SET search_path TO pg18_nn;
SELECT 'worker_shard_n_after_drop' AS label, contype, count(*)
FROM pg_constraint
WHERE conrelid = :'shard_regclass'::regclass
GROUP BY contype
ORDER BY contype;
label | contype | count
---------------------------------------------------------------------
worker_shard_n_after_drop | c | 1
worker_shard_n_after_drop | n | 1
(2 rows)
-- And on coordinator
\c - - - :master_port
SET search_path TO pg18_nn;
SELECT 'dist_n_after_drop' AS label, contype, count(*)
FROM pg_constraint
WHERE conrelid = 'pg18_nn.nn_dist'::regclass
GROUP BY contype
ORDER BY contype;
label | contype | count
---------------------------------------------------------------------
dist_n_after_drop | c | 1
dist_n_after_drop | n | 1
(2 rows)
-- Purpose: test self join elimination for distributed, citus local and local tables.
--
CREATE TABLE sje_d1 (id bigserial PRIMARY KEY, name text, created_at timestamptz DEFAULT now());
CREATE TABLE sje_d2 (id bigserial PRIMARY KEY, name text, created_at timestamptz DEFAULT now());
CREATE TABLE sje_local (id bigserial PRIMARY KEY, title text);
SET citus.next_shard_id TO 4754000;
SELECT create_distributed_table('sje_d1', 'id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
SELECT create_distributed_table('sje_d2', 'id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
INSERT INTO sje_d1 SELECT i, i::text, now() FROM generate_series(0,100)i;
INSERT INTO sje_d2 SELECT i, i::text, now() FROM generate_series(0,100)i;
INSERT INTO sje_local SELECT i, i::text FROM generate_series(0,100)i;
-- Self-join elimination is applied when distributed tables are involved
-- The query plan has only one join
EXPLAIN (costs off)
select count(1) from sje_d1 INNER
JOIN sje_d2 u1 USING (id) INNER
JOIN sje_d2 u2 USING (id) INNER
JOIN sje_d2 u3 USING (id) INNER
JOIN sje_d2 u4 USING (id) INNER
JOIN sje_d2 u5 USING (id) INNER
JOIN sje_d2 u6 USING (id);
QUERY PLAN
---------------------------------------------------------------------
Aggregate
-> Custom Scan (Citus Adaptive)
Task Count: 4
Tasks Shown: One of 4
-> Task
Node: host=localhost port=xxxxx dbname=regression
-> Aggregate
-> Hash Join
Hash Cond: (sje_d1.id = u6.id)
-> Seq Scan on sje_d1_4754000 sje_d1
-> Hash
-> Seq Scan on sje_d2_4754004 u6
(12 rows)
select count(1) from sje_d1 INNER
JOIN sje_d2 u1 USING (id) INNER
JOIN sje_d2 u2 USING (id) INNER
JOIN sje_d2 u3 USING (id) INNER
JOIN sje_d2 u4 USING (id) INNER
JOIN sje_d2 u5 USING (id) INNER
JOIN sje_d2 u6 USING (id);
count
---------------------------------------------------------------------
101
(1 row)
-- Self-join elimination applied to from list join
EXPLAIN (costs off)
SELECT count(1) from sje_d1 d1, sje_d2 u1, sje_d2 u2, sje_d2 u3
WHERE d1.id = u1.id and u1.id = u2.id and u3.id = d1.id;
QUERY PLAN
---------------------------------------------------------------------
Aggregate
-> Custom Scan (Citus Adaptive)
Task Count: 4
Tasks Shown: One of 4
-> Task
Node: host=localhost port=xxxxx dbname=regression
-> Aggregate
-> Hash Join
Hash Cond: (d1.id = u3.id)
-> Seq Scan on sje_d1_4754000 d1
-> Hash
-> Seq Scan on sje_d2_4754004 u3
(12 rows)
SELECT count(1) from sje_d1 d1, sje_d2 u1, sje_d2 u2, sje_d2 u3
WHERE d1.id = u1.id and u1.id = u2.id and u3.id = d1.id;
count
---------------------------------------------------------------------
101
(1 row)
-- Self-join elimination is not applied when a local table is involved
-- This is a limitation that will be resolved in citus 14
EXPLAIN (costs off)
select count(1) from sje_d1 INNER
JOIN sje_local u1 USING (id) INNER
JOIN sje_local u2 USING (id) INNER
JOIN sje_local u3 USING (id) INNER
JOIN sje_local u4 USING (id) INNER
JOIN sje_local u5 USING (id) INNER
JOIN sje_local u6 USING (id);
QUERY PLAN
---------------------------------------------------------------------
Aggregate
-> Custom Scan (Citus Adaptive)
-> Distributed Subplan XXX_1
-> Seq Scan on sje_local u1
-> Distributed Subplan XXX_2
-> Seq Scan on sje_local u2
-> Distributed Subplan XXX_3
-> Seq Scan on sje_local u3
-> Distributed Subplan XXX_4
-> Seq Scan on sje_local u4
-> Distributed Subplan XXX_5
-> Seq Scan on sje_local u5
-> Distributed Subplan XXX_6
-> Seq Scan on sje_local u6
Task Count: 4
Tasks Shown: One of 4
-> Task
Node: host=localhost port=xxxxx dbname=regression
-> Aggregate
-> Hash Join
Hash Cond: (intermediate_result_5.id = sje_d1.id)
-> Function Scan on read_intermediate_result intermediate_result_5
-> Hash
-> Hash Join
Hash Cond: (intermediate_result_4.id = sje_d1.id)
-> Function Scan on read_intermediate_result intermediate_result_4
-> Hash
-> Hash Join
Hash Cond: (intermediate_result_3.id = sje_d1.id)
-> Function Scan on read_intermediate_result intermediate_result_3
-> Hash
-> Hash Join
Hash Cond: (intermediate_result_2.id = sje_d1.id)
-> Function Scan on read_intermediate_result intermediate_result_2
-> Hash
-> Hash Join
Hash Cond: (intermediate_result_1.id = sje_d1.id)
-> Function Scan on read_intermediate_result intermediate_result_1
-> Hash
-> Hash Join
Hash Cond: (intermediate_result.id = sje_d1.id)
-> Function Scan on read_intermediate_result intermediate_result
-> Hash
-> Seq Scan on sje_d1_4754000 sje_d1
(44 rows)
select count(1) from sje_d1 INNER
JOIN sje_local u1 USING (id) INNER
JOIN sje_local u2 USING (id) INNER
JOIN sje_local u3 USING (id) INNER
JOIN sje_local u4 USING (id) INNER
JOIN sje_local u5 USING (id) INNER
JOIN sje_local u6 USING (id);
count
---------------------------------------------------------------------
101
(1 row)
-- to test USING vs ON equivalence
EXPLAIN (costs off)
SELECT count(1)
FROM sje_d1 d
JOIN sje_d2 u1 ON (d.id = u1.id)
JOIN sje_d2 u2 ON (u1.id = u2.id);
QUERY PLAN
---------------------------------------------------------------------
Aggregate
-> Custom Scan (Citus Adaptive)
Task Count: 4
Tasks Shown: One of 4
-> Task
Node: host=localhost port=xxxxx dbname=regression
-> Aggregate
-> Hash Join
Hash Cond: (d.id = u2.id)
-> Seq Scan on sje_d1_4754000 d
-> Hash
-> Seq Scan on sje_d2_4754004 u2
(12 rows)
SELECT count(1)
FROM sje_d1 d
JOIN sje_d2 u1 ON (d.id = u1.id)
JOIN sje_d2 u2 ON (u1.id = u2.id);
count
---------------------------------------------------------------------
101
(1 row)
-- Null-introducing join can have SJE
EXPLAIN (costs off)
SELECT count(*)
FROM sje_d1 d
LEFT JOIN sje_d2 u1 USING (id)
LEFT JOIN sje_d2 u2 USING (id);
QUERY PLAN
---------------------------------------------------------------------
Aggregate
-> Custom Scan (Citus Adaptive)
Task Count: 4
Tasks Shown: One of 4
-> Task
Node: host=localhost port=xxxxx dbname=regression
-> Aggregate
-> Seq Scan on sje_d1_4754000 d
(8 rows)
SELECT count(*)
FROM sje_d1 d
LEFT JOIN sje_d2 u1 USING (id)
LEFT JOIN sje_d2 u2 USING (id);
count
---------------------------------------------------------------------
101
(1 row)
-- prepared statement
PREPARE sje_p(int,int) AS
SELECT count(1)
FROM sje_d1 d
JOIN sje_d2 u1 USING (id)
JOIN sje_d2 u2 USING (id)
WHERE d.id BETWEEN $1 AND $2;
EXPLAIN (costs off)
EXECUTE sje_p(10,20);
QUERY PLAN
---------------------------------------------------------------------
Aggregate
-> Custom Scan (Citus Adaptive)
Task Count: 4
Tasks Shown: One of 4
-> Task
Node: host=localhost port=xxxxx dbname=regression
-> Aggregate
-> Hash Join
Hash Cond: (u2.id = d.id)
-> Seq Scan on sje_d2_4754004 u2
-> Hash
-> Bitmap Heap Scan on sje_d1_4754000 d
Recheck Cond: ((id >= 10) AND (id <= 20))
-> Bitmap Index Scan on sje_d1_pkey_4754000
Index Cond: ((id >= 10) AND (id <= 20))
(15 rows)
EXECUTE sje_p(10,20);
count
---------------------------------------------------------------------
11
(1 row)
-- cte
EXPLAIN (costs off)
WITH z AS (SELECT id FROM sje_d2 WHERE id % 2 = 0)
SELECT count(1)
FROM sje_d1 d
JOIN z USING (id)
JOIN sje_d2 u2 USING (id);
QUERY PLAN
---------------------------------------------------------------------
Aggregate
-> Custom Scan (Citus Adaptive)
Task Count: 4
Tasks Shown: One of 4
-> Task
Node: host=localhost port=xxxxx dbname=regression
-> Aggregate
-> Hash Join
Hash Cond: (d.id = u2.id)
-> Seq Scan on sje_d1_4754000 d
-> Hash
-> Seq Scan on sje_d2_4754004 u2
Filter: ((id % '2'::bigint) = 0)
(13 rows)
WITH z AS (SELECT id FROM sje_d2 WHERE id % 2 = 0)
SELECT count(1)
FROM sje_d1 d
JOIN z USING (id)
JOIN sje_d2 u2 USING (id);
count
---------------------------------------------------------------------
51
(1 row)
-- PG18 Feature: JSON functionality - JSON_TABLE has COLUMNS clause for
-- extracting multiple fields from JSON documents.
-- PG18 commit: https://github.com/postgres/postgres/commit/bb766cd
CREATE TABLE pg18_json_test (id serial PRIMARY KEY, data JSON);
INSERT INTO pg18_json_test (data) VALUES
('{ "user": {"name": "Alice", "age": 30, "city": "San Diego"} }'),
('{ "user": {"name": "Bob", "age": 25, "city": "Los Angeles"} }'),
('{ "user": {"name": "Charlie", "age": 35, "city": "Los Angeles"} }'),
('{ "user": {"name": "Diana", "age": 28, "city": "Seattle"} } '),
('{ "user": {"name": "Evan", "age": 40, "city": "Portland"} } '),
('{ "user": {"name": "Ethan", "age": 32, "city": "Seattle"} } '),
('{ "user": {"name": "Fiona", "age": 27, "city": "Seattle"} } '),
('{ "user": {"name": "George", "age": 29, "city": "San Francisco"} } '),
('{ "user": {"name": "Hannah", "age": 33, "city": "Seattle"} } '),
('{ "user": {"name": "Ian", "age": 26, "city": "Portland"} } '),
('{ "user": {"name": "Jane", "age": 38, "city": "San Francisco"} } ');
SELECT jt.name, jt.age FROM pg18_json_test, JSON_TABLE(
data,
'$.user'
COLUMNS (
age INT PATH '$.age',
name TEXT PATH '$.name'
)
) AS jt
WHERE jt.age between 25 and 35
ORDER BY jt.age, jt.name;
name | age
---------------------------------------------------------------------
Bob | 25
Ian | 26
Fiona | 27
Diana | 28
George | 29
Alice | 30
Ethan | 32
Hannah | 33
Charlie | 35
(9 rows)
SELECT jt.city, count(1) FROM pg18_json_test, JSON_TABLE(
data,
'$.user'
COLUMNS (
city TEXT PATH '$.city'
)
) AS jt
GROUP BY jt.city
ORDER BY count(1) DESC;
city | count
---------------------------------------------------------------------
Seattle | 4
San Francisco | 2
Portland | 2
Los Angeles | 2
San Diego | 1
(5 rows)
-- Make it distributed and repeat the queries
SELECT create_distributed_table('pg18_json_test', 'id');
NOTICE: Copying data from local table...
NOTICE: copying the data has completed
DETAIL: The local data in the table is no longer visible, but is still on disk.
HINT: To remove the local data, run: SELECT truncate_local_data_after_distributing_table($$pg18_nn.pg18_json_test$$)
create_distributed_table
---------------------------------------------------------------------
(1 row)
SELECT jt.name, jt.age FROM pg18_json_test, JSON_TABLE(
data,
'$.user'
COLUMNS (
age INT PATH '$.age',
name TEXT PATH '$.name'
)
) AS jt
WHERE jt.age between 25 and 35
ORDER BY jt.age, jt.name;
name | age
---------------------------------------------------------------------
Bob | 25
Ian | 26
Fiona | 27
Diana | 28
George | 29
Alice | 30
Ethan | 32
Hannah | 33
Charlie | 35
(9 rows)
SELECT jt.city, count(1) FROM pg18_json_test, JSON_TABLE(
data,
'$.user'
COLUMNS (
city TEXT PATH '$.city'
)
) AS jt
GROUP BY jt.city
ORDER BY count(1) DESC;
city | count
---------------------------------------------------------------------
Seattle | 4
Portland | 2
Los Angeles | 2
San Francisco | 2
San Diego | 1
(5 rows)
-- PG18 Feature: WITHOUT OVERLAPS can appear in PRIMARY KEY and UNIQUE constraints.
-- PG18 commit: https://github.com/postgres/postgres/commit/fc0438b4e
CREATE TABLE temporal_rng (
-- Since we can't depend on having btree_gist here,
-- use an int4range instead of an int.
-- (The rangetypes regression test uses the same trick.)
id int4range,
valid_at daterange,
CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
);
SELECT create_distributed_table('temporal_rng', 'id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
-- okay:
INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2018-01-01', '2018-01-05'));
INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01', NULL));
-- should fail:
INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
ERROR: conflicting key value violates exclusion constraint "temporal_rng_pk_4754013"
DETAIL: Key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)).
CONTEXT: while executing command on localhost:xxxxx
-- NULLs are not allowed in the shard key:
INSERT INTO temporal_rng (id, valid_at) VALUES (NULL, daterange('2018-01-01', '2018-01-05'));
ERROR: cannot perform an INSERT with NULL in the partition column
INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', NULL);
ERROR: null value in column "valid_at" violates not-null constraint
DETAIL: Failing row contains ([3,4), null).
CONTEXT: while executing command on localhost:xxxxx
-- rejects empty:
INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', 'empty');
ERROR: empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_rng_4754012"
CONTEXT: while executing command on localhost:xxxxx
SELECT * FROM temporal_rng ORDER BY id, valid_at;
id | valid_at
---------------------------------------------------------------------
[1,2) | [01-02-2018,02-03-2018)
[1,2) | [03-03-2018,04-04-2018)
[2,3) | [01-01-2018,01-05-2018)
[3,4) | [01-01-2018,)
(4 rows)
-- Repeat with UNIQUE constraint
CREATE TABLE temporal_rng_uq (
-- Since we can't depend on having btree_gist here,
-- use an int4range instead of an int.
id int4range,
valid_at daterange,
CONSTRAINT temporal_rng_uq_uk UNIQUE (id, valid_at WITHOUT OVERLAPS)
);
SELECT create_distributed_table('temporal_rng_uq', 'id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
-- okay:
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[2,3)', daterange('2018-01-01', '2018-01-05'));
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01', NULL));
-- should fail:
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
ERROR: conflicting key value violates exclusion constraint "temporal_rng_uq_uk_4754017"
DETAIL: Key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)).
CONTEXT: while executing command on localhost:xxxxx
-- NULLs are not allowed in the shard key:
INSERT INTO temporal_rng_uq (id, valid_at) VALUES (NULL, daterange('2018-01-01', '2018-01-05'));
ERROR: cannot perform an INSERT with NULL in the partition column
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[3,4)', NULL);
-- rejects empty:
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[3,4)', 'empty');
ERROR: empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_rng_uq_4754016"
CONTEXT: while executing command on localhost:xxxxx
SELECT * FROM temporal_rng_uq ORDER BY id, valid_at;
id | valid_at
---------------------------------------------------------------------
[1,2) | [01-02-2018,02-03-2018)
[1,2) | [03-03-2018,04-04-2018)
[2,3) | [01-01-2018,01-05-2018)
[3,4) | [01-01-2018,)
[3,4) |
(5 rows)
DROP TABLE temporal_rng CASCADE;
DROP TABLE temporal_rng_uq CASCADE;
-- Repeat the tests with the PRIMARY KEY and UNIQUE constraints added
-- after the table is created and distributed. INSERTs produce the
-- same results as before.
CREATE TABLE temporal_rng (
-- Since we can't depend on having btree_gist here,
-- use an int4range instead of an int.
-- (The rangetypes regression test uses the same trick.)
id int4range,
valid_at daterange
);
SELECT create_distributed_table('temporal_rng', 'id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
-- okay:
INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2018-01-01', '2018-01-05'));
INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01', NULL));
ALTER TABLE temporal_rng
ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
-- should fail:
INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
ERROR: conflicting key value violates exclusion constraint "temporal_rng_pk_4754021"
DETAIL: Key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)).
CONTEXT: while executing command on localhost:xxxxx
-- NULLs are not allowed in the shard key:
INSERT INTO temporal_rng (id, valid_at) VALUES (NULL, daterange('2018-01-01', '2018-01-05'));
ERROR: cannot perform an INSERT with NULL in the partition column
INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', NULL);
ERROR: null value in column "valid_at" violates not-null constraint
DETAIL: Failing row contains ([3,4), null).
CONTEXT: while executing command on localhost:xxxxx
-- rejects empty:
INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', 'empty');
ERROR: empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_rng_4754020"
CONTEXT: while executing command on localhost:xxxxx
SELECT * FROM temporal_rng ORDER BY id, valid_at;
id | valid_at
---------------------------------------------------------------------
[1,2) | [01-02-2018,02-03-2018)
[1,2) | [03-03-2018,04-04-2018)
[2,3) | [01-01-2018,01-05-2018)
[3,4) | [01-01-2018,)
(4 rows)
-- Repeat with UNIQUE constraint
CREATE TABLE temporal_rng_uq (
-- Since we can't depend on having btree_gist here,
-- use an int4range instead of an int.
id int4range,
valid_at daterange
);
SELECT create_distributed_table('temporal_rng_uq', 'id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
-- okay:
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[2,3)', daterange('2018-01-01', '2018-01-05'));
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01', NULL));
ALTER TABLE temporal_rng_uq
ADD CONSTRAINT temporal_rng_uq_uk UNIQUE (id, valid_at WITHOUT OVERLAPS);
-- should fail:
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
ERROR: conflicting key value violates exclusion constraint "temporal_rng_uq_uk_4754025"
DETAIL: Key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)).
CONTEXT: while executing command on localhost:xxxxx
-- NULLs are not allowed in the shard key:
INSERT INTO temporal_rng_uq (id, valid_at) VALUES (NULL, daterange('2018-01-01', '2018-01-05'));
ERROR: cannot perform an INSERT with NULL in the partition column
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[3,4)', NULL);
-- rejects empty:
INSERT INTO temporal_rng_uq (id, valid_at) VALUES ('[3,4)', 'empty');
ERROR: empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_rng_uq_4754024"
CONTEXT: while executing command on localhost:xxxxx
SELECT * FROM temporal_rng_uq ORDER BY id, valid_at;
id | valid_at
---------------------------------------------------------------------
[1,2) | [01-02-2018,02-03-2018)
[1,2) | [03-03-2018,04-04-2018)
[2,3) | [01-01-2018,01-05-2018)
[3,4) | [01-01-2018,)
[3,4) |
(5 rows)
-- PG18 Feature: RETURNING old and new values in DML statements
-- PG18 commit: https://github.com/postgres/postgres/commit/80feb727c
CREATE TABLE users (id SERIAL PRIMARY KEY, email text, category int);
INSERT INTO users (email, category) SELECT 'xxx@foo.com', i % 10 from generate_series (1,100) t(i);
SELECT create_distributed_table('users','id');
NOTICE: Copying data from local table...
NOTICE: copying the data has completed
DETAIL: The local data in the table is no longer visible, but is still on disk.
HINT: To remove the local data, run: SELECT truncate_local_data_after_distributing_table($$pg18_nn.users$$)
create_distributed_table
---------------------------------------------------------------------
(1 row)
UPDATE users SET email = 'colm@planet.com' WHERE id = 1
RETURNING OLD.email AS previous_email,
NEW.email AS current_email;
previous_email | current_email
---------------------------------------------------------------------
xxx@foo.com | colm@planet.com
(1 row)
SELECT * FROM users WHERE id = 1
ORDER BY id;
id | email | category
---------------------------------------------------------------------
1 | colm@planet.com | 1
(1 row)
UPDATE users SET email = 'tim@arctic.net' WHERE id = 22
RETURNING OLD.email AS previous_email,
NEW.email AS current_email;
previous_email | current_email
---------------------------------------------------------------------
xxx@foo.com | tim@arctic.net
(1 row)
UPDATE users SET email = 'john@farm.ie' WHERE id = 33
RETURNING OLD.email AS previous_email,
NEW.email AS current_email;
previous_email | current_email
---------------------------------------------------------------------
xxx@foo.com | john@farm.ie
(1 row)
SELECT * FROM users WHERE id = 22
ORDER BY id;
id | email | category
---------------------------------------------------------------------
22 | tim@arctic.net | 2
(1 row)
SELECT * FROM users
WHERE email not like 'xxx@%'
ORDER BY id;
id | email | category
---------------------------------------------------------------------
1 | colm@planet.com | 1
22 | tim@arctic.net | 2
33 | john@farm.ie | 3
(3 rows)
-- NULL values creep into the email column..
INSERT INTO users (email, category) VALUES (null, 5)
RETURNING OLD.email AS previous_email,
NEW.email AS current_email;
previous_email | current_email
---------------------------------------------------------------------
|
(1 row)
UPDATE users SET email = NULL WHERE id = 79
RETURNING OLD.email AS previous_email,
NEW.email AS current_email;
previous_email | current_email
---------------------------------------------------------------------
xxx@foo.com |
(1 row)
-- Now add a NOT NULL constraint on email, but do
-- not apply it to existing rows yet.
ALTER TABLE users
ADD CONSTRAINT users_email_not_null
CHECK (email IS NOT NULL) NOT VALID;
UPDATE users SET email = NULL WHERE id = 50
RETURNING OLD.email AS previous_email,
NEW.email AS current_email;
ERROR: new row for relation "users_4754028" violates check constraint "users_email_not_null_4754028"
DETAIL: Failing row contains (50, null, 0).
CONTEXT: while executing command on localhost:xxxxx
-- Validation should fail due to existing NULLs
ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null;
ERROR: check constraint "users_email_not_null_4754028" of relation "users_4754028" is violated by some row
CONTEXT: while executing command on localhost:xxxxx
-- Fix NULL emails to a default value
UPDATE users SET email = 'xxx@foo.com' WHERE email IS NULL
RETURNING OLD.email AS previous_email,
NEW.email AS current_email;
previous_email | current_email
---------------------------------------------------------------------
| xxx@foo.com
| xxx@foo.com
(2 rows)
-- Validation should now succeed
ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null;
-- And prevent future NULLs
INSERT INTO users (email, category) VALUES (null, 10)
RETURNING OLD.email AS previous_email,
NEW.email AS current_email;
ERROR: new row for relation "users_4754030" violates check constraint "users_email_not_null_4754030"
DETAIL: Failing row contains (102, null, 10).
CONTEXT: while executing command on localhost:xxxxx
-- PG18 Feature: support for LIKE in CREATE FOREIGN TABLE
-- PG18 commit: https://github.com/postgres/postgres/commit/302cf1575
SET citus.use_citus_managed_tables TO ON;
CREATE EXTENSION postgres_fdw;
CREATE SERVER foreign_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', port :'master_port', dbname 'regression');
CREATE USER MAPPING FOR CURRENT_USER
SERVER foreign_server
OPTIONS (user 'postgres');
CREATE TABLE ctl_table(a int PRIMARY KEY,
b varchar COMPRESSION pglz,
c int GENERATED ALWAYS AS (a * 2) STORED,
d bigint GENERATED ALWAYS AS IDENTITY,
e int DEFAULT 1);
CREATE INDEX ctl_table_ab_key ON ctl_table(a, b);
COMMENT ON COLUMN ctl_table.b IS 'Column b';
CREATE STATISTICS ctl_table_stat ON a,b FROM ctl_table;
INSERT INTO ctl_table VALUES (1, 'first'), (2, 'second'), (3, 'third'), (4, 'fourth');
-- Test EXCLUDING ALL
CREATE FOREIGN TABLE ctl_ft1(LIKE ctl_table EXCLUDING ALL)
SERVER foreign_server
OPTIONS (schema_name 'pg18_nn', table_name 'ctl_table');
-- Test INCLUDING ALL
CREATE FOREIGN TABLE ctl_ft2(LIKE ctl_table INCLUDING ALL)
SERVER foreign_server
OPTIONS (schema_name 'pg18_nn', table_name 'ctl_table');
-- check that the foreign tables are citus local table
SELECT partmethod, repmodel FROM pg_dist_partition
WHERE logicalrelid IN ('ctl_ft1'::regclass, 'ctl_ft2'::regclass) ORDER BY logicalrelid;
partmethod | repmodel
---------------------------------------------------------------------
n | s
n | s
(2 rows)
-- we can query the foreign tables
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM ctl_ft1 ORDER BY a;
QUERY PLAN
---------------------------------------------------------------------
Custom Scan (Citus Adaptive)
Output: remote_scan.a, remote_scan.b, remote_scan.c, remote_scan.d, remote_scan.e
Task Count: 1
Tasks Shown: All
-> Task
Query: SELECT a, b, c, d, e FROM pg18_nn.ctl_ft1_4754033 ctl_ft1 ORDER BY a
Node: host=localhost port=xxxxx dbname=regression
-> Foreign Scan on pg18_nn.ctl_ft1_4754033 ctl_ft1
Output: a, b, c, d, e
Remote SQL: SELECT a, b, c, d, e FROM pg18_nn.ctl_table ORDER BY a ASC NULLS LAST
(10 rows)
SELECT * FROM ctl_ft1 ORDER BY a;
a | b | c | d | e
---------------------------------------------------------------------
1 | first | 2 | 1 | 1
2 | second | 4 | 2 | 1
3 | third | 6 | 3 | 1
4 | fourth | 8 | 4 | 1
(4 rows)
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM ctl_ft2 ORDER BY a;
QUERY PLAN
---------------------------------------------------------------------
Custom Scan (Citus Adaptive)
Output: remote_scan.a, remote_scan.b, remote_scan.c, remote_scan.d, remote_scan.e
Task Count: 1
Tasks Shown: All
-> Task
Query: SELECT a, b, c, d, e FROM pg18_nn.ctl_ft2_4754034 ctl_ft2 ORDER BY a
Node: host=localhost port=xxxxx dbname=regression
-> Foreign Scan on pg18_nn.ctl_ft2_4754034 ctl_ft2
Output: a, b, c, d, e
Remote SQL: SELECT a, b, c, d, e FROM pg18_nn.ctl_table ORDER BY a ASC NULLS LAST
(10 rows)
SELECT * FROM ctl_ft2 ORDER BY a;
a | b | c | d | e
---------------------------------------------------------------------
1 | first | 2 | 1 | 1
2 | second | 4 | 2 | 1
3 | third | 6 | 3 | 1
4 | fourth | 8 | 4 | 1
(4 rows)
-- Clean up foreign table test
RESET citus.use_citus_managed_tables;
SELECT undistribute_table('ctl_ft1');
NOTICE: creating a new table for pg18_nn.ctl_ft1
NOTICE: dropping the old pg18_nn.ctl_ft1
NOTICE: renaming the new table to pg18_nn.ctl_ft1
undistribute_table
---------------------------------------------------------------------
(1 row)
SELECT undistribute_table('ctl_ft2');
NOTICE: creating a new table for pg18_nn.ctl_ft2
NOTICE: dropping the old pg18_nn.ctl_ft2
NOTICE: renaming the new table to pg18_nn.ctl_ft2
undistribute_table
---------------------------------------------------------------------
(1 row)
DROP SERVER foreign_server CASCADE;
NOTICE: drop cascades to 3 other objects
DETAIL: drop cascades to user mapping for postgres on server foreign_server
drop cascades to foreign table ctl_ft1
drop cascades to foreign table ctl_ft2
-- PG18 Feature: PERIOD clause in foreign key constraint definitions.
-- PG18 commit: https://github.com/postgres/postgres/commit/89f908a6d
-- This test verifies that the PG18 tests apply to Citus tables
CREATE EXTENSION btree_gist; -- needed for range type indexing
CREATE TABLE temporal_test (
id integer,
valid_at daterange,
CONSTRAINT temporal_test_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
);
SET citus.shard_count TO 4;
SELECT create_reference_table( 'temporal_test');
create_reference_table
---------------------------------------------------------------------
(1 row)
INSERT INTO temporal_test VALUES
(1, '[2000-01-01,2001-01-01)');
-- same key, doesn't overlap:
INSERT INTO temporal_test VALUES
(1, '[2001-01-01,2002-01-01)');
-- overlaps but different key:
INSERT INTO temporal_test VALUES
(2, '[2000-01-01,2001-01-01)');
-- should fail:
INSERT INTO temporal_test VALUES
(1, '[2000-06-01,2001-01-01)');
ERROR: conflicting key value violates exclusion constraint "temporal_test_pk_4754035"
DETAIL: Key (id, valid_at)=(1, [2000-06-01,2001-01-01)) conflicts with existing key (id, valid_at)=(1, [2000-01-01,2001-01-01)).
CONTEXT: while executing command on localhost:xxxxx
-- Required for foreign key constraint on distributed table
SET citus.shard_replication_factor TO 1;
-- Create and distribute a table with temporal foreign key constraints
CREATE TABLE temporal_fk_rng2rng (
id integer,
valid_at daterange,
parent_id integer,
CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
);
SELECT create_distributed_table( 'temporal_fk_rng2rng', 'id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
--
-- Add foreign key constraint with PERIOD clause
-- This is propagated to worker shards
ALTER TABLE temporal_fk_rng2rng
ADD CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_test (id, PERIOD valid_at);
INSERT INTO temporal_fk_rng2rng VALUES
(1, '[2000-01-01,2001-01-01)', 1);
-- okay spanning two parent records:
INSERT INTO temporal_fk_rng2rng VALUES
(2, '[2000-01-01,2002-01-01)', 1);
-- key is missing
INSERT INTO temporal_fk_rng2rng VALUES
(3, '[2000-01-01,2001-01-01)', 3);
ERROR: insert or update on table "temporal_fk_rng2rng_4754037" violates foreign key constraint "temporal_fk_rng2rng_fk_4754037"
DETAIL: Key (parent_id, valid_at)=(3, [2000-01-01,2001-01-01)) is not present in table "temporal_test_4754035".
CONTEXT: while executing command on localhost:xxxxx
-- key exist but is outside range
INSERT INTO temporal_fk_rng2rng VALUES
(4, '[2001-01-01,2002-01-01)', 2);
ERROR: insert or update on table "temporal_fk_rng2rng_4754037" violates foreign key constraint "temporal_fk_rng2rng_fk_4754037"
DETAIL: Key (parent_id, valid_at)=(2, [2001-01-01,2002-01-01)) is not present in table "temporal_test_4754035".
CONTEXT: while executing command on localhost:xxxxx
-- key exist but is partly outside range
INSERT INTO temporal_fk_rng2rng VALUES
(5, '[2000-01-01,2002-01-01)', 2);
ERROR: insert or update on table "temporal_fk_rng2rng_4754036" violates foreign key constraint "temporal_fk_rng2rng_fk_4754036"
DETAIL: Key (parent_id, valid_at)=(2, [2000-01-01,2002-01-01)) is not present in table "temporal_test_4754035".
CONTEXT: while executing command on localhost:xxxxx
-- PG18 Feature: REJECT_LIMIT option for COPY errors
-- PG18 commit: https://github.com/postgres/postgres/commit/4ac2a9bec
-- Citus does not support COPY with ON_ERROR so just need to
-- ensure the appropriate error is returned.
CREATE TABLE check_ign_err (n int, m int[], k int);
SELECT create_distributed_table('check_ign_err', 'n');
create_distributed_table
---------------------------------------------------------------------
(1 row)
COPY check_ign_err FROM STDIN WITH (on_error stop, reject_limit 5);
ERROR: Citus does not support COPY FROM with ON_ERROR option.
COPY check_ign_err FROM STDIN WITH (ON_ERROR ignore, REJECT_LIMIT 100);
ERROR: Citus does not support COPY FROM with ON_ERROR option.
COPY check_ign_err FROM STDIN WITH (on_error ignore, log_verbosity verbose, reject_limit 50);
ERROR: Citus does not support COPY FROM with ON_ERROR option.
COPY check_ign_err FROM STDIN WITH (reject_limt 77, log_verbosity verbose, on_error ignore);
ERROR: Citus does not support COPY FROM with ON_ERROR option.
-- PG requires on_error when reject_limit is specified
COPY check_ign_err FROM STDIN WITH (reject_limit 100);
ERROR: COPY REJECT_LIMIT requires ON_ERROR to be set to IGNORE
-- PG18 Feature: COPY TABLE TO on a materialized view
-- PG18 commit: https://github.com/postgres/postgres/commit/534874fac
-- This does not work in Citus as a materialized view cannot be distributed.
-- So just verify that the appropriate error is raised.
CREATE MATERIALIZED VIEW copytest_mv AS
SELECT i as id, md5(i::text) as hashval
FROM generate_series(1,100) i;
-- Attempting to make it distributed should fail with appropriate error as
-- Citus does not yet support materialized views.
SELECT create_distributed_table('copytest_mv', 'id');
ERROR: copytest_mv is not a regular, foreign or partitioned table
-- After that, any command on the materialized view is outside Citus support.
-- PG18: verify publish_generated_columns is preserved for distributed tables
-- https://github.com/postgres/postgres/commit/7054186c4
\c - - - :master_port
CREATE SCHEMA pg18_publication;
SET search_path TO pg18_publication;
-- table with a stored generated column
CREATE TABLE gen_pub_tab (
id int primary key,
a int,
b int GENERATED ALWAYS AS (a * 10) STORED
);
-- make it distributed so CREATE PUBLICATION goes through Citus metadata/DDL path
SELECT create_distributed_table('gen_pub_tab', 'id', colocate_with := 'none');
create_distributed_table
---------------------------------------------------------------------
(1 row)
-- publication using the new PG18 option: stored
CREATE PUBLICATION pub_gen_cols_stored
FOR TABLE gen_pub_tab
WITH (publish = 'insert, update', publish_generated_columns = stored);
-- second publication explicitly using "none" for completeness
CREATE PUBLICATION pub_gen_cols_none
FOR TABLE gen_pub_tab
WITH (publish = 'insert, update', publish_generated_columns = none);
-- On coordinator: pubgencols must be 's' and 'n' respectively
SELECT pubname, pubgencols
FROM pg_publication
WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none')
ORDER BY pubname;
pubname | pubgencols
---------------------------------------------------------------------
pub_gen_cols_none | n
pub_gen_cols_stored | s
(2 rows)
-- On worker 1: both publications must exist and keep pubgencols in sync
\c - - - :worker_1_port
SET search_path TO pg18_publication;
SELECT pubname, pubgencols
FROM pg_publication
WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none')
ORDER BY pubname;
pubname | pubgencols
---------------------------------------------------------------------
pub_gen_cols_none | n
pub_gen_cols_stored | s
(2 rows)
-- On worker 2: same check
\c - - - :worker_2_port
SET search_path TO pg18_publication;
SELECT pubname, pubgencols
FROM pg_publication
WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none')
ORDER BY pubname;
pubname | pubgencols
---------------------------------------------------------------------
pub_gen_cols_none | n
pub_gen_cols_stored | s
(2 rows)
-- Now verify ALTER PUBLICATION .. SET (publish_generated_columns = none)
-- propagates to workers as well.
\c - - - :master_port
SET search_path TO pg18_publication;
ALTER PUBLICATION pub_gen_cols_stored
SET (publish_generated_columns = none);
-- coordinator: both publications should now have pubgencols = 'n'
SELECT pubname, pubgencols
FROM pg_publication
WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none')
ORDER BY pubname;
pubname | pubgencols
---------------------------------------------------------------------
pub_gen_cols_none | n
pub_gen_cols_stored | n
(2 rows)
-- worker 1: pubgencols must match coordinator
\c - - - :worker_1_port
SET search_path TO pg18_publication;
SELECT pubname, pubgencols
FROM pg_publication
WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none')
ORDER BY pubname;
pubname | pubgencols
---------------------------------------------------------------------
pub_gen_cols_none | n
pub_gen_cols_stored | n
(2 rows)
-- worker 2: same check
\c - - - :worker_2_port
SET search_path TO pg18_publication;
SELECT pubname, pubgencols
FROM pg_publication
WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none')
ORDER BY pubname;
pubname | pubgencols
---------------------------------------------------------------------
pub_gen_cols_none | n
pub_gen_cols_stored | n
(2 rows)
-- Column list precedence test: Citus must preserve both prattrs and pubgencols
\c - - - :master_port
SET search_path TO pg18_publication;
-- Case 1: column list explicitly includes the generated column, flag = none
CREATE PUBLICATION pub_gen_cols_list_includes_b
FOR TABLE gen_pub_tab (id, a, b)
WITH (publish_generated_columns = none);
-- Case 2: column list excludes the generated column, flag = stored
CREATE PUBLICATION pub_gen_cols_list_excludes_b
FOR TABLE gen_pub_tab (id, a)
WITH (publish_generated_columns = stored);
-- Helper: show pubname, pubgencols, and column list (prattrs) for gen_pub_tab
SELECT p.pubname,
p.pubgencols,
r.prattrs
FROM pg_publication p
JOIN pg_publication_rel r ON p.oid = r.prpubid
JOIN pg_class c ON c.oid = r.prrelid
WHERE p.pubname IN ('pub_gen_cols_list_includes_b',
'pub_gen_cols_list_excludes_b')
AND c.relname = 'gen_pub_tab'
ORDER BY p.pubname;
pubname | pubgencols | prattrs
---------------------------------------------------------------------
pub_gen_cols_list_excludes_b | s | 1 2
pub_gen_cols_list_includes_b | n | 1 2 3
(2 rows)
-- worker 1: must see the same pubgencols + prattrs
\c - - - :worker_1_port
SET search_path TO pg18_publication;
SELECT p.pubname,
p.pubgencols,
r.prattrs
FROM pg_publication p
JOIN pg_publication_rel r ON p.oid = r.prpubid
JOIN pg_class c ON c.oid = r.prrelid
WHERE p.pubname IN ('pub_gen_cols_list_includes_b',
'pub_gen_cols_list_excludes_b')
AND c.relname = 'gen_pub_tab'
ORDER BY p.pubname;
pubname | pubgencols | prattrs
---------------------------------------------------------------------
pub_gen_cols_list_excludes_b | s | 1 2
pub_gen_cols_list_includes_b | n | 1 2 3
(2 rows)
-- worker 2: same check
\c - - - :worker_2_port
SET search_path TO pg18_publication;
SELECT p.pubname,
p.pubgencols,
r.prattrs
FROM pg_publication p
JOIN pg_publication_rel r ON p.oid = r.prpubid
JOIN pg_class c ON c.oid = r.prrelid
WHERE p.pubname IN ('pub_gen_cols_list_includes_b',
'pub_gen_cols_list_excludes_b')
AND c.relname = 'gen_pub_tab'
ORDER BY p.pubname;
pubname | pubgencols | prattrs
---------------------------------------------------------------------
pub_gen_cols_list_excludes_b | s | 1 2
pub_gen_cols_list_includes_b | n | 1 2 3
(2 rows)
-- back to coordinator for subsequent tests / cleanup
\c - - - :master_port
SET search_path TO pg18_publication;
DROP PUBLICATION pub_gen_cols_stored;
DROP PUBLICATION pub_gen_cols_none;
DROP PUBLICATION pub_gen_cols_list_includes_b;
DROP PUBLICATION pub_gen_cols_list_excludes_b;
-- ============================================================
-- PG18: replicate stored generated columns when included in publication column list
-- Upstream: 745217a05
-- Publisher: worker1 | Subscriber: worker2
-- We validate initial COPY + streaming INSERT/UPDATE semantics.
-- ============================================================
CREATE OR REPLACE FUNCTION wait_for_expected_rowcount_at_table(tableName text, expectedCount integer)
RETURNS void AS $$
DECLARE
actualCount integer;
i int;
BEGIN
FOR i IN 1..600 LOOP
EXECUTE FORMAT('SELECT COUNT(*) FROM %s', tableName) INTO actualCount;
EXIT WHEN actualCount = expectedCount;
PERFORM pg_sleep(0.05);
END LOOP;
IF actualCount IS DISTINCT FROM expectedCount THEN
RAISE EXCEPTION 'timeout waiting for % rows in %, got %',
expectedCount, tableName, actualCount;
END IF;
END$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION wait_for_expected_rowcount_at_query(query text, expectedCount integer)
RETURNS void AS $$
DECLARE
actualCount integer;
i int;
BEGIN
FOR i IN 1..600 LOOP
EXECUTE query INTO actualCount;
EXIT WHEN actualCount = expectedCount;
PERFORM pg_sleep(0.05);
END LOOP;
IF actualCount IS DISTINCT FROM expectedCount THEN
RAISE EXCEPTION 'timeout waiting for query [%] to return %, got %',
query, expectedCount, actualCount;
END IF;
END$$ LANGUAGE plpgsql;
-- Build conninfo safely (psql vars do NOT expand inside single quotes)
\set conninfo_w1 '\'user=postgres host=localhost port=' :worker_1_port ' dbname=regression\''
-- --------------------------
-- Case A: column list includes generated column b
-- Expectation: subscriber receives b values (publisher b = a*10) because b is published in the column list.
-- --------------------------
\c - - - :worker_1_port
SET search_path TO pg18_publication;
DROP PUBLICATION IF EXISTS pub_gen_repl_a;
NOTICE: publication "pub_gen_repl_a" does not exist, skipping
DROP TABLE IF EXISTS gen_pub_repl_a;
NOTICE: table "gen_pub_repl_a" does not exist, skipping
CREATE TABLE gen_pub_repl_a (
id int PRIMARY KEY,
a int,
b int GENERATED ALWAYS AS (a * 10) STORED
);
INSERT INTO gen_pub_repl_a (id, a) VALUES (1, 2), (2, 7);
SET citus.enable_ddl_propagation TO off;
CREATE PUBLICATION pub_gen_repl_a
FOR TABLE gen_pub_repl_a (id, a, b)
WITH (publish = 'insert, update', publish_generated_columns = none);
RESET citus.enable_ddl_propagation;
\c - - - :worker_2_port
SET search_path TO pg18_publication;
DROP SUBSCRIPTION IF EXISTS sub_gen_repl_a;
NOTICE: subscription "sub_gen_repl_a" does not exist, skipping
DROP TABLE IF EXISTS gen_pub_repl_a;
NOTICE: table "gen_pub_repl_a" does not exist, skipping
-- IMPORTANT: subscriber b is NOT generated. This is the PG18 “replicate generated values” use-case.
CREATE TABLE gen_pub_repl_a (
id int PRIMARY KEY,
a int,
b int
);
CREATE SUBSCRIPTION sub_gen_repl_a
CONNECTION :conninfo_w1
PUBLICATION pub_gen_repl_a
WITH (copy_data = true);
NOTICE: created replication slot "sub_gen_repl_a" on publisher
SELECT wait_for_expected_rowcount_at_table('gen_pub_repl_a', 2);
wait_for_expected_rowcount_at_table
---------------------------------------------------------------------
(1 row)
SELECT * FROM gen_pub_repl_a ORDER BY id; -- expect b=20,70
id | a | b
---------------------------------------------------------------------
1 | 2 | 20
2 | 7 | 70
(2 rows)
\c - - - :worker_1_port
SET search_path TO pg18_publication;
INSERT INTO gen_pub_repl_a (id, a) VALUES (3, 9);
UPDATE gen_pub_repl_a SET a = 5 WHERE id = 1;
\c - - - :worker_2_port
SET search_path TO pg18_publication;
SELECT wait_for_expected_rowcount_at_table('gen_pub_repl_a', 3);
wait_for_expected_rowcount_at_table
---------------------------------------------------------------------
(1 row)
SELECT wait_for_expected_rowcount_at_query(
$$SELECT COUNT(*) FROM gen_pub_repl_a WHERE id=1 AND a=5 AND b=50$$, 1);
wait_for_expected_rowcount_at_query
---------------------------------------------------------------------
(1 row)
SELECT * FROM gen_pub_repl_a ORDER BY id; -- expect (1,5,50) (2,7,70) (3,9,90)
id | a | b
---------------------------------------------------------------------
1 | 5 | 50
2 | 7 | 70
3 | 9 | 90
(3 rows)
-- Cleanup Case A
DROP SUBSCRIPTION sub_gen_repl_a;
NOTICE: dropped replication slot "sub_gen_repl_a" on publisher
DROP TABLE gen_pub_repl_a;
\c - - - :worker_1_port
SET search_path TO pg18_publication;
DROP PUBLICATION pub_gen_repl_a;
DROP TABLE gen_pub_repl_a;
-- --------------------------
-- Case B: column list excludes generated column b but publish_generated_columns=stored
-- Expectation (precedence): b is NOT replicated because column list excludes it, so subscriber b stays NULL.
-- --------------------------
\c - - - :worker_1_port
SET search_path TO pg18_publication;
DROP PUBLICATION IF EXISTS pub_gen_repl_b;
NOTICE: publication "pub_gen_repl_b" does not exist, skipping
DROP TABLE IF EXISTS gen_pub_repl_b;
NOTICE: table "gen_pub_repl_b" does not exist, skipping
CREATE TABLE gen_pub_repl_b (
id int PRIMARY KEY,
a int,
b int GENERATED ALWAYS AS (a * 10) STORED
);
INSERT INTO gen_pub_repl_b (id, a) VALUES (1, 2), (2, 7);
SET citus.enable_ddl_propagation TO off;
CREATE PUBLICATION pub_gen_repl_b
FOR TABLE gen_pub_repl_b (id, a)
WITH (publish = 'insert, update', publish_generated_columns = stored);
RESET citus.enable_ddl_propagation;
\c - - - :worker_2_port
SET search_path TO pg18_publication;
DROP SUBSCRIPTION IF EXISTS sub_gen_repl_b;
NOTICE: subscription "sub_gen_repl_b" does not exist, skipping
DROP TABLE IF EXISTS gen_pub_repl_b;
NOTICE: table "gen_pub_repl_b" does not exist, skipping
CREATE TABLE gen_pub_repl_b (
id int PRIMARY KEY,
a int,
b int
);
CREATE SUBSCRIPTION sub_gen_repl_b
CONNECTION :conninfo_w1
PUBLICATION pub_gen_repl_b
WITH (copy_data = true);
NOTICE: created replication slot "sub_gen_repl_b" on publisher
SELECT wait_for_expected_rowcount_at_table('gen_pub_repl_b', 2);
wait_for_expected_rowcount_at_table
---------------------------------------------------------------------
(1 row)
SELECT * FROM gen_pub_repl_b ORDER BY id; -- expect b is NULL
id | a | b
---------------------------------------------------------------------
1 | 2 |
2 | 7 |
(2 rows)
\c - - - :worker_1_port
SET search_path TO pg18_publication;
INSERT INTO gen_pub_repl_b (id, a) VALUES (3, 9);
UPDATE gen_pub_repl_b SET a = 5 WHERE id = 1;
\c - - - :worker_2_port
SET search_path TO pg18_publication;
SELECT wait_for_expected_rowcount_at_table('gen_pub_repl_b', 3);
wait_for_expected_rowcount_at_table
---------------------------------------------------------------------
(1 row)
SELECT wait_for_expected_rowcount_at_query(
$$SELECT COUNT(*) FROM gen_pub_repl_b WHERE id=1 AND a=5 AND b IS NULL$$, 1);
wait_for_expected_rowcount_at_query
---------------------------------------------------------------------
(1 row)
SELECT * FROM gen_pub_repl_b ORDER BY id;
id | a | b
---------------------------------------------------------------------
1 | 5 |
2 | 7 |
3 | 9 |
(3 rows)
-- Cleanup B
DROP SUBSCRIPTION sub_gen_repl_b;
NOTICE: dropped replication slot "sub_gen_repl_b" on publisher
DROP TABLE gen_pub_repl_b;
\c - - - :worker_1_port
SET search_path TO pg18_publication;
DROP PUBLICATION pub_gen_repl_b;
DROP TABLE gen_pub_repl_b;
\c - - - :master_port
SET search_path TO pg18_publication;
SET client_min_messages TO ERROR;
DROP SCHEMA pg18_publication CASCADE;
RESET client_min_messages;
SET search_path TO pg18_nn;
-- END: PG18: verify publish_generated_columns is preserved for distributed tables
-- PG18 Feature: FOREIGN KEY constraints can be specified as NOT ENFORCED
-- PG18 commit: https://github.com/postgres/postgres/commit/eec0040c4
CREATE TABLE customers(
customer_id INT GENERATED ALWAYS AS IDENTITY,
customer_name VARCHAR(255) NOT NULL,
PRIMARY KEY(customer_id)
);
SET citus.shard_replication_factor TO 1;
SELECT create_distributed_table('customers', 'customer_id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
CREATE TABLE contacts(
contact_id INT GENERATED ALWAYS AS IDENTITY,
customer_id INT,
contact_name VARCHAR(255) NOT NULL,
phone VARCHAR(15),
email VARCHAR(100),
CONSTRAINT fk_customer
FOREIGN KEY(customer_id)
REFERENCES customers(customer_id)
ON DELETE CASCADE NOT ENFORCED
);
-- The foreign key constraint is propagated to worker nodes.
SELECT create_distributed_table('contacts', 'customer_id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
SELECT pg_get_constraintdef(oid, true) AS "Definition" FROM pg_constraint
WHERE conrelid = 'contacts'::regclass AND conname = 'fk_customer';
Definition
---------------------------------------------------------------------
FOREIGN KEY (customer_id) REFERENCES customers(customer_id) ON DELETE CASCADE NOT ENFORCED
(1 row)
INSERT INTO customers(customer_name)
VALUES('BlueBird Inc'),
('Dolphin LLC');
INSERT INTO contacts(customer_id, contact_name, phone, email)
VALUES(1,'John Doe','(408)-111-1234','john.doe@example.com'),
(1,'Jane Doe','(408)-111-1235','jane.doe@example.com'),
(2,'David Wright','(408)-222-1234','david.wright@example.com');
DELETE FROM customers WHERE customer_name = 'Dolphin LLC';
-- After deleting 'Dolphin LLC' from customers, the corresponding contact
-- 'David Wright' is not deleted from contacts due to the NOT ENFORCED.
SELECT * FROM contacts ORDER BY contact_id;
contact_id | customer_id | contact_name | phone | email
---------------------------------------------------------------------
1 | 1 | John Doe | (408)-111-1234 | john.doe@example.com
2 | 1 | Jane Doe | (408)-111-1235 | jane.doe@example.com
3 | 2 | David Wright | (408)-222-1234 | david.wright@example.com
(3 rows)
-- Test that ALTER TABLE .. ADD CONSTRAINT .. FOREIGN KEY .. NOT ENFORCED
-- is propagated to worker nodes. First drop the foreign key:
ALTER TABLE contacts DROP CONSTRAINT fk_customer;
SELECT pg_get_constraintdef(oid, true) AS "Definition" FROM pg_constraint
WHERE conrelid = 'contacts'::regclass AND conname = 'fk_customer';
Definition
---------------------------------------------------------------------
(0 rows)
-- Now add the foreign key constraint back with NOT ENFORCED.
ALTER TABLE contacts ADD CONSTRAINT fk_customer
FOREIGN KEY(customer_id)
REFERENCES customers(customer_id)
ON DELETE CASCADE NOT ENFORCED;
-- The foreign key is propagated to worker nodes.
SELECT pg_get_constraintdef(oid, true) AS "Definition" FROM pg_constraint
WHERE conrelid = 'contacts'::regclass AND conname = 'fk_customer';
Definition
---------------------------------------------------------------------
FOREIGN KEY (customer_id) REFERENCES customers(customer_id) ON DELETE CASCADE NOT ENFORCED
(1 row)
DELETE FROM customers WHERE customer_name = 'BlueBird Inc';
-- The customers table is now empty but the contacts table still has
-- the contacts due to the NOT ENFORCED foreign key.
SELECT * FROM customers ORDER BY customer_id;
customer_id | customer_name
---------------------------------------------------------------------
(0 rows)
SELECT * FROM contacts ORDER BY contact_id;
contact_id | customer_id | contact_name | phone | email
---------------------------------------------------------------------
1 | 1 | John Doe | (408)-111-1234 | john.doe@example.com
2 | 1 | Jane Doe | (408)-111-1235 | jane.doe@example.com
3 | 2 | David Wright | (408)-222-1234 | david.wright@example.com
(3 rows)
-- ALTER TABLE .. ALTER CONSTRAINT is not supported in Citus,
-- so the following command should fail
ALTER TABLE contacts ALTER CONSTRAINT fk_customer ENFORCED;
ERROR: alter table command is currently unsupported
DETAIL: Only ADD|DROP COLUMN, SET|DROP NOT NULL, SET|DROP DEFAULT, ADD|DROP|VALIDATE CONSTRAINT, SET (), RESET (), ENABLE|DISABLE|NO FORCE|FORCE ROW LEVEL SECURITY, ATTACH|DETACH PARTITION and TYPE subcommands are supported.
-- PG18 Feature: ENFORCED / NOT ENFORCED check constraints
-- PG18 commit: https://github.com/postgres/postgres/commit/ca87c415e
-- In Citus, CHECK constraints are propagated on promoting a postgres table
-- to a citus table, on adding a new CHECK constraint to a citus table, and
-- on adding a node to a citus cluster. Postgres does not support altering a
-- check constraint's enforcement status, so Citus does not either.
CREATE TABLE NE_CHECK_TBL (x int, y int,
CONSTRAINT CHECK_X CHECK (x > 3) NOT ENFORCED,
CONSTRAINT CHECK_Y CHECK (y < 20) ENFORCED
);
SET citus.next_shard_id TO 4754044;
SELECT create_distributed_table('ne_check_tbl', 'x');
create_distributed_table
---------------------------------------------------------------------
(1 row)
-- CHECK_X is NOT ENFORCED, so these inserts should succeed
INSERT INTO NE_CHECK_TBL (x) VALUES (5), (4), (3), (2), (6), (1);
SELECT x FROM NE_CHECK_TBL ORDER BY x;
x
---------------------------------------------------------------------
1
2
3
4
5
6
(6 rows)
-- CHECK_Y is ENFORCED, so this insert should fail
INSERT INTO NE_CHECK_TBL (x, y) VALUES (1, 15), (2, 25), (3, 10), (4, 30);
ERROR: new row for relation "ne_check_tbl_4754045" violates check constraint "check_y"
DETAIL: Failing row contains (4, 30).
CONTEXT: while executing command on localhost:xxxxx
-- Test adding new constraints with enforcement status
ALTER TABLE NE_CHECK_TBL
ADD CONSTRAINT CHECK_Y2 CHECK (y > 10) NOT ENFORCED;
-- CHECK_Y2 is NOT ENFORCED, so these inserts should succeed
INSERT INTO NE_CHECK_TBL (x, y) VALUES (1, 8), (2, 9), (3, 10), (4, 11);
SELECT x, y FROM NE_CHECK_TBL ORDER BY x, y;
x | y
---------------------------------------------------------------------
1 | 8
1 |
2 | 9
2 |
3 | 10
3 |
4 | 11
4 |
5 |
6 |
(10 rows)
ALTER TABLE NE_CHECK_TBL
ADD CONSTRAINT CHECK_X2 CHECK (x < 10) ENFORCED;
-- CHECK_X2 is ENFORCED, so these inserts should fail
INSERT INTO NE_CHECK_TBL (x) VALUES (5), (15), (8), (12);
ERROR: new row for relation "ne_check_tbl_4754044" violates check constraint "check_x2_4754044"
DETAIL: Failing row contains (15, null).
CONTEXT: while executing command on localhost:xxxxx
-- PG18 Feature: dropping of constraints ONLY on partitioned tables
-- PG18 commit: https://github.com/postgres/postgres/commit/4dea33ce7
-- Here we verify that dropping constraints ONLY on partitioned tables
-- works correctly in Citus. This is done by repeating the tests of the
-- PG commit (4dea33ce7) on a table that is a distributed table in Citus,
-- in addition to a Postgres partitioned table.
CREATE TABLE partitioned_table (
a int,
b char(3)
) PARTITION BY LIST (a);
SELECT create_distributed_table('partitioned_table', 'a');
create_distributed_table
---------------------------------------------------------------------
(1 row)
-- check that violating rows are correctly reported
CREATE TABLE part_2 (LIKE partitioned_table);
INSERT INTO part_2 VALUES (3, 'aaa');
ALTER TABLE partitioned_table ATTACH PARTITION part_2 FOR VALUES IN (2);
NOTICE: Copying data from local table...
NOTICE: copying the data has completed
DETAIL: The local data in the table is no longer visible, but is still on disk.
HINT: To remove the local data, run: SELECT truncate_local_data_after_distributing_table($$pg18_nn.part_2$$)
ERROR: partition constraint of relation "part_2" is violated by some row
-- should be ok after deleting the bad row
DELETE FROM part_2;
ALTER TABLE partitioned_table ATTACH PARTITION part_2 FOR VALUES IN (2);
-- PG18's "cannot add NOT NULL or check constraints to *only* the parent, when
-- partitions exist" applies to Citus distributed tables as well.
ALTER TABLE ONLY partitioned_table ALTER b SET NOT NULL;
ERROR: constraint must be added to child tables too
HINT: Do not specify the ONLY keyword.
ALTER TABLE ONLY partitioned_table ADD CONSTRAINT check_b CHECK (b <> 'zzz');
ERROR: constraint must be added to child tables too
-- Dropping constraints from parent should be ok
ALTER TABLE partitioned_table ALTER b SET NOT NULL;
ALTER TABLE ONLY partitioned_table ALTER b DROP NOT NULL;
ALTER TABLE partitioned_table ADD CONSTRAINT check_b CHECK (b <> 'zzz');
ALTER TABLE ONLY partitioned_table DROP CONSTRAINT check_b;
-- ... and the partitions still have the NOT NULL constraint:
select relname, attname, attnotnull
from pg_class inner join pg_attribute on (oid=attrelid)
where relname = 'part_2' and attname = 'b' ;
relname | attname | attnotnull
---------------------------------------------------------------------
part_2 | b | t
(1 row)
-- ... and the check_b constraint:
select relname, conname, pg_get_expr(conbin, conrelid, true)
from pg_class inner join pg_constraint on (pg_class.oid=conrelid)
where relname = 'part_2' and conname = 'check_b' ;
relname | conname | pg_get_expr
---------------------------------------------------------------------
part_2 | check_b | b <> 'zzz'::bpchar
(1 row)
-- PG18 Feature: partitioned tables can have NOT VALID foreign keys
-- PG18 commit: https://github.com/postgres/postgres/commit/b663b9436
-- As with dropping constraints only on the partitioned tables, for
-- NOT VALID foreign keys, we verify that foreign key declarations
-- that use NOT VALID work correctly in Citus by repeating the tests
-- of the PG commit (b663b9436) on a table that is a distributed
-- table in Citus, in addition to a Postgres partitioned table.
CREATE TABLE fk_notpartitioned_pk (a int, b int, PRIMARY KEY (a, b), c int);
CREATE TABLE fk_partitioned_fk (b int, a int) PARTITION BY RANGE (a, b);
SELECT create_reference_table('fk_notpartitioned_pk');
create_reference_table
---------------------------------------------------------------------
(1 row)
SELECT create_distributed_table('fk_partitioned_fk', 'a');
create_distributed_table
---------------------------------------------------------------------
(1 row)
ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT VALID;
-- Attaching a child table with the same valid foreign key constraint.
CREATE TABLE fk_partitioned_fk_1 (a int, b int);
ALTER TABLE fk_partitioned_fk_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-- Child constraint will remain valid.
SELECT conname, convalidated, conrelid::regclass FROM pg_constraint
WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid;
conname | convalidated | conrelid
---------------------------------------------------------------------
fk_partitioned_fk_a_b_fkey | f | fk_partitioned_fk
fk_partitioned_fk_1_a_b_fkey | t | fk_partitioned_fk_1
(2 rows)
-- Validate the constraint
ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- All constraints are now valid.
SELECT conname, convalidated, conrelid::regclass FROM pg_constraint
WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid;
conname | convalidated | conrelid
---------------------------------------------------------------------
fk_partitioned_fk_a_b_fkey | t | fk_partitioned_fk
fk_partitioned_fk_1_a_b_fkey | t | fk_partitioned_fk_1
(2 rows)
-- Attaching a child with a NOT VALID constraint.
CREATE TABLE fk_partitioned_fk_2 (a int, b int);
INSERT INTO fk_partitioned_fk_2 VALUES(1000, 1000); -- doesn't exist in referenced table
ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT VALID;
-- It will fail because the attach operation implicitly validates the data.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
NOTICE: Copying data from local table...
NOTICE: copying the data has completed
DETAIL: The local data in the table is no longer visible, but is still on disk.
HINT: To remove the local data, run: SELECT truncate_local_data_after_distributing_table($$pg18_nn.fk_partitioned_fk_2$$)
ERROR: insert or update on table "fk_partitioned_fk_2_4754072" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey_4754072"
DETAIL: Key (a, b)=(1000, 1000) is not present in table "fk_notpartitioned_pk_4754060".
CONTEXT: while executing command on localhost:xxxxx
-- Remove the invalid data and try again.
TRUNCATE fk_partitioned_fk_2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
-- The child constraint will also be valid.
SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_partitioned_fk_2'::regclass;
conname | convalidated
---------------------------------------------------------------------
fk_partitioned_fk_2_a_b_fkey | t
(1 row)
-- Test case where the child constraint is invalid, the grandchild constraint
-- is valid, and the validation for the grandchild should be skipped when a
-- valid constraint is applied to the top parent.
CREATE TABLE fk_partitioned_fk_3 (a int, b int) PARTITION BY RANGE (a, b);
ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT VALID;
SELECT create_distributed_table('fk_partitioned_fk_3', 'a');
create_distributed_table
---------------------------------------------------------------------
(1 row)
CREATE TABLE fk_partitioned_fk_3_1 (a int, b int);
ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
SELECT create_distributed_table('fk_partitioned_fk_3_1', 'a');
create_distributed_table
---------------------------------------------------------------------
(1 row)
ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_1 FOR VALUES FROM (2000,2000) TO (3000,3000);
-- Fails because Citus does not support multi-level (grandchild) partitions
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3 FOR VALUES FROM (2000,2000) TO (3000,3000);
ERROR: Citus doesn't support multi-level partitioned tables
DETAIL: Relation "fk_partitioned_fk_3" is partitioned table itself and it is also partition of relation "fk_partitioned_fk".
-- All constraints are now valid, except for fk_partitioned_fk_3
-- because the attach failed because of Citus not yet supporting
-- multi-level partitions.
SELECT conname, convalidated, conrelid::regclass FROM pg_constraint
WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid;
conname | convalidated | conrelid
---------------------------------------------------------------------
fk_partitioned_fk_a_b_fkey | t | fk_partitioned_fk
fk_partitioned_fk_1_a_b_fkey | t | fk_partitioned_fk_1
fk_partitioned_fk_2_a_b_fkey | t | fk_partitioned_fk_2
fk_partitioned_fk_3_a_b_fkey | f | fk_partitioned_fk_3
fk_partitioned_fk_3_1_a_b_fkey | t | fk_partitioned_fk_3_1
(5 rows)
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk CASCADE;
NOTICE: drop cascades to constraint fk_partitioned_fk_3_a_b_fkey on table fk_partitioned_fk_3
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
SELECT create_distributed_table('fk_partitioned_pk', 'a');
create_distributed_table
---------------------------------------------------------------------
(1 row)
CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
SELECT create_distributed_table('fk_notpartitioned_fk', 'a');
create_distributed_table
---------------------------------------------------------------------
(1 row)
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
-- Constraint will be invalid.
SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass;
conname | convalidated
---------------------------------------------------------------------
fk_notpartitioned_fk_a_b_fkey | f
fk_notpartitioned_fk_a_b_fkey_1 | f
(2 rows)
ALTER TABLE fk_notpartitioned_fk VALIDATE CONSTRAINT fk_notpartitioned_fk_a_b_fkey;
-- All constraints are now valid.
SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass;
conname | convalidated
---------------------------------------------------------------------
fk_notpartitioned_fk_a_b_fkey | t
fk_notpartitioned_fk_a_b_fkey_1 | t
(2 rows)
DROP TABLE fk_notpartitioned_fk, fk_partitioned_pk;
-- PG18 Feature: Generated Virtual Columns
-- PG18 commit: https://github.com/postgres/postgres/commit/83ea6c540
-- Verify that generated virtual columns are supported on distributed tables.
CREATE TABLE v_reading (
celsius DECIMAL(5,2),
farenheit DECIMAL(6, 2) GENERATED ALWAYS AS (celsius * 9/5 + 32) VIRTUAL,
created_at TIMESTAMPTZ DEFAULT now(),
device_id INT
);
-- Cannot distribute on a generated column (#4616) applies
-- to VIRTUAL columns.
SELECT create_distributed_table('v_reading', 'farenheit');
ERROR: cannot distribute relation: v_reading
DETAIL: Distribution column must not use GENERATED ALWAYS AS (...) VIRTUAL.
SELECT create_distributed_table('v_reading', 'device_id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
INSERT INTO v_reading (celsius, device_id) VALUES (0, 1), (100, 1), (37.5, 2), (25, 2), (-40, 3);
SELECT device_id, celsius, farenheit FROM v_reading ORDER BY device_id;
device_id | celsius | farenheit
---------------------------------------------------------------------
1 | 0.00 | 32.00
1 | 100.00 | 212.00
2 | 37.50 | 99.50
2 | 25.00 | 77.00
3 | -40.00 | -40.00
(5 rows)
ALTER TABLE v_reading ADD COLUMN kelvin DECIMAL(6, 2) GENERATED ALWAYS AS (celsius + 273.15) VIRTUAL;
SELECT device_id, celsius, kelvin FROM v_reading ORDER BY device_id, celsius;
device_id | celsius | kelvin
---------------------------------------------------------------------
1 | 0.00 | 273.15
1 | 100.00 | 373.15
2 | 25.00 | 298.15
2 | 37.50 | 310.65
3 | -40.00 | 233.15
(5 rows)
-- Show all columns that are generated
SELECT s.relname, a.attname, a.attgenerated
FROM pg_class s
JOIN pg_attribute a ON a.attrelid=s.oid
WHERE s.relname LIKE 'v_reading%' and attgenerated::int != 0
ORDER BY 1,2;
relname | attname | attgenerated
---------------------------------------------------------------------
v_reading | farenheit | v
v_reading | kelvin | v
(2 rows)
-- Generated columns are virtual by default - repeat the test without VIRTUAL keyword
CREATE TABLE d_reading (
celsius DECIMAL(5,2),
farenheit DECIMAL(6, 2) GENERATED ALWAYS AS (celsius * 9/5 + 32),
created_at TIMESTAMPTZ DEFAULT now(),
device_id INT
);
SELECT create_distributed_table('d_reading', 'farenheit');
ERROR: cannot distribute relation: d_reading
DETAIL: Distribution column must not use GENERATED ALWAYS AS (...) VIRTUAL.
SELECT create_distributed_table('d_reading', 'device_id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
INSERT INTO d_reading (celsius, device_id) VALUES (0, 1), (100, 1), (37.5, 2), (25, 2), (-40, 3);
SELECT device_id, celsius, farenheit FROM d_reading ORDER BY device_id;
device_id | celsius | farenheit
---------------------------------------------------------------------
1 | 0.00 | 32.00
1 | 100.00 | 212.00
2 | 37.50 | 99.50
2 | 25.00 | 77.00
3 | -40.00 | -40.00
(5 rows)
ALTER TABLE d_reading ADD COLUMN kelvin DECIMAL(6, 2) GENERATED ALWAYS AS (celsius + 273.15) VIRTUAL;
SELECT device_id, celsius, kelvin FROM d_reading ORDER BY device_id, celsius;
device_id | celsius | kelvin
---------------------------------------------------------------------
1 | 0.00 | 273.15
1 | 100.00 | 373.15
2 | 25.00 | 298.15
2 | 37.50 | 310.65
3 | -40.00 | 233.15
(5 rows)
-- Show all columns that are generated
SELECT s.relname, a.attname, a.attgenerated
FROM pg_class s
JOIN pg_attribute a ON a.attrelid=s.oid
WHERE s.relname LIKE 'd_reading%' and attgenerated::int != 0
ORDER BY 1,2;
relname | attname | attgenerated
---------------------------------------------------------------------
d_reading | farenheit | v
d_reading | kelvin | v
(2 rows)
-- COPY implementation needs to handle GENERATED ALWAYS AS (...) VIRTUAL columns.
\COPY d_reading FROM STDIN WITH DELIMITER ','
SELECT device_id, count(device_id) as count, round(avg(celsius), 2) as avg, min(farenheit), max(farenheit)
FROM d_reading
GROUP BY device_id
ORDER BY count DESC;
device_id | count | avg | min | max
---------------------------------------------------------------------
1 | 12 | 20.00 | 32.00 | 212.00
5 | 10 | 13.20 | 33.80 | 73.40
2 | 2 | 31.25 | 77.00 | 99.50
3 | 1 | -40.00 | -40.00 | -40.00
(4 rows)
-- Test GROUP BY on tables with generated virtual columns - this requires
-- special case handling in distributed planning. Test it out on some
-- some queries involving joins and set operations.
SELECT device_id, max(kelvin) as Kel
FROM v_reading
WHERE (device_id, celsius) NOT IN (SELECT device_id, max(celsius) FROM v_reading GROUP BY device_id)
GROUP BY device_id
ORDER BY device_id ASC;
device_id | kel
---------------------------------------------------------------------
1 | 273.15
2 | 298.15
(2 rows)
SELECT device_id, round(AVG( (d_farenheit + v_farenheit) / 2), 2) as Avg_Far
FROM (SELECT *
FROM (SELECT device_id, round(AVG(farenheit),2) as d_farenheit
FROM d_reading
GROUP BY device_id) AS subq
RIGHT JOIN (SELECT device_id, MAX(farenheit) AS v_farenheit
FROM d_reading
GROUP BY device_id) AS subq2
USING (device_id)
) AS finalq
GROUP BY device_id
ORDER BY device_id ASC;
device_id | avg_far
---------------------------------------------------------------------
1 | 140.00
2 | 93.88
3 | -40.00
5 | 64.58
(4 rows)
SELECT device_id, MAX(farenheit) as farenheit
FROM
((SELECT device_id, round(AVG(farenheit),2) as farenheit
FROM d_reading
GROUP BY device_id)
UNION ALL (SELECT device_id, MAX(farenheit) AS farenheit
FROM d_reading
GROUP BY device_id) ) AS unioned
GROUP BY device_id
ORDER BY device_id ASC;
device_id | farenheit
---------------------------------------------------------------------
1 | 212.00
2 | 99.50
3 | -40.00
5 | 73.40
(4 rows)
SELECT device_id, MAX(farenheit) as farenheit
FROM
((SELECT device_id, round(AVG(farenheit),2) as farenheit
FROM d_reading
GROUP BY device_id)
INTERSECT (SELECT device_id, MAX(farenheit) AS farenheit
FROM d_reading
GROUP BY device_id) ) AS intersected
GROUP BY device_id
ORDER BY device_id ASC;
device_id | farenheit
---------------------------------------------------------------------
3 | -40.00
(1 row)
SELECT device_id, MAX(farenheit) as farenheit
FROM
((SELECT device_id, round(AVG(farenheit),2) as farenheit
FROM d_reading
GROUP BY device_id)
EXCEPT (SELECT device_id, MAX(farenheit) AS farenheit
FROM d_reading
GROUP BY device_id) ) AS excepted
GROUP BY device_id
ORDER BY device_id ASC;
device_id | farenheit
---------------------------------------------------------------------
1 | 68.00
2 | 88.25
5 | 55.76
(3 rows)
-- Ensure that UDFs such as alter_distributed_table, undistribute_table
-- and add_local_table_to_metadata work fine with VIRTUAL columns. For
-- this, PR #4616 changes are modified to handle VIRTUAL columns in
-- addition to STORED columns.
CREATE TABLE generated_stored_dist (
col_1 int,
"col\'_2" text,
col_3 text generated always as (UPPER("col\'_2")) virtual
);
SELECT create_distributed_table ('generated_stored_dist', 'col_1');
create_distributed_table
---------------------------------------------------------------------
(1 row)
INSERT INTO generated_stored_dist VALUES (1, 'text_1'), (2, 'text_2');
SELECT * FROM generated_stored_dist ORDER BY 1,2,3;
col_1 | col\'_2 | col_3
---------------------------------------------------------------------
1 | text_1 | TEXT_1
2 | text_2 | TEXT_2
(2 rows)
INSERT INTO generated_stored_dist VALUES (1, 'text_1'), (2, 'text_2');
SELECT alter_distributed_table('generated_stored_dist', shard_count := 5, cascade_to_colocated := false);
NOTICE: creating a new table for pg18_nn.generated_stored_dist
NOTICE: moving the data of pg18_nn.generated_stored_dist
NOTICE: dropping the old pg18_nn.generated_stored_dist
NOTICE: renaming the new table to pg18_nn.generated_stored_dist
alter_distributed_table
---------------------------------------------------------------------
(1 row)
SELECT * FROM generated_stored_dist ORDER BY 1,2,3;
col_1 | col\'_2 | col_3
---------------------------------------------------------------------
1 | text_1 | TEXT_1
1 | text_1 | TEXT_1
2 | text_2 | TEXT_2
2 | text_2 | TEXT_2
(4 rows)
CREATE TABLE generated_stored_local (
col_1 int,
"col\'_2" text,
col_3 text generated always as (UPPER("col\'_2")) stored
);
SELECT citus_add_local_table_to_metadata('generated_stored_local');
citus_add_local_table_to_metadata
---------------------------------------------------------------------
(1 row)
INSERT INTO generated_stored_local VALUES (1, 'text_1'), (2, 'text_2');
SELECT * FROM generated_stored_local ORDER BY 1,2,3;
col_1 | col\'_2 | col_3
---------------------------------------------------------------------
1 | text_1 | TEXT_1
2 | text_2 | TEXT_2
(2 rows)
SELECT create_distributed_table ('generated_stored_local', 'col_1');
NOTICE: Copying data from local table...
NOTICE: copying the data has completed
DETAIL: The local data in the table is no longer visible, but is still on disk.
HINT: To remove the local data, run: SELECT truncate_local_data_after_distributing_table($$pg18_nn.generated_stored_local$$)
create_distributed_table
---------------------------------------------------------------------
(1 row)
INSERT INTO generated_stored_local VALUES (1, 'text_1'), (2, 'text_2');
SELECT * FROM generated_stored_local ORDER BY 1,2,3;
col_1 | col\'_2 | col_3
---------------------------------------------------------------------
1 | text_1 | TEXT_1
1 | text_1 | TEXT_1
2 | text_2 | TEXT_2
2 | text_2 | TEXT_2
(4 rows)
CREATE TABLE generated_stored_ref (
col_1 int,
col_2 int,
col_3 int generated always as (col_1+col_2) virtual,
col_4 int,
col_5 int generated always as (col_4*2-col_1) virtual
);
SELECT create_reference_table ('generated_stored_ref');
create_reference_table
---------------------------------------------------------------------
(1 row)
INSERT INTO generated_stored_ref (col_1, col_4) VALUES (1,2), (11,12);
INSERT INTO generated_stored_ref (col_1, col_2, col_4) VALUES (100,101,102), (200,201,202);
SELECT * FROM generated_stored_ref ORDER BY 1,2,3,4,5;
col_1 | col_2 | col_3 | col_4 | col_5
---------------------------------------------------------------------
1 | | | 2 | 3
11 | | | 12 | 13
100 | 101 | 201 | 102 | 104
200 | 201 | 401 | 202 | 204
(4 rows)
BEGIN;
SELECT undistribute_table('generated_stored_ref');
NOTICE: creating a new table for pg18_nn.generated_stored_ref
NOTICE: moving the data of pg18_nn.generated_stored_ref
NOTICE: dropping the old pg18_nn.generated_stored_ref
NOTICE: renaming the new table to pg18_nn.generated_stored_ref
undistribute_table
---------------------------------------------------------------------
(1 row)
INSERT INTO generated_stored_ref (col_1, col_4) VALUES (11,12), (21,22);
INSERT INTO generated_stored_ref (col_1, col_2, col_4) VALUES (200,201,202), (300,301,302);
SELECT * FROM generated_stored_ref ORDER BY 1,2,3,4,5;
col_1 | col_2 | col_3 | col_4 | col_5
---------------------------------------------------------------------
1 | | | 2 | 3
11 | | | 12 | 13
11 | | | 12 | 13
21 | | | 22 | 23
100 | 101 | 201 | 102 | 104
200 | 201 | 401 | 202 | 204
200 | 201 | 401 | 202 | 204
300 | 301 | 601 | 302 | 304
(8 rows)
ROLLBACK;
BEGIN;
-- drop some of the columns not having "generated always as virtual" expressions
SET client_min_messages TO WARNING;
ALTER TABLE generated_stored_ref DROP COLUMN col_1 CASCADE;
RESET client_min_messages;
ALTER TABLE generated_stored_ref DROP COLUMN col_4;
-- show that undistribute_table works fine
SELECT undistribute_table('generated_stored_ref');
NOTICE: creating a new table for pg18_nn.generated_stored_ref
NOTICE: moving the data of pg18_nn.generated_stored_ref
NOTICE: dropping the old pg18_nn.generated_stored_ref
NOTICE: renaming the new table to pg18_nn.generated_stored_ref
undistribute_table
---------------------------------------------------------------------
(1 row)
INSERT INTO generated_stored_ref VALUES (5);
SELECT * FROM generated_stored_REF ORDER BY 1;
col_2
---------------------------------------------------------------------
5
101
201
(5 rows)
ROLLBACK;
BEGIN;
-- now drop all columns
ALTER TABLE generated_stored_ref DROP COLUMN col_3;
ALTER TABLE generated_stored_ref DROP COLUMN col_5;
ALTER TABLE generated_stored_ref DROP COLUMN col_1;
ALTER TABLE generated_stored_ref DROP COLUMN col_2;
ALTER TABLE generated_stored_ref DROP COLUMN col_4;
-- show that undistribute_table works fine
SELECT undistribute_table('generated_stored_ref');
NOTICE: creating a new table for pg18_nn.generated_stored_ref
NOTICE: moving the data of pg18_nn.generated_stored_ref
NOTICE: dropping the old pg18_nn.generated_stored_ref
NOTICE: renaming the new table to pg18_nn.generated_stored_ref
undistribute_table
---------------------------------------------------------------------
(1 row)
SELECT * FROM generated_stored_ref;
--
(4 rows)
ROLLBACK;
-- PG18 Feature: VACUUM/ANALYZE support ONLY to limit processing to the parent.
-- For Citus, ensure ONLY does not trigger shard propagation.
-- PG18 commit: https://github.com/postgres/postgres/commit/62ddf7ee9
CREATE SCHEMA pg18_vacuum_part;
SET search_path TO pg18_vacuum_part;
CREATE TABLE vac_analyze_only (a int);
SELECT create_distributed_table('vac_analyze_only', 'a');
create_distributed_table
---------------------------------------------------------------------
(1 row)
INSERT INTO vac_analyze_only VALUES (1), (2), (3);
-- ANALYZE (no ONLY) should recurse into shard placements
ANALYZE vac_analyze_only;
\c - - - :worker_1_port
SET search_path TO pg18_vacuum_part;
SELECT coalesce(max(last_analyze), 'epoch'::timestamptz) AS analyze_before_only
FROM pg_stat_user_tables
WHERE schemaname = 'pg18_vacuum_part'
AND relname LIKE 'vac_analyze_only_%'
\gset
\c - - - :master_port
SET search_path TO pg18_vacuum_part;
-- ANALYZE ONLY should not recurse into shard placements
ANALYZE ONLY vac_analyze_only;
\c - - - :worker_1_port
SET search_path TO pg18_vacuum_part;
SELECT max(last_analyze) = :'analyze_before_only'::timestamptz
AS analyze_only_skipped
FROM pg_stat_user_tables
WHERE schemaname = 'pg18_vacuum_part'
AND relname LIKE 'vac_analyze_only_%';
analyze_only_skipped
---------------------------------------------------------------------
t
(1 row)
\c - - - :master_port
SET search_path TO pg18_vacuum_part;
-- VACUUM (no ONLY) should recurse into shard placements
VACUUM vac_analyze_only;
\c - - - :worker_1_port
SET search_path TO pg18_vacuum_part;
SELECT coalesce(max(last_vacuum), 'epoch'::timestamptz) AS vacuum_before_only
FROM pg_stat_user_tables
WHERE schemaname = 'pg18_vacuum_part'
AND relname LIKE 'vac_analyze_only_%'
\gset
\c - - - :master_port
SET search_path TO pg18_vacuum_part;
-- VACUUM ONLY should not recurse into shard placements
VACUUM ONLY vac_analyze_only;
\c - - - :worker_1_port
SET search_path TO pg18_vacuum_part;
SELECT max(last_vacuum) = :'vacuum_before_only'::timestamptz
AS vacuum_only_skipped
FROM pg_stat_user_tables
WHERE schemaname = 'pg18_vacuum_part'
AND relname LIKE 'vac_analyze_only_%';
vacuum_only_skipped
---------------------------------------------------------------------
t
(1 row)
\c - - - :master_port
SET search_path TO pg18_vacuum_part;
DROP SCHEMA pg18_vacuum_part CASCADE;
NOTICE: drop cascades to table vac_analyze_only
SET search_path TO pg18_nn;
-- END PG18 Feature: VACUUM/ANALYZE support ONLY to limit processing to the parent
-- PG18 Feature: VACUUM/ANALYZE ONLY on a partitioned distributed table
-- Ensure Citus does not recurse into shard placements when ONLY is used
-- on the partitioned parent.
-- PG18 commit: https://github.com/postgres/postgres/commit/62ddf7ee9
CREATE SCHEMA pg18_vacuum_part_dist;
SET search_path TO pg18_vacuum_part_dist;
SET citus.shard_count = 2;
SET citus.shard_replication_factor = 1;
CREATE TABLE part_dist (id int, v int) PARTITION BY RANGE (id);
CREATE TABLE part_dist_1 PARTITION OF part_dist FOR VALUES FROM (1) TO (100);
CREATE TABLE part_dist_2 PARTITION OF part_dist FOR VALUES FROM (100) TO (200);
SELECT create_distributed_table('part_dist', 'id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
INSERT INTO part_dist
SELECT g, g FROM generate_series(1, 199) g;
-- ANALYZE (no ONLY) should recurse into partitions and shard placements
ANALYZE part_dist;
\c - - - :worker_1_port
SET search_path TO pg18_vacuum_part_dist;
SELECT coalesce(max(last_analyze), 'epoch'::timestamptz) AS analyze_before_only
FROM pg_stat_user_tables
WHERE schemaname = 'pg18_vacuum_part_dist'
AND relname LIKE 'part_dist_%'
\gset
\c - - - :master_port
SET search_path TO pg18_vacuum_part_dist;
-- ANALYZE ONLY should not recurse into shard placements
ANALYZE ONLY part_dist;
\c - - - :worker_1_port
SET search_path TO pg18_vacuum_part_dist;
SELECT max(last_analyze) = :'analyze_before_only'::timestamptz
AS analyze_only_partitioned_skipped
FROM pg_stat_user_tables
WHERE schemaname = 'pg18_vacuum_part_dist'
AND relname LIKE 'part_dist_%';
analyze_only_partitioned_skipped
---------------------------------------------------------------------
t
(1 row)
\c - - - :master_port
SET search_path TO pg18_vacuum_part_dist;
-- VACUUM (no ONLY) should recurse into partitions and shard placements
VACUUM part_dist;
\c - - - :worker_1_port
SET search_path TO pg18_vacuum_part_dist;
SELECT coalesce(max(last_vacuum), 'epoch'::timestamptz) AS vacuum_before_only
FROM pg_stat_user_tables
WHERE schemaname = 'pg18_vacuum_part_dist'
AND relname LIKE 'part_dist_%'
\gset
\c - - - :master_port
SET search_path TO pg18_vacuum_part_dist;
-- VACUUM ONLY parent: core warns and does no work; Citus must not
-- propagate to shard placements.
VACUUM ONLY part_dist;
WARNING: VACUUM ONLY of partitioned table "part_dist" has no effect
\c - - - :worker_1_port
SET search_path TO pg18_vacuum_part_dist;
SELECT max(last_vacuum) = :'vacuum_before_only'::timestamptz
AS vacuum_only_partitioned_skipped
FROM pg_stat_user_tables
WHERE schemaname = 'pg18_vacuum_part_dist'
AND relname LIKE 'part_dist_%';
vacuum_only_partitioned_skipped
---------------------------------------------------------------------
t
(1 row)
\c - - - :master_port
SET search_path TO pg18_vacuum_part_dist;
DROP SCHEMA pg18_vacuum_part_dist CASCADE;
NOTICE: drop cascades to table part_dist
SET search_path TO pg18_nn;
-- END PG18 Feature: VACUUM/ANALYZE ONLY on partitioned distributed table
-- PG18 Feature: text search with nondeterministic collations
-- PG18 commit: https://github.com/postgres/postgres/commit/329304c90
-- This test verifies that the PG18 tests apply to Citus tables; Citus
-- just passes through the collation info and text search queries to
-- worker shards.
CREATE COLLATION ignore_accents (provider = icu, locale = '@colStrength=primary;colCaseLevel=yes', deterministic = false);
NOTICE: using standard form "und-u-kc-ks-level1" for ICU locale "@colStrength=primary;colCaseLevel=yes"
-- nondeterministic collations
CREATE COLLATION ctest_det (provider = icu, locale = '', deterministic = true);
NOTICE: using standard form "und" for ICU locale ""
CREATE COLLATION ctest_nondet (provider = icu, locale = '', deterministic = false);
NOTICE: using standard form "und" for ICU locale ""
CREATE TABLE strtest1 (a int, b text);
SELECT create_distributed_table('strtest1', 'a');
create_distributed_table
---------------------------------------------------------------------
(1 row)
INSERT INTO strtest1 VALUES (1, U&'zy\00E4bc');
INSERT INTO strtest1 VALUES (2, U&'zy\0061\0308bc');
INSERT INTO strtest1 VALUES (3, U&'ab\00E4cd');
INSERT INTO strtest1 VALUES (4, U&'ab\0061\0308cd');
INSERT INTO strtest1 VALUES (5, U&'ab\00E4cd');
INSERT INTO strtest1 VALUES (6, U&'ab\0061\0308cd');
INSERT INTO strtest1 VALUES (7, U&'ab\00E4cd');
SELECT * FROM strtest1 WHERE b = 'zyäbc' COLLATE ctest_det ORDER BY a;
a | b
---------------------------------------------------------------------
1 | zyäbc
(1 row)
SELECT * FROM strtest1 WHERE b = 'zyäbc' COLLATE ctest_nondet ORDER BY a;
a | b
---------------------------------------------------------------------
1 | zyäbc
2 | zyäbc
(2 rows)
SELECT strpos(b COLLATE ctest_det, 'bc') FROM strtest1 ORDER BY a;
strpos
---------------------------------------------------------------------
4
5
0
0
0
0
0
(7 rows)
SELECT strpos(b COLLATE ctest_nondet, 'bc') FROM strtest1 ORDER BY a;
strpos
---------------------------------------------------------------------
4
5
0
0
0
0
0
(7 rows)
SELECT replace(b COLLATE ctest_det, U&'\00E4b', 'X') FROM strtest1 ORDER BY a;
replace
---------------------------------------------------------------------
zyXc
zyäbc
abäcd
abäcd
abäcd
abäcd
abäcd
(7 rows)
SELECT replace(b COLLATE ctest_nondet, U&'\00E4b', 'X') FROM strtest1 ORDER BY a;
replace
---------------------------------------------------------------------
zyXc
zyXc
abäcd
abäcd
abäcd
abäcd
abäcd
(7 rows)
SELECT a, split_part(b COLLATE ctest_det, U&'\00E4b', 2) FROM strtest1 ORDER BY a;
a | split_part
---------------------------------------------------------------------
1 | c
2 |
3 |
4 |
5 |
6 |
7 |
(7 rows)
SELECT a, split_part(b COLLATE ctest_nondet, U&'\00E4b', 2) FROM strtest1 ORDER BY a;
a | split_part
---------------------------------------------------------------------
1 | c
2 | c
3 |
4 |
5 |
6 |
7 |
(7 rows)
SELECT a, split_part(b COLLATE ctest_det, U&'\00E4b', -1) FROM strtest1 ORDER BY a;
a | split_part
---------------------------------------------------------------------
1 | c
2 | zyäbc
3 | abäcd
4 | abäcd
5 | abäcd
6 | abäcd
7 | abäcd
(7 rows)
SELECT a, split_part(b COLLATE ctest_nondet, U&'\00E4b', -1) FROM strtest1 ORDER BY a;
a | split_part
---------------------------------------------------------------------
1 | c
2 | c
3 | abäcd
4 | abäcd
5 | abäcd
6 | abäcd
7 | abäcd
(7 rows)
SELECT a, string_to_array(b COLLATE ctest_det, U&'\00E4b') FROM strtest1 ORDER BY a;
a | string_to_array
---------------------------------------------------------------------
1 | {zy,c}
2 | {zyäbc}
3 | {abäcd}
4 | {abäcd}
5 | {abäcd}
6 | {abäcd}
7 | {abäcd}
(7 rows)
SELECT a, string_to_array(b COLLATE ctest_nondet, U&'\00E4b') FROM strtest1 ORDER BY a;
a | string_to_array
---------------------------------------------------------------------
1 | {zy,c}
2 | {zy,c}
3 | {abäcd}
4 | {abäcd}
5 | {abäcd}
6 | {abäcd}
7 | {abäcd}
(7 rows)
SELECT * FROM strtest1 WHERE b LIKE 'zyäbc' COLLATE ctest_det ORDER BY a;
a | b
---------------------------------------------------------------------
1 | zyäbc
(1 row)
SELECT * FROM strtest1 WHERE b LIKE 'zyäbc' COLLATE ctest_nondet ORDER BY a;
a | b
---------------------------------------------------------------------
1 | zyäbc
2 | zyäbc
(2 rows)
CREATE TABLE strtest2 (a int, b text);
SELECT create_distributed_table('strtest2', 'a');
create_distributed_table
---------------------------------------------------------------------
(1 row)
INSERT INTO strtest2 VALUES (1, 'cote'), (2, 'côte'), (3, 'coté'), (4, 'côté');
CREATE TABLE strtest2nfd (a int, b text);
SELECT create_distributed_table('strtest2nfd', 'a');
create_distributed_table
---------------------------------------------------------------------
(1 row)
INSERT INTO strtest2nfd VALUES (1, 'cote'), (2, 'côte'), (3, 'coté'), (4, 'côté');
UPDATE strtest2nfd SET b = normalize(b, nfd);
-- This shows why replace should be greedy. Otherwise, in the NFD
-- case, the match would stop before the decomposed accents, which
-- would leave the accents in the results.
SELECT a, b, replace(b COLLATE ignore_accents, 'co', 'ma') FROM strtest2 ORDER BY a, b;
a | b | replace
---------------------------------------------------------------------
1 | cote | mate
2 | côte | mate
3 | coté | maté
4 | côté | maté
(4 rows)
SELECT a, b, replace(b COLLATE ignore_accents, 'co', 'ma') FROM strtest2nfd ORDER BY a, b;
a | b | replace
---------------------------------------------------------------------
1 | cote | mate
2 | côte | mate
3 | coté | maté
4 | côté | maté
(4 rows)
-- PG18 Feature: LIKE support for non-deterministic collations
-- PG18 commit: https://github.com/postgres/postgres/commit/85b7efa1c
-- As with non-deterministic collation text search, we verify that
-- LIKE with non-deterministic collation is passed through by Citus
-- and expected results are returned by the queries.
INSERT INTO strtest1 VALUES (8, U&'abc');
INSERT INTO strtest1 VALUES (9, 'abc');
SELECT a, b FROM strtest1
WHERE b LIKE 'abc' COLLATE ctest_det
ORDER BY a;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
(2 rows)
SELECT a, b FROM strtest1
WHERE b LIKE 'a\bc' COLLATE ctest_det
ORDER BY a;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
(2 rows)
SELECT a, b FROM strtest1
WHERE b LIKE 'abc' COLLATE ctest_nondet
ORDER BY a;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
(2 rows)
SELECT a, b FROM strtest1
WHERE b LIKE 'a\bc' COLLATE ctest_nondet
ORDER BY a;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
(2 rows)
CREATE COLLATION case_insensitive (provider = icu, locale = '@colStrength=secondary', deterministic = false);
NOTICE: using standard form "und-u-ks-level2" for ICU locale "@colStrength=secondary"
SELECT a, b FROM strtest1
WHERE b LIKE 'ABC' COLLATE case_insensitive
ORDER BY a;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
(2 rows)
SELECT a, b FROM strtest1
WHERE b LIKE 'ABC%' COLLATE case_insensitive
ORDER BY a;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
(2 rows)
INSERT INTO strtest1 VALUES (10, U&'\00E4bc');
INSERT INTO strtest1 VALUES (12, U&'\0061\0308bc');
SELECT * FROM strtest1
WHERE b LIKE 'äbc' COLLATE ctest_det
ORDER BY a;
a | b
---------------------------------------------------------------------
10 | äbc
(1 row)
SELECT * FROM strtest1
WHERE b LIKE 'äbc' COLLATE ctest_nondet
ORDER BY a;
a | b
---------------------------------------------------------------------
10 | äbc
12 | äbc
(2 rows)
-- Tests with ignore_accents collation. Taken from
-- PG18 regress tests and applied to a Citus table.
INSERT INTO strtest1 VALUES (10, U&'\0061\0308bc');
INSERT INTO strtest1 VALUES (11, U&'\00E4bc');
INSERT INTO strtest1 VALUES (12, U&'cb\0061\0308');
INSERT INTO strtest1 VALUES (13, U&'\0308bc');
INSERT INTO strtest1 VALUES (14, 'foox');
SELECT a, b FROM strtest1
WHERE b LIKE U&'\00E4_c' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
10 | äbc
10 | äbc
11 | äbc
12 | äbc
(6 rows)
-- and in reverse:
SELECT a, b FROM strtest1
WHERE b LIKE U&'\0061\0308_c' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
10 | äbc
10 | äbc
11 | äbc
12 | äbc
(6 rows)
-- inner % matches b:
SELECT a, b FROM strtest1
WHERE b LIKE U&'\00E4%c' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
10 | äbc
10 | äbc
11 | äbc
12 | äbc
(6 rows)
-- inner %% matches b then zero:
SELECT a, b FROM strtest1
WHERE b LIKE U&'\00E4%%c' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
10 | äbc
10 | äbc
11 | äbc
12 | äbc
(6 rows)
-- inner %% matches b then zero:
SELECT a, b FROM strtest1
WHERE b LIKE U&'c%%\00E4' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
12 | cbä
(1 row)
-- trailing _ matches two codepoints that form one grapheme:
SELECT a, b FROM strtest1
WHERE b LIKE U&'cb_' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
(0 rows)
-- trailing __ matches two codepoints that form one grapheme:
SELECT a, b FROM strtest1
WHERE b LIKE U&'cb__' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
12 | cbä
(1 row)
-- leading % matches zero:
SELECT a, b FROM strtest1
WHERE b LIKE U&'%\00E4bc' COLLATE ignore_accents
ORDER BY a;
a | b
---------------------------------------------------------------------
1 | zyäbc
2 | zyäbc
8 | abc
9 | abc
10 | äbc
10 | äbc
11 | äbc
12 | äbc
(8 rows)
-- leading % matches zero (with later %):
SELECT a, b FROM strtest1
WHERE b LIKE U&'%\00E4%c' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
1 | zyäbc
2 | zyäbc
8 | abc
9 | abc
10 | äbc
10 | äbc
11 | äbc
12 | äbc
(8 rows)
-- trailing % matches zero:
SELECT a, b FROM strtest1
WHERE b LIKE U&'\00E4bc%' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
10 | äbc
10 | äbc
11 | äbc
12 | äbc
(6 rows)
-- trailing % matches zero (with previous %):
SELECT a, b FROM strtest1
WHERE b LIKE U&'\00E4%c%' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
3 | abäcd
4 | abäcd
5 | abäcd
6 | abäcd
7 | abäcd
8 | abc
9 | abc
10 | äbc
10 | äbc
11 | äbc
12 | äbc
(11 rows)
-- _ versus two codepoints that form one grapheme:
SELECT a, b FROM strtest1
WHERE b LIKE U&'_bc' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
10 | äbc
10 | äbc
11 | äbc
12 | äbc
13 | ̈bc
(7 rows)
-- (actually this matches because)
SELECT a, b FROM strtest1
WHERE b = 'bc' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
13 | ̈bc
(1 row)
-- __ matches two codepoints that form one grapheme:
SELECT a, b FROM strtest1
WHERE b LIKE U&'__bc' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
10 | äbc
12 | äbc
(2 rows)
-- _ matches one codepoint that forms half a grapheme:
SELECT a, b FROM strtest1
WHERE b LIKE U&'_\0308bc' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
8 | abc
9 | abc
10 | äbc
10 | äbc
11 | äbc
12 | äbc
13 | ̈bc
(7 rows)
-- doesn't match because \00e4 doesn't match only \0308
SELECT a, b FROM strtest1
WHERE b LIKE U&'_\00e4bc' COLLATE ignore_accents ORDER BY a, b;
a | b
---------------------------------------------------------------------
(0 rows)
-- escape character at end of pattern
SELECT a, b FROM strtest1
WHERE b LIKE 'foo\' COLLATE ignore_accents ORDER BY a, b;
ERROR: LIKE pattern must not end with escape character
CONTEXT: while executing command on localhost:xxxxx
DROP TABLE strtest1;
DROP COLLATION ignore_accents;
DROP COLLATION ctest_det;
DROP COLLATION ctest_nondet;
DROP COLLATION case_insensitive;
-- PG18 Feature: GUC for CREATE DATABASE file copy method
-- PG18 commit: https://github.com/postgres/postgres/commit/f78ca6f3e
-- Citus supports the wal_log strategy only for CREATE DATABASE.
-- Here we show that the expected error (from PR #7249) occurs
-- when the file_copy strategy is attempted.
SET citus.enable_create_database_propagation=on;
SHOW file_copy_method;
file_copy_method
---------------------------------------------------------------------
copy
(1 row)
-- Error output is expected here
CREATE DATABASE copied_db WITH strategy file_copy;
ERROR: Only wal_log is supported as strategy parameter for CREATE DATABASE
SET file_copy_method TO clone;
-- Also errors out, per #7249
CREATE DATABASE cloned_db WITH strategy file_copy;
ERROR: Only wal_log is supported as strategy parameter for CREATE DATABASE
RESET file_copy_method;
-- This is okay
CREATE DATABASE copied_db
WITH strategy wal_log;
-- Show that file_copy works for ALTER DATABASE ... SET TABLESPACE
\set alter_db_tablespace :abs_srcdir '/tmp_check/ts3'
CREATE TABLESPACE alter_db_tablespace LOCATION :'alter_db_tablespace';
\c - - - :worker_1_port
\set alter_db_tablespace :abs_srcdir '/tmp_check/ts4'
CREATE TABLESPACE alter_db_tablespace LOCATION :'alter_db_tablespace';
\c - - - :worker_2_port
\set alter_db_tablespace :abs_srcdir '/tmp_check/ts5'
CREATE TABLESPACE alter_db_tablespace LOCATION :'alter_db_tablespace';
\c - - - :master_port
SET citus.enable_create_database_propagation TO on;
SET file_copy_method TO clone;
SET citus.log_remote_commands TO true;
SELECT datname, spcname
FROM pg_database d, pg_tablespace t
WHERE d.dattablespace = t.oid AND d.datname = 'copied_db';
datname | spcname
---------------------------------------------------------------------
copied_db | pg_default
(1 row)
ALTER DATABASE copied_db SET TABLESPACE alter_db_tablespace;
NOTICE: issuing BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;SELECT assign_distributed_transaction_id(xx, xx, 'xxxxxxx');
DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx
NOTICE: issuing SELECT citus_internal.acquire_citus_advisory_object_class_lock(26, 'copied_db')
DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx
NOTICE: issuing COMMIT
DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx
NOTICE: issuing SET citus.enable_ddl_propagation TO 'off'
DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx
NOTICE: issuing ALTER DATABASE copied_db SET TABLESPACE alter_db_tablespace
DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx
NOTICE: issuing SET citus.enable_ddl_propagation TO 'off'
DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx
NOTICE: issuing ALTER DATABASE copied_db SET TABLESPACE alter_db_tablespace
DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx
NOTICE: issuing SET citus.enable_ddl_propagation TO 'on'
DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx
NOTICE: issuing SET citus.enable_ddl_propagation TO 'on'
DETAIL: on server postgres@localhost:xxxxx connectionId: xxxxxxx
SELECT datname, spcname
FROM pg_database d, pg_tablespace t
WHERE d.dattablespace = t.oid AND d.datname = 'copied_db';
datname | spcname
---------------------------------------------------------------------
copied_db | alter_db_tablespace
(1 row)
RESET file_copy_method;
RESET citus.log_remote_commands;
-- Enable alter_db_tablespace to be dropped
ALTER DATABASE copied_db SET TABLESPACE pg_default;
DROP DATABASE copied_db;
-- Done with DATABASE commands
RESET citus.enable_create_database_propagation;
SELECT result FROM run_command_on_all_nodes(
$$
DROP TABLESPACE "alter_db_tablespace"
$$
);
result
---------------------------------------------------------------------
DROP TABLESPACE
DROP TABLESPACE
DROP TABLESPACE
(3 rows)
-- PG18 Feature: EXPLAIN (WAL) reports WAL buffers becoming full.
-- PG18 commit: https://github.com/postgres/postgres/commit/320545bfc
SET search_path TO pg18_nn;
SET citus.explain_all_tasks TO true;
CREATE TABLE wal_explain_dist(id int, payload text);
SELECT create_distributed_table('wal_explain_dist', 'id');
create_distributed_table
---------------------------------------------------------------------
(1 row)
INSERT INTO wal_explain_dist VALUES
(1, 'one'), (2, 'two'), (3, 'three');
-- Helper to capture distributed EXPLAIN ANALYZE (WAL) as JSON
CREATE OR REPLACE FUNCTION explain_analyze_wal_json(query text)
RETURNS jsonb
AS $$
DECLARE
result jsonb;
BEGIN
EXECUTE format('EXPLAIN (ANALYZE true, WAL true, FORMAT JSON) %s', query)
INTO result;
RETURN result;
END;
$$ LANGUAGE plpgsql;
CREATE TEMP TABLE wal_explain_plan(plan jsonb);
INSERT INTO wal_explain_plan(plan)
SELECT explain_analyze_wal_json($$SELECT count(*) FROM wal_explain_dist$$);
-- Ensure Citus keeps distributed-task context in the WAL-aware EXPLAIN output
SELECT jsonb_path_exists(plan, '$.**."Task Count"') AS wal_explain_distributed_plan
FROM wal_explain_plan;
wal_explain_distributed_plan
---------------------------------------------------------------------
t
(1 row)
-- New PG18 field: wal_buffers_full must survive the distributed EXPLAIN path
SELECT jsonb_path_exists(plan, '$.**."WAL Buffers Full"') AS wal_buffers_full_present
FROM wal_explain_plan;
wal_buffers_full_present
---------------------------------------------------------------------
t
(1 row)
SELECT jsonb_path_query_first(plan, '$.**."WAL Buffers Full"') AS wal_buffers_full_value
FROM wal_explain_plan;
wal_buffers_full_value
---------------------------------------------------------------------
0
(1 row)
DROP TABLE wal_explain_plan;
SET citus.explain_all_tasks TO default;
-- cleanup with minimum verbosity
SET client_min_messages TO ERROR;
RESET search_path;
RESET citus.shard_count;
RESET citus.shard_replication_factor;
DROP SCHEMA pg18_nn CASCADE;
RESET client_min_messages;