--
-- 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.
-- 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;
