diff --git a/.unreleased/fix_attach_chunk_attislocal b/.unreleased/fix_attach_chunk_attislocal new file mode 100644 index 00000000000..f84f497cdf0 --- /dev/null +++ b/.unreleased/fix_attach_chunk_attislocal @@ -0,0 +1 @@ +Fixes: #10352 Reset inherited column and constraint flags on chunks during attach_chunk diff --git a/sql/updates/latest-dev.sql b/sql/updates/latest-dev.sql index a3c95e7b1bb..5e56554e686 100644 --- a/sql/updates/latest-dev.sql +++ b/sql/updates/latest-dev.sql @@ -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 diff --git a/src/chunk.c b/src/chunk.c index 213fa3d4ea3..f671e76c8cd 100644 --- a/src/chunk.c +++ b/src/chunk.c @@ -4,6 +4,7 @@ * LICENSE-APACHE for a copy of the license. */ #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -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. * @@ -1149,6 +1226,8 @@ chunk_add_inheritance(Chunk *chunk, const Hypertable *ht) }; AlterTable(&alterstmt, lockmode, &atcontext); + + chunk_reset_inherited_flags(atcontext.relid); } static Chunk * diff --git a/tsl/test/expected/attach_chunk.out b/tsl/test/expected/attach_chunk-16.out similarity index 72% rename from tsl/test/expected/attach_chunk.out rename to tsl/test/expected/attach_chunk-16.out index dd63725a120..b4d6107dde0 100644 --- a/tsl/test/expected/attach_chunk.out +++ b/tsl/test/expected/attach_chunk-16.out @@ -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; diff --git a/tsl/test/expected/attach_chunk-17.out b/tsl/test/expected/attach_chunk-17.out new file mode 100644 index 00000000000..b4d6107dde0 --- /dev/null +++ b/tsl/test/expected/attach_chunk-17.out @@ -0,0 +1,356 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +\c :TEST_DBNAME :ROLE_SUPERUSER +GRANT CREATE ON DATABASE :"TEST_DBNAME" TO :ROLE_DEFAULT_PERM_USER; +SET ROLE :ROLE_DEFAULT_PERM_USER; +-- Create test tables and hypertables (reusing from detach_chunk test) +CREATE TABLE devices(id int PRIMARY KEY); +INSERT INTO devices VALUES (1), (2), (3); +CREATE TABLE attach_test(id int, time timestamptz not null, device int, temp float); +CREATE INDEX attach_test_device_idx ON attach_test (device); +ALTER TABLE attach_test + ADD CONSTRAINT attach_test_temp_check CHECK (temp > 0), + ADD CONSTRAINT attach_test_device_fkey FOREIGN KEY (device) REFERENCES devices(id), + ADD CONSTRAINT attach_test_id_time_unique UNIQUE (id, time); +SELECT * FROM create_hypertable('attach_test', 'time', 'id', 2); + hypertable_id | schema_name | table_name | created +---------------+-------------+-------------+--------- + 1 | public | attach_test | t + +CREATE TABLE attach_test_ref ( + id int PRIMARY KEY, + ref_id int, + ref_time timestamptz, + FOREIGN KEY (ref_id, ref_time) REFERENCES attach_test(id, time) +); +INSERT INTO attach_test VALUES + (1, '2025-06-01 05:00:00+3', 1, 23.4), + (2, '2025-06-15 05:00:00+3', 2, 24.5), + (3, '2025-06-30 05:00:00+3', 3, 25.6); +-- Get chunk information for testing +SELECT + chunk_id AS "CHUNK_ID", + hypertable_id AS "HYPERTABLE_ID", + schema_name AS "CHUNK_SCHEMA", + table_name AS "CHUNK_TABLE", + schema_name || '.' || table_name AS "CHUNK_NAME", + slices AS "CHUNK_SLICES" +FROM _timescaledb_functions.show_chunk((SELECT show_chunks('attach_test') LIMIT 1)); \gset + CHUNK_ID | HYPERTABLE_ID | CHUNK_SCHEMA | CHUNK_TABLE | CHUNK_NAME | CHUNK_SLICES +----------+---------------+-----------------------+------------------+----------------------------------------+------------------------------------------------------------------------------------------ + 1 | 1 | _timescaledb_internal | _hyper_1_1_chunk | _timescaledb_internal._hyper_1_1_chunk | {"id": [-9223372036854775808, 1073741823], "time": [1748476800000000, 1749081600000000]} + +-- Successful attachment +CREATE TABLE regular_table_to_attach(id int, time timestamptz not null, device int, temp float); +CREATE INDEX regular_table_device_idx ON regular_table_to_attach (device); +ALTER TABLE regular_table_to_attach + ADD CONSTRAINT attach_test_temp_check CHECK (temp > 0), + ADD CONSTRAINT pre_attach_temp_check CHECK (temp < 100), + ADD CONSTRAINT pre_attach_id_time_unique UNIQUE (id, time); +INSERT INTO regular_table_to_attach VALUES (10, '2025-07-05 13:00:00+3', 2, 27.8); +-- Attach it as a chunk +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +-- Verify data is accessible through the hypertable +SELECT count(*) > 0 FROM attach_test WHERE time >= '2025-07-5'::timestamptz; + ?column? +---------- + t + +-- Check the chunk and related metadata are set up correctly +SELECT id AS "ATTACHED_CHUNK" FROM _timescaledb_catalog.chunk +WHERE hypertable_id = :'HYPERTABLE_ID' AND relid = (SELECT oid FROM pg_class WHERE relname = 'regular_table_to_attach' LIMIT 1); \gset + ATTACHED_CHUNK +---------------- + 4 + +-- Verify that each constraint/index on a auto-created chunk is also present on the attached chunk +SELECT * FROM test.show_indexesp('regular_table_to_attach'); + Table | Index | Columns | Expr | Unique | Primary | Exclusion | Tablespace +-------------------------+----------------------------------------------+-----------+------+--------+---------+-----------+------------ + regular_table_to_attach | regular_table_device_idx | {device} | | f | f | f | + regular_table_to_attach | pre_attach_id_time_unique | {id,time} | | t | f | f | + regular_table_to_attach | regular_table_to_attach_attach_test_time_idx | {time} | | f | f | f | + +SELECT * FROM _timescaledb_catalog.dimension_slice WHERE chunk_id = :'ATTACHED_CHUNK'; + id | chunk_id | dimension_id | range_start | range_end +----+----------+--------------+----------------------+------------------ + 7 | 4 | 1 | 1751335200000000 | 1751853600000000 + 8 | 4 | 2 | -9223372036854775808 | 1073741823 + +-- Verify that the chunk is a child of the hypertable +SELECT count(*) > 0 FROM pg_inherits +WHERE inhrelid = 'regular_table_to_attach'::regclass::oid AND inhparent = 'attach_test'::regclass::oid; + ?column? +---------- + t + +-- Verify foreign key references work +SELECT count(*) > 0 FROM pg_constraint +WHERE contype = 'f' AND confrelid = 'regular_table_to_attach'::regclass::oid; + ?column? +---------- + t + +SELECT count(*) > 0 FROM pg_constraint +WHERE contype = 'f' AND conrelid = 'regular_table_to_attach'::regclass::oid; + ?column? +---------- + t + +-- Verify data is routed to the correct chunk +SELECT count(*) FROM attach_test WHERE time > '2025-07-01'::timestamptz; + count +------- + 1 + +INSERT INTO attach_test VALUES (5, '2025-07-4 05:00:00+3', 2, 19.5); +SELECT count(*) FROM regular_table_to_attach; + count +------- + 2 + +-- Detach and re-attach a chunk +CALL detach_chunk(:'CHUNK_NAME'); +CALL attach_chunk('attach_test', :'CHUNK_NAME', :'CHUNK_SLICES'); +-- Store the new chunk id +SELECT chunk_id AS "CHUNK_ID" FROM _timescaledb_functions.show_chunk(:'CHUNK_NAME'); \gset + CHUNK_ID +---------- + 5 + +-- Verify it's re-attached +SELECT count(*) > 0 FROM pg_inherits WHERE inhrelid = :'CHUNK_NAME'::regclass::oid; + ?column? +---------- + t + +-- Verify constraints and indexes are restored +SELECT * FROM _timescaledb_catalog.dimension_slice WHERE chunk_id = :'CHUNK_ID'; + id | chunk_id | dimension_id | range_start | range_end +----+----------+--------------+----------------------+------------------ + 9 | 5 | 1 | 1748476800000000 | 1749081600000000 + 10 | 5 | 2 | -9223372036854775808 | 1073741823 + +-- Attach a chunk to another hypertable with a different dimension +CALL detach_chunk('regular_table_to_attach'); +CREATE TABLE hypertable_with_different_dimension(id int, time timestamptz not null, device int, temp float); +SELECT * FROM create_hypertable('hypertable_with_different_dimension', 'time'); + hypertable_id | schema_name | table_name | created +---------------+-------------+-------------------------------------+--------- + 2 | public | hypertable_with_different_dimension | t + +CALL attach_chunk('hypertable_with_different_dimension', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"]}'); +CALL detach_chunk('regular_table_to_attach'); +CREATE TABLE not_a_hypertable(id int, time timestamptz not null, device int, temp float); +-- Error cases +\set ON_ERROR_STOP 0 +CALL attach_chunk('attach_test', 'nonexistent_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: relation "nonexistent_table" does not exist at character 34 +CALL attach_chunk('not_a_hypertable', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: table "not_a_hypertable" is not a hypertable +CALL attach_chunk('nonexistent_hypertable', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: relation "nonexistent_hypertable" does not exist at character 19 +CALL attach_chunk(98765, 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: relation with OID 98765 does not exist +CALL attach_chunk(0, 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: invalid hypertable relation OID +-- invalid json format +CALL attach_chunk('attach_test', 'regular_table_to_attach', '"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]'); +ERROR: invalid input syntax for type json at character 61 +-- incorrect dimension information +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025123-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: timestamp out of range: "2025123-07-01 05:00:00+3" +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "incorrect_key": [-9223372036854775808, 1073741823]}'); +ERROR: invalid hypercube for hypertable "attach_test" +CALL attach_chunk('attach_test', 'regular_table_to_attach', NULL); +ERROR: invalid dimension slices argument +CALL attach_chunk('attach_test', 'regular_table_to_attach', :'CHUNK_SLICES'); +ERROR: chunk creation failed due to collision +CALL attach_chunk('attach_test', :'CHUNK_NAME', :'CHUNK_SLICES'); +ERROR: cannot attach chunk that is already a child of another table +-- Attach a chunk of another hypertable +CALL attach_chunk('hypertable_with_different_dimension', :'CHUNK_NAME', :'CHUNK_SLICES'); +ERROR: cannot attach chunk that is already a child of another table +-- Try to attach a table that's already a child of another table +CREATE TABLE parent_table(id int); +CREATE TABLE child_table(time timestamptz not null, device int, temp float) INHERITS (parent_table); +CALL attach_chunk('attach_test', 'child_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: cannot attach chunk that is already a child of another table +-- Try to attach a table with incompatible types +CREATE TABLE incompatible_table(id int, time timestamptz not null, device text, temp float); +CALL attach_chunk('attach_test', 'incompatible_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: child table "incompatible_table" has different type for column "device" +-- Try to attach a table with missing columns +CREATE TABLE missing_col_table(id int, time timestamptz not null); +CALL attach_chunk('attach_test', 'missing_col_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: child table is missing column "device" +-- Try to attach a table with extra columns +CREATE TABLE extra_col_table(id int, time timestamptz not null, device int, temp float, extra_col int); +CALL attach_chunk('attach_test', 'extra_col_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: table "extra_col_table" contains column "extra_col" not found in parent "attach_test" +-- Attach by non-owner is not allowed +set role :ROLE_1; +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: must be owner of table regular_table_to_attach +set role :ROLE_DEFAULT_PERM_USER; +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-06-01 05:00:00+3", "2025-06-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: chunk creation failed due to collision +-- Attach hypertable as a chunk +CALL attach_chunk('attach_test', 'hypertable_with_different_dimension', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: cannot attach hypertable as a chunk +\set ON_ERROR_STOP 1 +-- Test rollback behavior +SELECT count(*) AS "PRE_ROLLBACK_CHUNKS" FROM _timescaledb_catalog.chunk WHERE hypertable_id = :'HYPERTABLE_ID'; \gset + PRE_ROLLBACK_CHUNKS +--------------------- + 3 + +BEGIN; +CREATE TABLE rollback_test_table(id int, time timestamptz not null, device int, temp float); +ALTER TABLE rollback_test_table ADD CONSTRAINT attach_test_temp_check CHECK (temp > 0); +CALL attach_chunk('attach_test', 'rollback_test_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ROLLBACK; +SELECT count(*) = :'PRE_ROLLBACK_CHUNKS' FROM _timescaledb_catalog.chunk WHERE hypertable_id = :'HYPERTABLE_ID'; + ?column? +---------- + t + +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00", "2025-07-07 05:00:00"], "id": [-9223372036854775808, 1073741823]}'); +CALL detach_chunk('regular_table_to_attach'); +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01", "2025-07-07"], "id": [-9223372036854775808, 1073741823]}'); +CALL detach_chunk('regular_table_to_attach'); +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["Tue July 1 05:00:00 2025 GMT+3", "Mon July 7 05:00:00 2025 GMT+3"], "id": [-9223372036854775808, 1073741823]}'); +CALL detach_chunk('regular_table_to_attach'); +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": [1751356800000000, 1751875200000000], "id": [-9223372036854775808, 1073741823]}'); +CALL detach_chunk('regular_table_to_attach'); +-- Attach a table with rows violating chunk constraints +INSERT INTO regular_table_to_attach VALUES (4, '2024-07-05 13:00:00+3', 2, 27.8); +\set ON_ERROR_STOP 0 +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: dimension constraint for column "time" violated by some row +\set ON_ERROR_STOP 1 +-- Clean up +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; diff --git a/tsl/test/expected/attach_chunk-18.out b/tsl/test/expected/attach_chunk-18.out new file mode 100644 index 00000000000..f296b15c511 --- /dev/null +++ b/tsl/test/expected/attach_chunk-18.out @@ -0,0 +1,357 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +\c :TEST_DBNAME :ROLE_SUPERUSER +GRANT CREATE ON DATABASE :"TEST_DBNAME" TO :ROLE_DEFAULT_PERM_USER; +SET ROLE :ROLE_DEFAULT_PERM_USER; +-- Create test tables and hypertables (reusing from detach_chunk test) +CREATE TABLE devices(id int PRIMARY KEY); +INSERT INTO devices VALUES (1), (2), (3); +CREATE TABLE attach_test(id int, time timestamptz not null, device int, temp float); +CREATE INDEX attach_test_device_idx ON attach_test (device); +ALTER TABLE attach_test + ADD CONSTRAINT attach_test_temp_check CHECK (temp > 0), + ADD CONSTRAINT attach_test_device_fkey FOREIGN KEY (device) REFERENCES devices(id), + ADD CONSTRAINT attach_test_id_time_unique UNIQUE (id, time); +SELECT * FROM create_hypertable('attach_test', 'time', 'id', 2); + hypertable_id | schema_name | table_name | created +---------------+-------------+-------------+--------- + 1 | public | attach_test | t + +CREATE TABLE attach_test_ref ( + id int PRIMARY KEY, + ref_id int, + ref_time timestamptz, + FOREIGN KEY (ref_id, ref_time) REFERENCES attach_test(id, time) +); +INSERT INTO attach_test VALUES + (1, '2025-06-01 05:00:00+3', 1, 23.4), + (2, '2025-06-15 05:00:00+3', 2, 24.5), + (3, '2025-06-30 05:00:00+3', 3, 25.6); +-- Get chunk information for testing +SELECT + chunk_id AS "CHUNK_ID", + hypertable_id AS "HYPERTABLE_ID", + schema_name AS "CHUNK_SCHEMA", + table_name AS "CHUNK_TABLE", + schema_name || '.' || table_name AS "CHUNK_NAME", + slices AS "CHUNK_SLICES" +FROM _timescaledb_functions.show_chunk((SELECT show_chunks('attach_test') LIMIT 1)); \gset + CHUNK_ID | HYPERTABLE_ID | CHUNK_SCHEMA | CHUNK_TABLE | CHUNK_NAME | CHUNK_SLICES +----------+---------------+-----------------------+------------------+----------------------------------------+------------------------------------------------------------------------------------------ + 1 | 1 | _timescaledb_internal | _hyper_1_1_chunk | _timescaledb_internal._hyper_1_1_chunk | {"id": [-9223372036854775808, 1073741823], "time": [1748476800000000, 1749081600000000]} + +-- Successful attachment +CREATE TABLE regular_table_to_attach(id int, time timestamptz not null, device int, temp float); +CREATE INDEX regular_table_device_idx ON regular_table_to_attach (device); +ALTER TABLE regular_table_to_attach + ADD CONSTRAINT attach_test_temp_check CHECK (temp > 0), + ADD CONSTRAINT pre_attach_temp_check CHECK (temp < 100), + ADD CONSTRAINT pre_attach_id_time_unique UNIQUE (id, time); +INSERT INTO regular_table_to_attach VALUES (10, '2025-07-05 13:00:00+3', 2, 27.8); +-- Attach it as a chunk +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +-- Verify data is accessible through the hypertable +SELECT count(*) > 0 FROM attach_test WHERE time >= '2025-07-5'::timestamptz; + ?column? +---------- + t + +-- Check the chunk and related metadata are set up correctly +SELECT id AS "ATTACHED_CHUNK" FROM _timescaledb_catalog.chunk +WHERE hypertable_id = :'HYPERTABLE_ID' AND relid = (SELECT oid FROM pg_class WHERE relname = 'regular_table_to_attach' LIMIT 1); \gset + ATTACHED_CHUNK +---------------- + 4 + +-- Verify that each constraint/index on a auto-created chunk is also present on the attached chunk +SELECT * FROM test.show_indexesp('regular_table_to_attach'); + Table | Index | Columns | Expr | Unique | Primary | Exclusion | Tablespace +-------------------------+----------------------------------------------+-----------+------+--------+---------+-----------+------------ + regular_table_to_attach | regular_table_device_idx | {device} | | f | f | f | + regular_table_to_attach | pre_attach_id_time_unique | {id,time} | | t | f | f | + regular_table_to_attach | regular_table_to_attach_attach_test_time_idx | {time} | | f | f | f | + +SELECT * FROM _timescaledb_catalog.dimension_slice WHERE chunk_id = :'ATTACHED_CHUNK'; + id | chunk_id | dimension_id | range_start | range_end +----+----------+--------------+----------------------+------------------ + 7 | 4 | 1 | 1751335200000000 | 1751853600000000 + 8 | 4 | 2 | -9223372036854775808 | 1073741823 + +-- Verify that the chunk is a child of the hypertable +SELECT count(*) > 0 FROM pg_inherits +WHERE inhrelid = 'regular_table_to_attach'::regclass::oid AND inhparent = 'attach_test'::regclass::oid; + ?column? +---------- + t + +-- Verify foreign key references work +SELECT count(*) > 0 FROM pg_constraint +WHERE contype = 'f' AND confrelid = 'regular_table_to_attach'::regclass::oid; + ?column? +---------- + t + +SELECT count(*) > 0 FROM pg_constraint +WHERE contype = 'f' AND conrelid = 'regular_table_to_attach'::regclass::oid; + ?column? +---------- + t + +-- Verify data is routed to the correct chunk +SELECT count(*) FROM attach_test WHERE time > '2025-07-01'::timestamptz; + count +------- + 1 + +INSERT INTO attach_test VALUES (5, '2025-07-4 05:00:00+3', 2, 19.5); +SELECT count(*) FROM regular_table_to_attach; + count +------- + 2 + +-- Detach and re-attach a chunk +CALL detach_chunk(:'CHUNK_NAME'); +CALL attach_chunk('attach_test', :'CHUNK_NAME', :'CHUNK_SLICES'); +-- Store the new chunk id +SELECT chunk_id AS "CHUNK_ID" FROM _timescaledb_functions.show_chunk(:'CHUNK_NAME'); \gset + CHUNK_ID +---------- + 5 + +-- Verify it's re-attached +SELECT count(*) > 0 FROM pg_inherits WHERE inhrelid = :'CHUNK_NAME'::regclass::oid; + ?column? +---------- + t + +-- Verify constraints and indexes are restored +SELECT * FROM _timescaledb_catalog.dimension_slice WHERE chunk_id = :'CHUNK_ID'; + id | chunk_id | dimension_id | range_start | range_end +----+----------+--------------+----------------------+------------------ + 9 | 5 | 1 | 1748476800000000 | 1749081600000000 + 10 | 5 | 2 | -9223372036854775808 | 1073741823 + +-- Attach a chunk to another hypertable with a different dimension +CALL detach_chunk('regular_table_to_attach'); +CREATE TABLE hypertable_with_different_dimension(id int, time timestamptz not null, device int, temp float); +SELECT * FROM create_hypertable('hypertable_with_different_dimension', 'time'); + hypertable_id | schema_name | table_name | created +---------------+-------------+-------------------------------------+--------- + 2 | public | hypertable_with_different_dimension | t + +CALL attach_chunk('hypertable_with_different_dimension', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"]}'); +CALL detach_chunk('regular_table_to_attach'); +CREATE TABLE not_a_hypertable(id int, time timestamptz not null, device int, temp float); +-- Error cases +\set ON_ERROR_STOP 0 +CALL attach_chunk('attach_test', 'nonexistent_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: relation "nonexistent_table" does not exist at character 34 +CALL attach_chunk('not_a_hypertable', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: table "not_a_hypertable" is not a hypertable +CALL attach_chunk('nonexistent_hypertable', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: relation "nonexistent_hypertable" does not exist at character 19 +CALL attach_chunk(98765, 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: relation with OID 98765 does not exist +CALL attach_chunk(0, 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: invalid hypertable relation OID +-- invalid json format +CALL attach_chunk('attach_test', 'regular_table_to_attach', '"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]'); +ERROR: invalid input syntax for type json at character 61 +-- incorrect dimension information +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025123-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: timestamp out of range: "2025123-07-01 05:00:00+3" +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "incorrect_key": [-9223372036854775808, 1073741823]}'); +ERROR: invalid hypercube for hypertable "attach_test" +CALL attach_chunk('attach_test', 'regular_table_to_attach', NULL); +ERROR: invalid dimension slices argument +CALL attach_chunk('attach_test', 'regular_table_to_attach', :'CHUNK_SLICES'); +ERROR: chunk creation failed due to collision +CALL attach_chunk('attach_test', :'CHUNK_NAME', :'CHUNK_SLICES'); +ERROR: cannot attach chunk that is already a child of another table +-- Attach a chunk of another hypertable +CALL attach_chunk('hypertable_with_different_dimension', :'CHUNK_NAME', :'CHUNK_SLICES'); +ERROR: cannot attach chunk that is already a child of another table +-- Try to attach a table that's already a child of another table +CREATE TABLE parent_table(id int); +CREATE TABLE child_table(time timestamptz not null, device int, temp float) INHERITS (parent_table); +CALL attach_chunk('attach_test', 'child_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: cannot attach chunk that is already a child of another table +-- Try to attach a table with incompatible types +CREATE TABLE incompatible_table(id int, time timestamptz not null, device text, temp float); +CALL attach_chunk('attach_test', 'incompatible_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: child table "incompatible_table" has different type for column "device" +-- Try to attach a table with missing columns +CREATE TABLE missing_col_table(id int, time timestamptz not null); +CALL attach_chunk('attach_test', 'missing_col_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: child table is missing column "device" +-- Try to attach a table with extra columns +CREATE TABLE extra_col_table(id int, time timestamptz not null, device int, temp float, extra_col int); +CALL attach_chunk('attach_test', 'extra_col_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: table "extra_col_table" contains column "extra_col" not found in parent "attach_test" +-- Attach by non-owner is not allowed +set role :ROLE_1; +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: must be owner of table regular_table_to_attach +set role :ROLE_DEFAULT_PERM_USER; +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-06-01 05:00:00+3", "2025-06-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: chunk creation failed due to collision +-- Attach hypertable as a chunk +CALL attach_chunk('attach_test', 'hypertable_with_different_dimension', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: cannot attach hypertable as a chunk +\set ON_ERROR_STOP 1 +-- Test rollback behavior +SELECT count(*) AS "PRE_ROLLBACK_CHUNKS" FROM _timescaledb_catalog.chunk WHERE hypertable_id = :'HYPERTABLE_ID'; \gset + PRE_ROLLBACK_CHUNKS +--------------------- + 3 + +BEGIN; +CREATE TABLE rollback_test_table(id int, time timestamptz not null, device int, temp float); +ALTER TABLE rollback_test_table ADD CONSTRAINT attach_test_temp_check CHECK (temp > 0); +CALL attach_chunk('attach_test', 'rollback_test_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ROLLBACK; +SELECT count(*) = :'PRE_ROLLBACK_CHUNKS' FROM _timescaledb_catalog.chunk WHERE hypertable_id = :'HYPERTABLE_ID'; + ?column? +---------- + t + +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00", "2025-07-07 05:00:00"], "id": [-9223372036854775808, 1073741823]}'); +CALL detach_chunk('regular_table_to_attach'); +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01", "2025-07-07"], "id": [-9223372036854775808, 1073741823]}'); +CALL detach_chunk('regular_table_to_attach'); +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["Tue July 1 05:00:00 2025 GMT+3", "Mon July 7 05:00:00 2025 GMT+3"], "id": [-9223372036854775808, 1073741823]}'); +CALL detach_chunk('regular_table_to_attach'); +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": [1751356800000000, 1751875200000000], "id": [-9223372036854775808, 1073741823]}'); +CALL detach_chunk('regular_table_to_attach'); +-- Attach a table with rows violating chunk constraints +INSERT INTO regular_table_to_attach VALUES (4, '2024-07-05 13:00:00+3', 2, 27.8); +\set ON_ERROR_STOP 0 +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: dimension constraint for column "time" violated by some row +\set ON_ERROR_STOP 1 +-- Clean up +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_time_not_null | n | f | 1 + 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; diff --git a/tsl/test/expected/attach_chunk-19.out b/tsl/test/expected/attach_chunk-19.out new file mode 100644 index 00000000000..f296b15c511 --- /dev/null +++ b/tsl/test/expected/attach_chunk-19.out @@ -0,0 +1,357 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +\c :TEST_DBNAME :ROLE_SUPERUSER +GRANT CREATE ON DATABASE :"TEST_DBNAME" TO :ROLE_DEFAULT_PERM_USER; +SET ROLE :ROLE_DEFAULT_PERM_USER; +-- Create test tables and hypertables (reusing from detach_chunk test) +CREATE TABLE devices(id int PRIMARY KEY); +INSERT INTO devices VALUES (1), (2), (3); +CREATE TABLE attach_test(id int, time timestamptz not null, device int, temp float); +CREATE INDEX attach_test_device_idx ON attach_test (device); +ALTER TABLE attach_test + ADD CONSTRAINT attach_test_temp_check CHECK (temp > 0), + ADD CONSTRAINT attach_test_device_fkey FOREIGN KEY (device) REFERENCES devices(id), + ADD CONSTRAINT attach_test_id_time_unique UNIQUE (id, time); +SELECT * FROM create_hypertable('attach_test', 'time', 'id', 2); + hypertable_id | schema_name | table_name | created +---------------+-------------+-------------+--------- + 1 | public | attach_test | t + +CREATE TABLE attach_test_ref ( + id int PRIMARY KEY, + ref_id int, + ref_time timestamptz, + FOREIGN KEY (ref_id, ref_time) REFERENCES attach_test(id, time) +); +INSERT INTO attach_test VALUES + (1, '2025-06-01 05:00:00+3', 1, 23.4), + (2, '2025-06-15 05:00:00+3', 2, 24.5), + (3, '2025-06-30 05:00:00+3', 3, 25.6); +-- Get chunk information for testing +SELECT + chunk_id AS "CHUNK_ID", + hypertable_id AS "HYPERTABLE_ID", + schema_name AS "CHUNK_SCHEMA", + table_name AS "CHUNK_TABLE", + schema_name || '.' || table_name AS "CHUNK_NAME", + slices AS "CHUNK_SLICES" +FROM _timescaledb_functions.show_chunk((SELECT show_chunks('attach_test') LIMIT 1)); \gset + CHUNK_ID | HYPERTABLE_ID | CHUNK_SCHEMA | CHUNK_TABLE | CHUNK_NAME | CHUNK_SLICES +----------+---------------+-----------------------+------------------+----------------------------------------+------------------------------------------------------------------------------------------ + 1 | 1 | _timescaledb_internal | _hyper_1_1_chunk | _timescaledb_internal._hyper_1_1_chunk | {"id": [-9223372036854775808, 1073741823], "time": [1748476800000000, 1749081600000000]} + +-- Successful attachment +CREATE TABLE regular_table_to_attach(id int, time timestamptz not null, device int, temp float); +CREATE INDEX regular_table_device_idx ON regular_table_to_attach (device); +ALTER TABLE regular_table_to_attach + ADD CONSTRAINT attach_test_temp_check CHECK (temp > 0), + ADD CONSTRAINT pre_attach_temp_check CHECK (temp < 100), + ADD CONSTRAINT pre_attach_id_time_unique UNIQUE (id, time); +INSERT INTO regular_table_to_attach VALUES (10, '2025-07-05 13:00:00+3', 2, 27.8); +-- Attach it as a chunk +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +-- Verify data is accessible through the hypertable +SELECT count(*) > 0 FROM attach_test WHERE time >= '2025-07-5'::timestamptz; + ?column? +---------- + t + +-- Check the chunk and related metadata are set up correctly +SELECT id AS "ATTACHED_CHUNK" FROM _timescaledb_catalog.chunk +WHERE hypertable_id = :'HYPERTABLE_ID' AND relid = (SELECT oid FROM pg_class WHERE relname = 'regular_table_to_attach' LIMIT 1); \gset + ATTACHED_CHUNK +---------------- + 4 + +-- Verify that each constraint/index on a auto-created chunk is also present on the attached chunk +SELECT * FROM test.show_indexesp('regular_table_to_attach'); + Table | Index | Columns | Expr | Unique | Primary | Exclusion | Tablespace +-------------------------+----------------------------------------------+-----------+------+--------+---------+-----------+------------ + regular_table_to_attach | regular_table_device_idx | {device} | | f | f | f | + regular_table_to_attach | pre_attach_id_time_unique | {id,time} | | t | f | f | + regular_table_to_attach | regular_table_to_attach_attach_test_time_idx | {time} | | f | f | f | + +SELECT * FROM _timescaledb_catalog.dimension_slice WHERE chunk_id = :'ATTACHED_CHUNK'; + id | chunk_id | dimension_id | range_start | range_end +----+----------+--------------+----------------------+------------------ + 7 | 4 | 1 | 1751335200000000 | 1751853600000000 + 8 | 4 | 2 | -9223372036854775808 | 1073741823 + +-- Verify that the chunk is a child of the hypertable +SELECT count(*) > 0 FROM pg_inherits +WHERE inhrelid = 'regular_table_to_attach'::regclass::oid AND inhparent = 'attach_test'::regclass::oid; + ?column? +---------- + t + +-- Verify foreign key references work +SELECT count(*) > 0 FROM pg_constraint +WHERE contype = 'f' AND confrelid = 'regular_table_to_attach'::regclass::oid; + ?column? +---------- + t + +SELECT count(*) > 0 FROM pg_constraint +WHERE contype = 'f' AND conrelid = 'regular_table_to_attach'::regclass::oid; + ?column? +---------- + t + +-- Verify data is routed to the correct chunk +SELECT count(*) FROM attach_test WHERE time > '2025-07-01'::timestamptz; + count +------- + 1 + +INSERT INTO attach_test VALUES (5, '2025-07-4 05:00:00+3', 2, 19.5); +SELECT count(*) FROM regular_table_to_attach; + count +------- + 2 + +-- Detach and re-attach a chunk +CALL detach_chunk(:'CHUNK_NAME'); +CALL attach_chunk('attach_test', :'CHUNK_NAME', :'CHUNK_SLICES'); +-- Store the new chunk id +SELECT chunk_id AS "CHUNK_ID" FROM _timescaledb_functions.show_chunk(:'CHUNK_NAME'); \gset + CHUNK_ID +---------- + 5 + +-- Verify it's re-attached +SELECT count(*) > 0 FROM pg_inherits WHERE inhrelid = :'CHUNK_NAME'::regclass::oid; + ?column? +---------- + t + +-- Verify constraints and indexes are restored +SELECT * FROM _timescaledb_catalog.dimension_slice WHERE chunk_id = :'CHUNK_ID'; + id | chunk_id | dimension_id | range_start | range_end +----+----------+--------------+----------------------+------------------ + 9 | 5 | 1 | 1748476800000000 | 1749081600000000 + 10 | 5 | 2 | -9223372036854775808 | 1073741823 + +-- Attach a chunk to another hypertable with a different dimension +CALL detach_chunk('regular_table_to_attach'); +CREATE TABLE hypertable_with_different_dimension(id int, time timestamptz not null, device int, temp float); +SELECT * FROM create_hypertable('hypertable_with_different_dimension', 'time'); + hypertable_id | schema_name | table_name | created +---------------+-------------+-------------------------------------+--------- + 2 | public | hypertable_with_different_dimension | t + +CALL attach_chunk('hypertable_with_different_dimension', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"]}'); +CALL detach_chunk('regular_table_to_attach'); +CREATE TABLE not_a_hypertable(id int, time timestamptz not null, device int, temp float); +-- Error cases +\set ON_ERROR_STOP 0 +CALL attach_chunk('attach_test', 'nonexistent_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: relation "nonexistent_table" does not exist at character 34 +CALL attach_chunk('not_a_hypertable', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: table "not_a_hypertable" is not a hypertable +CALL attach_chunk('nonexistent_hypertable', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: relation "nonexistent_hypertable" does not exist at character 19 +CALL attach_chunk(98765, 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: relation with OID 98765 does not exist +CALL attach_chunk(0, 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: invalid hypertable relation OID +-- invalid json format +CALL attach_chunk('attach_test', 'regular_table_to_attach', '"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]'); +ERROR: invalid input syntax for type json at character 61 +-- incorrect dimension information +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025123-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: timestamp out of range: "2025123-07-01 05:00:00+3" +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "incorrect_key": [-9223372036854775808, 1073741823]}'); +ERROR: invalid hypercube for hypertable "attach_test" +CALL attach_chunk('attach_test', 'regular_table_to_attach', NULL); +ERROR: invalid dimension slices argument +CALL attach_chunk('attach_test', 'regular_table_to_attach', :'CHUNK_SLICES'); +ERROR: chunk creation failed due to collision +CALL attach_chunk('attach_test', :'CHUNK_NAME', :'CHUNK_SLICES'); +ERROR: cannot attach chunk that is already a child of another table +-- Attach a chunk of another hypertable +CALL attach_chunk('hypertable_with_different_dimension', :'CHUNK_NAME', :'CHUNK_SLICES'); +ERROR: cannot attach chunk that is already a child of another table +-- Try to attach a table that's already a child of another table +CREATE TABLE parent_table(id int); +CREATE TABLE child_table(time timestamptz not null, device int, temp float) INHERITS (parent_table); +CALL attach_chunk('attach_test', 'child_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: cannot attach chunk that is already a child of another table +-- Try to attach a table with incompatible types +CREATE TABLE incompatible_table(id int, time timestamptz not null, device text, temp float); +CALL attach_chunk('attach_test', 'incompatible_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: child table "incompatible_table" has different type for column "device" +-- Try to attach a table with missing columns +CREATE TABLE missing_col_table(id int, time timestamptz not null); +CALL attach_chunk('attach_test', 'missing_col_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: child table is missing column "device" +-- Try to attach a table with extra columns +CREATE TABLE extra_col_table(id int, time timestamptz not null, device int, temp float, extra_col int); +CALL attach_chunk('attach_test', 'extra_col_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: table "extra_col_table" contains column "extra_col" not found in parent "attach_test" +-- Attach by non-owner is not allowed +set role :ROLE_1; +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: must be owner of table regular_table_to_attach +set role :ROLE_DEFAULT_PERM_USER; +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-06-01 05:00:00+3", "2025-06-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: chunk creation failed due to collision +-- Attach hypertable as a chunk +CALL attach_chunk('attach_test', 'hypertable_with_different_dimension', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: cannot attach hypertable as a chunk +\set ON_ERROR_STOP 1 +-- Test rollback behavior +SELECT count(*) AS "PRE_ROLLBACK_CHUNKS" FROM _timescaledb_catalog.chunk WHERE hypertable_id = :'HYPERTABLE_ID'; \gset + PRE_ROLLBACK_CHUNKS +--------------------- + 3 + +BEGIN; +CREATE TABLE rollback_test_table(id int, time timestamptz not null, device int, temp float); +ALTER TABLE rollback_test_table ADD CONSTRAINT attach_test_temp_check CHECK (temp > 0); +CALL attach_chunk('attach_test', 'rollback_test_table', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ROLLBACK; +SELECT count(*) = :'PRE_ROLLBACK_CHUNKS' FROM _timescaledb_catalog.chunk WHERE hypertable_id = :'HYPERTABLE_ID'; + ?column? +---------- + t + +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00", "2025-07-07 05:00:00"], "id": [-9223372036854775808, 1073741823]}'); +CALL detach_chunk('regular_table_to_attach'); +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01", "2025-07-07"], "id": [-9223372036854775808, 1073741823]}'); +CALL detach_chunk('regular_table_to_attach'); +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["Tue July 1 05:00:00 2025 GMT+3", "Mon July 7 05:00:00 2025 GMT+3"], "id": [-9223372036854775808, 1073741823]}'); +CALL detach_chunk('regular_table_to_attach'); +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": [1751356800000000, 1751875200000000], "id": [-9223372036854775808, 1073741823]}'); +CALL detach_chunk('regular_table_to_attach'); +-- Attach a table with rows violating chunk constraints +INSERT INTO regular_table_to_attach VALUES (4, '2024-07-05 13:00:00+3', 2, 27.8); +\set ON_ERROR_STOP 0 +CALL attach_chunk('attach_test', 'regular_table_to_attach', '{"time": ["2025-07-01 05:00:00+3", "2025-07-07 05:00:00+3"], "id": [-9223372036854775808, 1073741823]}'); +ERROR: dimension constraint for column "time" violated by some row +\set ON_ERROR_STOP 1 +-- Clean up +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_time_not_null | n | f | 1 + 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; diff --git a/tsl/test/sql/.gitignore b/tsl/test/sql/.gitignore index 95bf6132d3b..c28b0348b16 100644 --- a/tsl/test/sql/.gitignore +++ b/tsl/test/sql/.gitignore @@ -1,4 +1,5 @@ /*.pgbinary +/attach_chunk-*.sql /cagg*-1[3-9].sql /chunk_utils_internal-*.sql /columnar_index_scan-*.sql diff --git a/tsl/test/sql/CMakeLists.txt b/tsl/test/sql/CMakeLists.txt index 47eee91fe12..289e86d52f9 100644 --- a/tsl/test/sql/CMakeLists.txt +++ b/tsl/test/sql/CMakeLists.txt @@ -128,7 +128,6 @@ if(CMAKE_BUILD_TYPE MATCHES Debug) list( APPEND TEST_FILES - attach_chunk.sql bgw_custom.sql bgw_db_scheduler.sql bgw_job_stat_history.sql @@ -208,7 +207,8 @@ if(CMAKE_BUILD_TYPE MATCHES Debug) vector_qual_default.sql vacuum.sql) - list(APPEND TEST_TEMPLATES compression_update_delete.sql.in + list(APPEND TEST_TEMPLATES attach_chunk.sql.in + compression_update_delete.sql.in transparent_decompression_queries.sql.in) if(USE_TELEMETRY) diff --git a/tsl/test/sql/attach_chunk.sql b/tsl/test/sql/attach_chunk.sql.in similarity index 73% rename from tsl/test/sql/attach_chunk.sql rename to tsl/test/sql/attach_chunk.sql.in index 5248a3c9ebc..f7066aa86dc 100644 --- a/tsl/test/sql/attach_chunk.sql +++ b/tsl/test/sql/attach_chunk.sql.in @@ -186,3 +186,91 @@ 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'); +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 + +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; + +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; + +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'); +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 + +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; + +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'; + +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 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'); + +-- 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; + +-- 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; + +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; + +-- Dropping the hypertable also drops the attached foreign chunk. +DROP TABLE osm_ht; +DROP SERVER attach_chunk_fdw; +DROP EXTENSION postgres_fdw;