Skip to content

Commit df12e05

Browse files
committed
Fix INSERT EXEC to fire INSTEAD OF triggers on view targets
When INSERT EXEC targets a view with an INSTEAD OF trigger, the trigger was not firing because the flush INSERT ran via SPI_execute without opening a composite trigger nesting level. Root cause: The composite trigger mechanism (BeginCompositeTriggers/ EndCompositeTriggers) is normally called from exec_stmt_execsql when enable_txn_in_triggers is true. But for INSERT EXEC, enable_txn_in_triggers is false, so INSTEAD OF triggers queued their events but the events were never fired (IsCompositeTriggerActive() returned false). The fix: 1. Clear INSERT EXEC rewrite context before the flush INSERT so it behaves like a normal INSERT 2. Call BeginCompositeTriggers() before SPI_execute to open a trigger nesting level 3. Call EndCompositeTriggers(false) after successful execution to fire queued INSTEAD OF triggers 4. Set pltsql_disable_txn_in_triggers=true during flush to force atomic SPI, preventing premature commits that would destroy the trigger's transition tables 5. Add commit_stmt guard to skip commits while INSERT EXEC rewrite is active, protecting the 'inserted' pseudo-table from being destroyed This matches SQL Server behavior where INSERT EXEC into a view with an INSTEAD OF trigger fires the trigger once with all rows from the procedure's result set
1 parent 9b0c625 commit df12e05

File tree

1 file changed

+113
-93
lines changed

1 file changed

+113
-93
lines changed

contrib/babelfishpg_tsql/src/pl_exec.c

Lines changed: 113 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)