--
-- 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');

-- fail, needs "locale"
CREATE COLLATION german_phonebook_test (provider = icu, lc_collate = 'de-u-co-phonebk', lc_ctype = 'de-u-co-phonebk');

-- 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'';
');
SELECT result FROM run_command_on_all_nodes('
    SELECT collctype FROM pg_collation WHERE collname = ''german_phonebook_test'';
');

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

-- 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'';
');
SELECT result FROM run_command_on_all_nodes('
    SELECT collctype FROM pg_collation WHERE collname = ''default_provider'';
');
SELECT result FROM run_command_on_all_nodes(:worker_query_2);

--
-- 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');
SELECT create_distributed_table('record_sale', 'state_code', colocate_with := 'sale');

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;
ALTER TRIGGER "record_sale_trigger" ON "pg15"."sale" RENAME TO "new_record_sale_trigger";
SELECT * FROM sale_triggers ORDER BY 1, 2;

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

--
-- 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');

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

-- fails, CASCADE must be specified
-- will test CASCADE inside the transcation
ALTER TABLE generated_stored_ref DROP COLUMN col_1;

BEGIN;
  -- drops col_1, col_3, col_5
  ALTER TABLE generated_stored_ref DROP COLUMN col_1 CASCADE;
  ALTER TABLE generated_stored_ref DROP COLUMN col_4;

  -- show that undistribute_table works fine
  SELECT undistribute_table('generated_stored_ref');
  INSERT INTO generated_stored_ref VALUES (5);
  SELECT * FROM generated_stored_REF ORDER BY 1;
ROLLBACK;

SELECT undistribute_table('generated_stored_ref');

--
-- 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');

MERGE INTO tbl1 USING tbl2 ON (true)
WHEN MATCHED THEN DELETE;

SELECT undistribute_table('tbl1');

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

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');

MERGE INTO tbl1 USING tbl2 ON (true)
WHEN MATCHED THEN DELETE;

-- now, both are reference, not supported
SELECT create_reference_table('tbl1');

MERGE INTO tbl1 USING tbl2 ON (true)
WHEN MATCHED THEN DELETE;

-- now, both distributed, not works
SELECT undistribute_table('tbl1');
SELECT undistribute_table('tbl2');

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

-- show that the definition is expected
SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'FKTABLE_local'::regclass::oid ORDER BY oid;

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

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

\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');
SELECT create_distributed_table('tbl2', 'x');

MERGE INTO tbl1 USING tbl2 ON (true)
WHEN MATCHED THEN DELETE;

-- also, inside subqueries & ctes
WITH targq AS (
    SELECT * FROM tbl2
)
MERGE INTO tbl1 USING targq ON (true)
WHEN MATCHED THEN DELETE;

MERGE INTO tbl1 t
USING tbl2
ON (true)
WHEN MATCHED THEN
    DO NOTHING;

MERGE INTO tbl1 t
USING tbl2
ON (true)
WHEN MATCHED THEN
    UPDATE SET x = (SELECT count(*) FROM tbl2);

-- 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');
-- However, we can distribute by other columns
SELECT create_distributed_table('numeric_negative_scale','orig_value');
-- Verify that we can not change the distribution column to the numeric column
SELECT alter_distributed_table('numeric_negative_scale',
                                distribution_column := 'numeric_column');

SELECT * FROM numeric_negative_scale ORDER BY 1,2;

-- 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');
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;
-- verify we can route queries to the right shards
SELECT * FROM numeric_scale_gt_precision WHERE numeric_column=0.027;

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

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

-- distribute tables and re-run the same queries
SELECT * FROM create_distributed_table('numeric_repartition_first','id');
SELECT * FROM create_distributed_table('numeric_repartition_second','id');

SET citus.enable_repartition_joins TO 1;

SELECT count(*)
FROM numeric_repartition_first f,
     numeric_repartition_second s
WHERE f.id = s.numeric_column;

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

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

-- 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;
-- 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;
-- 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;
-- print the substrings between two `fluffily`
SELECT regexp_substr(o_comment, 'fluffily.*fluffily') FROM public.orders ORDER BY 1 LIMIT 5;
-- 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;

-- 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');
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');
COPY copy_test2 FROM :'temp_dir''copy_test.txt' WITH ( HEADER match, FORMAT text);

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

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

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');

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

\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;
ALTER SEQUENCE IF EXISTS seq_non_exists SET LOGGED;

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;

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

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

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

\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');

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

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

\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');

-- ok, Citus could relax this constraint in the future
SELECT create_distributed_table('FKTABLE', 'tid');

-- with reference tables it should all work fine
SELECT create_reference_table('FKTABLE');

-- show that the definition is expected
SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'fktable'::regclass::oid ORDER BY 1;

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

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

\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 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') ;
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 *;

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

-- show all records in the table for fact checking
SELECT * FROM null_distinct_test ORDER BY c3;

-- 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');
INSERT INTO reference_uniq_test VALUES (1, 1), (1, NULL), (NULL, 1);
-- the following will fail
INSERT INTO reference_uniq_test VALUES (1, NULL);

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

-- verify that we can cluster the partition tables only when replication factor is 1
CLUSTER sale_newyork USING sale_newyork_pkey;

-- 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');

-- Should print a warning that it will not be propagated to worker nodes.
CLUSTER sale_repl_factor_1 USING sale_repl_factor_1_pk;

-- 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');

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

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

-- 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');
select min(x), max(x) from xid8_t1 ORDER BY 1,2;
select min(x), max(x) from xid8_t1 GROUP BY x ORDER BY 1,2;
select min(x), max(x) from xid8_t1 GROUP BY y ORDER BY 1,2;

--
-- 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');
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;

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

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

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

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

--
-- 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');

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

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');

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;

DROP TABLE set_on_default_test_referencing;

SET client_min_messages to ERROR;
SELECT 1 FROM citus_add_node('localhost', :master_port, groupId => 0);
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');

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

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

-- 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;
SELECT * FROM foreign_table WHERE c1 = 'foo' LIMIT 1;

-- Check that Remote SQL in the EXPLAIN doesn't contain casting
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM foreign_table WHERE 'foo' = c1 LIMIT 1;
SELECT * FROM foreign_table WHERE 'foo' = c1 LIMIT 1;

-- 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
SELECT * FROM foreign_table WHERE c1::text LIKE 'foo' LIMIT 1; -- ERROR; cast not pushed down

-- Clean up foreign table test
RESET citus.use_citus_managed_tables;
SELECT undistribute_table('foreign_table');
SELECT undistribute_table('foreign_table_test');
DROP SERVER foreign_server CASCADE;

-- PG15 now supports specifying oid on CREATE DATABASE
-- verify that we print meaningful notice messages.
CREATE DATABASE db_with_oid OID 987654;
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$$);
CREATE TABLE mx_ddl_table2 (
    key int primary key,
    value int
);
SELECT create_distributed_table('mx_ddl_table2', 'key', 'hash', shard_count=> 4);
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$$);

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);
SELECT create_distributed_table('referencing', null);
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;

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;

set citus.log_remote_commands = true;
set citus.grep_remote_commands = '%ALTER DATABASE%';
alter database regression REFRESH COLLATION VERSION;
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;
