Skip to content

Commit 413a3af

Browse files
committed
Reset inherited flags on chunks attached to a hypertable
Attaching an existing table as a chunk uses ALTER TABLE ... INHERIT, which raises the inheritance count of the child columns and constraints but leaves them marked as locally defined. A chunk is meant to be a pure inheritance child, so a later DROP COLUMN or DROP CONSTRAINT on the hypertable did not reach such a chunk and left an orphaned column or constraint behind instead. Clear the local flag on the inherited columns and constraints when a chunk is attached so schema changes on the hypertable propagate to it. Also add a migration that fixes chunks already left in this state by an earlier detach and attach round-trip.
1 parent 7d393ec commit 413a3af

10 files changed

Lines changed: 1381 additions & 2 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes: #10352 Reset inherited column and constraint flags on chunks during attach_chunk

sql/updates/latest-dev.sql

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
-- Reset the attislocal/conislocal flags on chunk columns and constraints left
2+
-- marked local by a detach_chunk/attach_chunk round-trip so a later hypertable
3+
-- DROP COLUMN or DROP CONSTRAINT propagates to them.
4+
-- Locally-defined objects (inhcount = 0), such as the chunk's dimension
5+
-- constraints, are left untouched.
6+
UPDATE pg_catalog.pg_attribute a
7+
SET attislocal = false
8+
FROM _timescaledb_catalog.chunk c
9+
WHERE a.attrelid = c.relid
10+
AND a.attnum > 0
11+
AND NOT a.attisdropped
12+
AND a.attislocal
13+
AND a.attinhcount > 0;
14+
15+
UPDATE pg_catalog.pg_constraint con
16+
SET conislocal = false
17+
FROM _timescaledb_catalog.chunk c
18+
WHERE con.conrelid = c.relid
19+
AND con.conislocal
20+
AND con.coninhcount > 0;
21+
122
-- Rebuild the catalog table `_timescaledb_catalog.continuous_aggs_hypertable_invalidation_log`
223
-- to add the `seqnum` column.
324
CREATE TABLE _timescaledb_catalog._tmp_continuous_aggs_hypertable_invalidation_log AS

src/chunk.c

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* LICENSE-APACHE for a copy of the license.
55
*/
66
#include <postgres.h>
7+
#include <access/genam.h>
78
#include <access/htup.h>
89
#include <access/htup_details.h>
910
#include <access/reloptions.h>
@@ -13,6 +14,7 @@
1314
#include <access/xact.h>
1415
#include <catalog/indexing.h>
1516
#include <catalog/namespace.h>
17+
#include <catalog/pg_attribute.h>
1618
#include <catalog/pg_class.h>
1719
#include <catalog/pg_constraint.h>
1820
#include <catalog/pg_inherits.h>
@@ -46,6 +48,7 @@
4648
#include <utils/builtins.h>
4749
#include <utils/datum.h>
4850
#include <utils/elog.h>
51+
#include <utils/fmgroids.h>
4952
#include <utils/hsearch.h>
5053
#include <utils/inval.h>
5154
#include <utils/lsyscache.h>
@@ -1116,6 +1119,80 @@ chunk_create_from_hypercube_after_lock(const Hypertable *ht, Hypercube *cube,
11161119
return chunk;
11171120
}
11181121

1122+
/*
1123+
* A chunk should have all columns and constraints inherited and none marked as
1124+
* local, so clear the attislocal/conislocal flags that ALTER TABLE ... INHERIT
1125+
* leaves set when attaching a pre-existing table. Otherwise a later DROP COLUMN
1126+
* or DROP CONSTRAINT on the hypertable would not propagate to the chunk.
1127+
* Locally-defined objects (inhcount == 0), such as the chunk's dimension
1128+
* constraints, are left untouched.
1129+
*/
1130+
static void
1131+
chunk_reset_inherited_flags(Oid chunk_relid)
1132+
{
1133+
/* Columns */
1134+
Relation attrel = table_open(AttributeRelationId, RowExclusiveLock);
1135+
Relation chunkrel = table_open(chunk_relid, AccessShareLock);
1136+
TupleDesc tupdesc = RelationGetDescr(chunkrel);
1137+
1138+
for (int i = 0; i < tupdesc->natts; i++)
1139+
{
1140+
Form_pg_attribute att = TupleDescAttr(tupdesc, i);
1141+
1142+
if (att->attisdropped || !att->attislocal || att->attinhcount == 0)
1143+
{
1144+
continue;
1145+
}
1146+
1147+
HeapTuple tuple = SearchSysCacheCopyAttNum(chunk_relid, att->attnum);
1148+
if (!HeapTupleIsValid(tuple))
1149+
{
1150+
elog(ERROR,
1151+
"cache lookup failed for attribute %d of relation %u",
1152+
att->attnum,
1153+
chunk_relid);
1154+
}
1155+
1156+
((Form_pg_attribute) GETSTRUCT(tuple))->attislocal = false;
1157+
CatalogTupleUpdate(attrel, &tuple->t_self, tuple);
1158+
heap_freetuple(tuple);
1159+
}
1160+
1161+
table_close(chunkrel, NoLock);
1162+
table_close(attrel, RowExclusiveLock);
1163+
1164+
/* Constraints (CHECK and NOT NULL) */
1165+
ScanKeyData skey;
1166+
ScanKeyInit(&skey,
1167+
Anum_pg_constraint_conrelid,
1168+
BTEqualStrategyNumber,
1169+
F_OIDEQ,
1170+
ObjectIdGetDatum(chunk_relid));
1171+
1172+
Relation conrel = table_open(ConstraintRelationId, RowExclusiveLock);
1173+
SysScanDesc scan =
1174+
systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, true, NULL, 1, &skey);
1175+
HeapTuple contup;
1176+
1177+
while (HeapTupleIsValid(contup = systable_getnext(scan)))
1178+
{
1179+
Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(contup);
1180+
1181+
if (con->coninhcount == 0 || !con->conislocal)
1182+
{
1183+
continue;
1184+
}
1185+
1186+
HeapTuple newtup = heap_copytuple(contup);
1187+
((Form_pg_constraint) GETSTRUCT(newtup))->conislocal = false;
1188+
CatalogTupleUpdate(conrel, &newtup->t_self, newtup);
1189+
heap_freetuple(newtup);
1190+
}
1191+
1192+
systable_endscan(scan);
1193+
table_close(conrel, RowExclusiveLock);
1194+
}
1195+
11191196
/*
11201197
* Make a chunk table inherit a hypertable.
11211198
*
@@ -1149,6 +1226,8 @@ chunk_add_inheritance(Chunk *chunk, const Hypertable *ht)
11491226
};
11501227

11511228
AlterTable(&alterstmt, lockmode, &atcontext);
1229+
1230+
chunk_reset_inherited_flags(atcontext.relid);
11521231
}
11531232

11541233
static Chunk *
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,122 @@ DROP TABLE regular_table_to_attach;
235235
DROP TABLE attach_test_ref;
236236
DROP TABLE attach_test;
237237
DROP TABLE devices CASCADE;
238+
-- Test dropping columns after detach/attach
239+
CREATE TABLE drop_after_attach(time timestamptz NOT NULL, x int);
240+
SELECT create_hypertable('drop_after_attach', 'time', chunk_time_interval => interval '1 day');
241+
create_hypertable
242+
--------------------------------
243+
(3,public,drop_after_attach,t)
244+
245+
INSERT INTO drop_after_attach VALUES ('2026-01-01', 1), ('2026-01-05', 1);
246+
SELECT schema_name || '.' || table_name AS "ROUNDTRIP_CHUNK", slices AS "ROUNDTRIP_SLICES"
247+
FROM _timescaledb_functions.show_chunk((SELECT show_chunks('drop_after_attach', older_than => '2026-01-02') LIMIT 1)); \gset
248+
ROUNDTRIP_CHUNK | ROUNDTRIP_SLICES
249+
-----------------------------------------+------------------------------------------------
250+
_timescaledb_internal._hyper_3_15_chunk | {"time": [1767225600000000, 1767312000000000]}
251+
252+
CALL detach_chunk(:'ROUNDTRIP_CHUNK');
253+
CALL attach_chunk('drop_after_attach', :'ROUNDTRIP_CHUNK', :'ROUNDTRIP_SLICES');
254+
-- The re-attached chunk inherits without any locally-defined columns.
255+
SELECT attname, attislocal, attinhcount
256+
FROM pg_attribute
257+
WHERE attrelid = :'ROUNDTRIP_CHUNK'::regclass AND attnum > 0 AND NOT attisdropped
258+
ORDER BY attnum;
259+
attname | attislocal | attinhcount
260+
---------+------------+-------------
261+
time | f | 1
262+
x | f | 1
263+
264+
ALTER TABLE drop_after_attach DROP COLUMN x;
265+
-- The column is gone from the round-tripped chunk, not left orphaned.
266+
SELECT attname, attislocal, attinhcount
267+
FROM pg_attribute
268+
WHERE attrelid = :'ROUNDTRIP_CHUNK'::regclass AND attname = 'x' AND NOT attisdropped;
269+
attname | attislocal | attinhcount
270+
---------+------------+-------------
271+
272+
DROP TABLE drop_after_attach;
273+
-- Test dropping a constraint after detach/attach
274+
CREATE TABLE drop_con_after_attach(time timestamptz NOT NULL, x int CHECK (x > 0));
275+
SELECT create_hypertable('drop_con_after_attach', 'time', chunk_time_interval => interval '1 day');
276+
create_hypertable
277+
------------------------------------
278+
(4,public,drop_con_after_attach,t)
279+
280+
INSERT INTO drop_con_after_attach VALUES ('2026-01-01', 1);
281+
SELECT schema_name || '.' || table_name AS "CON_CHUNK", slices AS "CON_SLICES"
282+
FROM _timescaledb_functions.show_chunk((SELECT show_chunks('drop_con_after_attach') LIMIT 1)); \gset
283+
CON_CHUNK | CON_SLICES
284+
-----------------------------------------+------------------------------------------------
285+
_timescaledb_internal._hyper_4_18_chunk | {"time": [1767225600000000, 1767312000000000]}
286+
287+
CALL detach_chunk(:'CON_CHUNK');
288+
CALL attach_chunk('drop_con_after_attach', :'CON_CHUNK', :'CON_SLICES');
289+
-- The inherited constraint is not marked local; the dimension constraint stays local.
290+
SELECT conname, contype, conislocal, coninhcount
291+
FROM pg_constraint
292+
WHERE conrelid = :'CON_CHUNK'::regclass
293+
ORDER BY conname;
294+
conname | contype | conislocal | coninhcount
295+
-------------------------------+---------+------------+-------------
296+
constraint_30 | c | t | 0
297+
drop_con_after_attach_x_check | c | f | 1
298+
299+
ALTER TABLE drop_con_after_attach DROP CONSTRAINT drop_con_after_attach_x_check;
300+
-- The dropped constraint is gone from the round-tripped chunk.
301+
SELECT count(*) AS leftover_check
302+
FROM pg_constraint
303+
WHERE conrelid = :'CON_CHUNK'::regclass AND conname = 'drop_con_after_attach_x_check';
304+
leftover_check
305+
----------------
306+
0
307+
308+
DROP TABLE drop_con_after_attach;
309+
-- Test attaching a foreign table as an OSM chunk
310+
-- A dummy server is enough; the foreign table is never queried by attach.
311+
\c :TEST_DBNAME :ROLE_SUPERUSER
312+
CREATE EXTENSION postgres_fdw;
313+
CREATE SERVER attach_chunk_fdw FOREIGN DATA WRAPPER postgres_fdw;
314+
CREATE TABLE osm_ht(time timestamptz NOT NULL, x int CHECK (x > 0));
315+
SELECT create_hypertable('osm_ht', 'time', chunk_time_interval => interval '1 day');
316+
create_hypertable
317+
---------------------
318+
(5,public,osm_ht,t)
319+
320+
CREATE FOREIGN TABLE osm_ft(time timestamptz NOT NULL, x int) SERVER attach_chunk_fdw;
321+
SELECT _timescaledb_functions.attach_osm_table_chunk('osm_ht', 'osm_ft');
322+
attach_osm_table_chunk
323+
------------------------
324+
t
325+
326+
-- The foreign chunk inherits its columns without any left marked local.
327+
SELECT attname, attislocal, attinhcount
328+
FROM pg_attribute
329+
WHERE attrelid = 'osm_ft'::regclass AND attnum > 0 AND NOT attisdropped
330+
ORDER BY attnum;
331+
attname | attislocal | attinhcount
332+
---------+------------+-------------
333+
time | f | 1
334+
x | f | 1
335+
336+
-- The CHECK constraint cloned from the hypertable is inherited, not local.
337+
SELECT conname, contype, conislocal, coninhcount
338+
FROM pg_constraint
339+
WHERE conrelid = 'osm_ft'::regclass AND contype = 'c'
340+
ORDER BY conname;
341+
conname | contype | conislocal | coninhcount
342+
----------------+---------+------------+-------------
343+
osm_ht_x_check | c | f | 1
344+
345+
ALTER TABLE osm_ht DROP COLUMN x;
346+
-- Dropping the column on the hypertable propagates to the foreign chunk.
347+
SELECT attname
348+
FROM pg_attribute
349+
WHERE attrelid = 'osm_ft'::regclass AND attname = 'x' AND NOT attisdropped;
350+
attname
351+
---------
352+
353+
-- Dropping the hypertable also drops the attached foreign chunk.
354+
DROP TABLE osm_ht;
355+
DROP SERVER attach_chunk_fdw;
356+
DROP EXTENSION postgres_fdw;

0 commit comments

Comments
 (0)