Skip to content

Commit 02e95d2

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 56f1656 commit 02e95d2

9 files changed

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

src/chunk.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ 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, List *schema_oids,
234+
bool add);
235+
extern TSDLLEXPORT void ts_chunk_publication_reconcile_ht_schema_change(int32 hypertable_id,
236+
Oid old_schema,
237+
Oid new_schema);
233238

234239
extern TSDLLEXPORT int64 ts_chunk_primary_dimension_start(const Chunk *chunk);
235240

src/process_utility.c

Lines changed: 142 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,24 @@ 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+
{
824+
ts_chunk_publication_reconcile_ht_schema_change(ht->fd.id, old_schema, new_schema);
825+
}
826+
808827
ts_hypertable_set_schema(ht, alterstmt->newschema);
809828
}
810829
else
@@ -6030,6 +6049,97 @@ preprocess_execute(ProcessUtilityArgs *args)
60306049
return DDL_CONTINUE;
60316050
}
60326051

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

6236+
/* ALTER PUBLICATION is reconciled here; CREATE PUBLICATION is reconciled
6237+
* later in process_ddl_event_command_end (where the publication's Oid is
6238+
* available). check_read_only stays off to match prior behavior; PG
6239+
* rejects publication DDL on read-only standbys itself. */
6240+
case T_AlterPublicationStmt:
6241+
check_read_only = false;
6242+
handler = process_publication;
6243+
break;
6244+
61266245
default:
61276246
handler = NULL;
61286247
break;
@@ -6141,6 +6260,25 @@ process_ddl_command_start(ProcessUtilityArgs *args)
61416260
return handler(args);
61426261
}
61436262

6263+
/*
6264+
* Backfill chunk membership for a publication created with FOR TABLES IN
6265+
* SCHEMA. Runs in the ddl_command_end event trigger, so the publication (and
6266+
* its Oid) already exists.
6267+
*/
6268+
static void
6269+
process_create_publication_end(Node *parsetree)
6270+
{
6271+
CreatePublicationStmt *stmt = castNode(CreatePublicationStmt, parsetree);
6272+
List *schema_oids = publication_schema_oids(stmt->pubobjects);
6273+
6274+
if (schema_oids == NIL)
6275+
{
6276+
return;
6277+
}
6278+
6279+
reconcile_publication_schemas(stmt->pubname, schema_oids, true);
6280+
}
6281+
61446282
/*
61456283
* Handle DDL commands after they've been processed by PostgreSQL.
61466284
*/
@@ -6155,6 +6293,9 @@ process_ddl_command_end(CollectedCommand *cmd)
61556293
case T_AlterTableStmt:
61566294
process_altertable_end(cmd->parsetree, cmd);
61576295
break;
6296+
case T_CreatePublicationStmt:
6297+
process_create_publication_end(cmd->parsetree);
6298+
break;
61586299
default:
61596300
break;
61606301
}
@@ -6418,6 +6559,7 @@ process_ddl_event_command_end(EventTriggerData *trigdata)
64186559
case T_CreateTrigStmt:
64196560
case T_CreateStmt:
64206561
case T_IndexStmt:
6562+
case T_CreatePublicationStmt:
64216563
foreach (lc, ts_event_trigger_ddl_commands())
64226564
{
64236565
process_ddl_command_end(lfirst(lc));

0 commit comments

Comments
 (0)