Hi,

We are building internal CDC tooling and have found it challenging to
identify which index is used as the replica identity for a publication. I
propose adding two new columns, replident and replidentidx, to the
pg_publication_tables catalog view so users can easily see this
configuration.

I have attached a patch with the implementation and updated regression
tests. Please let me know if you feel this is a valuable addition or if you
have any concerns with this approach.

Thanks,
Peter Travers

Engineer
linear.app
diff --git a/replica_identity_enhancements.patch b/replica_identity_enhancements.patch
new file mode 100644
index 0000000000..bbad40cdf9
--- /dev/null
+++ b/replica_identity_enhancements.patch
@@ -0,0 +1,199 @@
+diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
+index 8f129baec9..03cbf048d5 100644
+--- a/src/backend/catalog/system_views.sql
++++ b/src/backend/catalog/system_views.sql
+@@ -420,7 +420,9 @@ CREATE VIEW pg_publication_tables AS
+           WHERE a.attrelid = GPT.relid AND
+                 a.attnum = ANY(GPT.attrs)
+         ) AS attnames,
+-        pg_get_expr(GPT.qual, GPT.relid) AS rowfilter
++        pg_get_expr(GPT.qual, GPT.relid) AS rowfilter,
++        C.relreplident AS replident,
++        pg_get_replica_identity_index(C.oid) AS replidentidx
+     FROM pg_publication P,
+          LATERAL pg_get_publication_tables(P.pubname) GPT,
+          pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace)
+diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
+index 29e54b214a..b350e61ed4 100644
+--- a/src/test/regress/expected/publication.out
++++ b/src/test/regress/expected/publication.out
+@@ -2100,52 +2100,52 @@ CREATE TABLE sch2.tbl1_part1 PARTITION OF sch1.tbl1 FOR VALUES FROM (1) to (10);
+ -- Schema publication that does not include the schema that has the parent table
+ CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch2 WITH (PUBLISH_VIA_PARTITION_ROOT=1);
+ SELECT * FROM pg_publication_tables;
+- pubname | schemaname | tablename  | attnames | rowfilter 
+----------+------------+------------+----------+-----------
+- pub     | sch2       | tbl1_part1 | {a}      | 
++ pubname | schemaname | tablename  | attnames | rowfilter | replident | replidentidx 
++---------+------------+------------+----------+-----------+-----------+--------------
++ pub     | sch2       | tbl1_part1 | {a}      |           | d         | 
+ (1 row)
+ 
+ DROP PUBLICATION pub;
+ -- Table publication that does not include the parent table
+ CREATE PUBLICATION pub FOR TABLE sch2.tbl1_part1 WITH (PUBLISH_VIA_PARTITION_ROOT=1);
+ SELECT * FROM pg_publication_tables;
+- pubname | schemaname | tablename  | attnames | rowfilter 
+----------+------------+------------+----------+-----------
+- pub     | sch2       | tbl1_part1 | {a}      | 
++ pubname | schemaname | tablename  | attnames | rowfilter | replident | replidentidx 
++---------+------------+------------+----------+-----------+-----------+--------------
++ pub     | sch2       | tbl1_part1 | {a}      |           | d         | 
+ (1 row)
+ 
+ -- Table publication that includes both the parent table and the child table
+ ALTER PUBLICATION pub ADD TABLE sch1.tbl1;
+ SELECT * FROM pg_publication_tables;
+- pubname | schemaname | tablename | attnames | rowfilter 
+----------+------------+-----------+----------+-----------
+- pub     | sch1       | tbl1      | {a}      | 
++ pubname | schemaname | tablename | attnames | rowfilter | replident | replidentidx 
++---------+------------+-----------+----------+-----------+-----------+--------------
++ pub     | sch1       | tbl1      | {a}      |           | d         | 
+ (1 row)
+ 
+ DROP PUBLICATION pub;
+ -- Schema publication that does not include the schema that has the parent table
+ CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch2 WITH (PUBLISH_VIA_PARTITION_ROOT=0);
+ SELECT * FROM pg_publication_tables;
+- pubname | schemaname | tablename  | attnames | rowfilter 
+----------+------------+------------+----------+-----------
+- pub     | sch2       | tbl1_part1 | {a}      | 
++ pubname | schemaname | tablename  | attnames | rowfilter | replident | replidentidx 
++---------+------------+------------+----------+-----------+-----------+--------------
++ pub     | sch2       | tbl1_part1 | {a}      |           | d         | 
+ (1 row)
+ 
+ DROP PUBLICATION pub;
+ -- Table publication that does not include the parent table
+ CREATE PUBLICATION pub FOR TABLE sch2.tbl1_part1 WITH (PUBLISH_VIA_PARTITION_ROOT=0);
+ SELECT * FROM pg_publication_tables;
+- pubname | schemaname | tablename  | attnames | rowfilter 
+----------+------------+------------+----------+-----------
+- pub     | sch2       | tbl1_part1 | {a}      | 
++ pubname | schemaname | tablename  | attnames | rowfilter | replident | replidentidx 
++---------+------------+------------+----------+-----------+-----------+--------------
++ pub     | sch2       | tbl1_part1 | {a}      |           | d         | 
+ (1 row)
+ 
+ -- Table publication that includes both the parent table and the child table
+ ALTER PUBLICATION pub ADD TABLE sch1.tbl1;
+ SELECT * FROM pg_publication_tables;
+- pubname | schemaname | tablename  | attnames | rowfilter 
+----------+------------+------------+----------+-----------
+- pub     | sch2       | tbl1_part1 | {a}      | 
++ pubname | schemaname | tablename  | attnames | rowfilter | replident | replidentidx 
++---------+------------+------------+----------+-----------+-----------+--------------
++ pub     | sch2       | tbl1_part1 | {a}      |           | d         | 
+ (1 row)
+ 
+ DROP PUBLICATION pub;
+@@ -2158,9 +2158,9 @@ CREATE TABLE sch1.tbl1_part3 (a int) PARTITION BY RANGE(a);
+ ALTER TABLE sch1.tbl1 ATTACH PARTITION sch1.tbl1_part3 FOR VALUES FROM (20) to (30);
+ CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch1 WITH (PUBLISH_VIA_PARTITION_ROOT=1);
+ SELECT * FROM pg_publication_tables;
+- pubname | schemaname | tablename | attnames | rowfilter 
+----------+------------+-----------+----------+-----------
+- pub     | sch1       | tbl1      | {a}      | 
++ pubname | schemaname | tablename | attnames | rowfilter | replident | replidentidx 
++---------+------------+-----------+----------+-----------+-----------+--------------
++ pub     | sch1       | tbl1      | {a}      |           | d         | 
+ (1 row)
+ 
+ RESET client_min_messages;
+@@ -2526,6 +2526,38 @@ DROP VIEW gpt_test_view;
+ DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+ DROP SCHEMA gpt_test_sch CASCADE;
+ NOTICE:  drop cascades to table gpt_test_sch.tbl_sch
++-- ======================================================
++-- Test replica identity in pg_publication_tables view
++-- ======================================================
++CREATE TABLE testpub_replident_d (a int PRIMARY KEY, b int);
++CREATE TABLE testpub_replident_f (a int, b int);
++ALTER TABLE testpub_replident_f REPLICA IDENTITY FULL;
++CREATE TABLE testpub_replident_i (a int not null, b int);
++CREATE UNIQUE INDEX testpub_replident_i_idx ON testpub_replident_i (a);
++ALTER TABLE testpub_replident_i REPLICA IDENTITY USING INDEX testpub_replident_i_idx;
++CREATE TABLE testpub_replident_n (a int, b int);
++ALTER TABLE testpub_replident_n REPLICA IDENTITY NOTHING;
++CREATE PUBLICATION testpub_replident FOR TABLE 
++    testpub_replident_d, 
++    testpub_replident_f, 
++    testpub_replident_i, 
++    testpub_replident_n;
++-- Check that replident and replidentidx are populated correctly
++SELECT tablename, replident, replidentidx::regclass 
++FROM pg_publication_tables 
++WHERE pubname = 'testpub_replident' 
++ORDER BY tablename;
++      tablename      | replident |       replidentidx       
++---------------------+-----------+--------------------------
++ testpub_replident_d | d         | testpub_replident_d_pkey
++ testpub_replident_f | f         | 
++ testpub_replident_i | i         | testpub_replident_i_idx
++ testpub_replident_n | n         | 
++(4 rows)
++
++-- Clean up
++DROP PUBLICATION testpub_replident;
++DROP TABLE testpub_replident_d, testpub_replident_f, testpub_replident_i, testpub_replident_n;
+ -- stage objects for pg_dump tests
+ CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
+ CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
+diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
+index a65a5bf0c4..bf05610275 100644
+--- a/src/test/regress/expected/rules.out
++++ b/src/test/regress/expected/rules.out
+@@ -1478,7 +1478,9 @@ pg_publication_tables| SELECT p.pubname,
+     ( SELECT array_agg(a.attname ORDER BY a.attnum) AS array_agg
+            FROM pg_attribute a
+           WHERE ((a.attrelid = gpt.relid) AND (a.attnum = ANY ((gpt.attrs)::smallint[])))) AS attnames,
+-    pg_get_expr(gpt.qual, gpt.relid) AS rowfilter
++    pg_get_expr(gpt.qual, gpt.relid) AS rowfilter,
++    c.relreplident AS replident,
++    pg_get_replica_identity_index((c.oid)::regclass) AS replidentidx
+    FROM pg_publication p,
+     LATERAL pg_get_publication_tables(VARIADIC ARRAY[(p.pubname)::text]) gpt(pubid, relid, attrs, qual),
+     (pg_class c
+diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
+index 041e14a4de..e19f447047 100644
+--- a/src/test/regress/sql/publication.sql
++++ b/src/test/regress/sql/publication.sql
+@@ -1547,6 +1547,37 @@ DROP VIEW gpt_test_view;
+ DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+ DROP SCHEMA gpt_test_sch CASCADE;
+ 
++-- ======================================================
++-- Test replica identity in pg_publication_tables view
++-- ======================================================
++CREATE TABLE testpub_replident_d (a int PRIMARY KEY, b int);
++
++CREATE TABLE testpub_replident_f (a int, b int);
++ALTER TABLE testpub_replident_f REPLICA IDENTITY FULL;
++
++CREATE TABLE testpub_replident_i (a int not null, b int);
++CREATE UNIQUE INDEX testpub_replident_i_idx ON testpub_replident_i (a);
++ALTER TABLE testpub_replident_i REPLICA IDENTITY USING INDEX testpub_replident_i_idx;
++
++CREATE TABLE testpub_replident_n (a int, b int);
++ALTER TABLE testpub_replident_n REPLICA IDENTITY NOTHING;
++
++CREATE PUBLICATION testpub_replident FOR TABLE 
++    testpub_replident_d, 
++    testpub_replident_f, 
++    testpub_replident_i, 
++    testpub_replident_n;
++
++-- Check that replident and replidentidx are populated correctly
++SELECT tablename, replident, replidentidx::regclass 
++FROM pg_publication_tables 
++WHERE pubname = 'testpub_replident' 
++ORDER BY tablename;
++
++-- Clean up
++DROP PUBLICATION testpub_replident;
++DROP TABLE testpub_replident_d, testpub_replident_f, testpub_replident_i, testpub_replident_n;
++
+ -- stage objects for pg_dump tests
+ CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
+ CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 8f129baec9..e5759594d8 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -420,7 +420,18 @@ CREATE VIEW pg_publication_tables AS
           WHERE a.attrelid = GPT.relid AND
                 a.attnum = ANY(GPT.attrs)
         ) AS attnames,
-        pg_get_expr(GPT.qual, GPT.relid) AS rowfilter
+        pg_get_expr(GPT.qual, GPT.relid) AS rowfilter,
+        C.relreplident AS replident,
+        (CASE
+            WHEN C.relreplident = 'd' THEN
+                (SELECT indexrelid FROM pg_index i
+                 WHERE i.indrelid = C.oid AND i.indisprimary AND i.indisvalid AND i.indimmediate)
+            WHEN C.relreplident = 'i' THEN
+                (SELECT indexrelid FROM pg_index i
+                 WHERE i.indrelid = C.oid AND i.indisreplident AND i.indisvalid AND i.indimmediate
+                       AND i.indisunique AND i.indpred IS NULL)
+            ELSE NULL::oid
+        END) AS replidentidx
     FROM pg_publication P,
          LATERAL pg_get_publication_tables(P.pubname) GPT,
          pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace)
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 29e54b214a..7d09a45ee0 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2100,52 +2100,52 @@ CREATE TABLE sch2.tbl1_part1 PARTITION OF sch1.tbl1 FOR VALUES FROM (1) to (10);
 -- Schema publication that does not include the schema that has the parent table
 CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch2 WITH (PUBLISH_VIA_PARTITION_ROOT=1);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | replident | replidentidx 
+---------+------------+------------+----------+-----------+-----------+--------------
+ pub     | sch2       | tbl1_part1 | {a}      |           | d         |             
 (1 row)
 
 DROP PUBLICATION pub;
 -- Table publication that does not include the parent table
 CREATE PUBLICATION pub FOR TABLE sch2.tbl1_part1 WITH (PUBLISH_VIA_PARTITION_ROOT=1);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | replident | replidentidx 
+---------+------------+------------+----------+-----------+-----------+--------------
+ pub     | sch2       | tbl1_part1 | {a}      |           | d         |             
 (1 row)
 
 -- Table publication that includes both the parent table and the child table
 ALTER PUBLICATION pub ADD TABLE sch1.tbl1;
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename | attnames | rowfilter 
----------+------------+-----------+----------+-----------
- pub     | sch1       | tbl1      | {a}      | 
+ pubname | schemaname | tablename | attnames | rowfilter | replident | replidentidx 
+---------+------------+-----------+----------+-----------+-----------+--------------
+ pub     | sch1       | tbl1      | {a}      |           | d         |             
 (1 row)
 
 DROP PUBLICATION pub;
 -- Schema publication that does not include the schema that has the parent table
 CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch2 WITH (PUBLISH_VIA_PARTITION_ROOT=0);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | replident | replidentidx 
+---------+------------+------------+----------+-----------+-----------+--------------
+ pub     | sch2       | tbl1_part1 | {a}      |           | d         |             
 (1 row)
 
 DROP PUBLICATION pub;
 -- Table publication that does not include the parent table
 CREATE PUBLICATION pub FOR TABLE sch2.tbl1_part1 WITH (PUBLISH_VIA_PARTITION_ROOT=0);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | replident | replidentidx 
+---------+------------+------------+----------+-----------+-----------+--------------
+ pub     | sch2       | tbl1_part1 | {a}      |           | d         |             
 (1 row)
 
 -- Table publication that includes both the parent table and the child table
 ALTER PUBLICATION pub ADD TABLE sch1.tbl1;
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename  | attnames | rowfilter 
----------+------------+------------+----------+-----------
- pub     | sch2       | tbl1_part1 | {a}      | 
+ pubname | schemaname | tablename  | attnames | rowfilter | replident | replidentidx 
+---------+------------+------------+----------+-----------+-----------+--------------
+ pub     | sch2       | tbl1_part1 | {a}      |           | d         |             
 (1 row)
 
 DROP PUBLICATION pub;
@@ -2158,9 +2158,9 @@ CREATE TABLE sch1.tbl1_part3 (a int) PARTITION BY RANGE(a);
 ALTER TABLE sch1.tbl1 ATTACH PARTITION sch1.tbl1_part3 FOR VALUES FROM (20) to (30);
 CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch1 WITH (PUBLISH_VIA_PARTITION_ROOT=1);
 SELECT * FROM pg_publication_tables;
- pubname | schemaname | tablename | attnames | rowfilter 
----------+------------+-----------+----------+-----------
- pub     | sch1       | tbl1      | {a}      | 
+ pubname | schemaname | tablename | attnames | rowfilter | replident | replidentidx 
+---------+------------+-----------+----------+-----------+-----------+--------------
+ pub     | sch1       | tbl1      | {a}      |           | d         |             
 (1 row)
 
 RESET client_min_messages;
@@ -2526,6 +2526,38 @@ DROP VIEW gpt_test_view;
 DROP TABLE tbl_normal, tbl_parent, tbl_part1;
 DROP SCHEMA gpt_test_sch CASCADE;
 NOTICE:  drop cascades to table gpt_test_sch.tbl_sch
+-- ======================================================
+-- Test replica identity in pg_publication_tables view
+-- ======================================================
+CREATE TABLE testpub_replident_d (a int PRIMARY KEY, b int);
+CREATE TABLE testpub_replident_f (a int, b int);
+ALTER TABLE testpub_replident_f REPLICA IDENTITY FULL;
+CREATE TABLE testpub_replident_i (a int not null, b int);
+CREATE UNIQUE INDEX testpub_replident_i_idx ON testpub_replident_i (a);
+ALTER TABLE testpub_replident_i REPLICA IDENTITY USING INDEX testpub_replident_i_idx;
+CREATE TABLE testpub_replident_n (a int, b int);
+ALTER TABLE testpub_replident_n REPLICA IDENTITY NOTHING;
+CREATE PUBLICATION testpub_replident FOR TABLE 
+    testpub_replident_d, 
+    testpub_replident_f, 
+    testpub_replident_i, 
+    testpub_replident_n;
+-- Check that replident and replidentidx are populated correctly
+SELECT tablename, replident, replidentidx::regclass 
+FROM pg_publication_tables 
+WHERE pubname = 'testpub_replident' 
+ORDER BY tablename;
+      tablename      | replident |       replidentidx       
+---------------------+-----------+--------------------------
+ testpub_replident_d | d         | testpub_replident_d_pkey
+ testpub_replident_f | f         | 
+ testpub_replident_i | i         | testpub_replident_i_idx
+ testpub_replident_n | n         | 
+(4 rows)
+
+-- Clean up
+DROP PUBLICATION testpub_replident;
+DROP TABLE testpub_replident_d, testpub_replident_f, testpub_replident_i, testpub_replident_n;
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4..ebc824bb16 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1478,7 +1478,17 @@ pg_publication_tables| SELECT p.pubname,
     ( SELECT array_agg(a.attname ORDER BY a.attnum) AS array_agg
            FROM pg_attribute a
           WHERE ((a.attrelid = gpt.relid) AND (a.attnum = ANY ((gpt.attrs)::smallint[])))) AS attnames,
-    pg_get_expr(gpt.qual, gpt.relid) AS rowfilter
+    pg_get_expr(gpt.qual, gpt.relid) AS rowfilter,
+    c.relreplident AS replident,
+        CASE
+            WHEN (c.relreplident = 'd'::"char") THEN ( SELECT i.indexrelid
+               FROM pg_index i
+              WHERE ((i.indrelid = c.oid) AND i.indisprimary AND i.indisvalid AND i.indimmediate))
+            WHEN (c.relreplident = 'i'::"char") THEN ( SELECT i.indexrelid
+               FROM pg_index i
+              WHERE ((i.indrelid = c.oid) AND i.indisreplident AND i.indisvalid AND i.indimmediate AND i.indisunique AND (i.indpred IS NULL)))
+            ELSE NULL::oid
+        END AS replidentidx
    FROM pg_publication p,
     LATERAL pg_get_publication_tables(VARIADIC ARRAY[(p.pubname)::text]) gpt(pubid, relid, attrs, qual),
     (pg_class c
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 041e14a4de..e19f447047 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1547,6 +1547,37 @@ DROP VIEW gpt_test_view;
 DROP TABLE tbl_normal, tbl_parent, tbl_part1;
 DROP SCHEMA gpt_test_sch CASCADE;
 
+-- ======================================================
+-- Test replica identity in pg_publication_tables view
+-- ======================================================
+CREATE TABLE testpub_replident_d (a int PRIMARY KEY, b int);
+
+CREATE TABLE testpub_replident_f (a int, b int);
+ALTER TABLE testpub_replident_f REPLICA IDENTITY FULL;
+
+CREATE TABLE testpub_replident_i (a int not null, b int);
+CREATE UNIQUE INDEX testpub_replident_i_idx ON testpub_replident_i (a);
+ALTER TABLE testpub_replident_i REPLICA IDENTITY USING INDEX testpub_replident_i_idx;
+
+CREATE TABLE testpub_replident_n (a int, b int);
+ALTER TABLE testpub_replident_n REPLICA IDENTITY NOTHING;
+
+CREATE PUBLICATION testpub_replident FOR TABLE 
+    testpub_replident_d, 
+    testpub_replident_f, 
+    testpub_replident_i, 
+    testpub_replident_n;
+
+-- Check that replident and replidentidx are populated correctly
+SELECT tablename, replident, replidentidx::regclass 
+FROM pg_publication_tables 
+WHERE pubname = 'testpub_replident' 
+ORDER BY tablename;
+
+-- Clean up
+DROP PUBLICATION testpub_replident;
+DROP TABLE testpub_replident_d, testpub_replident_f, testpub_replident_i, testpub_replident_n;
+
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);

Reply via email to