Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/nodes/modify_hypertable.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
*/

#include <postgres.h>
#include <access/sysattr.h>
#include <nodes/execnodes.h>
#include <nodes/makefuncs.h>
#include <nodes/nodeFuncs.h>
#include <optimizer/optimizer.h>
#include <parser/parsetree.h>
#include <utils/snapmgr.h>

Expand Down Expand Up @@ -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)
{
Expand Down
102 changes: 101 additions & 1 deletion tsl/test/expected/direct_compress_insert.out
Original file line number Diff line number Diff line change
Expand Up @@ -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;
31 changes: 31 additions & 0 deletions tsl/test/sql/direct_compress_insert.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading