diff --git a/src/backend/distributed/commands/publication.c b/src/backend/distributed/commands/publication.c index 3e03c5505..7c1db06d6 100644 --- a/src/backend/distributed/commands/publication.c +++ b/src/backend/distributed/commands/publication.c @@ -196,6 +196,27 @@ BuildCreatePublicationStmt(Oid publicationId) -1); createPubStmt->options = lappend(createPubStmt->options, pubViaRootOption); +/* WITH (publish_generated_columns = ...) option (PG18+) */ +#if PG_VERSION_NUM >= PG_VERSION_18 + if (publicationForm->pubgencols == 's') /* stored */ + { + DefElem *pubGenColsOption = + makeDefElem("publish_generated_columns", + (Node *) makeString("stored"), + -1); + + createPubStmt->options = + lappend(createPubStmt->options, pubGenColsOption); + } + else if (publicationForm->pubgencols != 'n') /* 'n' = none (default) */ + { + ereport(ERROR, + (errmsg("unexpected pubgencols value '%c' for publication %u", + publicationForm->pubgencols, publicationId))); + } +#endif + + /* WITH (publish = 'insert, update, delete, truncate') option */ List *publishList = NIL; diff --git a/src/test/regress/expected/pg18.out b/src/test/regress/expected/pg18.out index 174da2457..08b6c46e3 100644 --- a/src/test/regress/expected/pg18.out +++ b/src/test/regress/expected/pg18.out @@ -1070,6 +1070,187 @@ CREATE MATERIALIZED VIEW copytest_mv AS SELECT create_distributed_table('copytest_mv', 'id'); ERROR: copytest_mv is not a regular, foreign or partitioned table -- After that, any command on the materialized view is outside Citus support. +-- PG18: verify publish_generated_columns is preserved for distributed tables +-- https://github.com/postgres/postgres/commit/7054186c4 +\c - - - :master_port +CREATE SCHEMA pg18_publication; +SET search_path TO pg18_publication; +-- table with a stored generated column +CREATE TABLE gen_pub_tab ( + id int primary key, + a int, + b int GENERATED ALWAYS AS (a * 10) STORED +); +-- make it distributed so CREATE PUBLICATION goes through Citus metadata/DDL path +SELECT create_distributed_table('gen_pub_tab', 'id', colocate_with := 'none'); + create_distributed_table +--------------------------------------------------------------------- + +(1 row) + +-- publication using the new PG18 option: stored +CREATE PUBLICATION pub_gen_cols_stored + FOR TABLE gen_pub_tab + WITH (publish = 'insert, update', publish_generated_columns = stored); +-- second publication explicitly using "none" for completeness +CREATE PUBLICATION pub_gen_cols_none + FOR TABLE gen_pub_tab + WITH (publish = 'insert, update', publish_generated_columns = none); +-- On coordinator: pubgencols must be 's' and 'n' respectively +SELECT pubname, pubgencols +FROM pg_publication +WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none') +ORDER BY pubname; + pubname | pubgencols +--------------------------------------------------------------------- + pub_gen_cols_none | n + pub_gen_cols_stored | s +(2 rows) + +-- On worker 1: both publications must exist and keep pubgencols in sync +\c - - - :worker_1_port +SET search_path TO pg18_publication; +SELECT pubname, pubgencols +FROM pg_publication +WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none') +ORDER BY pubname; + pubname | pubgencols +--------------------------------------------------------------------- + pub_gen_cols_none | n + pub_gen_cols_stored | s +(2 rows) + +-- On worker 2: same check +\c - - - :worker_2_port +SET search_path TO pg18_publication; +SELECT pubname, pubgencols +FROM pg_publication +WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none') +ORDER BY pubname; + pubname | pubgencols +--------------------------------------------------------------------- + pub_gen_cols_none | n + pub_gen_cols_stored | s +(2 rows) + +-- Now verify ALTER PUBLICATION .. SET (publish_generated_columns = none) +-- propagates to workers as well. +\c - - - :master_port +SET search_path TO pg18_publication; +ALTER PUBLICATION pub_gen_cols_stored + SET (publish_generated_columns = none); +-- coordinator: both publications should now have pubgencols = 'n' +SELECT pubname, pubgencols +FROM pg_publication +WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none') +ORDER BY pubname; + pubname | pubgencols +--------------------------------------------------------------------- + pub_gen_cols_none | n + pub_gen_cols_stored | n +(2 rows) + +-- worker 1: pubgencols must match coordinator +\c - - - :worker_1_port +SET search_path TO pg18_publication; +SELECT pubname, pubgencols +FROM pg_publication +WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none') +ORDER BY pubname; + pubname | pubgencols +--------------------------------------------------------------------- + pub_gen_cols_none | n + pub_gen_cols_stored | n +(2 rows) + +-- worker 2: same check +\c - - - :worker_2_port +SET search_path TO pg18_publication; +SELECT pubname, pubgencols +FROM pg_publication +WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none') +ORDER BY pubname; + pubname | pubgencols +--------------------------------------------------------------------- + pub_gen_cols_none | n + pub_gen_cols_stored | n +(2 rows) + +-- Column list precedence test: Citus must preserve both prattrs and pubgencols +\c - - - :master_port +SET search_path TO pg18_publication; +-- Case 1: column list explicitly includes the generated column, flag = none +CREATE PUBLICATION pub_gen_cols_list_includes_b + FOR TABLE gen_pub_tab (id, a, b) + WITH (publish_generated_columns = none); +-- Case 2: column list excludes the generated column, flag = stored +CREATE PUBLICATION pub_gen_cols_list_excludes_b + FOR TABLE gen_pub_tab (id, a) + WITH (publish_generated_columns = stored); +-- Helper: show pubname, pubgencols, and column list (prattrs) for gen_pub_tab +SELECT p.pubname, + p.pubgencols, + r.prattrs +FROM pg_publication p +JOIN pg_publication_rel r ON p.oid = r.prpubid +JOIN pg_class c ON c.oid = r.prrelid +WHERE p.pubname IN ('pub_gen_cols_list_includes_b', + 'pub_gen_cols_list_excludes_b') + AND c.relname = 'gen_pub_tab' +ORDER BY p.pubname; + pubname | pubgencols | prattrs +--------------------------------------------------------------------- + pub_gen_cols_list_excludes_b | s | 1 2 + pub_gen_cols_list_includes_b | n | 1 2 3 +(2 rows) + +-- worker 1: must see the same pubgencols + prattrs +\c - - - :worker_1_port +SET search_path TO pg18_publication; +SELECT p.pubname, + p.pubgencols, + r.prattrs +FROM pg_publication p +JOIN pg_publication_rel r ON p.oid = r.prpubid +JOIN pg_class c ON c.oid = r.prrelid +WHERE p.pubname IN ('pub_gen_cols_list_includes_b', + 'pub_gen_cols_list_excludes_b') + AND c.relname = 'gen_pub_tab' +ORDER BY p.pubname; + pubname | pubgencols | prattrs +--------------------------------------------------------------------- + pub_gen_cols_list_excludes_b | s | 1 2 + pub_gen_cols_list_includes_b | n | 1 2 3 +(2 rows) + +-- worker 2: same check +\c - - - :worker_2_port +SET search_path TO pg18_publication; +SELECT p.pubname, + p.pubgencols, + r.prattrs +FROM pg_publication p +JOIN pg_publication_rel r ON p.oid = r.prpubid +JOIN pg_class c ON c.oid = r.prrelid +WHERE p.pubname IN ('pub_gen_cols_list_includes_b', + 'pub_gen_cols_list_excludes_b') + AND c.relname = 'gen_pub_tab' +ORDER BY p.pubname; + pubname | pubgencols | prattrs +--------------------------------------------------------------------- + pub_gen_cols_list_excludes_b | s | 1 2 + pub_gen_cols_list_includes_b | n | 1 2 3 +(2 rows) + +-- back to coordinator for subsequent tests / cleanup +\c - - - :master_port +SET search_path TO pg18_publication; +DROP PUBLICATION pub_gen_cols_stored; +DROP PUBLICATION pub_gen_cols_none; +DROP SCHEMA pg18_publication CASCADE; +NOTICE: drop cascades to table gen_pub_tab +SET search_path TO pg18_nn; +-- PG18: verify publish_generated_columns is preserved for distributed tables -- cleanup with minimum verbosity SET client_min_messages TO ERROR; RESET search_path; diff --git a/src/test/regress/sql/pg18.sql b/src/test/regress/sql/pg18.sql index af077bf4c..df9f71869 100644 --- a/src/test/regress/sql/pg18.sql +++ b/src/test/regress/sql/pg18.sql @@ -632,6 +632,157 @@ CREATE MATERIALIZED VIEW copytest_mv AS SELECT create_distributed_table('copytest_mv', 'id'); -- After that, any command on the materialized view is outside Citus support. +-- PG18: verify publish_generated_columns is preserved for distributed tables +-- https://github.com/postgres/postgres/commit/7054186c4 +\c - - - :master_port +CREATE SCHEMA pg18_publication; +SET search_path TO pg18_publication; + +-- table with a stored generated column +CREATE TABLE gen_pub_tab ( + id int primary key, + a int, + b int GENERATED ALWAYS AS (a * 10) STORED +); + +-- make it distributed so CREATE PUBLICATION goes through Citus metadata/DDL path +SELECT create_distributed_table('gen_pub_tab', 'id', colocate_with := 'none'); + +-- publication using the new PG18 option: stored +CREATE PUBLICATION pub_gen_cols_stored + FOR TABLE gen_pub_tab + WITH (publish = 'insert, update', publish_generated_columns = stored); + +-- second publication explicitly using "none" for completeness +CREATE PUBLICATION pub_gen_cols_none + FOR TABLE gen_pub_tab + WITH (publish = 'insert, update', publish_generated_columns = none); + +-- On coordinator: pubgencols must be 's' and 'n' respectively +SELECT pubname, pubgencols +FROM pg_publication +WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none') +ORDER BY pubname; + +-- On worker 1: both publications must exist and keep pubgencols in sync +\c - - - :worker_1_port +SET search_path TO pg18_publication; + +SELECT pubname, pubgencols +FROM pg_publication +WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none') +ORDER BY pubname; + +-- On worker 2: same check +\c - - - :worker_2_port +SET search_path TO pg18_publication; + +SELECT pubname, pubgencols +FROM pg_publication +WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none') +ORDER BY pubname; + +-- Now verify ALTER PUBLICATION .. SET (publish_generated_columns = none) +-- propagates to workers as well. + +\c - - - :master_port +SET search_path TO pg18_publication; + +ALTER PUBLICATION pub_gen_cols_stored + SET (publish_generated_columns = none); + +-- coordinator: both publications should now have pubgencols = 'n' +SELECT pubname, pubgencols +FROM pg_publication +WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none') +ORDER BY pubname; + +-- worker 1: pubgencols must match coordinator +\c - - - :worker_1_port +SET search_path TO pg18_publication; + +SELECT pubname, pubgencols +FROM pg_publication +WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none') +ORDER BY pubname; + +-- worker 2: same check +\c - - - :worker_2_port +SET search_path TO pg18_publication; + +SELECT pubname, pubgencols +FROM pg_publication +WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none') +ORDER BY pubname; + +-- Column list precedence test: Citus must preserve both prattrs and pubgencols + +\c - - - :master_port +SET search_path TO pg18_publication; + +-- Case 1: column list explicitly includes the generated column, flag = none +CREATE PUBLICATION pub_gen_cols_list_includes_b + FOR TABLE gen_pub_tab (id, a, b) + WITH (publish_generated_columns = none); + +-- Case 2: column list excludes the generated column, flag = stored +CREATE PUBLICATION pub_gen_cols_list_excludes_b + FOR TABLE gen_pub_tab (id, a) + WITH (publish_generated_columns = stored); + +-- Helper: show pubname, pubgencols, and column list (prattrs) for gen_pub_tab +SELECT p.pubname, + p.pubgencols, + r.prattrs +FROM pg_publication p +JOIN pg_publication_rel r ON p.oid = r.prpubid +JOIN pg_class c ON c.oid = r.prrelid +WHERE p.pubname IN ('pub_gen_cols_list_includes_b', + 'pub_gen_cols_list_excludes_b') + AND c.relname = 'gen_pub_tab' +ORDER BY p.pubname; + +-- worker 1: must see the same pubgencols + prattrs +\c - - - :worker_1_port +SET search_path TO pg18_publication; + +SELECT p.pubname, + p.pubgencols, + r.prattrs +FROM pg_publication p +JOIN pg_publication_rel r ON p.oid = r.prpubid +JOIN pg_class c ON c.oid = r.prrelid +WHERE p.pubname IN ('pub_gen_cols_list_includes_b', + 'pub_gen_cols_list_excludes_b') + AND c.relname = 'gen_pub_tab' +ORDER BY p.pubname; + +-- worker 2: same check +\c - - - :worker_2_port +SET search_path TO pg18_publication; + +SELECT p.pubname, + p.pubgencols, + r.prattrs +FROM pg_publication p +JOIN pg_publication_rel r ON p.oid = r.prpubid +JOIN pg_class c ON c.oid = r.prrelid +WHERE p.pubname IN ('pub_gen_cols_list_includes_b', + 'pub_gen_cols_list_excludes_b') + AND c.relname = 'gen_pub_tab' +ORDER BY p.pubname; + +-- back to coordinator for subsequent tests / cleanup +\c - - - :master_port +SET search_path TO pg18_publication; +DROP PUBLICATION pub_gen_cols_stored; +DROP PUBLICATION pub_gen_cols_none; +DROP PUBLICATION pub_gen_cols_list_includes_b; +DROP PUBLICATION pub_gen_cols_list_excludes_b; +DROP SCHEMA pg18_publication CASCADE; +SET search_path TO pg18_nn; +-- END: PG18: verify publish_generated_columns is preserved for distributed tables + -- cleanup with minimum verbosity SET client_min_messages TO ERROR; RESET search_path;