@@ -766,9 +766,6 @@ pltsql_exec_function(PLtsql_function *func, FunctionCallInfo fcinfo,
766766 /* Obtain output parameters for Insert Execute */
767767 if (estate .insert_exec )
768768 {
769- elog (LOG , "INSERT-EXEC-OUTPUT: insert_exec=true, rettype=%u, retval=%p, retisnull=%d" ,
770- estate .rettype , (void * )estate .retval , estate .retisnull );
771-
772769 /*
773770 * Switch to TopTransactionContext for the datum copy.
774771 * We MUST use TopTransactionContext (not estate.func->fn_cxt) because:
@@ -783,37 +780,21 @@ pltsql_exec_function(PLtsql_function *func, FunctionCallInfo fcinfo,
783780 {
784781 /* Get return type properties */
785782 get_typlenbyval (estate .rettype , & typLen , & typByVal );
786- elog (LOG , "INSERT-EXEC-OUTPUT: rettype=%u is valid, typLen=%d, typByVal=%d" ,
787- estate .rettype , typLen , typByVal );
788783
789784 if (typByVal )
790785 {
791786 execute_call_insert_exec_retval = estate .retval ;
792- elog (LOG , "INSERT-EXEC-OUTPUT: typByVal=true, copied retval directly" );
793787 }
794788 else
795789 {
796790 /* Pass-by-reference, need to copy the data */
797791 execute_call_insert_exec_retval = datumCopy (estate .retval ,
798792 typByVal ,
799793 typLen );
800- elog (LOG , "INSERT-EXEC-OUTPUT: typByVal=false, called datumCopy to TopTransactionContext, result=%p" ,
801- (void * )execute_call_insert_exec_retval );
802-
803- /* Debug: check the HeapTupleHeader */
804- if (typLen == -1 && execute_call_insert_exec_retval != (Datum ) 0 )
805- {
806- HeapTupleHeader td = (HeapTupleHeader ) DatumGetPointer (execute_call_insert_exec_retval );
807- Oid tupType = HeapTupleHeaderGetTypeId (td );
808- int32 tupTypmod = HeapTupleHeaderGetTypMod (td );
809- elog (LOG , "INSERT-EXEC-OUTPUT: HeapTupleHeader tupType=%u, tupTypmod=%d" ,
810- tupType , tupTypmod );
811- }
812794 }
813795 }
814796 else
815797 {
816- elog (LOG , "INSERT-EXEC-OUTPUT: rettype=%u is NOT valid" , estate .rettype );
817798 /* For cases where rettype is not properly set, handle gracefully */
818799 typLen = -1 ; /* Variable length */
819800 typByVal = false; /* Pass by reference */
@@ -823,13 +804,11 @@ pltsql_exec_function(PLtsql_function *func, FunctionCallInfo fcinfo,
823804 {
824805 /* Copy to TopTransactionContext */
825806 execute_call_insert_exec_retval = datumCopy (estate .retval , typByVal , typLen );
826- elog (LOG , "INSERT-EXEC-OUTPUT: copied retval with datumCopy to TopTransactionContext" );
827807 }
828808 else
829809 {
830810 /* Skip the exec_move_row_from_datum call entirely */
831811 execute_call_insert_exec_retval = (Datum ) 0 ;
832- elog (LOG , "INSERT-EXEC-OUTPUT: retval is 0, skipping" );
833812 }
834813 }
835814 MemoryContextSwitchTo (oldcontext );
@@ -4549,15 +4528,35 @@ commit_stmt(PLtsql_execstate *estate, bool txnStarted)
45494528 SimpleEcontextStackEntry * topEntry = simple_econtext_stack ;
45504529 MemoryContext oldcontext = CurrentMemoryContext ;
45514530
4552- elog (LOG , "commit_stmt: txnStarted=%d, IsTransactionBlockActive=%d, insert_exec_rewrite_active=%d" ,
4553- txnStarted , IsTransactionBlockActive (), pltsql_insert_exec_rewrite_active ());
4531+ /*
4532+ * Do not commit when INSERT EXEC rewrite is active.
4533+ *
4534+ * When INSERT EXEC is in progress, the flush INSERT may fire triggers
4535+ * (INSTEAD OF or AFTER). The trigger body's statements (like INSERT INTO
4536+ * trigger_log) call commit_stmt. Calling CommitTransactionCommand() here
4537+ * would:
4538+ * 1. Destroy the INSTEAD OF trigger's 'inserted' transition table
4539+ * (it's registered against the current SPI context/transaction).
4540+ * 2. Invalidate the outer flush INSERT portal's snapshot state,
4541+ * causing "portal snapshots did not account for..." errors.
4542+ *
4543+ * Note: We can't check estate->trigdata here because the trigger body's
4544+ * nested INSERT statements have estate->trigdata = NULL (trigdata is only
4545+ * set for the trigger function itself, not for statements inside it).
4546+ *
4547+ * Skipping the commit here is safe: all trigger body changes are in
4548+ * the current transaction and will be committed by the INSERT EXEC
4549+ * cleanup commit that fires after exec_flush completes and the
4550+ * rewrite context is cleared.
4551+ */
4552+ if (pltsql_insert_exec_rewrite_active ())
4553+ return ;
45544554
45554555 /* Hold portals to make sure that cursors work */
45564556 HoldPinnedPortals ();
45574557
45584558 if (txnStarted )
45594559 {
4560- elog (LOG , "commit_stmt: Calling PLTsqlCommitTransaction" );
45614560 PLTsqlCommitTransaction (NULL , false);
45624561 }
45634562
@@ -4566,7 +4565,6 @@ commit_stmt(PLtsql_execstate *estate, bool txnStarted)
45664565 ForgetPortalSnapshots ();
45674566 }
45684567
4569- elog (LOG , "commit_stmt: Calling CommitTransactionCommand and StartTransactionCommand" );
45704568 CommitTransactionCommand ();
45714569 StartTransactionCommand ();
45724570 MemoryContextSwitchTo (oldcontext );
@@ -5027,10 +5025,6 @@ exec_stmt_execsql(PLtsql_execstate *estate,
50275025 */
50285026 if (stmt -> insert_exec_target != NULL )
50295027 {
5030- elog (DEBUG1 , "INSERT-EXEC: About to create temp table. target='%s', columns='%s'" ,
5031- stmt -> insert_exec_target ,
5032- stmt -> insert_exec_columns ? stmt -> insert_exec_columns : "(all)" );
5033-
50345028 /*
50355029 * SQL Server behavior for @@trancount during INSERT-EXEC:
50365030 * - If @@trancount == 0 (no outer transaction), INSERT-EXEC creates an
@@ -5055,8 +5049,6 @@ exec_stmt_execsql(PLtsql_execstate *estate,
50555049 exec_create_insert_exec_temp_table (estate ,
50565050 stmt -> insert_exec_target ,
50575051 stmt -> insert_exec_columns );
5058- elog (DEBUG1 , "INSERT-EXEC: Temp table created: '%s'. Setting rewrite context." ,
5059- estate -> insert_exec_temp_table );
50605052 pltsql_set_insert_exec_rewrite_context (estate -> insert_exec_temp_table ,
50615053 stmt -> insert_exec_columns ,
50625054 saved_nested_tran_count );
@@ -5300,16 +5292,8 @@ exec_stmt_execsql(PLtsql_execstate *estate,
53005292 * datum (e.g., from a RETURN statement) that would cause a crash.
53015293 */
53025294 row = (PLtsql_row * ) stmt -> target ;
5303- elog (LOG , "INSERT-EXEC-OUTPUT-CALLER: About to call exec_move_row_from_datum, nfields=%d, retval=%p" ,
5304- row -> nfields , (void * )execute_call_insert_exec_retval );
53055295 if (row -> nfields > 0 )
53065296 {
5307- /* Debug: check the HeapTupleHeader before calling exec_move_row_from_datum */
5308- HeapTupleHeader td = (HeapTupleHeader ) DatumGetPointer (execute_call_insert_exec_retval );
5309- Oid tupType = HeapTupleHeaderGetTypeId (td );
5310- int32 tupTypmod = HeapTupleHeaderGetTypMod (td );
5311- elog (LOG , "INSERT-EXEC-OUTPUT-CALLER: HeapTupleHeader tupType=%u, tupTypmod=%d" ,
5312- tupType , tupTypmod );
53135297 exec_move_row_from_datum (estate , stmt -> target , execute_call_insert_exec_retval );
53145298 }
53155299 }
@@ -5468,13 +5452,7 @@ exec_stmt_execsql(PLtsql_execstate *estate,
54685452 if ((!pltsql_disable_batch_auto_commit || (stmt -> txn_data != NULL )) &&
54695453 support_tsql_trans &&
54705454 (enable_txn_in_triggers || estate -> trigdata == NULL ) &&
5471- !ro_func && !estate -> insert_exec &&
5472- !pltsql_insert_exec_rewrite_active ()) /* Don't commit inside INSERT EXEC context:
5473- * triggers firing during the flush INSERT would
5474- * call CommitTransactionCommand() which destroys
5475- * the outer SPI portal's snapshot state, causing
5476- * "portal snapshots did not account for all
5477- * active snapshots" at portalmem.c:1292 */
5455+ !ro_func && !estate -> insert_exec )
54785456 {
54795457 commit_stmt (estate , (estate -> tsql_trigger_flags & TSQL_TRAN_STARTED ));
54805458
@@ -5511,42 +5489,40 @@ exec_stmt_execsql(PLtsql_execstate *estate,
55115489 exec_drop_insert_exec_temp_table (estate );
55125490 }
55135491
5492+ /*
5493+ * Clear the INSERT EXEC rewrite context HERE (success path) so that
5494+ * the cleanup commit_stmt below can fire. Without this, the context
5495+ * is cleared only in PG_FINALLY (after the commit check), making
5496+ * !pltsql_insert_exec_rewrite_active() permanently false in the
5497+ * success path and preventing the final commit.
5498+ *
5499+ * This is safe because:
5500+ * - exec_flush and exec_drop have both completed
5501+ * - No more CTE rewrites will run
5502+ * - Error path still clears context via PG_FINALLY
5503+ */
5504+ if (stmt -> insert_exec && stmt -> insert_exec_target != NULL )
5505+ pltsql_clear_insert_exec_rewrite_context ();
5506+
55145507 /*
55155508 * INSERT EXEC: Commit the transaction after successful completion.
55165509 * This is needed because we skip the normal commit_stmt call above
55175510 * (due to !estate->insert_exec condition) to avoid committing in the
55185511 * middle of INSERT EXEC processing. Now that the flush and cleanup
55195512 * are done, we need to commit to ensure the inserted rows are visible
55205513 * to subsequent statements.
5521- *
5522- * However, when using the query rewriting approach, we don't need to
5523- * call commit_stmt because we didn't start any implicit transactions
5524- * (we added !pltsql_insert_exec_rewrite_active() to the condition at
5525- * line 5091). The transaction state is already correct.
55265514 */
5527- elog (LOG , "INSERT EXEC cleanup: insert_exec=%d, insert_exec_rewrite_active=%d, TSQL_TRAN_STARTED=%d" ,
5528- stmt -> insert_exec , pltsql_insert_exec_rewrite_active (),
5529- (estate -> tsql_trigger_flags & TSQL_TRAN_STARTED ) ? 1 : 0 );
55305515 if (stmt -> insert_exec &&
55315516 !pltsql_disable_batch_auto_commit &&
55325517 support_tsql_trans &&
55335518 (enable_txn_in_triggers || estate -> trigdata == NULL ) &&
5534- !ro_func &&
5535- !pltsql_insert_exec_rewrite_active ())
5519+ !ro_func )
55365520 {
5537- elog (LOG , "INSERT EXEC cleanup: Calling commit_stmt" );
55385521 commit_stmt (estate , (estate -> tsql_trigger_flags & TSQL_TRAN_STARTED ));
55395522 }
5540- else if (stmt -> insert_exec )
5541- {
5542- elog (LOG , "INSERT EXEC cleanup: Skipping commit_stmt (query rewriting active)" );
5543- }
55445523 }
55455524 PG_FINALLY ();
55465525 {
5547- elog (LOG , "PG_FINALLY: insert_exec=%d, insert_exec_rewrite_active=%d" ,
5548- stmt -> insert_exec , pltsql_insert_exec_rewrite_active ());
5549-
55505526 original_query_string = NULL ;
55515527 is_schemabinding_view = true;
55525528
@@ -5586,12 +5562,11 @@ exec_stmt_execsql(PLtsql_execstate *estate,
55865562 estate -> insert_exec_identity_insert = false;
55875563 }
55885564
5589- /* Clear INSERT EXEC rewrite context on any exit */
5590- if ( stmt -> insert_exec && stmt -> insert_exec_target != NULL )
5591- {
5592- elog ( LOG , "PG_FINALLY: Clearing INSERT EXEC rewrite context" );
5565+ /* Clear INSERT EXEC rewrite context - only if not already cleared
5566+ * in the success path above. On error path, this is the only cleanup. */
5567+ if ( stmt -> insert_exec && stmt -> insert_exec_target != NULL &&
5568+ pltsql_insert_exec_rewrite_active ())
55935569 pltsql_clear_insert_exec_rewrite_context ();
5594- }
55955570
55965571 if (is_cross_db )
55975572 {
@@ -11763,12 +11738,12 @@ exec_flush_insert_exec_temp_table(PLtsql_execstate *estate)
1176311738 StringInfoData flush_query ;
1176411739 char * tsql_target_ref ;
1176511740 int rc ;
11766- MemoryContext oldcontext ;
11767- ResourceOwner oldowner ;
1176811741
1176911742 if (estate -> insert_exec_temp_table == NULL ||
1177011743 estate -> insert_exec_target_table == NULL )
11744+ {
1177111745 return ;
11746+ }
1177211747
1177311748 /*
1177411749 * Build a T-SQL-level table reference for the flush INSERT.
@@ -11855,20 +11830,64 @@ exec_flush_insert_exec_temp_table(PLtsql_execstate *estate)
1185511830 estate -> insert_exec_temp_table );
1185611831 }
1185711832
11858- elog (DEBUG1 , "INSERT-EXEC-TEMP-TABLE: Flushing: %s" , flush_query .data );
11833+ /*
11834+ * Temporarily force atomic SPI for any triggers that fire during the
11835+ * flush INSERT.
11836+ *
11837+ * Without this, pltsql_exec_trigger sets estate->atomic = false (non-atomic
11838+ * SPI context) because !pltsql_disable_txn_in_triggers is true. A non-atomic
11839+ * SPI connection calls CommitTransactionCommand() in SPI_finish() when the
11840+ * trigger completes — this commits+destroys the transaction before the INSERT
11841+ * EXEC cleanup commit runs, causing trigger body changes to be lost or causing
11842+ * "portal snapshots did not account for all active snapshots" errors.
11843+ *
11844+ * With pltsql_disable_txn_in_triggers = true:
11845+ * - estate->atomic stays true → atomic SPI used
11846+ * - SPI_finish() does NOT commit on exit
11847+ * - trigger body changes stay in the current transaction
11848+ * - cleanup commit (after pltsql_clear_insert_exec_rewrite_context) commits all
11849+ */
11850+ bool saved_disable_txn_in_triggers = pltsql_disable_txn_in_triggers ;
11851+ pltsql_disable_txn_in_triggers = true;
1185911852
1186011853 /*
11861- * Wrap in subtransaction to protect AfterTrigger state.
11862- * The INSERT triggers AfterTriggerBeginQuery which increments query_depth.
11863- * If the INSERT fails, the longjmp skips AfterTriggerEndQuery, leaving
11864- * query_depth orphaned. The subtransaction rollback cleans this up.
11854+ * Clear the INSERT EXEC rewrite context BEFORE running the flush INSERT.
11855+ *
11856+ * While the rewrite context is active, the commit_stmt guard prevents
11857+ * commits from firing. But more importantly, clearing it here ensures
11858+ * the flush INSERT behaves like a normal INSERT and fires triggers properly.
11859+ *
11860+ * The procedure has already run at this point — all its SELECTs have been
11861+ * CTE-rewritten to insert into the temp table. We no longer need the
11862+ * rewrite context for that purpose.
1186511863 */
11866- oldcontext = CurrentMemoryContext ;
11867- oldowner = CurrentResourceOwner ;
11864+ pltsql_clear_insert_exec_rewrite_context ();
1186811865
11869- BeginInternalSubTransaction ("insert_exec_flush" );
11870- MemoryContextSwitchTo (oldcontext );
11866+ /*
11867+ * Open composite trigger nesting level for the flush INSERT.
11868+ *
11869+ * This is critical for INSTEAD OF triggers on views. The composite trigger
11870+ * mechanism (BeginCompositeTriggers/EndCompositeTriggers) is normally called
11871+ * from exec_stmt_execsql when enable_txn_in_triggers is true. But for INSERT
11872+ * EXEC, enable_txn_in_triggers is false, so the flush INSERT via SPI_execute
11873+ * doesn't go through that path.
11874+ *
11875+ * Without this, INSTEAD OF triggers queue their events but the events are
11876+ * never fired because IsCompositeTriggerActive() returns false (no trigger
11877+ * level was opened).
11878+ */
11879+ BeginCompositeTriggers (CurrentMemoryContext );
1187111880
11881+ /*
11882+ * Execute the flush INSERT directly without a subtransaction.
11883+ *
11884+ * Previously we wrapped this in a subtransaction to protect AfterTrigger state,
11885+ * but that caused INSTEAD OF trigger changes to be lost because the trigger
11886+ * runs inside the subtransaction and its changes are isolated.
11887+ *
11888+ * With pltsql_disable_txn_in_triggers = true, triggers use atomic SPI which
11889+ * doesn't have the AfterTrigger depth issues that required the subtransaction.
11890+ */
1187211891 PG_TRY ();
1187311892 {
1187411893 rc = SPI_execute (flush_query .data , false, 0 );
@@ -11883,29 +11902,30 @@ exec_flush_insert_exec_temp_table(PLtsql_execstate *estate)
1188311902 SPI_result_code_string (rc ));
1188411903 }
1188511904
11886- elog (DEBUG1 , "INSERT-EXEC-TEMP-TABLE: Flushed %lu rows" ,
11887- (unsigned long ) SPI_processed );
11888-
11889- /* Capture results before releasing subtransaction */
11905+ /* Capture results */
1189011906 estate -> eval_processed = SPI_processed ;
1189111907 exec_set_rowcount (SPI_processed );
1189211908 exec_set_found (estate , SPI_processed > 0 );
1189311909
1189411910 /* Free the SPI tuple table (contains TupleDesc that must be released) */
1189511911 SPI_freetuptable (SPI_tuptable );
1189611912
11897- /* Success - release the subtransaction */
11898- ReleaseCurrentSubTransaction ();
11899- MemoryContextSwitchTo (oldcontext );
11900- CurrentResourceOwner = oldowner ;
11913+ /*
11914+ * Close composite trigger nesting level and fire any queued triggers.
11915+ * This is where INSTEAD OF triggers actually execute.
11916+ */
11917+ EndCompositeTriggers (false);
11918+
11919+ /* Restore the flag */
11920+ pltsql_disable_txn_in_triggers = saved_disable_txn_in_triggers ;
1190111921 }
1190211922 PG_CATCH ();
1190311923 {
11904- /* Rollback subtransaction - this cleans up AfterTrigger depth */
11905- MemoryContextSwitchTo ( oldcontext );
11906- RollbackAndReleaseCurrentSubTransaction ();
11907- MemoryContextSwitchTo ( oldcontext );
11908- CurrentResourceOwner = oldowner ;
11924+ /* Close composite trigger level on error (don't fire triggers) */
11925+ EndCompositeTriggers (true );
11926+
11927+ /* Restore the flag first */
11928+ pltsql_disable_txn_in_triggers = saved_disable_txn_in_triggers ;
1190911929
1191011930 /*
1191111931 * DO NOT try to drop the temp table here!
0 commit comments