Skip to content

Commit f09b7b3

Browse files
svenklemmtimescale-automation
authored andcommitted
Fix crash when deleting from compressed cagg source
Deleting whole batches from a compressed hypertable that has a continuous aggregate needs the time range of each deleted batch to record the aggregate invalidation. It read that range from the min and max metadata of the time column unless the time column is a segmentby column. When the time column was neither segmentby nor orderby invalidation for direct batch delete would crash. Add proper error handling when time column is neither segmentby nor orderby. (cherry picked from commit 56f1656)
1 parent c099696 commit f09b7b3

7 files changed

Lines changed: 231 additions & 0 deletions

File tree

.unreleased/pr_10339

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes: #10339 Fix crash when deleting from compressed cagg source

tsl/src/compression/compression_dml.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,14 @@ decompress_batches_for_update_delete(ModifyHypertableState *ht_state, Chunk *chu
912912
settings->fd.compress_relid,
913913
"max");
914914
}
915+
if (!AttributeNumberIsValid(invalidation_ctx.min_time_attno) ||
916+
!AttributeNumberIsValid(invalidation_ctx.max_time_attno))
917+
{
918+
ereport(ERROR,
919+
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
920+
errmsg("cannot perform direct batch delete on hypertable with continuous "
921+
"aggregates when time column is not segmentby or orderby"));
922+
}
915923
}
916924

917925
chunk_rel = table_open(chunk->fd.relid, RowExclusiveLock);

tsl/test/expected/compression_update_delete-16.out

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3733,3 +3733,53 @@ ROLLBACK;
37333733
ROLLBACK
37343734
DROP TABLE crash_test;
37353735
DROP TABLE
3736+
-- direct batch delete when the time column is in neither segmentby nor orderby
3737+
SET timescaledb.auto_sparse_indexes = off;
3738+
SET
3739+
CREATE TABLE cagg_inval_no_minmax(time timestamptz NOT NULL, gauge int, value float)
3740+
WITH (tsdb.hypertable, tsdb.segmentby='time',tsdb.orderby='value');
3741+
NOTICE: using column "time" as partitioning column
3742+
HINT: Use "timescaledb.partition_column" to specify a different column to use as partitioning column.
3743+
CREATE TABLE
3744+
SELECT segmentby, orderby, index FROM _timescaledb_catalog.compression_settings WHERE relid = 'cagg_inval_no_minmax'::regclass;
3745+
segmentby | orderby | index
3746+
-----------+---------+-----------------------------------------------------------------------------------------------------------------------------
3747+
{time} | {value} | [{"type": "minmax", "column": "value", "source": "orderby"}, {"type": "firstlast", "column": "value", "source": "orderby"}]
3748+
3749+
INSERT INTO cagg_inval_no_minmax
3750+
SELECT '2025-01-01 00:00:00+00'::timestamptz + (i * interval '5 minutes'), i % 2, random()
3751+
FROM generate_series(1, 50) i;
3752+
INSERT 0 50
3753+
CREATE MATERIALIZED VIEW cagg_inval_no_minmax_cagg
3754+
WITH (tsdb.continuous) AS
3755+
SELECT time_bucket('1 hour', time), gauge FROM cagg_inval_no_minmax GROUP BY 1, 2;
3756+
NOTICE: refreshing continuous aggregate "cagg_inval_no_minmax_cagg"
3757+
HINT: Use WITH NO DATA if you do not want to refresh the continuous aggregate on creation.
3758+
CREATE MATERIALIZED VIEW
3759+
-- Enable compression with segmentby only (time in neither segmentby nor orderby).
3760+
ALTER TABLE cagg_inval_no_minmax SET (timescaledb.compress, timescaledb.compress_segmentby = 'gauge');
3761+
NOTICE: updated compression settings will only apply to future compressions
3762+
DETAIL: Existing compressed chunks will not be recompressed.
3763+
HINT: Use compress_chunk(chunk, recompress => true) to recompress.
3764+
ALTER TABLE
3765+
SELECT segmentby, orderby, index FROM _timescaledb_catalog.compression_settings WHERE relid = 'cagg_inval_no_minmax'::regclass;
3766+
segmentby | orderby | index
3767+
-----------+---------+-----------------------------------------------------------------------------------------------------------------------------
3768+
{gauge} | {value} | [{"type": "minmax", "column": "value", "source": "orderby"}, {"type": "firstlast", "column": "value", "source": "orderby"}]
3769+
3770+
SELECT count(compress_chunk(c)) FROM show_chunks('cagg_inval_no_minmax') c;
3771+
count
3772+
-------
3773+
1
3774+
3775+
\set ON_ERROR_STOP 0
3776+
DELETE FROM cagg_inval_no_minmax WHERE gauge = 1;
3777+
ERROR: cannot perform direct batch delete on hypertable with continuous aggregates when time column is not segmentby or orderby
3778+
\set ON_ERROR_STOP 1
3779+
SELECT count(*) FROM cagg_inval_no_minmax WHERE gauge = 1;
3780+
count
3781+
-------
3782+
25
3783+
3784+
RESET timescaledb.auto_sparse_indexes;
3785+
RESET

tsl/test/expected/compression_update_delete-17.out

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3733,3 +3733,53 @@ ROLLBACK;
37333733
ROLLBACK
37343734
DROP TABLE crash_test;
37353735
DROP TABLE
3736+
-- direct batch delete when the time column is in neither segmentby nor orderby
3737+
SET timescaledb.auto_sparse_indexes = off;
3738+
SET
3739+
CREATE TABLE cagg_inval_no_minmax(time timestamptz NOT NULL, gauge int, value float)
3740+
WITH (tsdb.hypertable, tsdb.segmentby='time',tsdb.orderby='value');
3741+
NOTICE: using column "time" as partitioning column
3742+
HINT: Use "timescaledb.partition_column" to specify a different column to use as partitioning column.
3743+
CREATE TABLE
3744+
SELECT segmentby, orderby, index FROM _timescaledb_catalog.compression_settings WHERE relid = 'cagg_inval_no_minmax'::regclass;
3745+
segmentby | orderby | index
3746+
-----------+---------+-----------------------------------------------------------------------------------------------------------------------------
3747+
{time} | {value} | [{"type": "minmax", "column": "value", "source": "orderby"}, {"type": "firstlast", "column": "value", "source": "orderby"}]
3748+
3749+
INSERT INTO cagg_inval_no_minmax
3750+
SELECT '2025-01-01 00:00:00+00'::timestamptz + (i * interval '5 minutes'), i % 2, random()
3751+
FROM generate_series(1, 50) i;
3752+
INSERT 0 50
3753+
CREATE MATERIALIZED VIEW cagg_inval_no_minmax_cagg
3754+
WITH (tsdb.continuous) AS
3755+
SELECT time_bucket('1 hour', time), gauge FROM cagg_inval_no_minmax GROUP BY 1, 2;
3756+
NOTICE: refreshing continuous aggregate "cagg_inval_no_minmax_cagg"
3757+
HINT: Use WITH NO DATA if you do not want to refresh the continuous aggregate on creation.
3758+
CREATE MATERIALIZED VIEW
3759+
-- Enable compression with segmentby only (time in neither segmentby nor orderby).
3760+
ALTER TABLE cagg_inval_no_minmax SET (timescaledb.compress, timescaledb.compress_segmentby = 'gauge');
3761+
NOTICE: updated compression settings will only apply to future compressions
3762+
DETAIL: Existing compressed chunks will not be recompressed.
3763+
HINT: Use compress_chunk(chunk, recompress => true) to recompress.
3764+
ALTER TABLE
3765+
SELECT segmentby, orderby, index FROM _timescaledb_catalog.compression_settings WHERE relid = 'cagg_inval_no_minmax'::regclass;
3766+
segmentby | orderby | index
3767+
-----------+---------+-----------------------------------------------------------------------------------------------------------------------------
3768+
{gauge} | {value} | [{"type": "minmax", "column": "value", "source": "orderby"}, {"type": "firstlast", "column": "value", "source": "orderby"}]
3769+
3770+
SELECT count(compress_chunk(c)) FROM show_chunks('cagg_inval_no_minmax') c;
3771+
count
3772+
-------
3773+
1
3774+
3775+
\set ON_ERROR_STOP 0
3776+
DELETE FROM cagg_inval_no_minmax WHERE gauge = 1;
3777+
ERROR: cannot perform direct batch delete on hypertable with continuous aggregates when time column is not segmentby or orderby
3778+
\set ON_ERROR_STOP 1
3779+
SELECT count(*) FROM cagg_inval_no_minmax WHERE gauge = 1;
3780+
count
3781+
-------
3782+
25
3783+
3784+
RESET timescaledb.auto_sparse_indexes;
3785+
RESET

tsl/test/expected/compression_update_delete-18.out

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3733,3 +3733,53 @@ ROLLBACK;
37333733
ROLLBACK
37343734
DROP TABLE crash_test;
37353735
DROP TABLE
3736+
-- direct batch delete when the time column is in neither segmentby nor orderby
3737+
SET timescaledb.auto_sparse_indexes = off;
3738+
SET
3739+
CREATE TABLE cagg_inval_no_minmax(time timestamptz NOT NULL, gauge int, value float)
3740+
WITH (tsdb.hypertable, tsdb.segmentby='time',tsdb.orderby='value');
3741+
NOTICE: using column "time" as partitioning column
3742+
HINT: Use "timescaledb.partition_column" to specify a different column to use as partitioning column.
3743+
CREATE TABLE
3744+
SELECT segmentby, orderby, index FROM _timescaledb_catalog.compression_settings WHERE relid = 'cagg_inval_no_minmax'::regclass;
3745+
segmentby | orderby | index
3746+
-----------+---------+-----------------------------------------------------------------------------------------------------------------------------
3747+
{time} | {value} | [{"type": "minmax", "column": "value", "source": "orderby"}, {"type": "firstlast", "column": "value", "source": "orderby"}]
3748+
3749+
INSERT INTO cagg_inval_no_minmax
3750+
SELECT '2025-01-01 00:00:00+00'::timestamptz + (i * interval '5 minutes'), i % 2, random()
3751+
FROM generate_series(1, 50) i;
3752+
INSERT 0 50
3753+
CREATE MATERIALIZED VIEW cagg_inval_no_minmax_cagg
3754+
WITH (tsdb.continuous) AS
3755+
SELECT time_bucket('1 hour', time), gauge FROM cagg_inval_no_minmax GROUP BY 1, 2;
3756+
NOTICE: refreshing continuous aggregate "cagg_inval_no_minmax_cagg"
3757+
HINT: Use WITH NO DATA if you do not want to refresh the continuous aggregate on creation.
3758+
CREATE MATERIALIZED VIEW
3759+
-- Enable compression with segmentby only (time in neither segmentby nor orderby).
3760+
ALTER TABLE cagg_inval_no_minmax SET (timescaledb.compress, timescaledb.compress_segmentby = 'gauge');
3761+
NOTICE: updated compression settings will only apply to future compressions
3762+
DETAIL: Existing compressed chunks will not be recompressed.
3763+
HINT: Use compress_chunk(chunk, recompress => true) to recompress.
3764+
ALTER TABLE
3765+
SELECT segmentby, orderby, index FROM _timescaledb_catalog.compression_settings WHERE relid = 'cagg_inval_no_minmax'::regclass;
3766+
segmentby | orderby | index
3767+
-----------+---------+-----------------------------------------------------------------------------------------------------------------------------
3768+
{gauge} | {value} | [{"type": "minmax", "column": "value", "source": "orderby"}, {"type": "firstlast", "column": "value", "source": "orderby"}]
3769+
3770+
SELECT count(compress_chunk(c)) FROM show_chunks('cagg_inval_no_minmax') c;
3771+
count
3772+
-------
3773+
1
3774+
3775+
\set ON_ERROR_STOP 0
3776+
DELETE FROM cagg_inval_no_minmax WHERE gauge = 1;
3777+
ERROR: cannot perform direct batch delete on hypertable with continuous aggregates when time column is not segmentby or orderby
3778+
\set ON_ERROR_STOP 1
3779+
SELECT count(*) FROM cagg_inval_no_minmax WHERE gauge = 1;
3780+
count
3781+
-------
3782+
25
3783+
3784+
RESET timescaledb.auto_sparse_indexes;
3785+
RESET

tsl/test/expected/compression_update_delete-19.out

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3733,3 +3733,53 @@ ROLLBACK;
37333733
ROLLBACK
37343734
DROP TABLE crash_test;
37353735
DROP TABLE
3736+
-- direct batch delete when the time column is in neither segmentby nor orderby
3737+
SET timescaledb.auto_sparse_indexes = off;
3738+
SET
3739+
CREATE TABLE cagg_inval_no_minmax(time timestamptz NOT NULL, gauge int, value float)
3740+
WITH (tsdb.hypertable, tsdb.segmentby='time',tsdb.orderby='value');
3741+
NOTICE: using column "time" as partitioning column
3742+
HINT: Use "timescaledb.partition_column" to specify a different column to use as partitioning column.
3743+
CREATE TABLE
3744+
SELECT segmentby, orderby, index FROM _timescaledb_catalog.compression_settings WHERE relid = 'cagg_inval_no_minmax'::regclass;
3745+
segmentby | orderby | index
3746+
-----------+---------+-----------------------------------------------------------------------------------------------------------------------------
3747+
{time} | {value} | [{"type": "minmax", "column": "value", "source": "orderby"}, {"type": "firstlast", "column": "value", "source": "orderby"}]
3748+
3749+
INSERT INTO cagg_inval_no_minmax
3750+
SELECT '2025-01-01 00:00:00+00'::timestamptz + (i * interval '5 minutes'), i % 2, random()
3751+
FROM generate_series(1, 50) i;
3752+
INSERT 0 50
3753+
CREATE MATERIALIZED VIEW cagg_inval_no_minmax_cagg
3754+
WITH (tsdb.continuous) AS
3755+
SELECT time_bucket('1 hour', time), gauge FROM cagg_inval_no_minmax GROUP BY 1, 2;
3756+
NOTICE: refreshing continuous aggregate "cagg_inval_no_minmax_cagg"
3757+
HINT: Use WITH NO DATA if you do not want to refresh the continuous aggregate on creation.
3758+
CREATE MATERIALIZED VIEW
3759+
-- Enable compression with segmentby only (time in neither segmentby nor orderby).
3760+
ALTER TABLE cagg_inval_no_minmax SET (timescaledb.compress, timescaledb.compress_segmentby = 'gauge');
3761+
NOTICE: updated compression settings will only apply to future compressions
3762+
DETAIL: Existing compressed chunks will not be recompressed.
3763+
HINT: Use compress_chunk(chunk, recompress => true) to recompress.
3764+
ALTER TABLE
3765+
SELECT segmentby, orderby, index FROM _timescaledb_catalog.compression_settings WHERE relid = 'cagg_inval_no_minmax'::regclass;
3766+
segmentby | orderby | index
3767+
-----------+---------+-----------------------------------------------------------------------------------------------------------------------------
3768+
{gauge} | {value} | [{"type": "minmax", "column": "value", "source": "orderby"}, {"type": "firstlast", "column": "value", "source": "orderby"}]
3769+
3770+
SELECT count(compress_chunk(c)) FROM show_chunks('cagg_inval_no_minmax') c;
3771+
count
3772+
-------
3773+
1
3774+
3775+
\set ON_ERROR_STOP 0
3776+
DELETE FROM cagg_inval_no_minmax WHERE gauge = 1;
3777+
ERROR: cannot perform direct batch delete on hypertable with continuous aggregates when time column is not segmentby or orderby
3778+
\set ON_ERROR_STOP 1
3779+
SELECT count(*) FROM cagg_inval_no_minmax WHERE gauge = 1;
3780+
count
3781+
-------
3782+
25
3783+
3784+
RESET timescaledb.auto_sparse_indexes;
3785+
RESET

tsl/test/sql/compression_update_delete.sql.in

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1974,3 +1974,25 @@ ROLLBACK;
19741974

19751975
DROP TABLE crash_test;
19761976

1977+
-- direct batch delete when the time column is in neither segmentby nor orderby
1978+
SET timescaledb.auto_sparse_indexes = off;
1979+
CREATE TABLE cagg_inval_no_minmax(time timestamptz NOT NULL, gauge int, value float)
1980+
WITH (tsdb.hypertable, tsdb.segmentby='time',tsdb.orderby='value');
1981+
SELECT segmentby, orderby, index FROM _timescaledb_catalog.compression_settings WHERE relid = 'cagg_inval_no_minmax'::regclass;
1982+
INSERT INTO cagg_inval_no_minmax
1983+
SELECT '2025-01-01 00:00:00+00'::timestamptz + (i * interval '5 minutes'), i % 2, random()
1984+
FROM generate_series(1, 50) i;
1985+
CREATE MATERIALIZED VIEW cagg_inval_no_minmax_cagg
1986+
WITH (tsdb.continuous) AS
1987+
SELECT time_bucket('1 hour', time), gauge FROM cagg_inval_no_minmax GROUP BY 1, 2;
1988+
-- Enable compression with segmentby only (time in neither segmentby nor orderby).
1989+
ALTER TABLE cagg_inval_no_minmax SET (timescaledb.compress, timescaledb.compress_segmentby = 'gauge');
1990+
SELECT segmentby, orderby, index FROM _timescaledb_catalog.compression_settings WHERE relid = 'cagg_inval_no_minmax'::regclass;
1991+
SELECT count(compress_chunk(c)) FROM show_chunks('cagg_inval_no_minmax') c;
1992+
\set ON_ERROR_STOP 0
1993+
DELETE FROM cagg_inval_no_minmax WHERE gauge = 1;
1994+
\set ON_ERROR_STOP 1
1995+
SELECT count(*) FROM cagg_inval_no_minmax WHERE gauge = 1;
1996+
RESET timescaledb.auto_sparse_indexes;
1997+
1998+

0 commit comments

Comments
 (0)