Skip to content

Commit df450e2

Browse files
committed
Disable direct compress for tables with exclusion constraints
Direct compress writes data straight into compressed form without per-row index checks, so it cannot enforce an exclusion constraint. The fast path was only disabled for unique constraints, which meant exclusion constraints were silently ignored. Disable direct compress for both INSERT and COPY when the table has an exclusion constraint.
1 parent d1c2d33 commit df450e2

7 files changed

Lines changed: 97 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes: #10281 Disable Direct Compress when the destination table has an exclusion constraint so the constraint is still enforced

src/copy.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,12 @@ choose_copy_method(Hypertable *ht, CopyChunkState *ccstate, ResultRelInfo *resul
845845
(errmsg("disabling direct compress because the destination table has unique "
846846
"constraints")));
847847
}
848+
else if (ts_indexing_relation_has_exclusion_constraint(ccstate->rel))
849+
{
850+
ereport(WARNING,
851+
(errmsg("disabling direct compress because the destination table has exclusion "
852+
"constraints")));
853+
}
848854
else if (resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->numtriggers > 1)
849855
{
850856
ereport(WARNING,

src/indexing.c

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,40 @@ ts_indexing_relation_has_primary_or_unique_index(Relation htrel)
337337
return result;
338338
}
339339

340+
bool TSDLLEXPORT
341+
ts_indexing_relation_has_exclusion_constraint(Relation htrel)
342+
{
343+
List *indexoidlist = RelationGetIndexList(htrel);
344+
ListCell *lc;
345+
bool result = false;
346+
347+
foreach (lc, indexoidlist)
348+
{
349+
Oid indexoid = lfirst_oid(lc);
350+
HeapTuple index_tuple;
351+
Form_pg_index index;
352+
353+
index_tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
354+
if (!HeapTupleIsValid(index_tuple)) /* should not happen */
355+
{
356+
elog(ERROR,
357+
"cache lookup failed for index %u in \"%s\" ",
358+
indexoid,
359+
RelationGetRelationName(htrel));
360+
}
361+
index = (Form_pg_index) GETSTRUCT(index_tuple);
362+
result = index->indisexclusion;
363+
ReleaseSysCache(index_tuple);
364+
if (result)
365+
{
366+
break;
367+
}
368+
}
369+
370+
list_free(indexoidlist);
371+
return result;
372+
}
373+
340374
/*
341375
* Collect the heap attribute numbers covered by any valid unique index
342376
* (PRIMARY KEY included) on the relation. Returns a Bitmapset of raw attribute

src/indexing.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ extern TSDLLEXPORT Oid ts_indexing_find_clustered_index(Oid table_relid);
2424
extern void ts_indexing_mark_as_valid(Oid index_id);
2525
extern bool ts_indexing_mark_as_invalid(Oid index_id);
2626
extern bool TSDLLEXPORT ts_indexing_relation_has_primary_or_unique_index(Relation htrel);
27+
extern bool TSDLLEXPORT ts_indexing_relation_has_exclusion_constraint(Relation htrel);
2728
extern TSDLLEXPORT Bitmapset *ts_indexing_relation_get_unique_columns(Relation rel);
2829
extern TSDLLEXPORT bool ts_indexing_compare(Oid index1, Oid index2);
2930
extern TSDLLEXPORT Datum ts_index_matches(PG_FUNCTION_ARGS);

src/nodes/modify_hypertable.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ should_use_direct_compress(ModifyHypertableState *state)
6161
return false;
6262
}
6363

64+
if (ts_indexing_relation_has_exclusion_constraint(state->ctr->root_rel))
65+
{
66+
ereport(WARNING,
67+
(errmsg("disabling direct compress because the destination table has exclusion "
68+
"constraints")));
69+
return false;
70+
}
71+
6472
Plan *subplan = mtstate->ps.plan->lefttree;
6573
if (subplan->plan_rows < 10)
6674
{

tsl/test/expected/direct_compress_insert.out

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,3 +932,32 @@ SELECT count(*), min(value), max(value) FROM metrics_view;
932932
RESET timescaledb.enable_direct_compress_insert;
933933
DROP VIEW metrics_view_pos;
934934
DROP TABLE metrics_view;
935+
-- Direct compress must be disabled when the table has an exclusion constraint
936+
CREATE TABLE dc_excl(time timestamptz NOT NULL, device text NOT NULL, value float,
937+
EXCLUDE USING btree (time WITH =, device WITH =))
938+
WITH (tsdb.hypertable, tsdb.partition_column='time');
939+
ALTER TABLE dc_excl SET (timescaledb.compress, timescaledb.compress_segmentby='device');
940+
NOTICE: updated compression settings will only apply to future compressions
941+
SET timescaledb.enable_direct_compress_insert = true;
942+
-- insert warns, disables direct compress and falls back to a normal insert
943+
INSERT INTO dc_excl SELECT '2025-01-01'::timestamptz + (i || ' minute')::interval, 'd1', i::float FROM generate_series(1,100) i;
944+
WARNING: disabling direct compress because the destination table has exclusion constraints
945+
-- chunk stays uncompressed since direct compress was disabled
946+
SELECT DISTINCT _timescaledb_functions.chunk_status_text(chunk) FROM show_chunks('dc_excl') chunk;
947+
chunk_status_text
948+
-------------------
949+
{}
950+
951+
-- exclusion constraint is still enforced
952+
\set ON_ERROR_STOP 0
953+
INSERT INTO dc_excl VALUES ('2025-01-01'::timestamptz + INTERVAL '1 minute', 'd1', 99);
954+
WARNING: disabling direct compress because the destination table has exclusion constraints
955+
ERROR: conflicting key value violates exclusion constraint "62_dc_excl_time_device_excl"
956+
\set ON_ERROR_STOP 1
957+
SELECT count(*) FROM dc_excl;
958+
count
959+
-------
960+
100
961+
962+
RESET timescaledb.enable_direct_compress_insert;
963+
DROP TABLE dc_excl;

tsl/test/sql/direct_compress_insert.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,3 +551,21 @@ RESET timescaledb.enable_direct_compress_insert;
551551
DROP VIEW metrics_view_pos;
552552
DROP TABLE metrics_view;
553553

554+
-- Direct compress must be disabled when the table has an exclusion constraint
555+
CREATE TABLE dc_excl(time timestamptz NOT NULL, device text NOT NULL, value float,
556+
EXCLUDE USING btree (time WITH =, device WITH =))
557+
WITH (tsdb.hypertable, tsdb.partition_column='time');
558+
ALTER TABLE dc_excl SET (timescaledb.compress, timescaledb.compress_segmentby='device');
559+
SET timescaledb.enable_direct_compress_insert = true;
560+
-- insert warns, disables direct compress and falls back to a normal insert
561+
INSERT INTO dc_excl SELECT '2025-01-01'::timestamptz + (i || ' minute')::interval, 'd1', i::float FROM generate_series(1,100) i;
562+
-- chunk stays uncompressed since direct compress was disabled
563+
SELECT DISTINCT _timescaledb_functions.chunk_status_text(chunk) FROM show_chunks('dc_excl') chunk;
564+
-- exclusion constraint is still enforced
565+
\set ON_ERROR_STOP 0
566+
INSERT INTO dc_excl VALUES ('2025-01-01'::timestamptz + INTERVAL '1 minute', 'd1', 99);
567+
\set ON_ERROR_STOP 1
568+
SELECT count(*) FROM dc_excl;
569+
RESET timescaledb.enable_direct_compress_insert;
570+
DROP TABLE dc_excl;
571+

0 commit comments

Comments
 (0)