Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1403d36
INSERT EXEC query rewriting POC - per-SELECT rewriting approach
monk0707 Feb 15, 2026
1c2db74
BABEL-5522: Add savepoint-based transaction atomicity for INSERT EXEC
monk0707 Feb 16, 2026
d8ff25a
BABEL-5522: Support INSERT EXEC with dynamic SQL variable
monk0707 Feb 16, 2026
4a4f577
BABEL-5522: Support INSERT EXEC sp_executesql
monk0707 Feb 17, 2026
340db8c
BABEL-5522: Support INSERT EXEC with return value capture (@RC = P)
monk0707 Feb 17, 2026
ea0cd92
Fix IDENTITY_INSERT with INSERT EXEC and temp table creation
monk0707 Feb 21, 2026
495ff7f
Fix square bracket identifiers with spaces in INSERT EXEC
monk0707 Feb 21, 2026
923032f
Fix INSERT EXEC transaction count mismatch and CTE support
monk0707 Feb 21, 2026
ea270b7
Remove debug LOG statements from INSERT EXEC implementation
monk0707 Feb 24, 2026
c989d4e
Fix INSERT EXEC error handling and transaction state management
monk0707 Feb 24, 2026
c02e716
BABEL-5522: Add comprehensive JDBC test for INSERT EXEC functionality
monk0707 Feb 24, 2026
203ba40
allowing all jdbc tests to run
monk0707 Feb 24, 2026
680b2fd
Fix server crash in handle_error by reverting to PG_RE_THROW
monk0707 Feb 25, 2026
03f0bef
Fix mod_stmt assertion failure in exec_rewritten_insert_exec
monk0707 Feb 25, 2026
6f66a64
Fix CI build error by removing non-existent mod_stmt_set field
monk0707 Feb 25, 2026
50f57c1
Fix errstart error in INSERT EXEC error handling
monk0707 Feb 25, 2026
e665292
Fix INSERT EXEC with OUTPUT parameters
monk0707 Feb 26, 2026
3017c31
Fix INSERT EXEC transaction handling with OUTPUT parameters
monk0707 Feb 27, 2026
ea31b4a
Fix INSERT-EXEC with OUTPUT parameters using query rewriting approach
monk0707 Feb 28, 2026
204ec7d
Fix the ordering of results in test cases and Fix the error messages
monk0707 Feb 28, 2026
144719d
Fixed cannot drop temp table inside procedure issue
monk0707 Mar 1, 2026
3554297
fix C90 compliance
monk0707 Mar 1, 2026
5e29e4b
copying error msg in expected output file just to test db collations …
monk0707 Mar 1, 2026
8f6ee18
Fix for parallel query execution crash while error occurs
monk0707 Mar 2, 2026
01f4e68
Fix for BABEL-2999-vu-verify test failing due to error msg change
monk0707 Mar 2, 2026
1321e96
Resolved the error msg difference issue
monk0707 Mar 2, 2026
99d9d2f
Fix for parallel query test failures
monk0707 Mar 2, 2026
f580b8d
Retrigger CI
monk0707 Mar 2, 2026
31c1136
Fix parallel query crashes by adding NULL check for exec_state_call_s…
monk0707 Mar 2, 2026
1c80f61
Fix INSERT EXEC snapshot error when target table has AFTER triggers
monk0707 Mar 6, 2026
9b0c625
rerun github CI tests
monk0707 Mar 6, 2026
df12e05
Fix INSERT EXEC to fire INSTEAD OF triggers on view targets
monk0707 Mar 6, 2026
7d020a7
Fix C90 failure error in pl_exec.c file
monk0707 Mar 6, 2026
968709c
Revert parallel query NULL checks that caused CI timeout
monk0707 Mar 7, 2026
68f12ee
Fix INSERT EXEC crash/timeout cycle with proper TRY-CATCH tracking
monk0707 Mar 7, 2026
3b06bdd
Complete NULL guards for exec_state_call_stack in iterative_exec.c
monk0707 Mar 7, 2026
551ac36
Add NULL guards for exec_state_call_stack in pl_exec.c and pl_exec-2.c
monk0707 Mar 7, 2026
2b10de7
Fix INSERT EXEC to respect SQL Server ownership chaining
monk0707 Mar 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 101 additions & 12 deletions contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ SendPendingDone(bool more)
{
uint32_t tdsVersion = GetClientTDSVersion();

TDS_DEBUG(TDS_DEBUG1, "TDS_DEBUG_TOKEN: SendPendingDone: more=%d, token=0x%02x, status=0x%04x, cmd=0x%04x, rowcnt=%lu",
more, TdsPendingDoneToken, TdsPendingDoneStatus, TdsPendingDoneCurCmd, TdsPendingDoneRowCnt);

TdsHavePendingDone = false;

/* In NOCOUNT=ON mode we need to suppress the DONE_COUNT */
Expand Down Expand Up @@ -2038,6 +2041,8 @@ void
TdsSendInfo(int number, int state, int class,
char *message, int lineNo)
{
TDS_DEBUG(TDS_DEBUG1, "TDS_DEBUG_TOKEN: TdsSendInfo called: number=%d, state=%d, class=%d, message=%s",
number, state, class, message);
TdsSendInfoOrError(TDS_TOKEN_INFO, number, state, class,
message,
"BABELFISH", /* TODO: where to get this? */
Expand All @@ -2049,6 +2054,9 @@ void
TdsSendError(int number, int state, int class,
char *message, int lineNo)
{
TDS_DEBUG(TDS_DEBUG1, "TDS_DEBUG_TOKEN: TdsSendError called: number=%d, state=%d, class=%d, message=%s",
number, state, class, message);

/*
* If not already in RESPONSE mode, switch the TDS protocol to RESPONSE
* mode.
Expand Down Expand Up @@ -2602,6 +2610,7 @@ TdsSendDone(int token, int status, int curcmd, uint64_t nprocessed)
int
TdsFlush(void)
{
TDS_DEBUG(TDS_DEBUG1, "TDS_DEBUG_TOKEN: TdsFlush called, TdsHavePendingDone=%d", TdsHavePendingDone);
SendPendingDone(false);

/* reset flags */
Expand All @@ -2627,6 +2636,8 @@ TDSStatementBeginCallback(PLtsql_execstate *estate, PLtsql_stmt *stmt)
return;

TDS_DEBUG(TDS_DEBUG3, "begin %d", tds_estate->current_stack);
TDS_DEBUG(TDS_DEBUG1, "TDS_DEBUG_TOKEN: StatementBegin: cmd_type=%d, current_stack=%d->%d",
stmt ? stmt->cmd_type : -1, tds_estate->current_stack, tds_estate->current_stack + 1);
tds_estate->current_stack++;

/* shouldn't have any un-handled error while begining the next statement */
Expand All @@ -2636,11 +2647,28 @@ TDSStatementBeginCallback(PLtsql_execstate *estate, PLtsql_stmt *stmt)
return;

/*
* TODO: It's possible that for some statements, we've to send a done toke
* when we start the command and another done token when we end the
* command. TRY..CATCH is one such example. We can use this function to
* send the done token at the beginning of the command.
* When a batch starts with TRY/CATCH (SAVE_CTX is the first statement),
* we need to send a "priming" DONE token. Without this, if the first
* statement inside TRY fails, the TDS client receives the INSERT's DONE
* token as the first token in the response, which causes "Invalid cursor
* state" errors on subsequent batches.
*
* The priming DONE token ensures the TDS client is in the correct state
* to handle subsequent tokens properly. This mimics what happens when
* there's a PRINT or other statement before the BEGIN TRY.
*/
if (stmt->cmd_type == PLTSQL_STMT_SAVE_CTX && tds_estate->current_stack == 1)
{
TDS_DEBUG(TDS_DEBUG1, "TDS_DEBUG_TOKEN: SAVE_CTX at batch start - sending priming DONE token");
/*
* Send a priming DONE token with no flags (0).
* TDS_DONE_MORE will be added by SendPendingDone when the next token
* is sent. This mimics what a PRINT statement would send.
* We use TDS_CMD_UNKNOWN (0x02) as the command type since this is just
* a placeholder token to prime the client's state machine.
*/
TdsSendDone(TDS_TOKEN_DONE, 0, TDS_CMD_UNKNOWN, 0);
}
}

static void
Expand All @@ -2659,6 +2687,8 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error)
TDS_DEBUG(TDS_DEBUG3, "end %d", tds_estate->current_stack);
toplevel = (tds_estate->current_stack == 0);

TDS_DEBUG(TDS_DEBUG1, "TDS_DEBUG_TOKEN: StatementEnd_Internal: cmd_type=%d, current_stack=%d, toplevel=%d, error=%d",
stmt ? stmt->cmd_type : -1, tds_estate->current_stack, toplevel, error);

/*
* If we're ending a statement, that means we've already handled the
Expand All @@ -2676,6 +2706,30 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error)
if (stmt == NULL)
return;

/*
* INSERT EXEC query rewriting: suppress ALL done tokens for statements
* inside the called procedure. This includes not just EXECSQL (the
* rewritten SELECTs→INSERTs) but also BLOCK, RETURN, GOTO, and any
* other statement types that the procedure contains.
*
* Without this, the procedure's BLOCK statement sends a DONEINPROC
* token that the TDS client doesn't expect, causing "Invalid cursor
* state" errors on subsequent batches.
*
* We only suppress non-toplevel tokens. The outer INSERT EXEC statement
* itself (at toplevel) needs its done token. By the time the outer
* statement's stmt_end fires, the rewrite context has already been
* cleared in PG_FINALLY, so this check naturally excludes it.
*/
if (!toplevel &&
pltsql_plugin_handler_ptr->is_insert_exec_rewrite_active &&
pltsql_plugin_handler_ptr->is_insert_exec_rewrite_active())
{
TDS_DEBUG(TDS_DEBUG1, "TDS_DEBUG_TOKEN: SKIP_DONE (insert_exec_rewrite) cmd_type=%d, stack=%d",
stmt->cmd_type, tds_estate->current_stack);
return;
}

/* TODO: handle all the cases */
switch (stmt->cmd_type)
{
Expand All @@ -2686,6 +2740,10 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error)
/* Used in multi-statement table valued functions */
case PLTSQL_STMT_DECL_TABLE:
case PLTSQL_STMT_RETURN_TABLE:
case PLTSQL_STMT_TRY_CATCH:
case PLTSQL_STMT_SAVE_CTX:
case PLTSQL_STMT_RESTORE_CTX_FULL:
case PLTSQL_STMT_RESTORE_CTX_PARTIAL:
{
/* Done token is not expected for these commands */
skip_done = true;
Expand Down Expand Up @@ -2722,23 +2780,21 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error)

/*
* row_count should be invalid if the INSERT
* is inside the procedure of an INSERT-EXEC,
* or if the INSERT itself is an INSERT-EXEC
* and it just returned error.
* itself is an INSERT-EXEC and it just
* returned error.
*/
row_count_valid = !estate->insert_exec &&
!(markErrorFlag &&
row_count_valid = !(markErrorFlag &&
((PLtsql_stmt_execsql *) stmt)->insert_exec);
}
else if (plansource->commandTag == CMDTAG_UPDATE)
{
command_type = TDS_CMD_UPDATE;
row_count_valid = !estate->insert_exec;
row_count_valid = true;
}
else if (plansource->commandTag == CMDTAG_DELETE)
{
command_type = TDS_CMD_DELETE;
row_count_valid = !estate->insert_exec;
row_count_valid = true;
}

/*
Expand All @@ -2748,7 +2804,7 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error)
else if (plansource->commandTag == CMDTAG_SELECT)
{
command_type = TDS_CMD_SELECT;
row_count_valid = !estate->insert_exec;
row_count_valid = true;
}
}
}
Expand All @@ -2771,6 +2827,30 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error)
{
is_proc = true;
command_type = TDS_CMD_EXECUTE;

/*
* For INSERT EXEC with dynamic SQL (INSERT INTO t EXEC(@var)),
* we need to send the row count. The insert_exec field is set
* when this is an INSERT EXEC statement.
*/
if (stmt->cmd_type == PLTSQL_STMT_EXEC_BATCH)
{
PLtsql_stmt_exec_batch *batch_stmt = (PLtsql_stmt_exec_batch *) stmt;
if (batch_stmt->insert_exec)
{
command_type = TDS_CMD_INSERT;
row_count_valid = true;
}
}
else if (stmt->cmd_type == PLTSQL_STMT_EXEC)
{
PLtsql_stmt_exec *exec_stmt = (PLtsql_stmt_exec *) stmt;
if (exec_stmt->insert_exec)
{
command_type = TDS_CMD_INSERT;
row_count_valid = true;
}
}
}
break;
default:
Expand Down Expand Up @@ -2837,7 +2917,11 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error)
* return from here.
*/
if (skip_done)
{
TDS_DEBUG(TDS_DEBUG1, "TDS_DEBUG_TOKEN: SKIP_DONE cmd_type=%d, stack=%d",
stmt ? stmt->cmd_type : -1, tds_estate->current_stack);
return;
}

/*
* If count is valid for this command, set the count and the corresponding
Expand All @@ -2861,6 +2945,8 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error)
else
flags |= TDS_DONE_MORE;

TDS_DEBUG(TDS_DEBUG1, "TDS_DEBUG_TOKEN: SEND_DONE cmd_type=%d, token_type=%d, flags=0x%x, command_type=%d, nprocessed=%lu, toplevel=%d, is_proc=%d",
stmt ? stmt->cmd_type : -1, token_type, flags, command_type, (unsigned long)nprocessed, toplevel, is_proc);
TdsSendDone(token_type, flags, command_type, nprocessed);
}

Expand All @@ -2879,6 +2965,9 @@ TDSStatementExceptionCallback(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool
if (tds_estate == NULL)
return;

TDS_DEBUG(TDS_DEBUG1, "TDS_DEBUG_TOKEN: StatementException: cmd_type=%d, current_stack=%d, terminate_batch=%d",
stmt ? stmt->cmd_type : -1, tds_estate->current_stack, terminate_batch);

TDS_DEBUG(TDS_DEBUG3, "exception %d", tds_estate->current_stack);

SetTdsEstateErrorData();
Expand Down
67 changes: 65 additions & 2 deletions contrib/babelfishpg_tsql/src/hooks.c
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,55 @@ pltsql_bbfCustomProcessUtility(ParseState *pstate, PlannedStmt *pstmt, const cha
}
case T_TransactionStmt:
{
if (NestedTranCount > 0 || (sql_dialect == SQL_DIALECT_TSQL && !IsTransactionBlockActive()))
TransactionStmt *txn_stmt = (TransactionStmt *) parsetree;
bool is_active = IsTransactionBlockActive();
bool rewrite_active = pltsql_insert_exec_rewrite_active();
bool estate_insert_exec = exec_state_call_stack &&
exec_state_call_stack->estate &&
exec_state_call_stack->estate->insert_exec;
bool in_insert_exec = estate_insert_exec || rewrite_active;

/*
* Block COMMIT/ROLLBACK that would terminate the outer transaction during INSERT EXEC.
* This check must happen BEFORE we decide whether to call PLTsqlProcessTransaction
* or fall through to standard PostgreSQL handling, because when NestedTranCount == 0
* and IsTransactionBlockActive() is true, we would fall through to standard handling
* which would terminate the outer INSERT EXEC transaction.
*
* SQL Server error 3916: Cannot use the COMMIT statement within an INSERT-EXEC
* statement unless BEGIN TRANSACTION is used first.
*
* SQL Server behavior:
* - COMMIT is blocked if NestedTranCount <= 1 (would end the entire transaction)
* - COMMIT is allowed if NestedTranCount >= 2 (just decrements nested count)
*
* This is because with NestedTranCount >= 2, there are nested transactions and
* COMMIT just decrements the count. With NestedTranCount <= 1, COMMIT would
* end the entire transaction which is not allowed during INSERT-EXEC.
*/
if (in_insert_exec && txn_stmt->kind == TRANS_STMT_COMMIT)
{
if (NestedTranCount <= 1)
{
/*
* Set AbortCurTransaction to true so the transaction will be
* aborted and NestedTranCount will be reset to 0.
*/
AbortCurTransaction = true;
ereport(ERROR,
(errcode(ERRCODE_TRANSACTION_ROLLBACK),
errmsg("Cannot use the COMMIT statement within an INSERT-EXEC statement unless BEGIN TRANSACTION is used first.")));
}
}

if (in_insert_exec && txn_stmt->kind == TRANS_STMT_ROLLBACK)
{
ereport(ERROR,
(errcode(ERRCODE_TRANSACTION_ROLLBACK),
errmsg("Cannot use the ROLLBACK statement within an INSERT-EXEC statement.")));
}

if (NestedTranCount > 0 || (sql_dialect == SQL_DIALECT_TSQL && !is_active))
{
PLTsqlProcessTransaction(parsetree, params, qc);
return true;
Expand Down Expand Up @@ -1412,6 +1460,21 @@ pltsql_ExecutorStart(QueryDesc *queryDesc, int eflags)
{
int ef = pltsql_explain_only ? EXEC_FLAG_EXPLAIN_ONLY : eflags;

/*
* Skip AFTER triggers for INSERT EXEC rewritten queries when requested.
* This prevents AfterTriggerBeginQuery/AfterTriggerEndQuery calls which
* can cause crashes when errors occur during INSERT EXEC and are caught
* by TRY-CATCH blocks. The crash happens because:
* 1. ExecutorStart calls AfterTriggerBeginQuery (increments query_depth)
* 2. Error during ExecutorRun skips ExecutorFinish (no AfterTriggerEndQuery)
* 3. Error handling tries to clean up, causing query_depth mismatch
* By skipping triggers, we avoid this state tracking entirely.
*/
if (pltsql_insert_exec_skip_triggers())
{
ef |= EXEC_FLAG_SKIP_TRIGGERS;
}

if (pltsql_explain_analyze)
{
PLtsql_execstate *estate = get_current_tsql_estate();
Expand Down Expand Up @@ -2196,7 +2259,7 @@ check_insert_row(List *icolumns, List *exprList, Oid relid)
if (exprList != NIL && list_length(exprList) < list_length(icolumns))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("Number of given values does not match target table definition")));
errmsg("INSERT has more target columns than expressions")));
}

char *
Expand Down
Loading
Loading