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)
7580static void
7681modify_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)
149239static TupleTableSlot *
150240modify_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 );
0 commit comments