--
-- PG15
--
CREATE SCHEMA pg15;
SET search_path TO pg15;
SET citus.next_shard_id TO 960000;
SET citus.shard_count TO 4;
--
-- In PG15, there is an added option to use ICU as global locale provider.
-- pg_collation has three locale-related fields: collcollate and collctype,
-- which are libc-related fields, and a new one colliculocale, which is the
-- ICU-related field. Only the libc-related fields or the ICU-related field
-- is set, never both.
-- Relevant PG commits:
-- f2553d43060edb210b36c63187d52a632448e1d2
-- 54637508f87bd5f07fb9406bac6b08240283be3b
--
-- fail, needs "locale"
CREATE COLLATION german_phonebook_test (provider = icu, lc_collate = 'de-u-co-phonebk');
ERROR:  parameter "locale" must be specified
-- fail, needs "locale"
CREATE COLLATION german_phonebook_test (provider = icu, lc_collate = 'de-u-co-phonebk', lc_ctype = 'de-u-co-phonebk');
ERROR:  parameter "locale" must be specified
-- works
CREATE COLLATION german_phonebook_test (provider = icu, locale = 'de-u-co-phonebk');
-- with icu provider, colliculocale will be set, collcollate and collctype will be null
SELECT result FROM run_command_on_all_nodes('
    SELECT collcollate FROM pg_collation WHERE collname = ''german_phonebook_test'';
');
 result
---------------------------------------------------------------------



(3 rows)

SELECT result FROM run_command_on_all_nodes('
    SELECT collctype FROM pg_collation WHERE collname = ''german_phonebook_test'';
');
 result
---------------------------------------------------------------------



(3 rows)

-- PG17 renamed colliculocale to colllocale
-- Relevant PG commit:
-- https://github.com/postgres/postgres/commit/f696c0cd5f299f1b51e214efc55a22a782cc175d
SHOW server_version \gset
SELECT substring(:'server_version', '\d+')::int >= 17 AS server_version_ge_17
\gset
\if :server_version_ge_17
SELECT '$$' ||
       'SELECT colllocale FROM pg_collation WHERE collname = ''german_phonebook_test'';'
       || '$$'
    AS worker_query_1 \gset
SELECT '$$' ||
       'SELECT colllocale FROM pg_collation WHERE collname = ''default_provider'';'
       || '$$'
    AS worker_query_2 \gset
\else
SELECT '$$' ||
       'SELECT colliculocale FROM pg_collation WHERE collname = ''german_phonebook_test'';'
       || '$$'
    AS worker_query_1 \gset
SELECT '$$' ||
       'SELECT colliculocale FROM pg_collation WHERE collname = ''default_provider'';'
       || '$$'
    AS worker_query_2 \gset
\endif
SELECT result FROM run_command_on_all_nodes(:worker_query_1);
     result
---------------------------------------------------------------------
 de-u-co-phonebk
 de-u-co-phonebk
 de-u-co-phonebk
(3 rows)

-- with non-icu provider, colliculocale will be null, collcollate and collctype will be set
CREATE COLLATION default_provider (provider = libc, lc_collate = "POSIX", lc_ctype = "POSIX");
SELECT result FROM run_command_on_all_nodes('
    SELECT collcollate FROM pg_collation WHERE collname = ''default_provider'';
');
 result
---------------------------------------------------------------------
 POSIX
 POSIX
 POSIX
(3 rows)

SELECT result FROM run_command_on_all_nodes('
    SELECT collctype FROM pg_collation WHERE collname = ''default_provider'';
');
 result
---------------------------------------------------------------------
 POSIX
 POSIX
 POSIX
(3 rows)

SELECT result FROM run_command_on_all_nodes(:worker_query_2);
 result
---------------------------------------------------------------------



(3 rows)

--
-- In PG15, Renaming triggers on partitioned tables had two problems
-- recurses to renaming the triggers on the partitions as well.
-- Here we test that distributed triggers behave the same way.
-- Relevant PG commit:
-- 80ba4bb383538a2ee846fece6a7b8da9518b6866
--
SET citus.enable_unsafe_triggers TO true;
CREATE TABLE sale(
    sale_date date not null,
    state_code text,
    product_sku text,
    units integer)
    PARTITION BY list (state_code);
ALTER TABLE sale ADD CONSTRAINT sale_pk PRIMARY KEY (state_code, sale_date);
CREATE TABLE sale_newyork PARTITION OF sale FOR VALUES IN ('NY');
CREATE TABLE sale_california PARTITION OF sale FOR VALUES IN ('CA');
CREATE TABLE record_sale(
    operation_type text not null,
    product_sku text,
    state_code text,
    units integer,
    PRIMARY KEY(state_code, product_sku, operation_type, units));
SELECT create_distributed_table('sale', 'state_code');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

SELECT create_distributed_table('record_sale', 'state_code', colocate_with := 'sale');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

CREATE OR REPLACE FUNCTION record_sale()
RETURNS trigger
AS $$
BEGIN
    INSERT INTO pg15.record_sale(operation_type, product_sku, state_code, units)
    VALUES (TG_OP, NEW.product_sku, NEW.state_code, NEW.units);
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER record_sale_trigger
AFTER INSERT OR UPDATE OR DELETE ON sale
FOR EACH ROW EXECUTE FUNCTION pg15.record_sale();
CREATE VIEW sale_triggers AS
    SELECT tgname, tgrelid::regclass, tgenabled
    FROM pg_trigger
    WHERE tgrelid::regclass::text like 'sale%'
    ORDER BY 1, 2;
SELECT * FROM sale_triggers ORDER BY 1, 2;
         tgname         |     tgrelid     | tgenabled
---------------------------------------------------------------------
 record_sale_trigger    | sale            | O
 record_sale_trigger    | sale_newyork    | O
 record_sale_trigger    | sale_california | O
 truncate_trigger_xxxxxxx | sale            | O
 truncate_trigger_xxxxxxx | sale_california | O
 truncate_trigger_xxxxxxx | sale_newyork    | O
(6 rows)

ALTER TRIGGER "record_sale_trigger" ON "pg15"."sale" RENAME TO "new_record_sale_trigger";
SELECT * FROM sale_triggers ORDER BY 1, 2;
         tgname          |     tgrelid     | tgenabled
---------------------------------------------------------------------
 new_record_sale_trigger | sale            | O
 new_record_sale_trigger | sale_newyork    | O
 new_record_sale_trigger | sale_california | O
 truncate_trigger_xxxxxxx  | sale            | O
 truncate_trigger_xxxxxxx  | sale_california | O
 truncate_trigger_xxxxxxx  | sale_newyork    | O
(6 rows)

-- test that we can't rename a distributed clone trigger
ALTER TRIGGER "new_record_sale_trigger" ON "pg15"."sale_newyork" RENAME TO "another_trigger_name";
ERROR:  cannot rename trigger "new_record_sale_trigger" on table "sale_newyork"
HINT:  Rename the trigger on the partitioned table "sale" instead.
--
-- In PG15, For GENERATED columns, all dependencies of the generation
-- expression are recorded as NORMAL dependencies of the column itself.
-- This requires CASCADE to drop generated cols with the original col.
-- Test this behavior in distributed table, specifically with
-- undistribute_table within a transaction.
-- Relevant PG Commit: cb02fcb4c95bae08adaca1202c2081cfc81a28b5
--
CREATE TABLE generated_stored_ref (
  col_1 int,
  col_2 int,
  col_3 int generated always as (col_1+col_2) stored,
  col_4 int,
  col_5 int generated always as (col_4*2-col_1) stored
);
SELECT create_reference_table ('generated_stored_ref');
 create_reference_table
---------------------------------------------------------------------

(1 row)

-- populate the table
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)

-- fails, CASCADE must be specified
-- will test CASCADE inside the transcation
ALTER TABLE generated_stored_ref DROP COLUMN col_1;
ERROR:  cannot drop column col_1 of table generated_stored_ref because other objects depend on it
DETAIL:  column col_3 of table generated_stored_ref depends on column col_1 of table generated_stored_ref
column col_5 of table generated_stored_ref depends on column col_1 of table generated_stored_ref
HINT:  Use DROP ... CASCADE to drop the dependent objects too.
BEGIN;
  -- drops col_1, col_3, col_5
  ALTER TABLE generated_stored_ref DROP COLUMN col_1 CASCADE;
NOTICE:  drop cascades to 2 other objects
DETAIL:  drop cascades to column col_3 of table generated_stored_ref
drop cascades to column col_5 of table generated_stored_ref
NOTICE:  drop cascades to 2 other objects
DETAIL:  drop cascades to column col_3 of table generated_stored_ref_960016
drop cascades to column col_5 of table generated_stored_ref_960016
  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 pg15.generated_stored_ref
NOTICE:  moving the data of pg15.generated_stored_ref
NOTICE:  dropping the old pg15.generated_stored_ref
NOTICE:  renaming the new table to pg15.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;
SELECT undistribute_table('generated_stored_ref');
NOTICE:  creating a new table for pg15.generated_stored_ref
NOTICE:  moving the data of pg15.generated_stored_ref
NOTICE:  dropping the old pg15.generated_stored_ref
NOTICE:  renaming the new table to pg15.generated_stored_ref
 undistribute_table
---------------------------------------------------------------------

(1 row)

--
-- In PG15, there is a new command called MERGE
-- It is currently not supported for Citus non-local tables
-- Test the behavior with various commands with Citus table types
-- Relevant PG Commit: 7103ebb7aae8ab8076b7e85f335ceb8fe799097c
--
CREATE TABLE tbl1
(
   x INT
);
CREATE TABLE tbl2
(
    x INT
);
-- on local tables works fine
MERGE INTO tbl1 USING tbl2 ON (true)
WHEN MATCHED THEN DELETE;
-- one table is Citus local table, fails
SELECT citus_add_local_table_to_metadata('tbl1');
 citus_add_local_table_to_metadata
---------------------------------------------------------------------

(1 row)

MERGE INTO tbl1 USING tbl2 ON (true)
WHEN MATCHED THEN DELETE;
SELECT undistribute_table('tbl1');
NOTICE:  creating a new table for pg15.tbl1
NOTICE:  moving the data of pg15.tbl1
NOTICE:  dropping the old pg15.tbl1
NOTICE:  renaming the new table to pg15.tbl1
 undistribute_table
---------------------------------------------------------------------

(1 row)

-- the other table is Citus local table, fails
SELECT citus_add_local_table_to_metadata('tbl2');
 citus_add_local_table_to_metadata
---------------------------------------------------------------------

(1 row)

MERGE INTO tbl1 USING tbl2 ON (true)
WHEN MATCHED THEN DELETE;
-- source table is reference, the target is local, supported
SELECT create_reference_table('tbl2');
 create_reference_table
---------------------------------------------------------------------

(1 row)

MERGE INTO tbl1 USING tbl2 ON (true)
WHEN MATCHED THEN DELETE;
-- now, both are reference, not supported
SELECT create_reference_table('tbl1');
 create_reference_table
---------------------------------------------------------------------

(1 row)

MERGE INTO tbl1 USING tbl2 ON (true)
WHEN MATCHED THEN DELETE;
ERROR:  Reference table as target is not allowed in MERGE command
-- now, both distributed, not works
SELECT undistribute_table('tbl1');
NOTICE:  creating a new table for pg15.tbl1
NOTICE:  moving the data of pg15.tbl1
NOTICE:  dropping the old pg15.tbl1
NOTICE:  renaming the new table to pg15.tbl1
 undistribute_table
---------------------------------------------------------------------

(1 row)

SELECT undistribute_table('tbl2');
NOTICE:  creating a new table for pg15.tbl2
NOTICE:  moving the data of pg15.tbl2
NOTICE:  dropping the old pg15.tbl2
NOTICE:  renaming the new table to pg15.tbl2
 undistribute_table
---------------------------------------------------------------------

(1 row)

-- Make sure that we allow foreign key columns on local tables added to
-- metadata to have SET NULL/DEFAULT on column basis.
CREATE TABLE PKTABLE_local (tid int, id int, PRIMARY KEY (tid, id));
CREATE TABLE FKTABLE_local (
  tid int, id int,
  fk_id_del_set_null int,
  fk_id_del_set_default int DEFAULT 0,
  FOREIGN KEY (tid, fk_id_del_set_null) REFERENCES PKTABLE_local ON DELETE SET NULL (fk_id_del_set_null),
  FOREIGN KEY (tid, fk_id_del_set_default) REFERENCES PKTABLE_local ON DELETE SET DEFAULT (fk_id_del_set_default)
);
SELECT citus_add_local_table_to_metadata('FKTABLE_local', cascade_via_foreign_keys=>true);
 citus_add_local_table_to_metadata
---------------------------------------------------------------------

(1 row)

-- show that the definition is expected
SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'FKTABLE_local'::regclass::oid ORDER BY oid;
                                                   pg_get_constraintdef
---------------------------------------------------------------------
 FOREIGN KEY (tid, fk_id_del_set_default) REFERENCES pktable_local(tid, id) ON DELETE SET DEFAULT (fk_id_del_set_default)
 FOREIGN KEY (tid, fk_id_del_set_null) REFERENCES pktable_local(tid, id) ON DELETE SET NULL (fk_id_del_set_null)
(2 rows)

\c - - - :worker_1_port
SET search_path TO pg15;
-- show that the definition is expected on the worker as well
SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'FKTABLE_local'::regclass::oid ORDER BY oid;
                                                   pg_get_constraintdef
---------------------------------------------------------------------
 FOREIGN KEY (tid, fk_id_del_set_default) REFERENCES pktable_local(tid, id) ON DELETE SET DEFAULT (fk_id_del_set_default)
 FOREIGN KEY (tid, fk_id_del_set_null) REFERENCES pktable_local(tid, id) ON DELETE SET NULL (fk_id_del_set_null)
(2 rows)

-- also, make sure that it works as expected
INSERT INTO PKTABLE_local VALUES (1, 0), (1, 1), (1, 2);
INSERT INTO FKTABLE_local VALUES
  (1, 1, 1, NULL),
  (1, 2, NULL, 2);
DELETE FROM PKTABLE_local WHERE id = 1 OR id = 2;
SELECT * FROM FKTABLE_local ORDER BY id;
 tid | id | fk_id_del_set_null | fk_id_del_set_default
---------------------------------------------------------------------
   1 |  1 |                    |
   1 |  2 |                    |                     0
(2 rows)

\c - - - :master_port
SET search_path TO pg15;
SET client_min_messages to ERROR;
DROP TABLE FKTABLE_local, PKTABLE_local;
RESET client_min_messages;
SELECT create_distributed_table('tbl1', 'x');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

SELECT create_distributed_table('tbl2', 'x');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

MERGE INTO tbl1 USING tbl2 ON (true)
WHEN MATCHED THEN DELETE;
ERROR:  The required join operation is missing between the target's distribution column and any expression originating from the source. The issue may arise from a non-equi-join.
DETAIL:  Without a equi-join condition on the target's distribution column, the source rows cannot be efficiently redistributed, and the NOT-MATCHED condition cannot be evaluated unambiguously. This can result in incorrect or unexpected results when attempting to merge tables in a distributed setting
-- also, inside subqueries & ctes
WITH targq AS (
    SELECT * FROM tbl2
)
MERGE INTO tbl1 USING targq ON (true)
WHEN MATCHED THEN DELETE;
ERROR:  The required join operation is missing between the target's distribution column and any expression originating from the source. The issue may arise from a non-equi-join.
DETAIL:  Without a equi-join condition on the target's distribution column, the source rows cannot be efficiently redistributed, and the NOT-MATCHED condition cannot be evaluated unambiguously. This can result in incorrect or unexpected results when attempting to merge tables in a distributed setting
MERGE INTO tbl1 t
USING tbl2
ON (true)
WHEN MATCHED THEN
    DO NOTHING;
ERROR:  The required join operation is missing between the target's distribution column and any expression originating from the source. The issue may arise from a non-equi-join.
DETAIL:  Without a equi-join condition on the target's distribution column, the source rows cannot be efficiently redistributed, and the NOT-MATCHED condition cannot be evaluated unambiguously. This can result in incorrect or unexpected results when attempting to merge tables in a distributed setting
MERGE INTO tbl1 t
USING tbl2
ON (true)
WHEN MATCHED THEN
    UPDATE SET x = (SELECT count(*) FROM tbl2);
ERROR:  updating the distribution column is not allowed in MERGE actions
-- test numeric types with negative scale
CREATE TABLE numeric_negative_scale(numeric_column numeric(3,-1), orig_value int);
INSERT into numeric_negative_scale SELECT x,x FROM generate_series(111, 115) x;
-- verify that we can not distribute by a column that has numeric type with negative scale
SELECT create_distributed_table('numeric_negative_scale','numeric_column');
ERROR:  cannot distribute relation: numeric_negative_scale
DETAIL:  Distribution column must not use numeric type with negative scale
-- However, we can distribute by other columns
SELECT create_distributed_table('numeric_negative_scale','orig_value');
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($$pg15.numeric_negative_scale$$)
 create_distributed_table
---------------------------------------------------------------------

(1 row)

-- Verify that we can not change the distribution column to the numeric column
SELECT alter_distributed_table('numeric_negative_scale',
                                distribution_column := 'numeric_column');
NOTICE:  creating a new table for pg15.numeric_negative_scale
ERROR:  cannot distribute relation: numeric_negative_scale_xxxxxx"
DETAIL:  Distribution column must not use numeric type with negative scale
SELECT * FROM numeric_negative_scale ORDER BY 1,2;
 numeric_column | orig_value
---------------------------------------------------------------------
            110 |        111
            110 |        112
            110 |        113
            110 |        114
            120 |        115
(5 rows)

-- verify that numeric types with scale greater than precision are also ok
-- a precision of 2, and scale of 3 means that all the numbers should be less than 10^-1 and of the form 0,0XY
CREATE TABLE numeric_scale_gt_precision(numeric_column numeric(2,3));
SELECT * FROM create_distributed_table('numeric_scale_gt_precision','numeric_column');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

INSERT INTO numeric_scale_gt_precision SELECT x FROM generate_series(0.01234, 0.09, 0.005) x;
-- verify that we store only 2 digits, and discard the rest of them.
SELECT * FROM numeric_scale_gt_precision ORDER BY 1;
 numeric_column
---------------------------------------------------------------------
          0.012
          0.017
          0.022
          0.027
          0.032
          0.037
          0.042
          0.047
          0.052
          0.057
          0.062
          0.067
          0.072
          0.077
          0.082
          0.087
(16 rows)

-- verify we can route queries to the right shards
SELECT * FROM numeric_scale_gt_precision WHERE numeric_column=0.027;
 numeric_column
---------------------------------------------------------------------
          0.027
(1 row)

-- test repartition joins on tables distributed on numeric types with negative scale
CREATE TABLE numeric_repartition_first(id int, data int, numeric_column numeric(3,-1));
CREATE TABLE numeric_repartition_second(id int, data int, numeric_column numeric(3,-1));
-- populate tables
INSERT INTO numeric_repartition_first SELECT x, x, x FROM generate_series (100, 115) x;
INSERT INTO numeric_repartition_second SELECT x, x, x FROM generate_series (100, 115) x;
-- Run some queries before distributing the tables to see results in vanilla PG
SELECT count(*)
FROM numeric_repartition_first f,
     numeric_repartition_second s
WHERE f.id = s.numeric_column;
 count
---------------------------------------------------------------------
    15
(1 row)

SELECT count(*)
FROM numeric_repartition_first f,
     numeric_repartition_second s
WHERE f.numeric_column = s.numeric_column;
 count
---------------------------------------------------------------------
   126
(1 row)

-- distribute tables and re-run the same queries
SELECT * FROM create_distributed_table('numeric_repartition_first','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($$pg15.numeric_repartition_first$$)
 create_distributed_table
---------------------------------------------------------------------

(1 row)

SELECT * FROM create_distributed_table('numeric_repartition_second','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($$pg15.numeric_repartition_second$$)
 create_distributed_table
---------------------------------------------------------------------

(1 row)

SET citus.enable_repartition_joins TO 1;
SELECT count(*)
FROM numeric_repartition_first f,
     numeric_repartition_second s
WHERE f.id = s.numeric_column;
ERROR:  complex joins are only supported when all distributed tables are joined on their distribution columns with equal operator
-- show that the same query works if we use an int column instead of a numeric on the filter clause
SELECT count(*)
FROM numeric_repartition_first f,
     numeric_repartition_second s
WHERE f.id = s.data;
 count
---------------------------------------------------------------------
    16
(1 row)

SELECT count(*)
FROM numeric_repartition_first f,
     numeric_repartition_second s
WHERE f.numeric_column = s.numeric_column;
 count
---------------------------------------------------------------------
   126
(1 row)

-- test new regex functions
-- print order comments that contain the word `fluffily` at least twice
SELECT o_comment FROM public.orders WHERE regexp_count(o_comment, 'FluFFily', 1, 'i')>=2 ORDER BY 1;
                                 o_comment
---------------------------------------------------------------------
 al, bold deposits cajole fluffily fluffily final foxes. pending ideas beli
 ly regular packages are fluffily even ideas. fluffily final
 ng instructions integrate fluffily among the fluffily silent accounts. bli
 ructions wake fluffily fluffily final gifts! furiou
 s boost blithely fluffily idle ideas? fluffily even pin
(5 rows)

-- print the same items using a different regexp function
SELECT o_comment FROM public.orders WHERE regexp_like(o_comment, 'fluffily.*fluffily') ORDER BY 1;
                                 o_comment
---------------------------------------------------------------------
 al, bold deposits cajole fluffily fluffily final foxes. pending ideas beli
 ly regular packages are fluffily even ideas. fluffily final
 ng instructions integrate fluffily among the fluffily silent accounts. bli
 ructions wake fluffily fluffily final gifts! furiou
 s boost blithely fluffily idle ideas? fluffily even pin
(5 rows)

-- print the position where we find the second fluffily in the comment
SELECT o_comment, regexp_instr(o_comment, 'fluffily.*(fluffily)') FROM public.orders ORDER BY 2 desc LIMIT 5;
                                 o_comment                                  | regexp_instr
---------------------------------------------------------------------
 ng instructions integrate fluffily among the fluffily silent accounts. bli |           27
 al, bold deposits cajole fluffily fluffily final foxes. pending ideas beli |           26
 ly regular packages are fluffily even ideas. fluffily final                |           25
 s boost blithely fluffily idle ideas? fluffily even pin                    |           18
 ructions wake fluffily fluffily final gifts! furiou                        |           15
(5 rows)

-- print the substrings between two `fluffily`
SELECT regexp_substr(o_comment, 'fluffily.*fluffily') FROM public.orders ORDER BY 1 LIMIT 5;
         regexp_substr
---------------------------------------------------------------------
 fluffily among the fluffily
 fluffily even ideas. fluffily
 fluffily fluffily
 fluffily fluffily
 fluffily idle ideas? fluffily
(5 rows)

-- replace second `fluffily` with `silkily`
SELECT regexp_replace(o_comment, 'fluffily', 'silkily', 1, 2) FROM public.orders WHERE regexp_like(o_comment, 'fluffily.*fluffily') ORDER BY 1 desc;
                              regexp_replace
---------------------------------------------------------------------
 s boost blithely fluffily idle ideas? silkily even pin
 ructions wake fluffily silkily final gifts! furiou
 ng instructions integrate fluffily among the silkily silent accounts. bli
 ly regular packages are fluffily even ideas. silkily final
 al, bold deposits cajole fluffily silkily final foxes. pending ideas beli
(5 rows)

-- test new COPY features
-- COPY TO statements with text format and headers
CREATE TABLE copy_test(id int, data int);
SELECT create_distributed_table('copy_test', 'id');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

INSERT INTO copy_test SELECT x, x FROM generate_series(1,100) x;
COPY copy_test TO :'temp_dir''copy_test.txt' WITH ( HEADER true, FORMAT text);
-- Create another distributed table with different column names and test COPY FROM with header match
CREATE TABLE copy_test2(id int, data_ int);
SELECT create_distributed_table('copy_test2', 'id');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

COPY copy_test2 FROM :'temp_dir''copy_test.txt' WITH ( HEADER match, FORMAT text);
ERROR:  column name mismatch in header line field 2: got "data", expected "data_"
CONTEXT:  COPY copy_test2, line 1: "id	data"
-- verify that the command works if we rename the column
ALTER TABLE copy_test2 RENAME COLUMN data_ TO data;
COPY copy_test2 FROM :'temp_dir''copy_test.txt' WITH ( HEADER match, FORMAT text);
SELECT count(*)=100 FROM copy_test2;
 ?column?
---------------------------------------------------------------------
 t
(1 row)

--
-- In PG15, unlogged sequences are supported
-- we support this for distributed sequences as well
--
CREATE SEQUENCE seq1;
CREATE UNLOGGED SEQUENCE "pg15"."seq 2";
-- first, test that sequence persistence is distributed correctly
-- when the sequence is distributed
SELECT relname,
       CASE relpersistence
            WHEN 'u' THEN 'unlogged'
            WHEN 'p' then 'logged'
            ELSE 'unknown'
        END AS logged_info
FROM pg_class
WHERE relname IN ('seq1', 'seq 2') AND relnamespace='pg15'::regnamespace
ORDER BY relname;
 relname | logged_info
---------------------------------------------------------------------
 seq 2   | unlogged
 seq1    | logged
(2 rows)

CREATE TABLE "seq test"(a int, b int default nextval ('seq1'), c int default nextval ('"pg15"."seq 2"'));
SELECT create_distributed_table('"pg15"."seq test"','a');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

\c - - - :worker_1_port
SELECT relname,
       CASE relpersistence
            WHEN 'u' THEN 'unlogged'
            WHEN 'p' then 'logged'
            ELSE 'unknown'
        END AS logged_info
FROM pg_class
WHERE relname IN ('seq1', 'seq 2') AND relnamespace='pg15'::regnamespace
ORDER BY relname;
 relname | logged_info
---------------------------------------------------------------------
 seq 2   | unlogged
 seq1    | logged
(2 rows)

\c - - - :master_port
SET search_path TO pg15;
-- now, check that we can change sequence persistence using ALTER SEQUENCE
ALTER SEQUENCE seq1 SET UNLOGGED;
-- use IF EXISTS
ALTER SEQUENCE IF EXISTS "seq 2" SET LOGGED;
-- check non-existent sequence as well
ALTER SEQUENCE seq_non_exists SET LOGGED;
ERROR:  relation "seq_non_exists" does not exist
ALTER SEQUENCE IF EXISTS seq_non_exists SET LOGGED;
NOTICE:  relation "seq_non_exists" does not exist, skipping
SELECT relname,
       CASE relpersistence
            WHEN 'u' THEN 'unlogged'
            WHEN 'p' then 'logged'
            ELSE 'unknown'
        END AS logged_info
FROM pg_class
WHERE relname IN ('seq1', 'seq 2') AND relnamespace='pg15'::regnamespace
ORDER BY relname;
 relname | logged_info
---------------------------------------------------------------------
 seq 2   | logged
 seq1    | unlogged
(2 rows)

\c - - - :worker_1_port
SELECT relname,
       CASE relpersistence
            WHEN 'u' THEN 'unlogged'
            WHEN 'p' then 'logged'
            ELSE 'unknown'
        END AS logged_info
FROM pg_class
WHERE relname IN ('seq1', 'seq 2') AND relnamespace='pg15'::regnamespace
ORDER BY relname;
 relname | logged_info
---------------------------------------------------------------------
 seq 2   | logged
 seq1    | unlogged
(2 rows)

\c - - - :master_port
SET search_path TO pg15;
-- now, check that we can change sequence persistence using ALTER TABLE
ALTER TABLE seq1 SET LOGGED;
ALTER TABLE "seq 2" SET UNLOGGED;
SELECT relname,
       CASE relpersistence
            WHEN 'u' THEN 'unlogged'
            WHEN 'p' then 'logged'
            ELSE 'unknown'
        END AS logged_info
FROM pg_class
WHERE relname IN ('seq1', 'seq 2') AND relnamespace='pg15'::regnamespace
ORDER BY relname;
 relname | logged_info
---------------------------------------------------------------------
 seq 2   | unlogged
 seq1    | logged
(2 rows)

\c - - - :worker_1_port
SELECT relname,
       CASE relpersistence
            WHEN 'u' THEN 'unlogged'
            WHEN 'p' then 'logged'
            ELSE 'unknown'
        END AS logged_info
FROM pg_class
WHERE relname IN ('seq1', 'seq 2') AND relnamespace='pg15'::regnamespace
ORDER BY relname;
 relname | logged_info
---------------------------------------------------------------------
 seq 2   | unlogged
 seq1    | logged
(2 rows)

\c - - - :master_port
SET search_path TO pg15;
-- An identity/serial sequence now automatically gets and follows the
-- persistence level (logged/unlogged) of its owning table.
-- Test this behavior as well
CREATE UNLOGGED TABLE test(a bigserial, b bigserial);
SELECT create_distributed_table('test', 'a');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

-- show that associated sequence is unlooged
SELECT relname,
       CASE relpersistence
            WHEN 'u' THEN 'unlogged'
            WHEN 'p' then 'logged'
            ELSE 'unknown'
        END AS logged_info
FROM pg_class
WHERE relname IN ('test_a_seq', 'test_b_seq') AND relnamespace='pg15'::regnamespace
ORDER BY relname;
  relname   | logged_info
---------------------------------------------------------------------
 test_a_seq | unlogged
 test_b_seq | unlogged
(2 rows)

\c - - - :worker_1_port
SELECT relname,
       CASE relpersistence
            WHEN 'u' THEN 'unlogged'
            WHEN 'p' then 'logged'
            ELSE 'unknown'
        END AS logged_info
FROM pg_class
WHERE relname IN ('test_a_seq', 'test_b_seq') AND relnamespace='pg15'::regnamespace
ORDER BY relname;
  relname   | logged_info
---------------------------------------------------------------------
 test_a_seq | unlogged
 test_b_seq | unlogged
(2 rows)

\c - - - :master_port
SET search_path TO pg15;
-- allow foreign key columns to have SET NULL/DEFAULT on column basis
-- currently only reference tables can support that
CREATE TABLE PKTABLE (tid int, id int, PRIMARY KEY (tid, id));
CREATE TABLE FKTABLE (
  tid int, id int,
  fk_id_del_set_null int,
  fk_id_del_set_default int DEFAULT 0,
  FOREIGN KEY (tid, fk_id_del_set_null) REFERENCES PKTABLE ON DELETE SET NULL (fk_id_del_set_null),
  FOREIGN KEY (tid, fk_id_del_set_default) REFERENCES PKTABLE ON DELETE SET DEFAULT (fk_id_del_set_default)
);
SELECT create_reference_table('PKTABLE');
 create_reference_table
---------------------------------------------------------------------

(1 row)

-- ok, Citus could relax this constraint in the future
SELECT create_distributed_table('FKTABLE', 'tid');
ERROR:  cannot create foreign key constraint
DETAIL:  SET NULL or SET DEFAULT is not supported in ON DELETE operation when distribution key is included in the foreign key constraint
-- with reference tables it should all work fine
SELECT create_reference_table('FKTABLE');
 create_reference_table
---------------------------------------------------------------------

(1 row)

-- show that the definition is expected
SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'fktable'::regclass::oid ORDER BY 1;
                                                pg_get_constraintdef
---------------------------------------------------------------------
 FOREIGN KEY (tid, fk_id_del_set_default) REFERENCES pktable(tid, id) ON DELETE SET DEFAULT (fk_id_del_set_default)
 FOREIGN KEY (tid, fk_id_del_set_null) REFERENCES pktable(tid, id) ON DELETE SET NULL (fk_id_del_set_null)
(2 rows)

\c - - - :worker_1_port
SET search_path TO pg15;
-- show that the definition is expected on the worker as well
SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'fktable'::regclass::oid ORDER BY oid;
                                                pg_get_constraintdef
---------------------------------------------------------------------
 FOREIGN KEY (tid, fk_id_del_set_null) REFERENCES pktable(tid, id) ON DELETE SET NULL (fk_id_del_set_null)
 FOREIGN KEY (tid, fk_id_del_set_default) REFERENCES pktable(tid, id) ON DELETE SET DEFAULT (fk_id_del_set_default)
(2 rows)

-- also, make sure that it works as expected
INSERT INTO PKTABLE VALUES (1, 0), (1, 1), (1, 2);
INSERT INTO FKTABLE VALUES
  (1, 1, 1, NULL),
  (1, 2, NULL, 2);
DELETE FROM PKTABLE WHERE id = 1 OR id = 2;
SELECT * FROM FKTABLE ORDER BY id;
 tid | id | fk_id_del_set_null | fk_id_del_set_default
---------------------------------------------------------------------
   1 |  1 |                    |
   1 |  2 |                    |                     0
(2 rows)

\c - - - :master_port
SET search_path TO pg15;
-- test NULL NOT DISTINCT clauses
-- set the next shard id so that the error messages are easier to maintain
SET citus.next_shard_id TO 960150;
CREATE TABLE null_distinct_test(id INT, c1 INT, c2 INT, c3 VARCHAR(10)) ;
SELECT create_distributed_table('null_distinct_test', 'id');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

CREATE UNIQUE INDEX idx1_null_distinct_test ON null_distinct_test(id, c1) NULLS DISTINCT ;
CREATE UNIQUE INDEX idx2_null_distinct_test ON null_distinct_test(id, c2) NULLS NOT DISTINCT ;
-- populate with some initial data
INSERT INTO null_distinct_test VALUES (1, 1, 1, 'data1') ;
INSERT INTO null_distinct_test VALUES (1, 2, NULL, 'data2') ;
INSERT INTO null_distinct_test VALUES (1, NULL, 3, 'data3') ;
-- should fail as we already have a null value in c2 column
INSERT INTO null_distinct_test VALUES (1, NULL, NULL, 'data4') ;
ERROR:  duplicate key value violates unique constraint "idx2_null_distinct_test_960150"
DETAIL:  Key (id, c2)=(1, null) already exists.
CONTEXT:  while executing command on localhost:xxxxx
INSERT INTO null_distinct_test VALUES (1, NULL, NULL, 'data4') ON CONFLICT DO NOTHING;
INSERT INTO null_distinct_test VALUES (1, NULL, NULL, 'data4') ON CONFLICT (id, c2) DO UPDATE SET c2=100 RETURNING *;
 id | c1 | c2  |  c3
---------------------------------------------------------------------
  1 |  2 | 100 | data2
(1 row)

-- should not fail as null values are distinct for c1 column
INSERT INTO null_distinct_test VALUES (1, NULL, 5, 'data5') ;
-- test that unique constraints also work properly
-- since we have multiple (1,NULL) pairs for columns (id,c1) the first will work, second will fail
ALTER TABLE null_distinct_test ADD CONSTRAINT uniq_distinct_c1 UNIQUE NULLS DISTINCT (id,c1);
ALTER TABLE null_distinct_test ADD CONSTRAINT uniq_c1 UNIQUE NULLS NOT DISTINCT (id,c1);
ERROR:  could not create unique index "uniq_c1_960150"
DETAIL:  Key (id, c1)=(1, null) is duplicated.
CONTEXT:  while executing command on localhost:xxxxx
-- show all records in the table for fact checking
SELECT * FROM null_distinct_test ORDER BY c3;
 id | c1 | c2  |  c3
---------------------------------------------------------------------
  1 |  1 |   1 | data1
  1 |  2 | 100 | data2
  1 |    |   3 | data3
  1 |    |   5 | data5
(4 rows)

-- test unique nulls not distinct constraints on a reference table
CREATE TABLE reference_uniq_test (
    x int, y int,
    UNIQUE NULLS NOT DISTINCT (x, y)
);
SELECT create_reference_table('reference_uniq_test');
 create_reference_table
---------------------------------------------------------------------

(1 row)

INSERT INTO reference_uniq_test VALUES (1, 1), (1, NULL), (NULL, 1);
-- the following will fail
INSERT INTO reference_uniq_test VALUES (1, NULL);
ERROR:  duplicate key value violates unique constraint "reference_uniq_test_x_y_key_960154"
DETAIL:  Key (x, y)=(1, null) already exists.
CONTEXT:  while executing command on localhost:xxxxx
--
-- PG15 introduces CLUSTER command support for partitioned tables. However, similar to
-- CLUSTER commands with no table name, these queries can not be run inside a transaction
-- block. Therefore, we do not propagate such queries.
--
-- Should print a warning that it will not be propagated to worker nodes.
CLUSTER sale USING sale_pk;
WARNING:  not propagating CLUSTER command for partitioned table to worker nodes
HINT:  Provide a child partition table names in order to CLUSTER distributed partitioned tables.
-- verify that we can cluster the partition tables only when replication factor is 1
CLUSTER sale_newyork USING sale_newyork_pkey;
ERROR:  modifications on partitions when replication factor is greater than 1 is not supported
HINT:  Run the query on the parent table "sale" instead.
-- create a new partitioned table with shard replicaiton factor 1
SET citus.shard_replication_factor = 1;
CREATE TABLE sale_repl_factor_1 ( LIKE sale )
    PARTITION BY list (state_code);
ALTER TABLE sale_repl_factor_1 ADD CONSTRAINT sale_repl_factor_1_pk PRIMARY KEY (state_code, sale_date);
CREATE TABLE sale_newyork_repl_factor_1 PARTITION OF sale_repl_factor_1 FOR VALUES IN ('NY');
CREATE TABLE sale_california_repl_factor_1 PARTITION OF sale_repl_factor_1 FOR VALUES IN ('CA');
SELECT create_distributed_table('sale_repl_factor_1', 'state_code');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

-- Should print a warning that it will not be propagated to worker nodes.
CLUSTER sale_repl_factor_1 USING sale_repl_factor_1_pk;
WARNING:  not propagating CLUSTER command for partitioned table to worker nodes
HINT:  Provide a child partition table names in order to CLUSTER distributed partitioned tables.
-- verify that we can still cluster the partition tables now since replication factor is 1
CLUSTER sale_newyork_repl_factor_1 USING sale_newyork_repl_factor_1_pkey;
create table reservations ( room_id integer not null, booked_during daterange );
insert into reservations values
-- 1: has a meets and a gap
(1, daterange('2018-07-01', '2018-07-07')),
(1, daterange('2018-07-07', '2018-07-14')),
(1, daterange('2018-07-20', '2018-07-22')),
-- 2: just a single row
(2, daterange('2018-07-01', '2018-07-03')),
-- 3: one null range
(3, NULL),
-- 4: two null ranges
(4, NULL),
(4, NULL),
-- 5: a null range and a non-null range
(5, NULL),
(5, daterange('2018-07-01', '2018-07-03')),
-- 6: has overlap
(6, daterange('2018-07-01', '2018-07-07')),
(6, daterange('2018-07-05', '2018-07-10')),
-- 7: two ranges that meet: no gap or overlap
(7, daterange('2018-07-01', '2018-07-07')),
(7, daterange('2018-07-07', '2018-07-14')),
-- 8: an empty range
(8, 'empty'::daterange);
SELECT create_distributed_table('reservations', 'room_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($$pg15.reservations$$)
 create_distributed_table
---------------------------------------------------------------------

(1 row)

-- should be fine to pushdown range_agg
SELECT   room_id, range_agg(booked_during ORDER BY booked_during)
FROM     reservations
GROUP BY room_id
ORDER BY room_id;
 room_id |                     range_agg
---------------------------------------------------------------------
       1 | {[07-01-2018,07-14-2018),[07-20-2018,07-22-2018)}
       2 | {[07-01-2018,07-03-2018)}
       3 |
       4 |
       5 | {[07-01-2018,07-03-2018)}
       6 | {[07-01-2018,07-10-2018)}
       7 | {[07-01-2018,07-14-2018)}
       8 | {}
(8 rows)

-- should be fine to apply range_agg on the coordinator
SELECT   room_id + 1, range_agg(booked_during ORDER BY booked_during)
FROM     reservations
GROUP BY room_id + 1
ORDER BY room_id + 1;
 ?column? |                     range_agg
---------------------------------------------------------------------
        2 | {[07-01-2018,07-14-2018),[07-20-2018,07-22-2018)}
        3 | {[07-01-2018,07-03-2018)}
        4 |
        5 |
        6 | {[07-01-2018,07-03-2018)}
        7 | {[07-01-2018,07-10-2018)}
        8 | {[07-01-2018,07-14-2018)}
        9 | {}
(8 rows)

-- min() and max() for xid8
create table xid8_t1 (x xid8, y int);
insert into xid8_t1 values ('0', 1), ('010', 2), ('42', 3), ('0xffffffffffffffff', 4), ('-1', 5);
SELECT create_distributed_table('xid8_t1', 'x');
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($$pg15.xid8_t1$$)
 create_distributed_table
---------------------------------------------------------------------

(1 row)

select min(x), max(x) from xid8_t1 ORDER BY 1,2;
 min |         max
---------------------------------------------------------------------
   0 | 18446744073709551615
(1 row)

select min(x), max(x) from xid8_t1 GROUP BY x ORDER BY 1,2;
         min          |         max
---------------------------------------------------------------------
                    0 |                    0
                    8 |                    8
                   42 |                   42
 18446744073709551615 | 18446744073709551615
(4 rows)

select min(x), max(x) from xid8_t1 GROUP BY y ORDER BY 1,2;
         min          |         max
---------------------------------------------------------------------
                    0 |                    0
                    8 |                    8
                   42 |                   42
 18446744073709551615 | 18446744073709551615
 18446744073709551615 | 18446744073709551615
(5 rows)

--
-- PG15 introduces security invoker views
-- Citus supports these views because permissions in the shards
-- are already checked for the view invoker
--
-- create a distributed table and populate it
CREATE TABLE events (tenant_id int, event_id int, descr text);
SELECT create_distributed_table('events','tenant_id');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

INSERT INTO events VALUES (1, 1, 'push');
INSERT INTO events VALUES (2, 2, 'push');
-- create a security invoker view with underlying distributed table
-- the view will be distributed with security_invoker option as well
CREATE VIEW sec_invoker_view WITH (security_invoker=true) AS SELECT * FROM events;
\c - - - :worker_1_port
SELECT relname, reloptions FROM pg_class
WHERE relname = 'sec_invoker_view' AND relnamespace = 'pg15'::regnamespace;
     relname      |       reloptions
---------------------------------------------------------------------
 sec_invoker_view | {security_invoker=true}
(1 row)

\c - - - :master_port
SET search_path TO pg15;
-- test altering the security_invoker flag
ALTER VIEW sec_invoker_view SET (security_invoker = false);
\c - - - :worker_1_port
SELECT relname, reloptions FROM pg_class
WHERE relname = 'sec_invoker_view' AND relnamespace = 'pg15'::regnamespace;
     relname      |        reloptions
---------------------------------------------------------------------
 sec_invoker_view | {security_invoker=false}
(1 row)

\c - - - :master_port
SET search_path TO pg15;
ALTER VIEW sec_invoker_view SET (security_invoker = true);
-- create a new user but don't give select permission to events table
-- only give select permission to the view
CREATE ROLE rls_tenant_1 WITH LOGIN;
GRANT USAGE ON SCHEMA pg15 TO rls_tenant_1;
GRANT SELECT ON sec_invoker_view TO rls_tenant_1;
-- this user shouldn't be able to query the view
-- because the view is security invoker
-- which means it will check the invoker's rights
-- against the view's underlying tables
SET ROLE rls_tenant_1;
SELECT * FROM sec_invoker_view ORDER BY event_id;
ERROR:  permission denied for table events
RESET ROLE;
-- now grant select on the underlying distributed table
-- and try again
-- now it should work!
GRANT SELECT ON TABLE events TO rls_tenant_1;
SET ROLE rls_tenant_1;
SELECT * FROM sec_invoker_view ORDER BY event_id;
 tenant_id | event_id | descr
---------------------------------------------------------------------
         1 |        1 | push
         2 |        2 | push
(2 rows)

RESET ROLE;
-- Enable row level security
ALTER TABLE events ENABLE ROW LEVEL SECURITY;
-- Create policy for tenants to read access their own rows
CREATE POLICY user_mod ON events
  FOR SELECT TO rls_tenant_1
  USING (current_user = 'rls_tenant_' || tenant_id::text);
-- all rows should be visible because we are querying with
-- the table owner user now
SELECT * FROM sec_invoker_view ORDER BY event_id;
 tenant_id | event_id | descr
---------------------------------------------------------------------
         1 |        1 | push
         2 |        2 | push
(2 rows)

-- Switch user that has been granted rights,
-- should be able to see rows that the policy allows
SET ROLE rls_tenant_1;
SELECT * FROM sec_invoker_view ORDER BY event_id;
 tenant_id | event_id | descr
---------------------------------------------------------------------
         1 |        1 | push
(1 row)

RESET ROLE;
-- ordinary view on top of security invoker view permissions
-- ordinary means security definer view
-- The PG expected behavior is that this doesn't change anything!!!
-- Can't escape security invoker views by defining a security definer view on top of it!
CREATE VIEW sec_definer_view AS SELECT * FROM sec_invoker_view ORDER BY event_id;
\c - - - :worker_1_port
SELECT relname, reloptions FROM pg_class
WHERE relname = 'sec_definer_view' AND relnamespace = 'pg15'::regnamespace;
     relname      | reloptions
---------------------------------------------------------------------
 sec_definer_view |
(1 row)

\c - - - :master_port
SET search_path TO pg15;
CREATE ROLE rls_tenant_2 WITH LOGIN;
GRANT USAGE ON SCHEMA pg15 TO rls_tenant_2;
GRANT SELECT ON sec_definer_view TO rls_tenant_2;
-- it doesn't matter that the parent view is security definer
-- still the security invoker view will check the invoker's permissions
-- and will not allow rls_tenant_2 to query the view
SET ROLE rls_tenant_2;
SELECT * FROM sec_definer_view ORDER BY event_id;
ERROR:  permission denied for table events
RESET ROLE;
-- grant select rights to rls_tenant_2
GRANT SELECT ON TABLE events TO rls_tenant_2;
-- we still have row level security so rls_tenant_2
-- will be able to query but won't be able to see anything
SET ROLE rls_tenant_2;
SELECT * FROM sec_definer_view ORDER BY event_id;
 tenant_id | event_id | descr
---------------------------------------------------------------------
(0 rows)

RESET ROLE;
-- give some rights to rls_tenant_2
CREATE POLICY user_mod_1 ON events
  FOR SELECT TO rls_tenant_2
  USING (current_user = 'rls_tenant_' || tenant_id::text);
-- Row level security will be applied as well! We are safe!
SET ROLE rls_tenant_2;
SELECT * FROM sec_definer_view ORDER BY event_id;
 tenant_id | event_id | descr
---------------------------------------------------------------------
         2 |        2 | push
(1 row)

RESET ROLE;
-- no need to test updatable views because they are currently not
-- supported in Citus when the query view contains citus tables
UPDATE sec_invoker_view SET event_id = 5;
ERROR:  cannot modify views when the query contains citus tables
--
-- Not allow ON DELETE/UPDATE SET DEFAULT actions on columns that
-- default to sequences
-- Adding a special test here since in PG15 we can
-- specify column list for foreign key ON DELETE SET actions
-- Relevant PG commit:
-- d6f96ed94e73052f99a2e545ed17a8b2fdc1fb8a
--
CREATE TABLE set_on_default_test_referenced(
    col_1 int, col_2 int, col_3 int, col_4 int,
    unique (col_1, col_3)
);
SELECT create_reference_table('set_on_default_test_referenced');
 create_reference_table
---------------------------------------------------------------------

(1 row)

-- should error since col_3 defaults to a sequence
CREATE TABLE set_on_default_test_referencing(
    col_1 int, col_2 int, col_3 serial, col_4 int,
    FOREIGN KEY(col_1, col_3)
    REFERENCES set_on_default_test_referenced(col_1, col_3)
    ON DELETE SET DEFAULT (col_1)
    ON UPDATE SET DEFAULT
);
ERROR:  cannot create foreign key constraint since Citus does not support ON DELETE / UPDATE SET DEFAULT actions on the columns that default to sequences
CREATE TABLE set_on_default_test_referencing(
    col_1 int, col_2 int, col_3 serial, col_4 int,
    FOREIGN KEY(col_1, col_3)
    REFERENCES set_on_default_test_referenced(col_1, col_3)
    ON DELETE SET DEFAULT (col_1)
);
-- should not error since this doesn't set any sequence based columns to default
SELECT create_reference_table('set_on_default_test_referencing');
 create_reference_table
---------------------------------------------------------------------

(1 row)

INSERT INTO set_on_default_test_referenced (col_1, col_3) VALUES (1, 1);
INSERT INTO set_on_default_test_referencing (col_1, col_3) VALUES (1, 1);
DELETE FROM set_on_default_test_referenced;
SELECT * FROM set_on_default_test_referencing ORDER BY 1,2;
 col_1 | col_2 | col_3 | col_4
---------------------------------------------------------------------
       |       |     1 |
(1 row)

DROP TABLE set_on_default_test_referencing;
SET client_min_messages to ERROR;
SELECT 1 FROM citus_add_node('localhost', :master_port, groupId => 0);
 ?column?
---------------------------------------------------------------------
        1
(1 row)

RESET client_min_messages;
-- this works around bug #6476: the CREATE TABLE below will
-- self-deadlock on PG15 if it also replicates reference
-- tables to the coordinator.
SELECT replicate_reference_tables(shard_transfer_mode := 'block_writes');
 replicate_reference_tables
---------------------------------------------------------------------

(1 row)

-- should error since col_3 defaults to a sequence
CREATE TABLE set_on_default_test_referencing(
    col_1 int, col_2 int, col_3 serial, col_4 int,
    FOREIGN KEY(col_1, col_3)
    REFERENCES set_on_default_test_referenced(col_1, col_3)
    ON DELETE SET DEFAULT (col_3)
);
ERROR:  cannot create foreign key constraint since Citus does not support ON DELETE / UPDATE SET DEFAULT actions on the columns that default to sequences
--
-- PG15 has suppressed some casts on constants when querying foreign tables
-- For example, we can use text to represent a type that's an enum on the remote side
-- A comparison on such a column will get shipped as "var = 'foo'::text"
-- But there's no enum = text operator on the remote side
-- If we leave off the explicit cast, the comparison will work
-- Test we behave in the same way with a Citus foreign table
-- Reminder: foreign tables cannot be distributed/reference, can only be Citus local
-- Relevant PG commit:
-- f8abb0f5e114d8c309239f0faa277b97f696d829
--
\set VERBOSITY terse
SET citus.next_shard_id TO 960200;
SET citus.enable_local_execution TO ON;
-- add the foreign table to metadata with the guc
SET citus.use_citus_managed_tables TO ON;
CREATE TYPE user_enum AS ENUM ('foo', 'bar', 'buz');
CREATE TABLE foreign_table_test (c0 integer NOT NULL, c1 user_enum);
INSERT INTO foreign_table_test VALUES (1, 'foo');
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 FOREIGN TABLE foreign_table (
        c0 integer NOT NULL,
        c1 text
)
        SERVER foreign_server
        OPTIONS (schema_name 'pg15', table_name 'foreign_table_test');
-- check that the foreign table is a citus local table
SELECT partmethod, repmodel FROM pg_dist_partition WHERE logicalrelid = 'foreign_table'::regclass ORDER BY logicalrelid;
 partmethod | repmodel
---------------------------------------------------------------------
 n          | s
(1 row)

-- same tests as in the relevant PG commit
-- Check that Remote SQL in the EXPLAIN doesn't contain casting
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM foreign_table WHERE c1 = 'foo' LIMIT 1;
                                                           QUERY PLAN
---------------------------------------------------------------------
 Custom Scan (Citus Adaptive)
   Output: remote_scan.c0, remote_scan.c1
   Task Count: 1
   Tasks Shown: All
   ->  Task
         Query: SELECT c0, c1 FROM pg15.foreign_table_960201 foreign_table WHERE (c1 OPERATOR(pg_catalog.=) 'foo'::text) LIMIT 1
         Node: host=localhost port=xxxxx dbname=regression
         ->  Foreign Scan on pg15.foreign_table_960201 foreign_table
               Output: c0, c1
               Remote SQL: SELECT c0, c1 FROM pg15.foreign_table_test WHERE ((c1 = 'foo')) LIMIT 1::bigint
(10 rows)

SELECT * FROM foreign_table WHERE c1 = 'foo' LIMIT 1;
 c0 | c1
---------------------------------------------------------------------
  1 | foo
(1 row)

-- Check that Remote SQL in the EXPLAIN doesn't contain casting
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM foreign_table WHERE 'foo' = c1 LIMIT 1;
                                                           QUERY PLAN
---------------------------------------------------------------------
 Custom Scan (Citus Adaptive)
   Output: remote_scan.c0, remote_scan.c1
   Task Count: 1
   Tasks Shown: All
   ->  Task
         Query: SELECT c0, c1 FROM pg15.foreign_table_960201 foreign_table WHERE ('foo'::text OPERATOR(pg_catalog.=) c1) LIMIT 1
         Node: host=localhost port=xxxxx dbname=regression
         ->  Foreign Scan on pg15.foreign_table_960201 foreign_table
               Output: c0, c1
               Remote SQL: SELECT c0, c1 FROM pg15.foreign_table_test WHERE (('foo' = c1)) LIMIT 1::bigint
(10 rows)

SELECT * FROM foreign_table WHERE 'foo' = c1 LIMIT 1;
 c0 | c1
---------------------------------------------------------------------
  1 | foo
(1 row)

-- we declared c1 to be text locally, but it's still the same type on
-- the remote which will balk if we try to do anything incompatible
-- with that remote type
SELECT * FROM foreign_table WHERE c1 LIKE 'foo' LIMIT 1; -- ERROR
ERROR:  operator does not exist: pg15.user_enum ~~ unknown
SELECT * FROM foreign_table WHERE c1::text LIKE 'foo' LIMIT 1; -- ERROR; cast not pushed down
ERROR:  operator does not exist: pg15.user_enum ~~ unknown
-- Clean up foreign table test
RESET citus.use_citus_managed_tables;
SELECT undistribute_table('foreign_table');
NOTICE:  creating a new table for pg15.foreign_table
NOTICE:  dropping the old pg15.foreign_table
NOTICE:  renaming the new table to pg15.foreign_table
 undistribute_table
---------------------------------------------------------------------

(1 row)

SELECT undistribute_table('foreign_table_test');
NOTICE:  creating a new table for pg15.foreign_table_test
NOTICE:  moving the data of pg15.foreign_table_test
NOTICE:  dropping the old pg15.foreign_table_test
NOTICE:  renaming the new table to pg15.foreign_table_test
 undistribute_table
---------------------------------------------------------------------

(1 row)

DROP SERVER foreign_server CASCADE;
NOTICE:  drop cascades to 2 other objects
-- PG15 now supports specifying oid on CREATE DATABASE
-- verify that we print meaningful notice messages.
CREATE DATABASE db_with_oid OID 987654;
NOTICE:  Citus partially supports CREATE DATABASE for distributed databases
DROP DATABASE db_with_oid;
-- SET ACCESS METHOD
-- Create a heap2 table am handler with heapam handler
CREATE ACCESS METHOD heap2 TYPE TABLE HANDLER heap_tableam_handler;
SELECT run_command_on_workers($$CREATE ACCESS METHOD heap2 TYPE TABLE HANDLER heap_tableam_handler$$);
           run_command_on_workers
---------------------------------------------------------------------
 (localhost,57637,t,"CREATE ACCESS METHOD")
 (localhost,57638,t,"CREATE ACCESS METHOD")
(2 rows)

CREATE TABLE mx_ddl_table2 (
    key int primary key,
    value int
);
SELECT create_distributed_table('mx_ddl_table2', 'key', 'hash', shard_count=> 4);
 create_distributed_table
---------------------------------------------------------------------

(1 row)

ALTER TABLE mx_ddl_table2 SET ACCESS METHOD heap2;
DROP TABLE mx_ddl_table2;
DROP ACCESS METHOD heap2;
SELECT run_command_on_workers($$DROP ACCESS METHOD heap2$$);
          run_command_on_workers
---------------------------------------------------------------------
 (localhost,57637,t,"DROP ACCESS METHOD")
 (localhost,57638,t,"DROP ACCESS METHOD")
(2 rows)

CREATE TABLE referenced (int_col integer PRIMARY KEY);
CREATE TABLE referencing (text_col text);
SET citus.shard_replication_factor TO 1;
SELECT create_distributed_table('referenced', null);
 create_distributed_table
---------------------------------------------------------------------

(1 row)

SELECT create_distributed_table('referencing', null);
 create_distributed_table
---------------------------------------------------------------------

(1 row)

RESET citus.shard_replication_factor;
CREATE OR REPLACE FUNCTION my_random(numeric)
  RETURNS numeric AS
$$
BEGIN
  RETURN 7 * $1;
END;
$$
LANGUAGE plpgsql IMMUTABLE;
ALTER TABLE referencing ADD COLUMN test_2 integer UNIQUE NULLS DISTINCT REFERENCES referenced(int_col);
ALTER TABLE referencing ADD COLUMN test_3 integer GENERATED ALWAYS AS (text_col::int * my_random(1)) STORED UNIQUE NULLS NOT DISTINCT;
SELECT (groupid = 0) AS is_coordinator, result FROM run_command_on_all_nodes(
  $$SELECT get_grouped_fkey_constraints FROM get_grouped_fkey_constraints('pg15.referencing')$$
)
JOIN pg_dist_node USING (nodeid)
ORDER BY is_coordinator DESC, result;
 is_coordinator |                                                                                                                                                                                                                  result
---------------------------------------------------------------------
 t              | [{"deferred": false, "deferable": false, "on_delete": "a", "on_update": "a", "match_type": "s", "constraint_names": ["referencing__fkey"], "referenced_tables": ["pg15.referenced"], "referenced_columns": ["int_col"], "referencing_tables": ["pg15.referencing"], "referencing_columns": ["test_2"], "referencing_columns_set_null_or_default": null}]
 f              | [{"deferred": false, "deferable": false, "on_delete": "a", "on_update": "a", "match_type": "s", "constraint_names": ["referencing__fkey", "referencing__fkey_960207"], "referenced_tables": ["pg15.referenced", "pg15.referenced_960206"], "referenced_columns": ["int_col"], "referencing_tables": ["pg15.referencing", "pg15.referencing_960207"], "referencing_columns": ["test_2"], "referencing_columns_set_null_or_default": null}]
 f              | [{"deferred": false, "deferable": false, "on_delete": "a", "on_update": "a", "match_type": "s", "constraint_names": ["referencing__fkey"], "referenced_tables": ["pg15.referenced"], "referenced_columns": ["int_col"], "referencing_tables": ["pg15.referencing"], "referencing_columns": ["test_2"], "referencing_columns_set_null_or_default": null}]
(3 rows)

SELECT (groupid = 0) AS is_coordinator, result FROM run_command_on_all_nodes(
  $$SELECT get_index_defs FROM get_index_defs('pg15', 'referencing')$$
)
JOIN pg_dist_node USING (nodeid)
ORDER BY is_coordinator DESC, result;
 is_coordinator |                                                                                                                                                                                                                                                                                   result
---------------------------------------------------------------------
 t              | [{"indexdefs": ["CREATE UNIQUE INDEX referencing__key ON pg15.referencing USING btree (test_2)"], "indexnames": ["referencing__key"]}, {"indexdefs": ["CREATE UNIQUE INDEX referencing__key1 ON pg15.referencing USING btree (test_3) NULLS NOT DISTINCT"], "indexnames": ["referencing__key1"]}]
 f              | [{"indexdefs": ["CREATE UNIQUE INDEX referencing__key ON pg15.referencing USING btree (test_2)", "CREATE UNIQUE INDEX referencing__key_960207 ON pg15.referencing_960207 USING btree (test_2)"], "indexnames": ["referencing__key", "referencing__key_960207"]}, {"indexdefs": ["CREATE UNIQUE INDEX referencing__key1 ON pg15.referencing USING btree (test_3) NULLS NOT DISTINCT", "CREATE UNIQUE INDEX referencing__key1_960207 ON pg15.referencing_960207 USING btree (test_3) NULLS NOT DISTINCT"], "indexnames": ["referencing__key1", "referencing__key1_960207"]}]
 f              | [{"indexdefs": ["CREATE UNIQUE INDEX referencing__key ON pg15.referencing USING btree (test_2)"], "indexnames": ["referencing__key"]}, {"indexdefs": ["CREATE UNIQUE INDEX referencing__key1 ON pg15.referencing USING btree (test_3) NULLS NOT DISTINCT"], "indexnames": ["referencing__key1"]}]
(3 rows)

set citus.log_remote_commands = true;
set citus.grep_remote_commands = '%ALTER DATABASE%';
alter database regression REFRESH COLLATION VERSION;
NOTICE:  version has not changed
NOTICE:  issuing ALTER DATABASE regression REFRESH COLLATION VERSION;
NOTICE:  issuing ALTER DATABASE regression REFRESH COLLATION VERSION;
SET citus.enable_create_database_propagation TO OFF;
CREATE DATABASE local_database_1;
NOTICE:  Citus partially supports CREATE DATABASE for distributed databases
RESET citus.enable_create_database_propagation;
CREATE ROLE local_role_1;
ALTER DATABASE local_database_1 REFRESH COLLATION VERSION;
NOTICE:  version has not changed
REVOKE CONNECT, TEMPORARY, CREATE ON DATABASE local_database_1 FROM local_role_1;
DROP ROLE local_role_1;
DROP DATABASE local_database_1;
set citus.log_remote_commands = false;
-- Clean up
\set VERBOSITY terse
SET client_min_messages TO ERROR;
DROP SCHEMA pg15 CASCADE;
DROP ROLE rls_tenant_1;
DROP ROLE rls_tenant_2;
