Skip to content

Commit 9636ef2

Browse files
authored
Defer initialization of child plan states in ModifyHypertable node (#9952)
The underlying scans must be initialized after we decompress the data potentially affected by DML operations, so that the scans see this data. At the moment we're restarting the scans after decompression, but this is not the proper Postgres protocol, so it breaks. Fixes #9913
1 parent 3b07c9e commit 9636ef2

16 files changed

Lines changed: 595 additions & 709 deletions

.unreleased/dml-bitmap-crash

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes: #9913: Potential crash on DML on compressed tables when the plan uses Bitmap Heap Scan

src/nodes/modify_hypertable.c

Lines changed: 195 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
#include <postgres.h>
88
#include <nodes/execnodes.h>
99
#include <nodes/makefuncs.h>
10+
#include <nodes/nodeFuncs.h>
11+
#include <parser/parsetree.h>
12+
#include <utils/snapmgr.h>
1013

1114
#include "compat/compat.h"
1215
#include "chunk_tuple_routing.h"
@@ -67,6 +70,8 @@ should_use_direct_compress(ModifyHypertableState *state)
6770
return true;
6871
}
6972

73+
static void modify_hypertable_init_child_plan_states(CustomScanState *node);
74+
7075
/*
7176
* ModifyHypertable is a plan node that implements DML for hypertables.
7277
* It is a wrapper around the ModifyTable plan node that calls the wrapped ModifyTable
@@ -75,67 +80,152 @@ should_use_direct_compress(ModifyHypertableState *state)
7580
static void
7681
modify_hypertable_begin(CustomScanState *node, EState *estate, int eflags)
7782
{
78-
ModifyHypertableState *state = (ModifyHypertableState *) node;
79-
ModifyTableState *mtstate;
80-
PlanState *ps;
83+
ModifyHypertableState *modify_hypertable_state = (ModifyHypertableState *) node;
84+
ModifyTable *modify_table_plan = castNode(ModifyTable, &modify_hypertable_state->mt->plan);
8185

82-
ModifyTable *mt = castNode(ModifyTable, &state->mt->plan);
8386
/*
8487
* To make statement trigger defined on the hypertable work
8588
* we need to set the hypertable as the rootRelation otherwise
8689
* statement trigger defined only on the hypertable will not fire.
8790
*/
88-
if (mt->operation == CMD_DELETE || mt->operation == CMD_UPDATE || mt->operation == CMD_MERGE)
91+
if (modify_table_plan->operation == CMD_DELETE || modify_table_plan->operation == CMD_UPDATE ||
92+
modify_table_plan->operation == CMD_MERGE)
8993
{
90-
mt->rootRelation = mt->nominalRelation;
94+
modify_table_plan->rootRelation = modify_table_plan->nominalRelation;
9195
}
92-
ps = ExecInitNode(&mt->plan, estate, eflags);
93-
node->custom_ps = list_make1(ps);
94-
mtstate = castNode(ModifyTableState, ps);
96+
97+
Oid result_relid = rt_fetch(modify_table_plan->nominalRelation, estate->es_range_table)->relid;
98+
modify_hypertable_state->ht =
99+
ts_hypertable_cache_get_cache_and_entry(result_relid,
100+
CACHE_FLAG_MISSING_OK,
101+
&modify_hypertable_state->ht_cache);
95102

96103
/*
97-
* If this is not the primary ModifyTable node, postgres added it to the
98-
* beginning of es_auxmodifytables, to be executed by ExecPostprocessPlan.
99-
* Unfortunately that strips off the HypertableInsert node leading to
100-
* tuple routing not working in INSERTs inside CTEs. To make INSERTs
101-
* inside CTEs work we have to fix es_auxmodifytables and add back the
102-
* ModifyHypertableState.
104+
* If we are inserting into a chunk directly, rri will point to the chunk
105+
* itself, so we need to get the hypertable from the chunk.
103106
*/
104-
if (estate->es_auxmodifytables && linitial(estate->es_auxmodifytables) == mtstate)
107+
if (!modify_hypertable_state->ht)
105108
{
106-
linitial(estate->es_auxmodifytables) = node;
109+
Chunk *chunk = ts_chunk_get_by_relid(result_relid, true);
110+
modify_hypertable_state->ht =
111+
ts_hypertable_cache_get_entry(modify_hypertable_state->ht_cache,
112+
chunk->hypertable_relid,
113+
CACHE_FLAG_NONE);
107114
}
115+
modify_hypertable_state->has_continuous_aggregate =
116+
ts_hypertable_has_continuous_aggregates(modify_hypertable_state->ht->fd.id);
108117

109-
state->ht =
110-
ts_hypertable_cache_get_cache_and_entry(RelationGetRelid(
111-
mtstate->resultRelInfo->ri_RelationDesc),
112-
CACHE_FLAG_MISSING_OK,
113-
&state->ht_cache);
118+
/*
119+
* The ModifyTable node itself must be initialized now, so that it's properly
120+
* added to the es_auxmodifytables list. For secondary data-modifying CTEs,
121+
* this can be the last time our code is called before ExecPostprocessPlan(),
122+
* if the CTE is not referenced by the main query.
123+
*
124+
* The actual initialization of the child plan states is deferred until after
125+
* we decompress the data that might potentially be involved in DML operations.
126+
* We substitute them with a dummy Result here, so that the Postgres code
127+
* can work.
128+
*/
129+
modify_hypertable_state->deferred_eflags = eflags;
130+
modify_hypertable_state->deferred_modify_table_subplan = outerPlan(modify_table_plan);
131+
132+
Plan *dummy_child = (Plan *) makeNode(Result);
133+
castNode(Result, dummy_child)->resconstantqual =
134+
(Node *) list_make1(makeBoolConst(false, false));
114135

115136
/*
116-
* If we are inserting into a chunk directly, rri will point to the chunk
117-
* itself, so we need to get the hypertable from the chunk.
137+
* The child targetlist can contain Aggrefs which are not allowed on a Result
138+
* targetlist. Just replace every expression with a null constant of the
139+
* same type.
118140
*/
119-
if (!state->ht)
141+
dummy_child->targetlist =
142+
copyObject(modify_hypertable_state->deferred_modify_table_subplan->targetlist);
143+
ListCell *lc;
144+
foreach (lc, dummy_child->targetlist)
120145
{
121-
Chunk *chunk =
122-
ts_chunk_get_by_relid(RelationGetRelid(mtstate->resultRelInfo->ri_RelationDesc), true);
123-
state->ht = ts_hypertable_cache_get_entry(state->ht_cache,
124-
chunk->hypertable_relid,
125-
CACHE_FLAG_NONE);
146+
TargetEntry *entry = lfirst(lc);
147+
Node *expr = (Node *) entry->expr;
148+
entry->expr = (Expr *) makeNullConst(exprType(expr), exprTypmod(expr), exprCollation(expr));
126149
}
127-
state->has_continuous_aggregate = ts_hypertable_has_continuous_aggregates(state->ht->fd.id);
128150

129-
if (mtstate->operation == CMD_INSERT || mtstate->operation == CMD_MERGE)
151+
/*
152+
* Initialize the Postgres ModifyTableState with dummy Result plan as a
153+
* child. The plan nodes here might come from the plan cache for prepared
154+
* statements, and they outlive a single query. We shouldn't change them
155+
* directly, so make a copy.
156+
*/
157+
ModifyTable *modify_table_plan_copy = makeNode(ModifyTable);
158+
memcpy(modify_table_plan_copy, modify_table_plan, sizeof(ModifyTable));
159+
outerPlan(modify_table_plan_copy) = dummy_child;
160+
PlanState *modify_table_state = ExecInitNode((Plan *) modify_table_plan_copy, estate, eflags);
161+
162+
node->custom_ps = list_make1(modify_table_state);
163+
164+
/*
165+
* If Postgres adds our node to the secondary data-modifying CTE list, it
166+
* adds just the Postgres ModifyTableState. Make it point to our
167+
* ModifyHypertableState instead, so that our custom code is called.
168+
*/
169+
if (list_length(estate->es_auxmodifytables) > 0 &&
170+
linitial(estate->es_auxmodifytables) == modify_table_state)
130171
{
131-
/* setup chunk tuple routing state for INSERT/MERGE */
132-
state->ctr = ts_chunk_tuple_routing_create(estate, state->ht, mtstate->resultRelInfo);
133-
state->ctr->mht_state = state;
172+
linitial(estate->es_auxmodifytables) = node;
173+
}
134174

135-
if (mtstate->operation == CMD_INSERT && should_use_direct_compress(state))
175+
/*
176+
* In some cases, the plain deferred initialization from exec doesn't work,
177+
* we handle these below.
178+
*/
179+
if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
180+
{
181+
/*
182+
* With plain EXPLAIN, the node is not actually executed, so we have to
183+
* finish the initialization now.
184+
*/
185+
modify_hypertable_init_child_plan_states(node);
186+
}
187+
}
188+
189+
/*
190+
* Initialize the child plan states after we have decompressed the data that can
191+
* potentially be involved in DML operations. This is done to delay the
192+
* initialization of scans over uncompressed chunk tables until after
193+
* decompression, so that they properly pick up the decompressed data.
194+
*/
195+
static void
196+
modify_hypertable_init_child_plan_states(CustomScanState *node)
197+
{
198+
EState *estate = node->ss.ps.state;
199+
200+
ModifyHypertableState *modify_hypertable_state = (ModifyHypertableState *) node;
201+
202+
Assert(modify_hypertable_state->deferred_modify_table_subplan != NULL);
203+
204+
PlanState *subplan_state = ExecInitNode(modify_hypertable_state->deferred_modify_table_subplan,
205+
estate,
206+
modify_hypertable_state->deferred_eflags);
207+
208+
ModifyTableState *modify_table_state = castNode(ModifyTableState, linitial(node->custom_ps));
209+
210+
outerPlanState(modify_table_state) = subplan_state;
211+
outerPlan(modify_table_state->ps.plan) = subplan_state->plan;
212+
213+
modify_hypertable_state->deferred_modify_table_subplan = NULL;
214+
215+
if (modify_table_state->operation == CMD_INSERT || modify_table_state->operation == CMD_MERGE)
216+
{
217+
/* setup chunk tuple routing state for INSERT/MERGE */
218+
modify_hypertable_state->ctr =
219+
ts_chunk_tuple_routing_create(estate,
220+
modify_hypertable_state->ht,
221+
modify_table_state->resultRelInfo);
222+
modify_hypertable_state->ctr->mht_state = modify_hypertable_state;
223+
224+
if (modify_table_state->operation == CMD_INSERT &&
225+
should_use_direct_compress(modify_hypertable_state))
136226
{
137-
state->columnstore_insert = true;
138-
state->ctr->create_compressed_chunk = true;
227+
modify_hypertable_state->columnstore_insert = true;
228+
modify_hypertable_state->ctr->create_compressed_chunk = true;
139229
}
140230

141231
/* setup per tuple exprcontext for tuple routing */
@@ -149,8 +239,67 @@ modify_hypertable_begin(CustomScanState *node, EState *estate, int eflags)
149239
static TupleTableSlot *
150240
modify_hypertable_exec(CustomScanState *node)
151241
{
152-
ModifyTableState *mtstate = linitial_node(ModifyTableState, node->custom_ps);
153-
TupleTableSlot *result;
242+
ModifyHypertableState *modify_hypertable_state = (ModifyHypertableState *) node;
243+
244+
if (modify_hypertable_state->deferred_modify_table_subplan != NULL)
245+
{
246+
EState *estate = node->ss.ps.state;
247+
CmdType op = modify_hypertable_state->mt->operation;
248+
249+
/*
250+
* For UPDATE/DELETE/MERGE on compressed hypertable, decompress chunks and
251+
* move rows to uncompressed chunks. For MERGE, decompression is needed
252+
* even for DO NOTHING or INSERT-only actions because the join evaluation
253+
* must see the actual rows to correctly determine matched vs not-matched.
254+
*/
255+
if (op == CMD_DELETE || op == CMD_UPDATE || op == CMD_MERGE)
256+
{
257+
/* Modify snapshot only if something got decompressed */
258+
if (ts_cm_functions->decompress_target_segments &&
259+
ts_cm_functions->decompress_target_segments(modify_hypertable_state))
260+
{
261+
modify_hypertable_state->comp_chunks_processed = true;
262+
/*
263+
* save snapshot set during ExecutorStart(), since this is the same
264+
* snapshot used to SeqScan of uncompressed chunks
265+
*/
266+
modify_hypertable_state->snapshot = estate->es_snapshot;
267+
CommandCounterIncrement();
268+
/* use a static copy of current transaction snapshot
269+
* this needs to be a copy so we don't read trigger updates
270+
*/
271+
estate->es_snapshot = RegisterSnapshot(GetTransactionSnapshot());
272+
/* mark rows visible */
273+
estate->es_output_cid = GetCurrentCommandId(true);
274+
275+
if (ts_guc_max_tuples_decompressed_per_dml > 0 &&
276+
modify_hypertable_state->tuples_decompressed >
277+
ts_guc_max_tuples_decompressed_per_dml)
278+
{
279+
ereport(ERROR,
280+
(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
281+
errmsg("tuple decompression limit exceeded by operation"),
282+
errdetail("current limit: %d, tuples decompressed: %lld",
283+
ts_guc_max_tuples_decompressed_per_dml,
284+
(long long int)
285+
modify_hypertable_state->tuples_decompressed),
286+
errhint("Consider increasing "
287+
"timescaledb.max_tuples_decompressed_per_dml_transaction "
288+
"or set to 0 (unlimited).")));
289+
}
290+
}
291+
/* Account for tuples deleted via batch DELETE in compressed chunks */
292+
if (op == CMD_DELETE && modify_hypertable_state->tuples_deleted > 0)
293+
{
294+
estate->es_processed += modify_hypertable_state->tuples_deleted;
295+
}
296+
}
297+
298+
modify_hypertable_init_child_plan_states(node);
299+
}
300+
Assert(modify_hypertable_state->deferred_modify_table_subplan == NULL);
301+
302+
ModifyTableState *modify_table_state = linitial_node(ModifyTableState, node->custom_ps);
154303

155304
/*
156305
* The wrapped ModifyTable is not reached through ExecProcNode, so its
@@ -160,16 +309,16 @@ modify_hypertable_exec(CustomScanState *node)
160309
* what makes it safe for extensions that call ExplainPrintPlan at
161310
* arbitrary points (see issues #7583 and #8531).
162311
*/
163-
if (mtstate->ps.instrument)
312+
if (modify_table_state->ps.instrument)
164313
{
165-
InstrStartNode(mtstate->ps.instrument);
314+
InstrStartNode(modify_table_state->ps.instrument);
166315
}
167316

168-
result = ExecModifyTable(node, &mtstate->ps);
317+
TupleTableSlot *result = ExecModifyTable(node, &modify_table_state->ps);
169318

170-
if (mtstate->ps.instrument)
319+
if (modify_table_state->ps.instrument)
171320
{
172-
InstrStopNode(mtstate->ps.instrument, TupIsNull(result) ? 0.0 : 1.0);
321+
InstrStopNode(modify_table_state->ps.instrument, TupIsNull(result) ? 0.0 : 1.0);
173322
}
174323

175324
return result;
@@ -203,6 +352,7 @@ modify_hypertable_end(CustomScanState *node)
203352
Assert(!state->compressor);
204353

205354
ExecEndNode(linitial(node->custom_ps));
355+
206356
if (state->ctr)
207357
{
208358
ts_chunk_tuple_routing_destroy(state->ctr);

src/nodes/modify_hypertable.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ typedef struct ModifyHypertableState
6969
List *explain_saved_tlist;
7070
List *explain_saved_custom_scan_tlist;
7171

72+
int deferred_eflags;
73+
Plan *deferred_modify_table_subplan;
7274
} ModifyHypertableState;
7375

7476
extern TSDLLEXPORT bool ts_is_modify_hypertable_plan(Plan *plan);

src/nodes/modify_hypertable_exec.c

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2311,54 +2311,6 @@ ExecModifyTable(CustomScanState *cs_node, PlanState *pstate)
23112311
context.epqstate = &node->mt_epqstate;
23122312
context.estate = estate;
23132313

2314-
/*
2315-
* For UPDATE/DELETE/MERGE on compressed hypertable, decompress chunks and
2316-
* move rows to uncompressed chunks. For MERGE, decompression is needed
2317-
* even for DO NOTHING or INSERT-only actions because the join evaluation
2318-
* must see the actual rows to correctly determine matched vs not-matched.
2319-
*/
2320-
if ((operation == CMD_DELETE || operation == CMD_UPDATE || operation == CMD_MERGE) &&
2321-
!ht_state->comp_chunks_processed)
2322-
{
2323-
/* Modify snapshot only if something got decompressed */
2324-
if (ts_cm_functions->decompress_target_segments &&
2325-
ts_cm_functions->decompress_target_segments(ht_state))
2326-
{
2327-
ht_state->comp_chunks_processed = true;
2328-
/*
2329-
* save snapshot set during ExecutorStart(), since this is the same
2330-
* snapshot used to SeqScan of uncompressed chunks
2331-
*/
2332-
ht_state->snapshot = estate->es_snapshot;
2333-
2334-
CommandCounterIncrement();
2335-
/* use a static copy of current transaction snapshot
2336-
* this needs to be a copy so we don't read trigger updates
2337-
*/
2338-
estate->es_snapshot = RegisterSnapshot(GetTransactionSnapshot());
2339-
/* mark rows visible */
2340-
estate->es_output_cid = GetCurrentCommandId(true);
2341-
2342-
if (ts_guc_max_tuples_decompressed_per_dml > 0)
2343-
{
2344-
if (ht_state->tuples_decompressed > ts_guc_max_tuples_decompressed_per_dml)
2345-
{
2346-
ereport(ERROR,
2347-
(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
2348-
errmsg("tuple decompression limit exceeded by operation"),
2349-
errdetail("current limit: %d, tuples decompressed: %lld",
2350-
ts_guc_max_tuples_decompressed_per_dml,
2351-
(long long int) ht_state->tuples_decompressed),
2352-
errhint("Consider increasing "
2353-
"timescaledb.max_tuples_decompressed_per_dml_transaction or "
2354-
"set to 0 (unlimited).")));
2355-
}
2356-
}
2357-
}
2358-
/* Account for tuples deleted via batch DELETE in compressed chunks */
2359-
if (operation == CMD_DELETE && ht_state->tuples_deleted > 0)
2360-
estate->es_processed += ht_state->tuples_deleted;
2361-
}
23622314
/*
23632315
* Fetch rows from subplan, and execute the required table modification
23642316
* for each row.

0 commit comments

Comments
 (0)