PG18 - Handle publish_generated_columns in distributed publications (#8360)

https://github.com/postgres/postgres/commit/7054186c4

fixes #8358 

This PR wires up PostgreSQL 18’s `publish_generated_columns` publication
option in Citus and adds regression coverage to ensure it behaves
correctly for distributed tables, without changing existing DDL output
for publications that rely on the default.

---

### 1. Preserve `publish_generated_columns` when rebuilding publications

In `BuildCreatePublicationStmt`:

* On PG18+ we now read the new `pubgencols` field from `pg_publication`
and map it as follows:

  * `'n'` → default (`none`)
  * `'s'` → `stored`

* For `pubgencols == 's'` we append a `publish_generated_columns`
defelem to the reconstructed statement:

  ```c
  #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
  ```

* For `pubgencols == 'n'` we do **not** emit an option and rely on
PostgreSQL’s default.

* Any value other than `'n'` or `'s'` raises an error rather than
silently producing incorrect DDL.

This ensures:

* Publications that explicitly use `publish_generated_columns = stored`
are reconstructed with that option on workers, so workers get
`pubgencols = 's'`.
* Publications that use the default (`none`) continue to produce the
same `CREATE PUBLICATION ... WITH (...)` text as before (no extra
`publish_generated_columns = 'none'` noise), fixing the unintended diffs
in existing publication tests.

---

### 2. New PG18 regression coverage for distributed publications

In `src/test/regress/sql/pg18.sql`:

* Create a table with a stored generated column and make it distributed
so the publication goes through Citus DDL propagation:

  ```sql
  CREATE TABLE gen_pub_tab (
      id int primary key,
      a  int,
      b  int GENERATED ALWAYS AS (a * 10) STORED
  );

SELECT create_distributed_table('gen_pub_tab', 'id', colocate_with :=
'none');
  ```

* Create two publications that exercise both `pubgencols` values:

  ```sql
  CREATE PUBLICATION pub_gen_cols_stored
      FOR TABLE gen_pub_tab
WITH (publish = 'insert, update', publish_generated_columns = stored);

  CREATE PUBLICATION pub_gen_cols_none
      FOR TABLE gen_pub_tab
WITH (publish = 'insert, update', publish_generated_columns = none);
  ```

* On coordinator and both workers, assert the catalog contents:

  ```sql
  SELECT pubname, pubgencols
  FROM pg_publication
  WHERE pubname IN ('pub_gen_cols_stored', 'pub_gen_cols_none')
  ORDER BY pubname;
  ```

  Expected on all three nodes:

  * `pub_gen_cols_stored | s`
  * `pub_gen_cols_none   | n`

This test verifies that:

* `pubgencols` is correctly set on the coordinator for both `stored` and
`none`.
* Citus propagates the setting unchanged to all workers for a
distributed table.
pull/8336/head
Mehmet YILMAZ 2025-12-01 12:17:57 +03:00 committed by GitHub
parent 662b7248db
commit c600eabd82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 353 additions and 0 deletions

View File

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

View File

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

View File

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