[Bug]: Column used to set up granular refresh can be dropped using `DROP DOMAIN CASCADE`
Author: kpan2034Created Sep 15, 2026Updated Sep 15, 2026
Labelsbugcaggs::granular-refresh
What type of bug is this?
Crash
What subsystems and features are affected?
Continuous aggregate
What happened?
DROP DOMAIN ... CASCADE drops a hypertable column even if it has been used to set up granular refresh. This happens since DROP DOMAIN doesn't follow the ALTER COLUMN path, where this is disallowed (introduced by this PR).
This can lead to a crash when a subsequent insert causes the tracker to throw an Internal Program Error, likely at this Ensure
Repro script below. Found by the LLM-fuzzer workflow while back porting
TimescaleDB version affected
2.30.x
PostgreSQL version used
18
What operating system did you use?
N/A
What installation method did you use?
Not applicable
What platform did you run on?
Not applicable
Relevant log output and stack trace
How can we reproduce the bug?
-- Reproducer: assertion failure in tenant tracking when the granular
-- refresh column is dropped via DROP DOMAIN CASCADE and re-added with
-- an unsupported type (float8).
--
-- The ALTER TABLE ... DROP COLUMN check introduced by this PR correctly
-- blocks direct column drops, but DROP DOMAIN CASCADE bypasses it
-- because the column removal goes through PostgreSQL's dependency
-- system rather than ALTER TABLE subcommand processing.
--
-- After the column is re-added with an unsupported type, the DML
-- invalidation path hits Ensure(ts_tenant_type_is_supported(...))
-- which fires Assert(false) in debug builds (SIGABRT) or
-- ereport(ERROR, ERRCODE_INTERNAL_ERROR) in release builds.
CREATE EXTENSION timescaledb;
-- Step 1: Create a domain so we can CASCADE-drop the column later.
CREATE DOMAIN device_dom AS integer;
-- Step 2: Create a hypertable whose tracking column uses the domain.
CREATE TABLE ht (
time timestamptz NOT NULL,
device device_dom NOT NULL,
val float8
);
SELECT create_hypertable('ht', 'time', chunk_time_interval => '1 day'::interval);
-- Step 3: Configure granular refresh on the domain-typed column.
ALTER TABLE ht SET (
timescaledb.granular_refresh_column = 'device',
timescaledb.granular_refresh_start_offset = '30 days',
timescaledb.granular_refresh_end_offset = '1 day'
);
-- Step 4: Drop the domain. CASCADE removes the column from the
-- hypertable and its chunks, bypassing the ALTER TABLE DROP COLUMN
-- hook that would have blocked the drop.
DROP DOMAIN device_dom CASCADE;
-- Step 5: Re-add the column with a type that is NOT in the
-- supported-tenant-type list (float8 / double precision).
ALTER TABLE ht ADD COLUMN device float8;
-- Step 6: Create a continuous aggregate so the DML invalidation
-- trigger is installed on the hypertable.
CREATE MATERIALIZED VIEW cagg
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 hour'::interval, time) AS bucket,
device,
avg(val) AS avg_val
FROM ht
GROUP BY bucket, device
WITH NO DATA;
-- Step 7: Enable granular refresh on the CAgg. The validation only
-- checks the column *name*, not its type, so it succeeds.
ALTER MATERIALIZED VIEW cagg SET (timescaledb.enable_granular_refresh = true);
-- Step 8: Insert a row. The continuous-aggregate DML trigger fires,
-- resolves the tracking column to float8, and hits
-- Ensure(ts_tenant_type_is_supported(FLOAT8OID)) → crash.
INSERT INTO ht VALUES ('2024-01-01', 1.5, 10.0);Source: timescale/timescaledb