Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .unreleased/fix_attach_chunk_attislocal
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixes: #10352 Reset inherited column and constraint flags on chunks during attach_chunk
21 changes: 21 additions & 0 deletions sql/updates/latest-dev.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
-- Reset the attislocal/conislocal flags on chunk columns and constraints left
-- marked local by a detach_chunk/attach_chunk round-trip so a later hypertable
-- DROP COLUMN or DROP CONSTRAINT propagates to them.
-- Locally-defined objects (inhcount = 0), such as the chunk's dimension
-- constraints, are left untouched.
UPDATE pg_catalog.pg_attribute a
SET attislocal = false
FROM _timescaledb_catalog.chunk c
WHERE a.attrelid = c.relid
AND a.attnum > 0
AND NOT a.attisdropped
AND a.attislocal
AND a.attinhcount > 0;

UPDATE pg_catalog.pg_constraint con
SET conislocal = false
FROM _timescaledb_catalog.chunk c
WHERE con.conrelid = c.relid
AND con.conislocal
AND con.coninhcount > 0;

-- Rebuild the catalog table `_timescaledb_catalog.continuous_aggs_hypertable_invalidation_log`
-- to add the `seqnum` column.
CREATE TABLE _timescaledb_catalog._tmp_continuous_aggs_hypertable_invalidation_log AS
Expand Down
79 changes: 79 additions & 0 deletions src/chunk.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* LICENSE-APACHE for a copy of the license.
*/
#include <postgres.h>
#include <access/genam.h>
#include <access/htup.h>
#include <access/htup_details.h>
#include <access/reloptions.h>
Expand All @@ -13,6 +14,7 @@
#include <access/xact.h>
#include <catalog/indexing.h>
#include <catalog/namespace.h>
#include <catalog/pg_attribute.h>
#include <catalog/pg_class.h>
#include <catalog/pg_constraint.h>
#include <catalog/pg_inherits.h>
Expand Down Expand Up @@ -46,6 +48,7 @@
#include <utils/builtins.h>
#include <utils/datum.h>
#include <utils/elog.h>
#include <utils/fmgroids.h>
#include <utils/hsearch.h>
#include <utils/inval.h>
#include <utils/lsyscache.h>
Expand Down Expand Up @@ -1116,6 +1119,80 @@ chunk_create_from_hypercube_after_lock(const Hypertable *ht, Hypercube *cube,
return chunk;
}

/*
* A chunk should have all columns and constraints inherited and none marked as
* local, so clear the attislocal/conislocal flags that ALTER TABLE ... INHERIT
* leaves set when attaching a pre-existing table. Otherwise a later DROP COLUMN
* or DROP CONSTRAINT on the hypertable would not propagate to the chunk.
* Locally-defined objects (inhcount == 0), such as the chunk's dimension
* constraints, are left untouched.
*/
static void
chunk_reset_inherited_flags(Oid chunk_relid)
{
/* Columns */
Relation attrel = table_open(AttributeRelationId, RowExclusiveLock);
Relation chunkrel = table_open(chunk_relid, AccessShareLock);
TupleDesc tupdesc = RelationGetDescr(chunkrel);

for (int i = 0; i < tupdesc->natts; i++)
{
Form_pg_attribute att = TupleDescAttr(tupdesc, i);

if (att->attisdropped || !att->attislocal || att->attinhcount == 0)
{
continue;
}

HeapTuple tuple = SearchSysCacheCopyAttNum(chunk_relid, att->attnum);
if (!HeapTupleIsValid(tuple))
{
elog(ERROR,
"cache lookup failed for attribute %d of relation %u",
att->attnum,
chunk_relid);
}

((Form_pg_attribute) GETSTRUCT(tuple))->attislocal = false;
CatalogTupleUpdate(attrel, &tuple->t_self, tuple);
heap_freetuple(tuple);
}

table_close(chunkrel, NoLock);
table_close(attrel, RowExclusiveLock);

/* Constraints (CHECK and NOT NULL) */
ScanKeyData skey;
ScanKeyInit(&skey,
Anum_pg_constraint_conrelid,
BTEqualStrategyNumber,
F_OIDEQ,
ObjectIdGetDatum(chunk_relid));

Relation conrel = table_open(ConstraintRelationId, RowExclusiveLock);
SysScanDesc scan =
systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, true, NULL, 1, &skey);
HeapTuple contup;

while (HeapTupleIsValid(contup = systable_getnext(scan)))
{
Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(contup);

if (con->coninhcount == 0 || !con->conislocal)
{
continue;
}

HeapTuple newtup = heap_copytuple(contup);
((Form_pg_constraint) GETSTRUCT(newtup))->conislocal = false;
CatalogTupleUpdate(conrel, &newtup->t_self, newtup);
heap_freetuple(newtup);
}

systable_endscan(scan);
table_close(conrel, RowExclusiveLock);
}

/*
* Make a chunk table inherit a hypertable.
*
Expand Down Expand Up @@ -1149,6 +1226,8 @@ chunk_add_inheritance(Chunk *chunk, const Hypertable *ht)
};

AlterTable(&alterstmt, lockmode, &atcontext);

chunk_reset_inherited_flags(atcontext.relid);
Comment on lines 1228 to +1230

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chunk_add_inheritance is also called by add_foreign_table_as_chunk. would that caller be affected by this change?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

}

static Chunk *
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,122 @@ DROP TABLE regular_table_to_attach;
DROP TABLE attach_test_ref;
DROP TABLE attach_test;
DROP TABLE devices CASCADE;
-- Test dropping columns after detach/attach
CREATE TABLE drop_after_attach(time timestamptz NOT NULL, x int);
SELECT create_hypertable('drop_after_attach', 'time', chunk_time_interval => interval '1 day');
create_hypertable
--------------------------------
(3,public,drop_after_attach,t)

INSERT INTO drop_after_attach VALUES ('2026-01-01', 1), ('2026-01-05', 1);
SELECT schema_name || '.' || table_name AS "ROUNDTRIP_CHUNK", slices AS "ROUNDTRIP_SLICES"
FROM _timescaledb_functions.show_chunk((SELECT show_chunks('drop_after_attach', older_than => '2026-01-02') LIMIT 1)); \gset
ROUNDTRIP_CHUNK | ROUNDTRIP_SLICES
-----------------------------------------+------------------------------------------------
_timescaledb_internal._hyper_3_15_chunk | {"time": [1767225600000000, 1767312000000000]}

CALL detach_chunk(:'ROUNDTRIP_CHUNK');
CALL attach_chunk('drop_after_attach', :'ROUNDTRIP_CHUNK', :'ROUNDTRIP_SLICES');
-- The re-attached chunk inherits without any locally-defined columns.
SELECT attname, attislocal, attinhcount
FROM pg_attribute
WHERE attrelid = :'ROUNDTRIP_CHUNK'::regclass AND attnum > 0 AND NOT attisdropped
ORDER BY attnum;
attname | attislocal | attinhcount
---------+------------+-------------
time | f | 1
x | f | 1

ALTER TABLE drop_after_attach DROP COLUMN x;
-- The column is gone from the round-tripped chunk, not left orphaned.
SELECT attname, attislocal, attinhcount
FROM pg_attribute
WHERE attrelid = :'ROUNDTRIP_CHUNK'::regclass AND attname = 'x' AND NOT attisdropped;
attname | attislocal | attinhcount
---------+------------+-------------

DROP TABLE drop_after_attach;
-- Test dropping a constraint after detach/attach
CREATE TABLE drop_con_after_attach(time timestamptz NOT NULL, x int CHECK (x > 0));
SELECT create_hypertable('drop_con_after_attach', 'time', chunk_time_interval => interval '1 day');
create_hypertable
------------------------------------
(4,public,drop_con_after_attach,t)

INSERT INTO drop_con_after_attach VALUES ('2026-01-01', 1);
SELECT schema_name || '.' || table_name AS "CON_CHUNK", slices AS "CON_SLICES"
FROM _timescaledb_functions.show_chunk((SELECT show_chunks('drop_con_after_attach') LIMIT 1)); \gset
CON_CHUNK | CON_SLICES
-----------------------------------------+------------------------------------------------
_timescaledb_internal._hyper_4_18_chunk | {"time": [1767225600000000, 1767312000000000]}

CALL detach_chunk(:'CON_CHUNK');
CALL attach_chunk('drop_con_after_attach', :'CON_CHUNK', :'CON_SLICES');
-- The inherited constraint is not marked local; the dimension constraint stays local.
SELECT conname, contype, conislocal, coninhcount
FROM pg_constraint
WHERE conrelid = :'CON_CHUNK'::regclass
ORDER BY conname;
conname | contype | conislocal | coninhcount
-------------------------------+---------+------------+-------------
constraint_30 | c | t | 0
drop_con_after_attach_x_check | c | f | 1

ALTER TABLE drop_con_after_attach DROP CONSTRAINT drop_con_after_attach_x_check;
-- The dropped constraint is gone from the round-tripped chunk.
SELECT count(*) AS leftover_check
FROM pg_constraint
WHERE conrelid = :'CON_CHUNK'::regclass AND conname = 'drop_con_after_attach_x_check';
leftover_check
----------------
0

DROP TABLE drop_con_after_attach;
-- Test attaching a foreign table as an OSM chunk
-- A dummy server is enough; the foreign table is never queried by attach.
\c :TEST_DBNAME :ROLE_SUPERUSER
CREATE EXTENSION postgres_fdw;
CREATE SERVER attach_chunk_fdw FOREIGN DATA WRAPPER postgres_fdw;
CREATE TABLE osm_ht(time timestamptz NOT NULL, x int CHECK (x > 0));
SELECT create_hypertable('osm_ht', 'time', chunk_time_interval => interval '1 day');
create_hypertable
---------------------
(5,public,osm_ht,t)

CREATE FOREIGN TABLE osm_ft(time timestamptz NOT NULL, x int) SERVER attach_chunk_fdw;
SELECT _timescaledb_functions.attach_osm_table_chunk('osm_ht', 'osm_ft');
attach_osm_table_chunk
------------------------
t

-- The foreign chunk inherits its columns without any left marked local.
SELECT attname, attislocal, attinhcount
FROM pg_attribute
WHERE attrelid = 'osm_ft'::regclass AND attnum > 0 AND NOT attisdropped
ORDER BY attnum;
attname | attislocal | attinhcount
---------+------------+-------------
time | f | 1
x | f | 1

-- The CHECK constraint cloned from the hypertable is inherited, not local.
SELECT conname, contype, conislocal, coninhcount
FROM pg_constraint
WHERE conrelid = 'osm_ft'::regclass AND contype = 'c'
ORDER BY conname;
conname | contype | conislocal | coninhcount
----------------+---------+------------+-------------
osm_ht_x_check | c | f | 1

ALTER TABLE osm_ht DROP COLUMN x;
-- Dropping the column on the hypertable propagates to the foreign chunk.
SELECT attname
FROM pg_attribute
WHERE attrelid = 'osm_ft'::regclass AND attname = 'x' AND NOT attisdropped;
attname
---------

-- Dropping the hypertable also drops the attached foreign chunk.
DROP TABLE osm_ht;
DROP SERVER attach_chunk_fdw;
DROP EXTENSION postgres_fdw;
Loading
Loading