diff --git a/src/nodes/modify_hypertable.c b/src/nodes/modify_hypertable.c index 42b7d453873..1daa2959804 100644 --- a/src/nodes/modify_hypertable.c +++ b/src/nodes/modify_hypertable.c @@ -5,9 +5,11 @@ */ #include +#include #include #include #include +#include #include #include @@ -69,6 +71,34 @@ should_use_direct_compress(ModifyHypertableState *state) return false; } + /* + * Direct compress stores the tuple in compressed form instead of a + * regular heap tuple, so system columns like ctid or xmin never get a + * meaningful value. Fall back to the normal insert path when RETURNING + * references them. tableoid is fine because the returning projection + * sets it explicitly. + */ + ModifyTable *mt = castNode(ModifyTable, mtstate->ps.plan); + if (mt->returningLists) + { + Bitmapset *attnos = NULL; + pull_varattnos((Node *) linitial(mt->returningLists), + resultRelInfo->ri_RangeTableIndex, + &attnos); + int attno = -1; + while ((attno = bms_next_member(attnos, attno)) >= 0) + { + AttrNumber sysattno = attno + FirstLowInvalidHeapAttributeNumber; + if (sysattno < 0 && sysattno != TableOidAttributeNumber) + { + ereport(WARNING, + (errmsg("disabling direct compress because the RETURNING clause " + "references system columns"))); + 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 4bd446b3657..132ffd8c1a5 100644 --- a/tsl/test/expected/direct_compress_insert.out +++ b/tsl/test/expected/direct_compress_insert.out @@ -997,11 +997,111 @@ SELECT count(*), min(label), max(label) FROM inserted; -------+-------+------ 20 | d0:10 | d1:9 +-- RETURNING a system column falls back to the normal insert path, except +-- tableoid which the returning projection sets explicitly. +EXPLAIN (COSTS OFF, SUMMARY OFF, TIMING OFF) +INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING ctid; +WARNING: disabling direct compress because the RETURNING clause references system columns +--- QUERY PLAN --- + Custom Scan (ModifyHypertable) + Direct Compress: false + -> Insert on dc_ret + -> Function Scan on generate_series i + +EXPLAIN (COSTS OFF, SUMMARY OFF, TIMING OFF) +INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING xmin; +WARNING: disabling direct compress because the RETURNING clause references system columns +--- QUERY PLAN --- + Custom Scan (ModifyHypertable) + Direct Compress: false + -> Insert on dc_ret + -> Function Scan on generate_series i + +EXPLAIN (COSTS OFF, SUMMARY OFF, TIMING OFF) +INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING cmin; +WARNING: disabling direct compress because the RETURNING clause references system columns +--- QUERY PLAN --- + Custom Scan (ModifyHypertable) + Direct Compress: false + -> Insert on dc_ret + -> Function Scan on generate_series i + +EXPLAIN (COSTS OFF, SUMMARY OFF, TIMING OFF) +INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING xmax; +WARNING: disabling direct compress because the RETURNING clause references system columns +--- QUERY PLAN --- + Custom Scan (ModifyHypertable) + Direct Compress: false + -> Insert on dc_ret + -> Function Scan on generate_series i + +EXPLAIN (COSTS OFF, SUMMARY OFF, TIMING OFF) +INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING cmax; +WARNING: disabling direct compress because the RETURNING clause references system columns +--- QUERY PLAN --- + Custom Scan (ModifyHypertable) + Direct Compress: false + -> Insert on dc_ret + -> Function Scan on generate_series i + +-- tableoid stays on the direct compress path +EXPLAIN (COSTS OFF, SUMMARY OFF, TIMING OFF) +INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING tableoid; +--- QUERY PLAN --- + Custom Scan (ModifyHypertable) + Direct Compress: true + -> Insert on dc_ret + -> Function Scan on generate_series i + +-- ctid falls back and returns real tuple ids +WITH inserted AS ( + INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 day'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING ctid AS c, val +) +SELECT count(*), bool_and(c IS NOT NULL), min(val), max(val) FROM inserted; +WARNING: disabling direct compress because the RETURNING clause references system columns + count | bool_and | min | max +-------+----------+-----+----- + 20 | t | 1 | 20 + +-- tableoid on the direct compress path holds the target chunk (all 20 rows +-- land in one chunk, so every RETURNING row reports the same chunk) +INSERT INTO dc_ret SELECT '2025-06-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING tableoid::regclass AS chunk; + chunk +------------------------------------------ + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + _timescaledb_internal._hyper_16_67_chunk + -- rows were actually inserted SELECT count(*) FROM dc_ret; count ------- - 40 + 80 RESET timescaledb.enable_direct_compress_insert; DROP TABLE dc_ret; diff --git a/tsl/test/sql/direct_compress_insert.sql b/tsl/test/sql/direct_compress_insert.sql index c9b80e58122..cb30ae5dde3 100644 --- a/tsl/test/sql/direct_compress_insert.sql +++ b/tsl/test/sql/direct_compress_insert.sql @@ -590,6 +590,37 @@ WITH inserted AS ( FROM generate_series(1, 20) i RETURNING device || ':' || val AS label ) SELECT count(*), min(label), max(label) FROM inserted; +-- RETURNING a system column falls back to the normal insert path, except +-- tableoid which the returning projection sets explicitly. +EXPLAIN (COSTS OFF, SUMMARY OFF, TIMING OFF) +INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING ctid; +EXPLAIN (COSTS OFF, SUMMARY OFF, TIMING OFF) +INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING xmin; +EXPLAIN (COSTS OFF, SUMMARY OFF, TIMING OFF) +INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING cmin; +EXPLAIN (COSTS OFF, SUMMARY OFF, TIMING OFF) +INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING xmax; +EXPLAIN (COSTS OFF, SUMMARY OFF, TIMING OFF) +INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING cmax; +-- tableoid stays on the direct compress path +EXPLAIN (COSTS OFF, SUMMARY OFF, TIMING OFF) +INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING tableoid; +-- ctid falls back and returns real tuple ids +WITH inserted AS ( + INSERT INTO dc_ret SELECT '2025-01-01'::timestamptz + (i * INTERVAL '1 day'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING ctid AS c, val +) +SELECT count(*), bool_and(c IS NOT NULL), min(val), max(val) FROM inserted; +-- tableoid on the direct compress path holds the target chunk (all 20 rows +-- land in one chunk, so every RETURNING row reports the same chunk) +INSERT INTO dc_ret SELECT '2025-06-01'::timestamptz + (i * INTERVAL '1 minute'), 'd' || (i % 2)::text, i + FROM generate_series(1, 20) i RETURNING tableoid::regclass AS chunk; -- rows were actually inserted SELECT count(*) FROM dc_ret; RESET timescaledb.enable_direct_compress_insert;