diff --git a/.unreleased/fix_direct_compress_exclusion b/.unreleased/fix_direct_compress_exclusion new file mode 100644 index 00000000000..9b399d60608 --- /dev/null +++ b/.unreleased/fix_direct_compress_exclusion @@ -0,0 +1 @@ +Fixes: #10281 Disable Direct Compress when the destination table has an exclusion constraint so the constraint is still enforced diff --git a/src/copy.c b/src/copy.c index ffc33dde099..29fa14f2d8b 100644 --- a/src/copy.c +++ b/src/copy.c @@ -845,6 +845,12 @@ choose_copy_method(Hypertable *ht, CopyChunkState *ccstate, ResultRelInfo *resul (errmsg("disabling direct compress because the destination table has unique " "constraints"))); } + else if (ts_indexing_relation_has_exclusion_constraint(ccstate->rel)) + { + ereport(WARNING, + (errmsg("disabling direct compress because the destination table has exclusion " + "constraints"))); + } else if (resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->numtriggers > 1) { ereport(WARNING, diff --git a/src/indexing.c b/src/indexing.c index 38678f056e4..b5652476989 100644 --- a/src/indexing.c +++ b/src/indexing.c @@ -337,6 +337,40 @@ ts_indexing_relation_has_primary_or_unique_index(Relation htrel) return result; } +bool TSDLLEXPORT +ts_indexing_relation_has_exclusion_constraint(Relation htrel) +{ + List *indexoidlist = RelationGetIndexList(htrel); + ListCell *lc; + bool result = false; + + foreach (lc, indexoidlist) + { + Oid indexoid = lfirst_oid(lc); + HeapTuple index_tuple; + Form_pg_index index; + + index_tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid)); + if (!HeapTupleIsValid(index_tuple)) /* should not happen */ + { + elog(ERROR, + "cache lookup failed for index %u in \"%s\" ", + indexoid, + RelationGetRelationName(htrel)); + } + index = (Form_pg_index) GETSTRUCT(index_tuple); + result = index->indisexclusion; + ReleaseSysCache(index_tuple); + if (result) + { + break; + } + } + + list_free(indexoidlist); + return result; +} + /* * Collect the heap attribute numbers covered by any valid unique index * (PRIMARY KEY included) on the relation. Returns a Bitmapset of raw attribute diff --git a/src/indexing.h b/src/indexing.h index 5e5afdd46f1..b423e923a6e 100644 --- a/src/indexing.h +++ b/src/indexing.h @@ -24,6 +24,7 @@ extern TSDLLEXPORT Oid ts_indexing_find_clustered_index(Oid table_relid); extern void ts_indexing_mark_as_valid(Oid index_id); extern bool ts_indexing_mark_as_invalid(Oid index_id); extern bool TSDLLEXPORT ts_indexing_relation_has_primary_or_unique_index(Relation htrel); +extern bool TSDLLEXPORT ts_indexing_relation_has_exclusion_constraint(Relation htrel); extern TSDLLEXPORT Bitmapset *ts_indexing_relation_get_unique_columns(Relation rel); extern TSDLLEXPORT bool ts_indexing_compare(Oid index1, Oid index2); extern TSDLLEXPORT Datum ts_index_matches(PG_FUNCTION_ARGS); diff --git a/src/nodes/modify_hypertable.c b/src/nodes/modify_hypertable.c index c95348ef578..1a83f61b3d3 100644 --- a/src/nodes/modify_hypertable.c +++ b/src/nodes/modify_hypertable.c @@ -61,6 +61,14 @@ should_use_direct_compress(ModifyHypertableState *state) return false; } + if (ts_indexing_relation_has_exclusion_constraint(state->ctr->root_rel)) + { + ereport(WARNING, + (errmsg("disabling direct compress because the destination table has exclusion " + "constraints"))); + return false; + } + Plan *subplan = mtstate->ps.plan->lefttree; if (subplan->plan_rows < 10) { diff --git a/tsl/test/expected/direct_compress_insert.out b/tsl/test/expected/direct_compress_insert.out index f2c2cba285e..2e6149091ae 100644 --- a/tsl/test/expected/direct_compress_insert.out +++ b/tsl/test/expected/direct_compress_insert.out @@ -932,3 +932,32 @@ SELECT count(*), min(value), max(value) FROM metrics_view; RESET timescaledb.enable_direct_compress_insert; DROP VIEW metrics_view_pos; DROP TABLE metrics_view; +-- Direct compress must be disabled when the table has an exclusion constraint +CREATE TABLE dc_excl(time timestamptz NOT NULL, device text NOT NULL, value float, + EXCLUDE USING btree (time WITH =, device WITH =)) + WITH (tsdb.hypertable, tsdb.partition_column='time'); +ALTER TABLE dc_excl SET (timescaledb.compress, timescaledb.compress_segmentby='device'); +NOTICE: updated compression settings will only apply to future compressions +SET timescaledb.enable_direct_compress_insert = true; +-- insert warns, disables direct compress and falls back to a normal insert +INSERT INTO dc_excl SELECT '2025-01-01'::timestamptz + (i || ' minute')::interval, 'd1', i::float FROM generate_series(1,100) i; +WARNING: disabling direct compress because the destination table has exclusion constraints +-- chunk stays uncompressed since direct compress was disabled +SELECT DISTINCT _timescaledb_functions.chunk_status_text(chunk) FROM show_chunks('dc_excl') chunk; + chunk_status_text +------------------- + {} + +-- exclusion constraint is still enforced +\set ON_ERROR_STOP 0 +INSERT INTO dc_excl VALUES ('2025-01-01'::timestamptz + INTERVAL '1 minute', 'd1', 99); +WARNING: disabling direct compress because the destination table has exclusion constraints +ERROR: conflicting key value violates exclusion constraint "62_dc_excl_time_device_excl" +\set ON_ERROR_STOP 1 +SELECT count(*) FROM dc_excl; + count +------- + 100 + +RESET timescaledb.enable_direct_compress_insert; +DROP TABLE dc_excl; diff --git a/tsl/test/sql/direct_compress_insert.sql b/tsl/test/sql/direct_compress_insert.sql index a93e6d19942..6f22e9a8251 100644 --- a/tsl/test/sql/direct_compress_insert.sql +++ b/tsl/test/sql/direct_compress_insert.sql @@ -551,3 +551,21 @@ RESET timescaledb.enable_direct_compress_insert; DROP VIEW metrics_view_pos; DROP TABLE metrics_view; +-- Direct compress must be disabled when the table has an exclusion constraint +CREATE TABLE dc_excl(time timestamptz NOT NULL, device text NOT NULL, value float, + EXCLUDE USING btree (time WITH =, device WITH =)) + WITH (tsdb.hypertable, tsdb.partition_column='time'); +ALTER TABLE dc_excl SET (timescaledb.compress, timescaledb.compress_segmentby='device'); +SET timescaledb.enable_direct_compress_insert = true; +-- insert warns, disables direct compress and falls back to a normal insert +INSERT INTO dc_excl SELECT '2025-01-01'::timestamptz + (i || ' minute')::interval, 'd1', i::float FROM generate_series(1,100) i; +-- chunk stays uncompressed since direct compress was disabled +SELECT DISTINCT _timescaledb_functions.chunk_status_text(chunk) FROM show_chunks('dc_excl') chunk; +-- exclusion constraint is still enforced +\set ON_ERROR_STOP 0 +INSERT INTO dc_excl VALUES ('2025-01-01'::timestamptz + INTERVAL '1 minute', 'd1', 99); +\set ON_ERROR_STOP 1 +SELECT count(*) FROM dc_excl; +RESET timescaledb.enable_direct_compress_insert; +DROP TABLE dc_excl; +