Skip to content

Commit dbd57cb

Browse files
committed
Add chunks to schema-based publications
Chunks live in _timescaledb_internal, so FOR TABLES IN SCHEMA publications miss them and replication silently skips chunk data. Backfill chunk membership by reading the schema set straight from the CREATE/ALTER PUBLICATION parse tree (no before/after snapshots): - CREATE PUBLICATION is reconciled in the ddl_command_end event trigger (the publication Oid exists by then); add CREATE PUBLICATION to the timescaledb_ddl_command_end WHEN TAG list. - ALTER PUBLICATION ADD/DROP/SET TABLES IN SCHEMA is reconciled in process_ddl_command_start after prev_ProcessUtility. SET rebuilds the full new schema set (PG drops every tracked chunk row on SET); only DROP removes them. Options-only ALTER is a no-op. ALTER TABLE ... SET SCHEMA on a hypertable now reconciles its chunks: remove from the old schema publications, add to the new ones. Runs before ts_hypertable_set_schema flips the catalog schema. Skip adding a redundant explicit pg_publication_rel row when a chunk already lives in a schema this publication covers natively (chunks placed via associated_schema_name); the explicit row would survive a later DROP TABLES IN SCHEMA and orphan. OSM (foreign-table) chunks are skipped throughout; publication_add_relation would fail on them. Tests 6b-6i cover CREATE/ADD/SET/DROP/CURRENT_SCHEMA, schema moves, the redundancy guard (counts of pg_publication_rel rows), options-only ALTER, and GUC-off on the ALTER/schema-change paths. Signed-off-by: Arunprasad Rajkumar <ar.arunprasad@gmail.com>
1 parent 77d9d32 commit dbd57cb

9 files changed

Lines changed: 1128 additions & 46 deletions

File tree

.unreleased/pr_10128

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes: #10128 Include hypertable chunks in publications created with FOR TABLES IN SCHEMA

sql/ddl_triggers.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ AS '@MODULE_PATHNAME@', 'ts_timescaledb_process_ddl_event' LANGUAGE C;
99

1010
--EVENT TRIGGER MUST exclude the ALTER EXTENSION tag.
1111
CREATE EVENT TRIGGER timescaledb_ddl_command_end ON ddl_command_end
12-
WHEN TAG IN ('ALTER TABLE','CREATE TRIGGER','CREATE TABLE','CREATE INDEX','ALTER INDEX', 'DROP TABLE', 'DROP INDEX', 'DROP SCHEMA')
12+
WHEN TAG IN ('ALTER TABLE','CREATE TRIGGER','CREATE TABLE','CREATE INDEX','ALTER INDEX', 'DROP TABLE', 'DROP INDEX', 'DROP SCHEMA', 'CREATE PUBLICATION')
1313
EXECUTE FUNCTION _timescaledb_functions.process_ddl_event();
1414

1515
DROP EVENT TRIGGER IF EXISTS timescaledb_ddl_sql_drop;

src/chunk.c

Lines changed: 151 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <access/tableam.h>
1212
#include <access/tupdesc.h>
1313
#include <access/xact.h>
14+
#include <catalog/dependency.h>
1415
#include <catalog/indexing.h>
1516
#include <catalog/namespace.h>
1617
#include <catalog/pg_class.h>
@@ -1048,14 +1049,159 @@ chunk_add_to_publication(Oid puboid, const Chunk *chunk)
10481049
static void
10491050
chunk_add_to_publications(const Chunk *chunk)
10501051
{
1051-
List *puboids;
1052+
Oid ht_nspid = get_rel_namespace(chunk->hypertable_relid);
1053+
Oid chunk_nspid = get_rel_namespace(chunk->table_id);
1054+
List *puboids = list_concat_unique_oid(GetRelationIncludedPublications(chunk->hypertable_relid),
1055+
GetSchemaPublications(ht_nspid));
1056+
/* If the chunk lives in a published schema, PostgreSQL already covers it
1057+
* via the schema publication; skip the redundant explicit row. */
1058+
List *chunk_schema_pubs = GetSchemaPublications(chunk_nspid);
10521059
ListCell *lc;
1053-
1054-
puboids = GetRelationIncludedPublications(chunk->hypertable_relid);
10551060
foreach (lc, puboids)
10561061
{
1057-
Oid puboid = lfirst_oid(lc);
1058-
chunk_add_to_publication(puboid, chunk);
1062+
if (list_member_oid(chunk_schema_pubs, lfirst_oid(lc)))
1063+
continue;
1064+
chunk_add_to_publication(lfirst_oid(lc), chunk);
1065+
}
1066+
}
1067+
1068+
static void
1069+
chunk_remove_from_publication(Oid puboid, const Chunk *chunk)
1070+
{
1071+
ObjectAddress obj;
1072+
Oid prid = GetSysCacheOid2(PUBLICATIONRELMAP,
1073+
Anum_pg_publication_rel_oid,
1074+
ObjectIdGetDatum(chunk->table_id),
1075+
ObjectIdGetDatum(puboid));
1076+
if (!OidIsValid(prid))
1077+
{
1078+
return;
1079+
}
1080+
1081+
/* No core PG object depends on a pg_publication_rel row, so RESTRICT is
1082+
* sufficient and matches PG's own AlterPublication convention. */
1083+
ObjectAddressSet(obj, PublicationRelRelationId, prid);
1084+
performDeletion(&obj, DROP_RESTRICT, 0);
1085+
}
1086+
1087+
/*
1088+
* Add or remove the chunks of every hypertable whose root table lives in the
1089+
* given schemas to/from a publication. Schema publications miss chunks
1090+
* (chunks live in _timescaledb_internal, not the hypertable's schema);
1091+
* FOR TABLE / FOR ALL TABLES are handled by PostgreSQL, which expands
1092+
* inheritance children itself.
1093+
*
1094+
* Honors enable_chunk_auto_publication: when off, the whole reconciliation is
1095+
* skipped. This is consistent with the opt-in design of the feature - if the
1096+
* user disables the GUC after chunks were auto-published, they own any cleanup
1097+
* (a subsequent ALTER PUBLICATION ... DROP TABLES IN SCHEMA will not remove
1098+
* the previously backfilled chunk rows in that case). Drop the publication or
1099+
* re-enable the GUC before such an ALTER to have the rows removed here.
1100+
*/
1101+
void
1102+
ts_chunk_publication_reconcile_schema_chunks(Oid pubid, List *schema_oids, bool add)
1103+
{
1104+
if (!ts_guc_enable_chunk_auto_publication)
1105+
{
1106+
return;
1107+
}
1108+
1109+
Cache *hcache = ts_hypertable_cache_pin();
1110+
ListCell *sc;
1111+
foreach (sc, schema_oids)
1112+
{
1113+
Oid published_schema = lfirst_oid(sc);
1114+
ListCell *rc;
1115+
foreach (rc, GetSchemaPublicationRelations(published_schema, PUBLICATION_PART_ROOT))
1116+
{
1117+
Hypertable *ht =
1118+
ts_hypertable_cache_get_entry(hcache, lfirst_oid(rc), CACHE_FLAG_MISSING_OK);
1119+
if (ht == NULL)
1120+
{
1121+
continue;
1122+
}
1123+
ListCell *cc;
1124+
foreach (cc, ts_chunk_get_by_hypertable_id(ht->fd.id))
1125+
{
1126+
Chunk *chunk = lfirst(cc);
1127+
/* OSM chunks are foreign tables; publication_add_relation would fail. */
1128+
if (IS_OSM_CHUNK(chunk))
1129+
{
1130+
continue;
1131+
}
1132+
/*
1133+
* Skip adding an explicit row for a chunk that lives in a schema this
1134+
* publication already covers natively: the explicit row would be
1135+
* redundant and would survive a DROP that removes the schema mapping.
1136+
* (Mirrors chunk_add_to_publications' guard.)
1137+
*/
1138+
if (add)
1139+
{
1140+
if (list_member_oid(GetSchemaPublications(get_rel_namespace(chunk->table_id)),
1141+
pubid))
1142+
continue;
1143+
chunk_add_to_publication(pubid, chunk);
1144+
}
1145+
else
1146+
chunk_remove_from_publication(pubid, chunk);
1147+
}
1148+
}
1149+
}
1150+
ts_cache_release(&hcache);
1151+
}
1152+
1153+
/*
1154+
* Reconcile chunk publication membership when a hypertable's schema changes
1155+
* (ALTER TABLE ... SET SCHEMA). Chunks live in _timescaledb_internal and do not
1156+
* move with the root table, so PostgreSQL's schema publication membership
1157+
* (which follows the root table's schema) leaves chunk rows behind: chunks that
1158+
* were backfilled for the old schema's publications must be removed, and they
1159+
* must be added for the new schema's publications. Existing chunks are handled
1160+
* here; chunks created later are covered by the per-chunk create hook.
1161+
*/
1162+
void
1163+
ts_chunk_publication_reconcile_ht_schema_change(int32 hypertable_id, Oid old_schema,
1164+
Oid new_schema)
1165+
{
1166+
ListCell *cc;
1167+
List *old_pubs;
1168+
List *new_pubs;
1169+
1170+
if (!ts_guc_enable_chunk_auto_publication)
1171+
{
1172+
return;
1173+
}
1174+
1175+
if (old_schema == new_schema)
1176+
{
1177+
return;
1178+
}
1179+
1180+
/* The old/new schema publication lists are the same for every chunk. */
1181+
old_pubs = GetSchemaPublications(old_schema);
1182+
new_pubs = GetSchemaPublications(new_schema);
1183+
1184+
foreach (cc, ts_chunk_get_by_hypertable_id(hypertable_id))
1185+
{
1186+
Chunk *chunk = lfirst(cc);
1187+
ListCell *pc;
1188+
1189+
/* OSM chunks are foreign tables; publication_add_relation would fail. */
1190+
if (IS_OSM_CHUNK(chunk))
1191+
continue;
1192+
1193+
foreach (pc, old_pubs)
1194+
chunk_remove_from_publication(lfirst_oid(pc), chunk);
1195+
1196+
/* A chunk whose schema is already covered natively by a new-schema
1197+
* publication needs no explicit row (it would survive a later DROP
1198+
* that removes the schema mapping). */
1199+
List *chunk_schema_pubs = GetSchemaPublications(get_rel_namespace(chunk->table_id));
1200+
foreach (pc, new_pubs)
1201+
{
1202+
if (!list_member_oid(chunk_schema_pubs, lfirst_oid(pc)))
1203+
chunk_add_to_publication(lfirst_oid(pc), chunk);
1204+
}
10591205
}
10601206
}
10611207

src/chunk.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,12 @@ extern TSDLLEXPORT Datum ts_chunk_id_from_relid(PG_FUNCTION_ARGS);
230230
extern TSDLLEXPORT Datum ts_chunk_status_text(PG_FUNCTION_ARGS);
231231
extern TSDLLEXPORT List *ts_chunk_get_chunk_ids_by_hypertable_id(int32 hypertable_id);
232232
extern TSDLLEXPORT List *ts_chunk_get_by_hypertable_id(int32 hypertable_id);
233+
extern TSDLLEXPORT void ts_chunk_publication_reconcile_schema_chunks(Oid pubid,
234+
List *schema_oids,
235+
bool add);
236+
extern TSDLLEXPORT void ts_chunk_publication_reconcile_ht_schema_change(int32 hypertable_id,
237+
Oid old_schema,
238+
Oid new_schema);
233239

234240
extern TSDLLEXPORT int64 ts_chunk_primary_dimension_start(const Chunk *chunk);
235241

src/process_utility.c

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <catalog/pg_class_d.h>
1717
#include <catalog/pg_constraint.h>
1818
#include <catalog/pg_inherits.h>
19+
#include <catalog/pg_publication.h>
1920
#include <catalog/pg_trigger.h>
2021
#include <commands/alter.h>
2122
#include <commands/copy.h>
@@ -805,6 +806,23 @@ process_altertableschema(ProcessUtilityArgs *args)
805806

806807
if (ht)
807808
{
809+
Oid old_schema = get_rel_namespace(relid);
810+
Oid new_schema = get_namespace_oid(alterstmt->newschema, true);
811+
812+
/*
813+
* Chunks live in _timescaledb_internal and don't move with the root
814+
* table, so a schema publication (which follows the root table's
815+
* schema) leaves chunk rows behind. Reconcile them for the schema the
816+
* hypertable is leaving and the one it is entering. This must run
817+
* before ts_hypertable_set_schema flips the catalog schema, since
818+
* ts_chunk_publication_reconcile_ht_schema_change resolves the
819+
* hypertable's relid from the catalog (which still points at the old
820+
* schema's physical table); the chunk rows themselves are stable.
821+
*/
822+
if (OidIsValid(new_schema))
823+
ts_chunk_publication_reconcile_ht_schema_change(ht->fd.id, old_schema,
824+
new_schema);
825+
808826
ts_hypertable_set_schema(ht, alterstmt->newschema);
809827
}
810828
else
@@ -6030,6 +6048,93 @@ preprocess_execute(ProcessUtilityArgs *args)
60306048
return DDL_CONTINUE;
60316049
}
60326050

6051+
/*
6052+
* Collect the Oids of schemas named by FOR TABLES IN SCHEMA in a
6053+
* CREATE/ALTER PUBLICATION object list. Mirrors PostgreSQL's schema handling
6054+
* in ObjectsInPublicationToOids; FOR TABLE / EXCEPT TABLE objects are left to
6055+
* PostgreSQL, which already expands inheritance children (chunks) itself.
6056+
*/
6057+
static List *
6058+
publication_schema_oids(List *pubobjects)
6059+
{
6060+
List *schemas = NIL;
6061+
ListCell *lc;
6062+
6063+
foreach (lc, pubobjects)
6064+
{
6065+
PublicationObjSpec *pubobj = lfirst(lc);
6066+
Oid schemaid;
6067+
6068+
switch (pubobj->pubobjtype)
6069+
{
6070+
case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
6071+
schemaid = get_namespace_oid(pubobj->name, false);
6072+
break;
6073+
case PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA:
6074+
{
6075+
List *search_path = fetch_search_path(false);
6076+
6077+
if (search_path == NIL)
6078+
ereport(ERROR,
6079+
(errcode(ERRCODE_UNDEFINED_SCHEMA),
6080+
errmsg("no schema has been selected for CURRENT_SCHEMA")));
6081+
schemaid = linitial_oid(search_path);
6082+
list_free(search_path);
6083+
break;
6084+
}
6085+
default:
6086+
continue;
6087+
}
6088+
schemas = list_append_unique_oid(schemas, schemaid);
6089+
}
6090+
return schemas;
6091+
}
6092+
6093+
/*
6094+
* Reconcile chunk membership for the schemas named in a publication command.
6095+
* Chunks live in _timescaledb_internal, so FOR TABLES IN SCHEMA never adds them
6096+
* (PostgreSQL only expands inheritance children for FOR TABLE / FOR ALL
6097+
* TABLES). For SET, PostgreSQL has already dropped every chunk row it
6098+
* tracked, so we rebuild for the full new schema set rather than a difference;
6099+
* only DROP removes chunks, because PostgreSQL leaves our backfilled chunk
6100+
* rows untouched when a schema is dropped from the publication. We read the
6101+
* schema set straight from the parse tree, so only the schemas the command
6102+
* touched are reconciled -- no before/after snapshot is needed.
6103+
*/
6104+
static void
6105+
reconcile_publication_schemas(const char *pubname, List *schema_oids, bool add)
6106+
{
6107+
Publication *pub = GetPublicationByName(pubname, true);
6108+
6109+
if (pub)
6110+
ts_chunk_publication_reconcile_schema_chunks(pub->oid, schema_oids, add);
6111+
}
6112+
6113+
/*
6114+
* Keep chunk membership of a schema publication in sync with the schemas named
6115+
* by ALTER PUBLICATION ... ADD/DROP/SET TABLES IN SCHEMA. CREATE PUBLICATION is
6116+
* handled later in process_ddl_event_command_end, where the publication's Oid
6117+
* is already available.
6118+
*/
6119+
static DDLResult
6120+
process_publication(ProcessUtilityArgs *args)
6121+
{
6122+
AlterPublicationStmt *stmt = castNode(AlterPublicationStmt, args->parsetree);
6123+
List *schema_oids;
6124+
6125+
/* Let PostgreSQL alter the publication first. */
6126+
prev_ProcessUtility(args);
6127+
6128+
schema_oids = publication_schema_oids(stmt->pubobjects);
6129+
if (schema_oids == NIL)
6130+
return DDL_DONE;
6131+
6132+
reconcile_publication_schemas(stmt->pubname, schema_oids,
6133+
stmt->action != AP_DropObjects);
6134+
6135+
return DDL_DONE;
6136+
}
6137+
60336138
/*
60346139
* Handle DDL commands before they have been processed by PostgreSQL.
60356140
*/
@@ -6123,6 +6228,15 @@ process_ddl_command_start(ProcessUtilityArgs *args)
61236228
handler = preprocess_execute;
61246229
break;
61256230

6231+
/* ALTER PUBLICATION is reconciled here; CREATE PUBLICATION is reconciled
6232+
* later in process_ddl_event_command_end (where the publication's Oid is
6233+
* available). check_read_only stays off to match prior behavior; PG
6234+
* rejects publication DDL on read-only standbys itself. */
6235+
case T_AlterPublicationStmt:
6236+
check_read_only = false;
6237+
handler = process_publication;
6238+
break;
6239+
61266240
default:
61276241
handler = NULL;
61286242
break;
@@ -6141,6 +6255,23 @@ process_ddl_command_start(ProcessUtilityArgs *args)
61416255
return handler(args);
61426256
}
61436257

6258+
/*
6259+
* Backfill chunk membership for a publication created with FOR TABLES IN
6260+
* SCHEMA. Runs in the ddl_command_end event trigger, so the publication (and
6261+
* its Oid) already exists.
6262+
*/
6263+
static void
6264+
process_create_publication_end(Node *parsetree)
6265+
{
6266+
CreatePublicationStmt *stmt = castNode(CreatePublicationStmt, parsetree);
6267+
List *schema_oids = publication_schema_oids(stmt->pubobjects);
6268+
6269+
if (schema_oids == NIL)
6270+
return;
6271+
6272+
reconcile_publication_schemas(stmt->pubname, schema_oids, true);
6273+
}
6274+
61446275
/*
61456276
* Handle DDL commands after they've been processed by PostgreSQL.
61466277
*/
@@ -6155,6 +6286,9 @@ process_ddl_command_end(CollectedCommand *cmd)
61556286
case T_AlterTableStmt:
61566287
process_altertable_end(cmd->parsetree, cmd);
61576288
break;
6289+
case T_CreatePublicationStmt:
6290+
process_create_publication_end(cmd->parsetree);
6291+
break;
61586292
default:
61596293
break;
61606294
}
@@ -6418,6 +6552,7 @@ process_ddl_event_command_end(EventTriggerData *trigdata)
64186552
case T_CreateTrigStmt:
64196553
case T_CreateStmt:
64206554
case T_IndexStmt:
6555+
case T_CreatePublicationStmt:
64216556
foreach (lc, ts_event_trigger_ddl_commands())
64226557
{
64236558
process_ddl_command_end(lfirst(lc));

0 commit comments

Comments
 (0)