1818#include "executor/tstoreReceiver.h"
1919#include "executor/tuptable.h"
2020#include "miscadmin.h"
21+ #include "nodes/makefuncs.h"
2122#include "nodes/parsenodes.h"
2223#include "parser/parse_coerce.h"
2324#include "utils/acl.h"
@@ -96,6 +97,8 @@ static int insert_exec_call_stack_depth = 0; /* Call stack depth when INSERT EX
9697static bool insert_exec_incremented_tran_count = false; /* True if INSERT EXEC incremented NestedTranCount */
9798static bool insert_exec_had_error = false; /* True if INSERT EXEC had an error - used to skip trancount mismatch check */
9899static 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 */
99102
100103/* DestReceiver struct for INSERT EXEC */
101104typedef struct
@@ -319,6 +322,109 @@ pltsql_insert_exec_check_pending_drop(void)
319322 }
320323}
321324
325+ /*
326+ * Open and hold the target table during INSERT EXEC execution.
327+ *
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".
332+ *
333+ * We use RowExclusiveLock because we will be inserting into this table.
334+ * This also prevents concurrent modifications to the table structure.
335+ *
336+ * 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.
340+ */
341+ void
342+ pltsql_insert_exec_open_target_table (const char * target_table )
343+ {
344+ RangeVar * rv ;
345+ Oid relid ;
346+ char * schema_name = NULL ;
347+ char * table_name = NULL ;
348+ char * physical_schema = NULL ;
349+ char * target_copy ;
350+ char * dot_pos ;
351+ char * second_dot ;
352+
353+ /* Skip for temp tables and table variables */
354+ if (target_table == NULL || target_table [0 ] == '#' || target_table [0 ] == '@' )
355+ return ;
356+
357+ /* Parse schema and table name from target_table */
358+ target_copy = pstrdup (target_table );
359+
360+ /* Find the last dot to separate schema from table */
361+ dot_pos = strrchr (target_copy , '.' );
362+ if (dot_pos != NULL )
363+ {
364+ * dot_pos = '\0' ;
365+ table_name = pstrdup (dot_pos + 1 );
366+
367+ /* Check if there's another dot (db.schema.table) */
368+ second_dot = strrchr (target_copy , '.' );
369+ if (second_dot != NULL )
370+ {
371+ /* db.schema.table - schema is after the second dot */
372+ schema_name = pstrdup (second_dot + 1 );
373+ }
374+ else
375+ {
376+ /* schema.table */
377+ schema_name = pstrdup (target_copy );
378+ }
379+ }
380+ else
381+ {
382+ /* Just table name, use dbo as default */
383+ table_name = pstrdup (target_copy );
384+ schema_name = pstrdup ("dbo" );
385+ }
386+ pfree (target_copy );
387+
388+ /* Convert logical schema name to physical schema name */
389+ physical_schema = get_physical_schema_name (get_cur_db_name (), schema_name );
390+
391+ /* Create RangeVar and get the relation OID */
392+ rv = makeRangeVar (physical_schema , table_name , -1 );
393+ relid = RangeVarGetRelid (rv , NoLock , true);
394+
395+ if (schema_name )
396+ pfree (schema_name );
397+ if (table_name )
398+ pfree (table_name );
399+ if (physical_schema )
400+ pfree (physical_schema );
401+
402+ if (!OidIsValid (relid ))
403+ {
404+ /* Table doesn't exist - will be caught later during flush */
405+ return ;
406+ }
407+
408+ /* Open the relation and hold it open */
409+ insert_exec_target_rel = table_open (relid , RowExclusiveLock );
410+ insert_exec_target_rel_oid = relid ;
411+ }
412+
413+ /*
414+ * Close the target table that was held open during INSERT EXEC.
415+ * Called after the flush completes or on error cleanup.
416+ */
417+ void
418+ pltsql_insert_exec_close_target_table (void )
419+ {
420+ if (insert_exec_target_rel != NULL )
421+ {
422+ table_close (insert_exec_target_rel , NoLock );
423+ insert_exec_target_rel = NULL ;
424+ insert_exec_target_rel_oid = InvalidOid ;
425+ }
426+ }
427+
322428/*
323429 * Check if INSERT EXEC context is active (target table info is set).
324430 * This returns true even before temp table is created.
@@ -2858,6 +2964,14 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt)
28582964 /* Set global context info for flush function */
28592965 pltsql_set_insert_exec_context_info (stmt -> insert_exec_target , stmt -> insert_exec_columns );
28602966
2967+ /*
2968+ * Open and hold the target table during INSERT EXEC execution.
2969+ * This is critical for detecting schema alterations (SQL Server error 556).
2970+ * By holding the target table open, PostgreSQL's CheckTableNotInUse()
2971+ * will detect if the procedure tries to ALTER TABLE on the target.
2972+ */
2973+ pltsql_insert_exec_open_target_table (stmt -> insert_exec_target );
2974+
28612975 /* Create temp table based on target table structure - NO subtransaction wrapper */
28622976 insert_exec_temp_oid = create_insert_exec_temp_table (stmt -> insert_exec_target ,
28632977 stmt -> insert_exec_columns );
@@ -3380,6 +3494,7 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt)
33803494 {
33813495 pltsql_insert_exec_set_error_flag ();
33823496 pltsql_insert_exec_set_pending_drop ();
3497+ pltsql_insert_exec_close_target_table (); /* Close target table on error */
33833498 pltsql_clear_insert_exec_context ();
33843499 }
33853500
@@ -3471,6 +3586,12 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt)
34713586 /* Flush temp table to target table */
34723587 flush_insert_exec_temp_table (estate );
34733588
3589+ /*
3590+ * Close the target table that was held open during INSERT EXEC.
3591+ * This must be done AFTER the flush completes.
3592+ */
3593+ pltsql_insert_exec_close_target_table ();
3594+
34743595 /*
34753596 * Clear the INSERT EXEC context AFTER the flush completes.
34763597 * This ensures the flush INSERT has access to target table info.
@@ -3479,7 +3600,8 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt)
34793600 }
34803601 PG_CATCH ();
34813602 {
3482- /* Clear context and drop temp table before re-throwing */
3603+ /* Close target table and clear context before re-throwing */
3604+ pltsql_insert_exec_close_target_table ();
34833605 pltsql_clear_insert_exec_context ();
34843606 drop_insert_exec_temp_table (insert_exec_temp_oid );
34853607 PG_RE_THROW ();
@@ -3743,6 +3865,14 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt)
37433865 /* Set global context info for flush function */
37443866 pltsql_set_insert_exec_context_info (stmt -> insert_exec_target , stmt -> insert_exec_columns );
37453867
3868+ /*
3869+ * Open and hold the target table during INSERT EXEC execution.
3870+ * This is critical for detecting schema alterations (SQL Server error 556).
3871+ * By holding the target table open, PostgreSQL's CheckTableNotInUse()
3872+ * will detect if the procedure tries to ALTER TABLE on the target.
3873+ */
3874+ pltsql_insert_exec_open_target_table (stmt -> insert_exec_target );
3875+
37463876 /* Create temp table based on target table structure */
37473877 insert_exec_temp_oid = create_insert_exec_temp_table (stmt -> insert_exec_target ,
37483878 stmt -> insert_exec_columns );
@@ -3792,6 +3922,7 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt)
37923922 {
37933923 pltsql_insert_exec_set_error_flag ();
37943924 pltsql_insert_exec_set_pending_drop ();
3925+ pltsql_insert_exec_close_target_table (); /* Close target table on error */
37953926 if (!pltsql_insert_exec_in_trycatch ())
37963927 {
37973928 pltsql_clear_insert_exec_context ();
@@ -3865,6 +3996,12 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt)
38653996 /* Flush temp table to target table - no subtransaction wrapper */
38663997 flush_insert_exec_temp_table (estate );
38673998
3999+ /*
4000+ * Close the target table that was held open during INSERT EXEC.
4001+ * This must be done AFTER the flush completes.
4002+ */
4003+ pltsql_insert_exec_close_target_table ();
4004+
38684005 /*
38694006 * Clear the INSERT EXEC context AFTER the flush completes.
38704007 * This ensures the flush INSERT has access to target table info.
@@ -3873,7 +4010,8 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt)
38734010 }
38744011 PG_CATCH ();
38754012 {
3876- /* Clear context and drop temp table before re-throwing */
4013+ /* Close target table and clear context before re-throwing */
4014+ pltsql_insert_exec_close_target_table ();
38774015 pltsql_clear_insert_exec_context ();
38784016 drop_insert_exec_temp_table (insert_exec_temp_oid );
38794017 PG_RE_THROW ();
@@ -4589,6 +4727,14 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt)
45894727 /* Set global context info for flush function */
45904728 pltsql_set_insert_exec_context_info (stmt -> insert_exec_target , stmt -> insert_exec_columns );
45914729
4730+ /*
4731+ * Open and hold the target table during INSERT EXEC execution.
4732+ * This is critical for detecting schema alterations (SQL Server error 556).
4733+ * By holding the target table open, PostgreSQL's CheckTableNotInUse()
4734+ * will detect if the procedure tries to ALTER TABLE on the target.
4735+ */
4736+ pltsql_insert_exec_open_target_table (stmt -> insert_exec_target );
4737+
45924738 /* Create temp table based on target table structure */
45934739 insert_exec_temp_oid = create_insert_exec_temp_table (stmt -> insert_exec_target ,
45944740 stmt -> insert_exec_columns );
@@ -4624,9 +4770,13 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt)
46244770 * Let the TRY-CATCH handler (exec_stmt_try_catch) do the cleanup, because
46254771 * it has access to SPI and can properly drop the temp table.
46264772 */
4627- if (insert_exec_setup_done && ! pltsql_insert_exec_in_trycatch () )
4773+ if (insert_exec_setup_done )
46284774 {
4629- pltsql_clear_insert_exec_context ();
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+ }
46304780 }
46314781 pltsql_revert_guc (save_nestlevel );
46324782 pltsql_revert_last_scope_identity (scope_level );
@@ -4655,6 +4805,12 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt)
46554805 /* Flush temp table to target table - no subtransaction wrapper */
46564806 flush_insert_exec_temp_table (estate );
46574807
4808+ /*
4809+ * Close the target table that was held open during INSERT EXEC.
4810+ * This must be done AFTER the flush completes.
4811+ */
4812+ pltsql_insert_exec_close_target_table ();
4813+
46584814 /*
46594815 * Clear the INSERT EXEC context AFTER the flush completes.
46604816 * This ensures the flush INSERT has access to target table info.
@@ -4663,7 +4819,8 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt)
46634819 }
46644820 PG_CATCH ();
46654821 {
4666- /* Clear context and drop temp table before re-throwing */
4822+ /* Close target table and clear context before re-throwing */
4823+ pltsql_insert_exec_close_target_table ();
46674824 pltsql_clear_insert_exec_context ();
46684825 drop_insert_exec_temp_table (insert_exec_temp_oid );
46694826 PG_RE_THROW ();
0 commit comments