Skip to content

Commit 1bee46c

Browse files
committed
Fix INSERT EXEC error 556: Detect schema changes during execution
Problem: SQL Server raises error 556 ('INSERT EXEC failed because the stored procedure altered the schema of the target table') when a procedure executed via INSERT EXEC modifies the target table's schema (e.g., ALTER TABLE ADD COLUMN). Babelfish was not detecting this scenario. Previous Approaches (Failed): 1. Holding target table open with table_open() - Fixed 556_1 test but caused 'relcache reference not owned by resource owner' errors in upgrade tests because Relation pointers cannot be held across subtransaction boundaries. 2. Using locks only with LockRelationOid() - Fixed upgrade tests but broke 556_1 because CheckTableNotInUse() only checks reference count, not locks. Solution (Schema Signature Approach): Instead of holding the Relation pointer open, we now: 1. Capture the target table's 'schema signature' at INSERT EXEC start: - Column count (natts) - Column type OIDs (atttypids) - Column type modifiers (atttypmods) 2. Acquire RowExclusiveLock on the target table (released at transaction end) 3. Before flushing buffered data to the target, verify the schema signature hasn't changed 4. If schema changed, raise error 556 with the appropriate SQL Server message This approach: - Detects ALTER TABLE operations that change column count or types - Avoids holding Relation pointers across subtransaction boundaries - Works correctly with upgrade tests (no relcache reference issues) - Properly handles temp tables and table variables (skips schema capture) Implementation Details: - Added InsertExecSchemaSignature struct to store column metadata - Added pltsql_insert_exec_verify_schema() to check schema before flush - Modified pltsql_insert_exec_open_target_table() to capture schema signature - Modified pltsql_insert_exec_close_target_table() to free schema signature - Modified flush_insert_exec_temp_table() to call verify and raise error 556 - Added error mapping for the new error message to map to SQL Server error 556 Files Changed: - contrib/babelfishpg_tsql/src/pl_exec-2.c: Schema signature implementation - contrib/babelfishpg_tsql/src/pltsql.h: Added pltsql_insert_exec_verify_schema() - contrib/babelfishpg_tds/error_mapping.txt: Added error 556 mapping - test/JDBC/expected/TestErrorHelperFunctions.out: Updated expected output Tests Verified: - 556_1: Passed (schema change detection) - BABEL-1944: Passed (upgrade test - no relcache issues) - BABEL-INSERT-EXEC: Passed (general INSERT EXEC functionality) - BABEL-INSERT-EXECUTE: Passed (INSERT EXECUTE functionality) - TestErrorHelperFunctions: Passed (error mapping validation)
1 parent 2d9d515 commit 1bee46c

File tree

4 files changed

+238
-39
lines changed

4 files changed

+238
-39
lines changed

contrib/babelfishpg_tds/error_mapping.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,5 @@ XX000 ERRCODE_INTERNAL_ERROR "The table-valued parameter \"%s\" must be declared
188188
42P01 ERRCODE_UNDEFINED_TABLE "FOR JSON AUTO requires at least one table for generating JSON objects. Use FOR JSON PATH or add a FROM clause with a table name." SQL_ERROR_13600 16
189189
42P01 ERRCODE_FEATURE_NOT_SUPPORTED "sub-select and values for json auto are not currently supported." SQL_ERROR_13600 16
190190
54000 ERRCODE_PROGRAM_LIMIT_EXCEEDED "nested INSERT ... EXECUTE statements are not allowed" SQL_ERROR_8164 16
191+
0A000 ERRCODE_FEATURE_NOT_SUPPORTED "INSERT EXEC failed because the stored procedure altered the schema of the target table." SQL_ERROR_556 16
191192

contrib/babelfishpg_tsql/src/pl_exec-2.c

Lines changed: 234 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,22 @@ static int insert_exec_call_stack_depth = 0; /* Call stack depth when INSERT EX
9797
static bool insert_exec_incremented_tran_count = false; /* True if INSERT EXEC incremented NestedTranCount */
9898
static bool insert_exec_had_error = false; /* True if INSERT EXEC had an error - used to skip trancount mismatch check */
9999
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 */
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 */
104118
typedef 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
*/
341356
void
342357
pltsql_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
*/
417492
void
418493
pltsql_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);

contrib/babelfishpg_tsql/src/pltsql.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2422,6 +2422,7 @@ extern void pltsql_insert_exec_set_pending_drop(void);
24222422
extern void pltsql_insert_exec_check_pending_drop(void);
24232423
extern void pltsql_insert_exec_open_target_table(const char *target_table);
24242424
extern void pltsql_insert_exec_close_target_table(void);
2425+
extern bool pltsql_insert_exec_verify_schema(void);
24252426
extern DestReceiver *CreateInsertExecDestReceiver(Oid temp_table_oid);
24262427
extern Oid create_insert_exec_temp_table(const char *target_table, const char *column_list);
24272428
extern void drop_insert_exec_temp_table(Oid temp_table_oid);

0 commit comments

Comments
 (0)