@@ -97,8 +97,22 @@ static int insert_exec_call_stack_depth = 0; /* Call stack depth when INSERT EX
9797static bool insert_exec_incremented_tran_count = false; /* True if INSERT EXEC incremented NestedTranCount */
9898static bool insert_exec_had_error = false; /* True if INSERT EXEC had an error - used to skip trancount mismatch check */
9999static bool insert_exec_pending_drop = false; /* True if temp table needs to be dropped when SPI is available */
100- static Relation insert_exec_target_rel = NULL ; /* Target table relation held open during INSERT EXEC */
101- static Oid insert_exec_target_rel_oid = InvalidOid ; /* OID of target table for schema change detection */
100+ static Oid insert_exec_target_rel_oid = InvalidOid ; /* OID of target table - lock held to detect schema changes */
101+
102+ /*
103+ * Schema signature for detecting schema changes during INSERT EXEC.
104+ * We store the column count and column type OIDs at the start of INSERT EXEC,
105+ * then verify they haven't changed before flushing data to the target table.
106+ * This detects ALTER TABLE operations that would cause SQL Server error 556.
107+ */
108+ typedef struct InsertExecSchemaSignature
109+ {
110+ int natts ; /* Number of columns */
111+ Oid * atttypids ; /* Array of column type OIDs */
112+ int32 * atttypmods ; /* Array of column type modifiers */
113+ } InsertExecSchemaSignature ;
114+
115+ static InsertExecSchemaSignature * insert_exec_schema_sig = NULL ;
102116
103117/* DestReceiver struct for INSERT EXEC */
104118typedef struct
@@ -325,18 +339,19 @@ pltsql_insert_exec_check_pending_drop(void)
325339/*
326340 * Open and hold the target table during INSERT EXEC execution.
327341 *
328- * This is critical for detecting schema alterations (SQL Server error 556).
329- * By holding the target table open with a lock, PostgreSQL's CheckTableNotInUse()
330- * will detect if the procedure tries to ALTER TABLE on the target, and raise
331- * an error "cannot ALTER TABLE because it is being used by active queries".
342+ * This function captures the schema signature (column count and types) of the
343+ * target table at the start of INSERT EXEC. Before flushing data to the target,
344+ * we verify the schema hasn't changed. If it has, we raise SQL Server error 556:
345+ * "INSERT EXEC failed because the stored procedure altered the schema of the target table."
332346 *
333- * We use RowExclusiveLock because we will be inserting into this table.
334- * This also prevents concurrent modifications to the table structure.
347+ * IMPORTANT: We only acquire a lock, not hold the Relation pointer open.
348+ * Holding a Relation pointer across subtransaction boundaries causes issues
349+ * with resource owners and relcache invalidation, especially in upgrade tests
350+ * where table OIDs may change.
335351 *
336352 * For temp tables and table variables (starting with # or @), we don't need
337- * to hold them open because they're session-local and can't be modified by
338- * other sessions. Also, temp tables may not be resolvable via RangeVarGetRelid
339- * in all contexts.
353+ * to lock them because they're session-local and can't be modified by
354+ * other sessions.
340355 */
341356void
342357pltsql_insert_exec_open_target_table (const char * target_table )
@@ -349,10 +364,20 @@ pltsql_insert_exec_open_target_table(const char *target_table)
349364 char * target_copy ;
350365 char * dot_pos ;
351366 char * second_dot ;
367+ Relation rel ;
368+ TupleDesc tupdesc ;
369+ int i ;
370+ MemoryContext oldcontext ;
371+
372+ elog (DEBUG1 , "INSERT-EXEC: open_target_table called with target='%s'" ,
373+ target_table ? target_table : "NULL" );
352374
353375 /* Skip for temp tables and table variables */
354376 if (target_table == NULL || target_table [0 ] == '#' || target_table [0 ] == '@' )
377+ {
378+ elog (DEBUG1 , "INSERT-EXEC: Skipping schema capture for temp table or table variable" );
355379 return ;
380+ }
356381
357382 /* Parse schema and table name from target_table */
358383 target_copy = pstrdup (target_table );
@@ -405,24 +430,146 @@ pltsql_insert_exec_open_target_table(const char *target_table)
405430 return ;
406431 }
407432
408- /* Open the relation and hold it open */
409- insert_exec_target_rel = table_open (relid , RowExclusiveLock );
433+ /*
434+ * Acquire RowExclusiveLock on the target table.
435+ * This lock will be held until the end of the transaction (or until
436+ * explicitly released).
437+ */
438+ LockRelationOid (relid , RowExclusiveLock );
410439 insert_exec_target_rel_oid = relid ;
440+
441+ /*
442+ * Capture the schema signature of the target table.
443+ * We open the relation briefly to get the tuple descriptor, then close it.
444+ * The lock we acquired above will remain held.
445+ */
446+ rel = table_open (relid , NoLock ); /* Already have RowExclusiveLock */
447+ tupdesc = RelationGetDescr (rel );
448+
449+ /* Allocate schema signature in TopMemoryContext so it survives error handling */
450+ oldcontext = MemoryContextSwitchTo (TopMemoryContext );
451+
452+ /* Free any previous schema signature */
453+ if (insert_exec_schema_sig != NULL )
454+ {
455+ if (insert_exec_schema_sig -> atttypids )
456+ pfree (insert_exec_schema_sig -> atttypids );
457+ if (insert_exec_schema_sig -> atttypmods )
458+ pfree (insert_exec_schema_sig -> atttypmods );
459+ pfree (insert_exec_schema_sig );
460+ insert_exec_schema_sig = NULL ;
461+ }
462+
463+ insert_exec_schema_sig = palloc (sizeof (InsertExecSchemaSignature ));
464+ insert_exec_schema_sig -> natts = tupdesc -> natts ;
465+ insert_exec_schema_sig -> atttypids = palloc (tupdesc -> natts * sizeof (Oid ));
466+ insert_exec_schema_sig -> atttypmods = palloc (tupdesc -> natts * sizeof (int32 ));
467+
468+ for (i = 0 ; i < tupdesc -> natts ; i ++ )
469+ {
470+ Form_pg_attribute attr = TupleDescAttr (tupdesc , i );
471+ insert_exec_schema_sig -> atttypids [i ] = attr -> atttypid ;
472+ insert_exec_schema_sig -> atttypmods [i ] = attr -> atttypmod ;
473+ }
474+
475+ elog (DEBUG1 , "INSERT-EXEC: Captured schema signature for target table OID %u with %d columns" ,
476+ relid , insert_exec_schema_sig -> natts );
477+
478+ MemoryContextSwitchTo (oldcontext );
479+
480+ table_close (rel , NoLock ); /* Keep the lock */
411481}
412482
413483/*
414484 * Close the target table that was held open during INSERT EXEC.
415485 * Called after the flush completes or on error cleanup.
486+ *
487+ * Note: We only release the lock if we're not in an aborted transaction state.
488+ * If the transaction was aborted (e.g., due to COMMIT without BEGIN TRAN error),
489+ * the lock has already been released by the transaction abort, and trying to
490+ * unlock it would cause an error.
416491 */
417492void
418493pltsql_insert_exec_close_target_table (void )
419494{
420- if (insert_exec_target_rel != NULL )
495+ if (OidIsValid ( insert_exec_target_rel_oid ) )
421496 {
422- table_close (insert_exec_target_rel , NoLock );
423- insert_exec_target_rel = NULL ;
497+ /*
498+ * Only release the lock if we're not in an aborted transaction state.
499+ * In an aborted transaction, all locks have already been released.
500+ */
501+ if (!IsAbortedTransactionBlockState ())
502+ {
503+ UnlockRelationOid (insert_exec_target_rel_oid , RowExclusiveLock );
504+ }
424505 insert_exec_target_rel_oid = InvalidOid ;
425506 }
507+
508+ /* Free the schema signature */
509+ if (insert_exec_schema_sig != NULL )
510+ {
511+ if (insert_exec_schema_sig -> atttypids )
512+ pfree (insert_exec_schema_sig -> atttypids );
513+ if (insert_exec_schema_sig -> atttypmods )
514+ pfree (insert_exec_schema_sig -> atttypmods );
515+ pfree (insert_exec_schema_sig );
516+ insert_exec_schema_sig = NULL ;
517+ }
518+ }
519+
520+ /*
521+ * Verify that the target table schema hasn't changed since INSERT EXEC started.
522+ * Returns true if schema is unchanged, false if it has changed.
523+ *
524+ * This is called before flushing data to the target table to detect if the
525+ * executed procedure altered the target table's schema (SQL Server error 556).
526+ */
527+ bool
528+ pltsql_insert_exec_verify_schema (void )
529+ {
530+ Relation rel ;
531+ TupleDesc tupdesc ;
532+ int i ;
533+ bool schema_changed = false;
534+
535+ /* If no schema signature was captured, skip the check */
536+ if (insert_exec_schema_sig == NULL || !OidIsValid (insert_exec_target_rel_oid ))
537+ return true;
538+
539+ /* Open the relation to get current schema */
540+ rel = table_open (insert_exec_target_rel_oid , NoLock ); /* Already have lock */
541+ tupdesc = RelationGetDescr (rel );
542+
543+ /* Check if column count changed */
544+ if (tupdesc -> natts != insert_exec_schema_sig -> natts )
545+ {
546+ elog (DEBUG1 , "INSERT-EXEC: Schema changed - column count: original=%d, current=%d" ,
547+ insert_exec_schema_sig -> natts , tupdesc -> natts );
548+ schema_changed = true;
549+ }
550+ else
551+ {
552+ /* Check if any column type changed */
553+ for (i = 0 ; i < tupdesc -> natts ; i ++ )
554+ {
555+ Form_pg_attribute attr = TupleDescAttr (tupdesc , i );
556+ if (attr -> atttypid != insert_exec_schema_sig -> atttypids [i ] ||
557+ attr -> atttypmod != insert_exec_schema_sig -> atttypmods [i ])
558+ {
559+ elog (DEBUG1 , "INSERT-EXEC: Schema changed - column %d type: original=%u, current=%u" ,
560+ i , insert_exec_schema_sig -> atttypids [i ], attr -> atttypid );
561+ schema_changed = true;
562+ break ;
563+ }
564+ }
565+ }
566+
567+ if (!schema_changed )
568+ elog (DEBUG1 , "INSERT-EXEC: Schema unchanged, %d columns verified" , tupdesc -> natts );
569+
570+ table_close (rel , NoLock ); /* Keep the lock */
571+
572+ return !schema_changed ;
426573}
427574
428575/*
@@ -968,6 +1115,9 @@ create_insert_exec_temp_table(const char *target_table, const char *column_list)
9681115 char * second_dot ;
9691116 char * target_copy ;
9701117
1118+ elog (DEBUG1 , "INSERT-EXEC: create_insert_exec_temp_table called with target='%s'" ,
1119+ target_table ? target_table : "NULL" );
1120+
9711121 /* Generate unique temp table name using backend PID */
9721122 snprintf (temp_table_name , sizeof (temp_table_name ),
9731123 "#insert_exec_buf_%d" , MyProcPid );
@@ -1463,6 +1613,19 @@ flush_insert_exec_temp_table(PLtsql_execstate *estate)
14631613 return ;
14641614 }
14651615
1616+ /*
1617+ * Verify that the target table schema hasn't changed since INSERT EXEC started.
1618+ * If the executed procedure altered the target table's schema (e.g., ALTER TABLE
1619+ * ADD COLUMN), we must raise SQL Server error 556:
1620+ * "INSERT EXEC failed because the stored procedure altered the schema of the target table."
1621+ */
1622+ if (!pltsql_insert_exec_verify_schema ())
1623+ {
1624+ ereport (ERROR ,
1625+ (errcode (ERRCODE_FEATURE_NOT_SUPPORTED ),
1626+ errmsg ("INSERT EXEC failed because the stored procedure altered the schema of the target table." )));
1627+ }
1628+
14661629 /*
14671630 * Save the INSERT EXEC context info before clearing it.
14681631 * We need to temporarily clear the context so that the flush INSERT
@@ -2947,6 +3110,8 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt)
29473110 * at the subtransaction level, so they won't be cleaned up if a nested
29483111 * subtransaction (e.g., inner TRY/CATCH) rolls back.
29493112 */
3113+ elog (DEBUG1 , "INSERT-EXEC: exec_stmt_exec - insert_exec=%d, insert_exec_target='%s'" ,
3114+ stmt -> insert_exec , stmt -> insert_exec_target ? stmt -> insert_exec_target : "NULL" );
29503115 if (stmt -> insert_exec && stmt -> insert_exec_target != NULL )
29513116 {
29523117 /*
@@ -3489,12 +3654,24 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt)
34893654 * cannot be used in an error context (transaction is aborted).
34903655 * We set the pending drop flag so the temp table will be dropped at the
34913656 * next opportunity when SPI is available.
3657+ *
3658+ * IMPORTANT: We MUST clear the INSERT EXEC context even if other cleanup
3659+ * fails. Otherwise, subsequent queries will see pltsql_insert_exec_active()
3660+ * as true and try to use the stale temp table OID.
34923661 */
34933662 if (insert_exec_setup_done )
34943663 {
34953664 pltsql_insert_exec_set_error_flag ();
34963665 pltsql_insert_exec_set_pending_drop ();
3497- pltsql_insert_exec_close_target_table (); /* Close target table on error */
3666+
3667+ /*
3668+ * Close target table. This function already checks
3669+ * IsAbortedTransactionBlockState() and skips the unlock if
3670+ * the transaction is aborted.
3671+ */
3672+ pltsql_insert_exec_close_target_table ();
3673+
3674+ /* Always clear the context, even if close_target_table failed */
34983675 pltsql_clear_insert_exec_context ();
34993676 }
35003677
@@ -3909,24 +4086,32 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt)
39094086 /*
39104087 * Cleanup INSERT EXEC state on error.
39114088 *
3912- * IMPORTANT: We cannot call drop_insert_exec_temp_table() here because
3913- * SPI_execute cannot be used in an error context (transaction is aborted).
4089+ * IMPORTANT: We MUST clear the INSERT EXEC context immediately, even if
4090+ * we're inside a TRY-CATCH block. This is because:
4091+ * 1. The temp table may have been dropped due to transaction abort
4092+ * 2. Between this PG_CATCH and the TRY-CATCH handler, other code might
4093+ * check pltsql_insert_exec_active() and try to use the stale temp table OID
4094+ * 3. This leads to "could not open relation with OID" errors
4095+ *
4096+ * We cannot call drop_insert_exec_temp_table() here because SPI_execute
4097+ * cannot be used in an error context (transaction is aborted).
39144098 * We set the pending drop flag so the temp table will be dropped at the
39154099 * next opportunity when SPI is available.
3916- *
3917- * If we're inside a TRY-CATCH block, do NOT clear the INSERT EXEC context.
3918- * Let the TRY-CATCH handler (exec_stmt_try_catch) do the cleanup, because
3919- * it has access to SPI and can properly drop the temp table.
39204100 */
39214101 if (insert_exec_setup_done )
39224102 {
39234103 pltsql_insert_exec_set_error_flag ();
39244104 pltsql_insert_exec_set_pending_drop ();
3925- pltsql_insert_exec_close_target_table (); /* Close target table on error */
3926- if (!pltsql_insert_exec_in_trycatch ())
3927- {
3928- pltsql_clear_insert_exec_context ();
3929- }
4105+
4106+ /*
4107+ * Close target table. This function already checks
4108+ * IsAbortedTransactionBlockState() and skips the unlock if
4109+ * the transaction is aborted.
4110+ */
4111+ pltsql_insert_exec_close_target_table ();
4112+
4113+ /* Always clear the context to prevent stale state */
4114+ pltsql_clear_insert_exec_context ();
39304115 }
39314116
39324117 /* Restore GUC and scope identity settings before re-throwing */
@@ -4761,22 +4946,32 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt)
47614946 /*
47624947 * Cleanup INSERT EXEC state on error.
47634948 *
4764- * IMPORTANT: We cannot call drop_insert_exec_temp_table() here because
4765- * SPI_execute cannot be used in an error context (transaction is aborted).
4949+ * IMPORTANT: We MUST clear the INSERT EXEC context immediately, even if
4950+ * we're inside a TRY-CATCH block. This is because:
4951+ * 1. The temp table may have been dropped due to transaction abort
4952+ * 2. Between this PG_CATCH and the TRY-CATCH handler, other code might
4953+ * check pltsql_insert_exec_active() and try to use the stale temp table OID
4954+ * 3. This leads to "could not open relation with OID" errors
4955+ *
4956+ * We cannot call drop_insert_exec_temp_table() here because SPI_execute
4957+ * cannot be used in an error context (transaction is aborted).
47664958 * The temp table will be automatically cleaned up when the transaction
47674959 * rolls back.
4768- *
4769- * If we're inside a TRY-CATCH block, do NOT clear the INSERT EXEC context.
4770- * Let the TRY-CATCH handler (exec_stmt_try_catch) do the cleanup, because
4771- * it has access to SPI and can properly drop the temp table.
47724960 */
47734961 if (insert_exec_setup_done )
47744962 {
4775- pltsql_insert_exec_close_target_table (); /* Close target table on error */
4776- if (!pltsql_insert_exec_in_trycatch ())
4777- {
4778- pltsql_clear_insert_exec_context ();
4779- }
4963+ pltsql_insert_exec_set_error_flag ();
4964+ pltsql_insert_exec_set_pending_drop ();
4965+
4966+ /*
4967+ * Close target table. This function already checks
4968+ * IsAbortedTransactionBlockState() and skips the unlock if
4969+ * the transaction is aborted.
4970+ */
4971+ pltsql_insert_exec_close_target_table ();
4972+
4973+ /* Always clear the context to prevent stale state */
4974+ pltsql_clear_insert_exec_context ();
47804975 }
47814976 pltsql_revert_guc (save_nestlevel );
47824977 pltsql_revert_last_scope_identity (scope_level );
0 commit comments