Skip to content

Commit 2d9d515

Browse files
committed
Fix INSERT EXEC error 556: Hold target table open during execution
This fix addresses SQL Server error 556: 'INSERT EXEC failed because the stored procedure altered the schema of the target table.' The issue was that our INSERT EXEC implementation didn't hold the target table open during procedure execution. This meant PostgreSQL's CheckTableNotInUse() couldn't detect when the procedure tried to ALTER TABLE on the target, allowing the ALTER to succeed when it should fail. Changes: - Add insert_exec_target_rel and insert_exec_target_rel_oid globals to track the held target table relation - Add pltsql_insert_exec_open_target_table() to open and hold the target table with RowExclusiveLock at INSERT EXEC start - Add pltsql_insert_exec_close_target_table() to close the target table after flush completes or on error cleanup - Call open_target_table after setting up INSERT EXEC context in all three code paths (exec_stmt_exec, exec_stmt_execsql, sp_executesql) - Call close_target_table in error handlers and after successful flush Benefits: - Properly detects schema alterations during INSERT EXEC (error 556) - Prevents concurrent modifications to the target table structure - Maintains consistency with SQL Server behavior Note: Temp tables and table variables (starting with # or @) are skipped since they're session-local and can't be modified by other sessions.
1 parent 1668473 commit 2d9d515

File tree

2 files changed

+164
-5
lines changed

2 files changed

+164
-5
lines changed

contrib/babelfishpg_tsql/src/pl_exec-2.c

Lines changed: 162 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
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
9697
static bool insert_exec_incremented_tran_count = false; /* True if INSERT EXEC incremented NestedTranCount */
9798
static bool insert_exec_had_error = false; /* True if INSERT EXEC had an error - used to skip trancount mismatch check */
9899
static 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 */
101104
typedef 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();

contrib/babelfishpg_tsql/src/pltsql.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2420,6 +2420,8 @@ extern bool pltsql_insert_exec_had_error(void);
24202420
extern void pltsql_insert_exec_clear_error_flag(void);
24212421
extern void pltsql_insert_exec_set_pending_drop(void);
24222422
extern void pltsql_insert_exec_check_pending_drop(void);
2423+
extern void pltsql_insert_exec_open_target_table(const char *target_table);
2424+
extern void pltsql_insert_exec_close_target_table(void);
24232425
extern DestReceiver *CreateInsertExecDestReceiver(Oid temp_table_oid);
24242426
extern Oid create_insert_exec_temp_table(const char *target_table, const char *column_list);
24252427
extern void drop_insert_exec_temp_table(Oid temp_table_oid);

0 commit comments

Comments
 (0)