From efb879d66eeb5c017a6fc5e3c6ba9dc9d66d3f94 Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Wed, 3 Jun 2026 11:49:45 +0000 Subject: [PATCH 01/11] Complete Insert Exec Implementation --- .github/scripts/scan-warnings.sh | 4 +- .../src/backend/tds/tdsresponse.c | 46 +- contrib/babelfishpg_tsql/src/guc.c | 4 +- contrib/babelfishpg_tsql/src/hooks.c | 11 + contrib/babelfishpg_tsql/src/iterative_exec.c | 18 +- contrib/babelfishpg_tsql/src/pl_exec-2.c | 146 +- contrib/babelfishpg_tsql/src/pl_exec.c | 126 +- contrib/babelfishpg_tsql/src/pl_handler.c | 8 + contrib/babelfishpg_tsql/src/pl_insert_exec.c | 640 +++++++- contrib/babelfishpg_tsql/src/pltsql-2.h | 6 + contrib/babelfishpg_tsql/src/pltsql.h | 28 +- contrib/babelfishpg_tsql/src/pltsql_utils.c | 59 +- contrib/babelfishpg_tsql/src/tsqlIface.cpp | 16 + test/JDBC/expected/BABEL-INSERT-EXEC.out | 1316 +++++++++++++++++ test/JDBC/input/BABEL-INSERT-EXEC.sql | 938 ++++++++++++ 15 files changed, 3245 insertions(+), 121 deletions(-) create mode 100644 test/JDBC/expected/BABEL-INSERT-EXEC.out create mode 100644 test/JDBC/input/BABEL-INSERT-EXEC.sql diff --git a/.github/scripts/scan-warnings.sh b/.github/scripts/scan-warnings.sh index d487423b50d..89ccb100e89 100755 --- a/.github/scripts/scan-warnings.sh +++ b/.github/scripts/scan-warnings.sh @@ -49,8 +49,8 @@ if [[ "$SNAPSHOT_ACTIVE_COUNT" -ne 44 ]]; then ERROR_FOUND=true fi -if [[ "$LEAK_COUNT" -ne 416 ]]; then - echo "Error: Expected 416 leak warnings, but found $LEAK_COUNT" +if [[ "$LEAK_COUNT" -ne 29 ]]; then + echo "Error: Expected 29 leak warnings, but found $LEAK_COUNT" ERROR_FOUND=true fi diff --git a/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c b/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c index 9ddd879a50f..424f656a4a0 100644 --- a/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c +++ b/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c @@ -2730,20 +2730,34 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error) * is inside the procedure of an INSERT-EXEC, * or if the INSERT itself is an INSERT-EXEC * and it just returned error. + * + * INSERT EXEC detection covers both paths: the + * new path uses the global context + * (pltsql_insert_exec_active), the legacy path + * uses the per-estate flag (estate->insert_exec). */ - row_count_valid = !estate->insert_exec && + row_count_valid = + !(estate->insert_exec || + (pltsql_plugin_handler_ptr->pltsql_insert_exec_active && + pltsql_plugin_handler_ptr->pltsql_insert_exec_active())) && !(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 = + !(estate->insert_exec || + (pltsql_plugin_handler_ptr->pltsql_insert_exec_active && + pltsql_plugin_handler_ptr->pltsql_insert_exec_active())); } else if (plansource->commandTag == CMDTAG_DELETE) { command_type = TDS_CMD_DELETE; - row_count_valid = !estate->insert_exec; + row_count_valid = + !(estate->insert_exec || + (pltsql_plugin_handler_ptr->pltsql_insert_exec_active && + pltsql_plugin_handler_ptr->pltsql_insert_exec_active())); } /* @@ -2753,7 +2767,10 @@ 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 = + !(estate->insert_exec || + (pltsql_plugin_handler_ptr->pltsql_insert_exec_active && + pltsql_plugin_handler_ptr->pltsql_insert_exec_active())); } } } @@ -2776,6 +2793,27 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error) { is_proc = true; command_type = TDS_CMD_EXECUTE; + /* + * For INSERT EXEC, report the row count set in + * flush_insert_exec_temp_table(). Suppress it when an error is + * pending: the rows are rolled back, no count is sent to the + * client, and a counted DONE left pending here would otherwise + * carry a stale count into the following error DONE token. + */ + if (!markErrorFlag && + stmt->cmd_type == PLTSQL_STMT_EXEC && + ((PLtsql_stmt_exec *) stmt)->insert_exec != NULL) + { + command_type = TDS_CMD_INSERT; + row_count_valid = true; + } + else if (!markErrorFlag && + stmt->cmd_type == PLTSQL_STMT_EXEC_BATCH && + ((PLtsql_stmt_exec_batch *) stmt)->insert_exec != NULL) + { + command_type = TDS_CMD_INSERT; + row_count_valid = true; + } } break; default: diff --git a/contrib/babelfishpg_tsql/src/guc.c b/contrib/babelfishpg_tsql/src/guc.c index 0d29b156051..816b48ee1ec 100644 --- a/contrib/babelfishpg_tsql/src/guc.c +++ b/contrib/babelfishpg_tsql/src/guc.c @@ -59,7 +59,7 @@ bool pltsql_disable_batch_auto_commit = false; bool pltsql_disable_internal_savepoint = false; bool pltsql_disable_txn_in_triggers = false; bool pltsql_recursive_triggers = false; -bool pltsql_enable_new_insert_exec = false; +bool pltsql_enable_new_insert_exec = true; bool pltsql_noexec = false; bool pltsql_showplan_all = false; bool pltsql_showplan_text = false; @@ -957,7 +957,7 @@ define_custom_variables(void) gettext_noop("Enables INSERT...EXEC redesign code path"), NULL, &pltsql_enable_new_insert_exec, - false, + true, PGC_SUSET, GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_DISALLOW_IN_AUTO_FILE, NULL, NULL, NULL); diff --git a/contrib/babelfishpg_tsql/src/hooks.c b/contrib/babelfishpg_tsql/src/hooks.c index ea035176b7a..ef83309ab14 100644 --- a/contrib/babelfishpg_tsql/src/hooks.c +++ b/contrib/babelfishpg_tsql/src/hooks.c @@ -3622,6 +3622,17 @@ bbf_object_access_hook(ObjectAccessType access, Oid classId, Oid objectId, int s /* Call view dependency handling function */ handle_bbf_view_binding_on_object_drop(&obj, false); } + + /* + * Detect DROP of the INSERT EXEC target table. + * If the executed procedure drops the target table, we need to fail + * the INSERT EXEC to prevent errors during flush. + */ + if (OidIsValid(insert_exec_ctx.target_rel_oid) && + objectId == insert_exec_ctx.target_rel_oid) + { + insert_exec_ctx.is_target_relation_modified = true; + } } if (access == OAT_DROP && classId == ProcedureRelationId) { diff --git a/contrib/babelfishpg_tsql/src/iterative_exec.c b/contrib/babelfishpg_tsql/src/iterative_exec.c index a31643cf3a7..20e98243e7f 100644 --- a/contrib/babelfishpg_tsql/src/iterative_exec.c +++ b/contrib/babelfishpg_tsql/src/iterative_exec.c @@ -1333,6 +1333,7 @@ dispatch_stmt_handle_error(PLtsql_execstate *estate, if (!pltsql_implicit_transactions && is_batch_command(stmt) && !is_part_of_pltsql_trigger(estate) && + !pltsql_insert_exec_active() && before_tran_count != NestedTranCount) ereport(ERROR, (errcode(ERRCODE_T_R_INTEGRITY_CONSTRAINT_VIOLATION), @@ -1370,7 +1371,7 @@ dispatch_stmt_handle_error(PLtsql_execstate *estate, } else if (!IsTransactionBlockActive()) { - if (is_part_of_pltsql_trycatch_block(estate)) + if (is_part_of_pltsql_trycatch_block(estate) && !pltsql_insert_exec_active()) { HOLD_INTERRUPTS(); elog(DEBUG1, "TSQL TXN PG semantics : Rollback current transaction"); @@ -1603,6 +1604,21 @@ exec_stmt_iterative(PLtsql_execstate *estate, ExecCodes *exec_codes, ExecConfig_ estate->cur_error->severity = exec_state_call_stack->error_data.error_severity; estate->cur_error->state = exec_state_call_stack->error_data.error_state; + /* + * If a TRY-CATCH is inside the executed procedure, INSERT + * EXEC is still in progress. Re-throw column mismatch errors + * to roll back all rows. Other errors (e.g., division by + * zero) are caught by TRY-CATCH, preserving rows inserted + * before the error. + */ + if (!pltsql_insert_exec_error_at_trycatch_level() && + pltsql_insert_exec_active()) + { + if (estate->cur_error->error != NULL && + estate->cur_error->error->sqlerrcode == ERRCODE_DATATYPE_MISMATCH) + ReThrowError(estate->cur_error->error); + } + /* Goto error handling blocks */ *pc = err_handler_pc - 1; /* same as how goto handles PC */ diff --git a/contrib/babelfishpg_tsql/src/pl_exec-2.c b/contrib/babelfishpg_tsql/src/pl_exec-2.c index db2b18ee701..63beaaeeb87 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec-2.c +++ b/contrib/babelfishpg_tsql/src/pl_exec-2.c @@ -743,14 +743,29 @@ exec_stmt_push_result(PLtsql_execstate *estate, Assert(stmt->query != NULL); - /* Handle naked SELECT stmt differently for INSERT ... EXECUTE */ - if (estate->insert_exec) + /* + * Legacy INSERT EXEC path: handle naked SELECT stmt differently. + * estate->insert_exec is only set when the new INSERT EXEC GUC is off. + */ + if (!pltsql_enable_new_insert_exec && estate->insert_exec) return exec_stmt_insert_execute_select(estate, stmt->query); exec_run_select(estate, stmt->query, &portal); - receiver = CreateDestReceiver(DestRemote); - SetRemoteDestReceiverParams(receiver, portal); + /* + * When INSERT EXEC is active (new path), redirect results to the temp + * table instead of sending to client. + */ + if (pltsql_enable_new_insert_exec && pltsql_insert_exec_active()) + { + receiver = CreateInsertExecDestReceiver(); + receiver->rStartup(receiver, CMD_SELECT, portal->tupDesc); + } + else + { + receiver = CreateDestReceiver(DestRemote); + SetRemoteDestReceiverParams(receiver, portal); + } if (PortalRun(portal, FETCH_ALL, @@ -796,8 +811,20 @@ exec_run_dml_with_output(PLtsql_execstate *estate, PLtsql_stmt_push_result *stmt elog(ERROR, "could not open implicit cursor for query \"%s\": %s", expr->query, SPI_result_code_string(SPI_result)); - receiver = CreateDestReceiver(DestRemote); - SetRemoteDestReceiverParams(receiver, portal); + /* + * INSERT EXEC context check - redirect OUTPUT clause results to temp table + * instead of sending to client. + */ + if (pltsql_enable_new_insert_exec && pltsql_insert_exec_active()) + { + receiver = CreateInsertExecDestReceiver(); + receiver->rStartup(receiver, CMD_SELECT, portal->tupDesc); + } + else + { + receiver = CreateDestReceiver(DestRemote); + SetRemoteDestReceiverParams(receiver, portal); + } success = PortalRun(portal, FETCH_ALL, @@ -843,6 +870,12 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) /* whether procedure was created WITH RECOMPILE */ bool created_with_recompile = false; + /* INSERT EXEC handling - temp table lifecycle */ + bool insert_exec_setup_done = false; + + /* set true at the end of the PG_TRY body to distinguish success from error in PG_FINALLY */ + volatile bool exec_succeeded = false; + /* * We need to disable the explain gucs incase of sp_reset_connection * execution otherwise we will get explain output for it which is @@ -866,10 +899,17 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) int32 rettypmod; /* used for scalar function */ bool is_scalar_func; - /* for EXEC as part of inline code under INSERT ... EXECUTE */ + /* for EXEC as part of inline code under INSERT ... EXECUTE (legacy path) */ Tuplestorestate *tss; DestReceiver *dest; + /* + * Setup INSERT EXEC (new path): create temp table to capture procedure + * output. After procedure completes, temp table is flushed to target. + */ + if (pltsql_enable_new_insert_exec) + insert_exec_setup_done = insert_exec_setup(estate, stmt->insert_exec, true); + if (IS_TDS_CONN()) { if (strncmp(stmt->proc_name, "sp_", 3) == 0 && @@ -936,12 +976,18 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) stmt->is_scalar_func = is_scalar_func; - /* T-SQL doesn't allow procedure calls in a function */ + /* + * T-SQL doesn't allow procedure calls in a function, EXCEPT when + * the procedure is being called as part of INSERT EXEC. In that case, + * the procedure's output is captured into a table variable, which is + * allowed in T-SQL functions. + */ if (estate->func && estate->func->fn_oid != InvalidOid && estate->func->fn_prokind == PROKIND_FUNCTION && estate->func->fn_is_trigger == PLTSQL_NOT_TRIGGER /* check EXEC is running * in the body of * function */ - && !is_scalar_func) /* in case of EXEC on scalar function, it is + && !is_scalar_func /* in case of EXEC on scalar function, it is * allowed in T-SQL. do not throw an error */ + && stmt->insert_exec == NULL) /* INSERT EXEC into table variable is allowed in functions */ { ereport(ERROR, (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), @@ -1154,12 +1200,13 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) stmt->target = (PLtsql_variable *) row; } - if (estate->insert_exec) + if (!pltsql_enable_new_insert_exec && estate->insert_exec) { /* * For EXEC under INSERT ... EXECUTE, get the expected TupleDesc, * create a DestReceiver and pass both to the CallStmt so that it * will know to accumulate result rows and send them back here. + * Note : Legacy codepath for insert-exec, remove it during cleanup. */ Node *node; @@ -1245,13 +1292,14 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) } } - if (estate->insert_exec) + if (!pltsql_enable_new_insert_exec && estate->insert_exec) { /* * For EXEC under INSERT ... EXECUTE, get the rows sent back by * the CallStmt, and store them into estate->tuple_store so that * at the end of function execution they will be sent to the right * place. + * Note : Legacy insert-exec codepath, needs to remove during cleanup. */ TupleTableSlot *slot = MakeSingleTupleTableSlot(estate->rsi->expectedDesc, &TTSOpsMinimalTuple); @@ -1271,9 +1319,19 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) dest->rShutdown(dest); dest->rDestroy(dest); } + + exec_succeeded = true; } PG_FINALLY(); { + /* + * On the error path (new INSERT EXEC path only), tear down the temp + * table context before re-throwing. + */ + if (!exec_succeeded && + (insert_exec_setup_done || pltsql_insert_exec_active())) + pltsql_insert_exec_reset_all(); + if (strcmp(get_current_pltsql_db_name(), save_db_name) != 0) set_cur_user_db_and_path(save_db_name, false, false); @@ -1317,6 +1375,10 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) exec_eval_cleanup(estate); SPI_freetuptable(SPI_tuptable); + /* Flush temp table to target table and cleanup after procedure completes */ + if (insert_exec_setup_done) + insert_exec_success_cleanup(estate, stmt->insert_exec); + return PLTSQL_RC_OK; } @@ -1491,6 +1553,11 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt) char *old_db_name = get_cur_db_name(); char *cur_db_name = NULL; + /* INSERT EXEC handling - temp table lifecycle */ + bool insert_exec_setup_done = false; + + /* set true at the end of the PG_TRY body to distinguish success from error in PG_FINALLY */ + volatile bool exec_succeeded = false; LOCAL_FCINFO(fcinfo, 1); /* @@ -1508,6 +1575,14 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt) PG_TRY(); { + /* + * Setup INSERT EXEC (new path): create temp table to capture procedure + * output. No implicit transaction for dynamic SQL (different semantics + * than stored procs). + */ + if (pltsql_enable_new_insert_exec) + insert_exec_setup_done = insert_exec_setup(estate, stmt->insert_exec, false); + /* Get the C-String representation */ querystr = convert_value_to_string(estate, query, restype); @@ -1528,9 +1603,16 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt) if (fcinfo->isnull) elog(ERROR, "pltsql_inline_handler failed"); + + exec_succeeded = true; } PG_FINALLY(); { + /* On the error path (new INSERT EXEC path only), tear down the temp table context. */ + if (!exec_succeeded && + (insert_exec_setup_done || pltsql_insert_exec_active())) + pltsql_insert_exec_reset_all(); + /* Restore past settings */ pltsql_revert_guc(save_nestlevel); pltsql_revert_last_scope_identity(scope_level); @@ -1557,6 +1639,11 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt) pltsql_create_econtext(estate); } exec_eval_cleanup(estate); + + /* Flush temp table to target and cleanup */ + if (insert_exec_setup_done) + insert_exec_success_cleanup(estate, stmt->insert_exec); + return PLTSQL_RC_OK; } @@ -2155,6 +2242,12 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt) int scope_level; InlineCodeBlockArgs *args = NULL; + /* INSERT EXEC handling - temp table lifecycle */ + bool insert_exec_setup_done = false; + + /* set true at the end of the PG_TRY body to distinguish success from error in PG_FINALLY */ + volatile bool exec_succeeded = false; + batch = exec_eval_expr(estate, stmt->query, &isnull1, &restype1, &restypmod1); if (isnull1) { @@ -2200,6 +2293,15 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt) PG_TRY(); { + /* + * INSERT EXEC handling (new path): + * If this is an INSERT EXEC statement (set by parser), create temp table here. + * The procedure output will be redirected to this temp table. + * After procedure completes, we flush temp table to target and cleanup. + */ + if (pltsql_enable_new_insert_exec) + insert_exec_setup_done = insert_exec_setup(estate, stmt->insert_exec, true); + if (strcmp(batchstr, "") != 0) /* check edge cases for * sp_executesql */ { @@ -2210,13 +2312,25 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt) { exec_assign_value(estate, estate->datums[stmt->return_code_dno], Int32GetDatum(ret), false, INT4OID, 0); } + + exec_succeeded = true; } PG_FINALLY(); { + /* On the error path (new INSERT EXEC path only), tear down the temp table context. */ + if (!exec_succeeded && + (insert_exec_setup_done || pltsql_insert_exec_active())) + pltsql_insert_exec_reset_all(); + pltsql_revert_guc(save_nestlevel); pltsql_revert_last_scope_identity(scope_level); } PG_END_TRY(); + + /* Flush temp table to target and cleanup after sp_executesql completes */ + if (insert_exec_setup_done) + insert_exec_success_cleanup(estate, stmt->insert_exec); + break; } case PLTSQL_EXEC_SP_EXECUTE: @@ -3217,6 +3331,7 @@ bool called_from_tsql_insert_exec() * the client, we accumulate the result in estate->tuple_store (similar to * exec_stmt_return_query). Finally the EXECUTE stmt will return the result to * the INSERT stmt as rows to insert. + * Note : Used by the legacy INSERT EXEC path, needs to remove during cleanup. */ static int exec_stmt_insert_execute_select(PLtsql_execstate *estate, PLtsql_expr *query) @@ -3822,6 +3937,15 @@ execute_plan_and_push_result(PLtsql_execstate *estate, PLtsql_expr *expr, ParamL { receiver = None_Receiver; } + else if (pltsql_enable_new_insert_exec && pltsql_insert_exec_active()) + { + /* + * INSERT EXEC context is active (new path) - redirect results to temp + * table instead of sending to client. + */ + receiver = CreateInsertExecDestReceiver(); + receiver->rStartup(receiver, CMD_SELECT, portal->tupDesc); + } else { receiver = CreateDestReceiver(DestRemote); diff --git a/contrib/babelfishpg_tsql/src/pl_exec.c b/contrib/babelfishpg_tsql/src/pl_exec.c index e15a9fcdba8..b66c51c3d39 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_exec.c @@ -442,9 +442,9 @@ static pltsql_CastHashEntry *get_cast_hashentry(PLtsql_execstate *estate, Oid srctype, int32 srctypmod, Oid dsttype, int32 dsttypmod); static void exec_init_tuple_store(PLtsql_execstate *estate); -static void exec_set_found(PLtsql_execstate *estate, bool state); +void exec_set_found(PLtsql_execstate *estate, bool state); static void exec_set_fetch_status(PLtsql_execstate *estate, int status); -static void exec_set_rowcount(uint64 rowno); +void exec_set_rowcount(uint64 rowno); static void exec_set_error(PLtsql_execstate *estate, int error, int pg_error, bool error_mapping_failed); static void pltsql_create_econtext(PLtsql_execstate *estate); static void pltsql_commit_not_required_impl_txn(PLtsql_execstate *estate); @@ -496,7 +496,7 @@ extern int static void pltsql_exec_function_cleanup(PLtsql_execstate *estate, PLtsql_function *func, ErrorContextCallback *plerrcontext); -/* Function to set up row Datum */ +/* Function to set up row Datum for INSERT EXEC (legacy path) */ static void setup_procedure_output_target_for_insert_exec(PLtsql_execstate *estate, PLtsql_stmt_execsql *stmt); @@ -740,7 +740,10 @@ pltsql_exec_function(PLtsql_function *func, FunctionCallInfo fcinfo, MemoryContextSwitchTo(oldcxt); } - /* Obtain output parameters for Insert Execute */ + /* + * Obtain output parameters for Insert Execute + * Note : It's insert-exec legacy codepath need to remove during cleanup. + */ if (estate.insert_exec) { /* Switch to function's memory context */ @@ -4394,7 +4397,11 @@ pltsql_estate_setup(PLtsql_execstate *estate, /* * When executing a procedure or inline code block, if a ReturnSetInfo is * passed in, then it's invoked by INSERT ... EXECUTE. + * Note : It's used by legacy insert-exec codepath, need to remove during cleanup. */ + if (pltsql_enable_new_insert_exec) + estate->insert_exec = false; + else estate->insert_exec = (func->fn_prokind == PROKIND_PROCEDURE || strcmp(func->fn_signature, "inline_code_block") == 0) && rsi; @@ -4476,7 +4483,7 @@ execute_txn_command(PLtsql_execstate *estate, PLtsql_stmt_execsql *stmt) * is recreated when needed for cases like commit/ * rollbck/rollback to savepoint */ -static void +void commit_stmt(PLtsql_execstate *estate, bool txnStarted) { SimpleEcontextStackEntry *topEntry = simple_econtext_stack; @@ -4665,6 +4672,7 @@ is_impl_txn_required_for_execsql(PLtsql_stmt_execsql *stmt) * * This is a helper to adapt logic from exec_stmt_call. It constructs a PLtsql_row to capture * output parameters from a procedure call within an INSERT EXECUTE context. + * Used by the legacy INSERT EXEC path (GUC babelfishpg_tsql.enable_new_insert_exec = false). */ static void setup_procedure_output_target_for_insert_exec(PLtsql_execstate *estate, PLtsql_stmt_execsql *stmt) @@ -4841,10 +4849,33 @@ exec_stmt_execsql(PLtsql_execstate *estate, set_cur_user_db_and_path(stmt->db_name, true, false); } + /* + * For INSERT EXEC (new path), validate column count BEFORE plan + * preparation so column mismatch errors take priority over runtime errors + * (e.g., 1/0). PostgreSQL's eval_const_expressions() would evaluate + * expressions first. + * + * Only validate inside TRY blocks; system procedures like sp_columns + * have internal SELECTs with varying column counts outside TRY blocks. + */ + if (pltsql_enable_new_insert_exec && + pltsql_insert_exec_active() && + is_part_of_pltsql_trycatch_block(estate)) + { + if (stmt->sqlstmt && stmt->sqlstmt->query) + { + pltsql_insert_exec_validate_column_count_from_query(stmt->sqlstmt->query); + } + } + PG_TRY(); { - /* Handle naked SELECT stmt differently for INSERT ... EXECUTE */ - if (stmt->need_to_push_result && estate->insert_exec) + /* + * Legacy INSERT EXEC path: handle naked SELECT stmt differently. + * estate->insert_exec is only set when the new INSERT EXEC GUC is off. + */ + if (!pltsql_enable_new_insert_exec && + stmt->need_to_push_result && estate->insert_exec) { int ret = exec_stmt_insert_execute_select(estate, expr); @@ -4884,28 +4915,36 @@ exec_stmt_execsql(PLtsql_execstate *estate, */ paramLI = setup_param_list(estate, expr); - /* Check for nested INSERT EXECUTE statements */ - if (stmt->insert_exec) + /* + * Legacy INSERT EXEC path: nested-INSERT-EXEC check and output target + * setup. estate->insert_exec / stmt->insert_exec are only set when the + * new INSERT EXEC GUC is off. + */ + if (!pltsql_enable_new_insert_exec) { - /* Walk existing stack for any parent insert exec */ - PLExecStateCallStack *cur = exec_state_call_stack; - while (cur != NULL) + /* Check for nested INSERT EXECUTE statements */ + if (stmt->insert_exec) { - /* Found parent insert exec - this is a nested INSERT EXECUTE */ - if (cur->estate->insert_exec) + /* Walk existing stack for any parent insert exec */ + PLExecStateCallStack *cur = exec_state_call_stack; + while (cur != NULL) { - ereport(ERROR, - (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("nested INSERT ... EXECUTE statements are not allowed"))); + /* Found parent insert exec - this is a nested INSERT EXECUTE */ + if (cur->estate->insert_exec) + { + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("nested INSERT ... EXECUTE statements are not allowed"))); + } + cur = cur->next; } - cur = cur->next; } - } - /* Setup output target for procedure parameters */ - if (stmt->insert_exec && stmt->target == NULL) - { - setup_procedure_output_target_for_insert_exec(estate, stmt); + /* Setup output target for procedure parameters */ + if (stmt->insert_exec && stmt->target == NULL) + { + setup_procedure_output_target_for_insert_exec(estate, stmt); + } } /* @@ -5082,8 +5121,12 @@ exec_stmt_execsql(PLtsql_execstate *estate, break; } - /* Update the output parameter */ - if (stmt->insert_exec && stmt->target && execute_call_insert_exec_retval != (Datum) 0) + /* + * Update the output parameter + * Note : It's insert-exec legacy codepath, need to remove during cleanup. + */ + if (!pltsql_enable_new_insert_exec && + stmt->insert_exec && stmt->target && execute_call_insert_exec_retval != (Datum) 0) { exec_move_row_from_datum(estate, stmt->target, execute_call_insert_exec_retval); } @@ -5237,12 +5280,20 @@ exec_stmt_execsql(PLtsql_execstate *estate, * Always commit to match auto commit behavior for each statement * inside batch or procedure, but not user-defined function or * procedure invoked by INSERT ... EXECUTE. + * + * Also skip commit during INSERT EXEC or its flush phase to avoid + * orphaning SPI portal snapshots. + * + * INSERT EXEC detection differs by path: the new path uses the global + * context (pltsql_insert_exec_active), the legacy path uses the + * per-estate flag (estate->insert_exec). */ /* TODO To let procedure call from PSQL work with old semantics */ if ((!pltsql_disable_batch_auto_commit || (stmt->txn_data != NULL)) && support_tsql_trans && (enable_txn_in_triggers || estate->trigdata == NULL) && - !ro_func && !estate->insert_exec) + !ro_func && + (pltsql_enable_new_insert_exec ? !pltsql_insert_exec_active() : !estate->insert_exec)) { commit_stmt(estate, (estate->tsql_trigger_flags & TSQL_TRAN_STARTED)); @@ -9666,7 +9717,7 @@ contains_target_param(Node *node, int *target_dno) * exec_set_found Set the global found variable to true/false * ---------- */ -static void +void exec_set_found(PLtsql_execstate *estate, bool state) { PLtsql_var *var; @@ -9693,7 +9744,7 @@ exec_set_fetch_status(PLtsql_execstate *estate, int status) fetch_status_var = status; } -static void +void exec_set_rowcount(uint64 rowno) { rowcount_var = rowno; @@ -9883,6 +9934,17 @@ pltsql_estate_cleanup(void) top_es_entry->estate->stmt_mcontext_parent); pfree(exec_state_call_stack); exec_state_call_stack = top_es_entry; + + /* + * Clear stale INSERT EXEC context when the call stack becomes empty. + * This is a safety net to prevent context from leaking between batches. + * Primary cleanup happens in exec_stmt_exec error handlers, but this + * ensures cleanup even if those paths are not taken. + */ + if (exec_state_call_stack == NULL && pltsql_insert_exec_active()) + { + pltsql_insert_exec_reset_all(); + } } /* @@ -9926,6 +9988,14 @@ pltsql_xact_cb(XactEvent event, void *arg) if (event == XACT_EVENT_COMMIT || event == XACT_EVENT_ABORT) { ResetTopTransactionName(); + + /* + * Clean up INSERT EXEC context on transaction end. This is a safety + * net for timeouts, interrupts, and other cases where normal cleanup + * paths are bypassed. On commit, any remaining context is stale. + */ + if (pltsql_insert_exec_active()) + pltsql_insert_exec_reset_all(); } /* diff --git a/contrib/babelfishpg_tsql/src/pl_handler.c b/contrib/babelfishpg_tsql/src/pl_handler.c index 5475d0d49ca..bfe802c9b72 100644 --- a/contrib/babelfishpg_tsql/src/pl_handler.c +++ b/contrib/babelfishpg_tsql/src/pl_handler.c @@ -6502,6 +6502,7 @@ _PG_init(void) (*pltsql_protocol_plugin_ptr)->sql_bytea_from_geography = common_utility_plugin_ptr->bytea_from_geography; (*pltsql_protocol_plugin_ptr)->sql_geometry_from_bytea = common_utility_plugin_ptr->geometry_from_bytea; (*pltsql_protocol_plugin_ptr)->sql_geography_from_bytea = common_utility_plugin_ptr->geography_from_bytea; + (*pltsql_protocol_plugin_ptr)->pltsql_insert_exec_active = &pltsql_insert_exec_active; } get_language_procs("pltsql", &lang_handler_oid, &lang_validator_oid); @@ -6678,6 +6679,13 @@ terminate_batch(bool send_error, bool compile_error, int SPI_depth) pltsql_non_tsql_proc_entry_count = 0; Assert(pltsql_sys_func_entry_count == 0); + /* + * Clear stale INSERT EXEC context at the end of each top-level batch. + * This is a safety net to prevent context from leaking between batches. + */ + if (pltsql_insert_exec_active()) + pltsql_insert_exec_reset_all(); + if (pltsql_snapshot_portal != NULL) { /* Must be active portal, otherwise should not be installed */ diff --git a/contrib/babelfishpg_tsql/src/pl_insert_exec.c b/contrib/babelfishpg_tsql/src/pl_insert_exec.c index f26e340851a..4c6e40fa363 100644 --- a/contrib/babelfishpg_tsql/src/pl_insert_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_insert_exec.c @@ -4,6 +4,7 @@ * It implements: * - Temp table lifecycle: creation, flush, and drop * - Dest Receiver callback functions implementation + * - InsertExecContext: global state tracking for an active INSERT EXEC *------------------------------------------------------------------------- */ @@ -11,35 +12,50 @@ #include "pltsql.h" #include "pltsql-2.h" +#include "funcapi.h" + +#include "access/parallel.h" #include "access/table.h" +#include "access/heapam.h" +#include "parser/parser.h" +#include "access/tableam.h" +#include "access/attmap.h" +#include "access/tupconvert.h" #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/pg_attribute.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_proc.h" #include "commands/defrem.h" #include "executor/executor.h" +#include "executor/spi_priv.h" +#include "executor/tstoreReceiver.h" #include "executor/tuptable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/parsenodes.h" +#include "optimizer/optimizer.h" #include "parser/parse_coerce.h" #include "parser/scansup.h" +#include "parser/parse_oper.h" #include "tcop/dest.h" #include "utils/builtins.h" #include "utils/lsyscache.h" +#include "utils/syscache.h" +#include "utils/varlena.h" +#include "storage/lmgr.h" #include "catalog.h" #include "multidb.h" +#include "pltsql_permissions.h" #include "session.h" -extern int execute_batch(PLtsql_execstate *estate, char *batch, InlineCodeBlockArgs *args, List *params); - /* * DestReceiver struct for INSERT EXEC - captures procedure output into a temp table. */ typedef struct { DestReceiver pub; /* public fields */ - Relation temp_rel; /* open relation, closed in cleanup */ /* Projection infrastructure — coerce_to_target_type is a no-op when types already match */ ExprContext *econtext; /* expression context for projection */ ProjectionInfo *proj_info; /* projection info for coercion */ @@ -58,6 +74,36 @@ static void insertexec_destroy(DestReceiver *self); */ InsertExecContext insert_exec_ctx; +extern void exec_set_rowcount(uint64 rowno); +extern void exec_set_found(PLtsql_execstate *estate, bool state); + +/* + * Build a comma-separated list of quoted column identifiers from the parser's + * List of column-name strings, for use in the temp table CREATE and the flush + * INSERT. Returns NULL (no explicit column list) when columns is NIL. The + * caller is responsible for pfree'ing the returned string. + */ +static char * +build_quoted_column_list(List *columns) +{ + StringInfoData cols; + ListCell *lc; + bool first = true; + + if (columns == NIL) + return NULL; + + initStringInfo(&cols); + foreach(lc, columns) + { + if (!first) + appendStringInfoString(&cols, ", "); + first = false; + appendStringInfoString(&cols, quote_identifier((char *) lfirst(lc))); + } + return cols.data; +} + /* * Get a comma-separated list of non-IDENTITY, non-computed column names * for a table by opening the relation and iterating over its tuple descriptor. @@ -132,6 +178,325 @@ get_insertable_column_list(const char *table_name, const char *physical_schema) } + +/* + * Set the global INSERT EXEC context with target table info. + * Called from ANTLR parser when INSERT EXEC is detected. + * This is called BEFORE temp table creation - just stores the target info. + */ +void +pltsql_set_insert_exec_context_info(const char *target_table) +{ + insert_exec_ctx.target_table = target_table ? pstrdup(target_table) : NULL; + /* + * Snapshot the call stack entry at INSERT EXEC start. Comparing this + * pointer later tells us whether an error occurred at the INSERT EXEC + * level or inside the executed procedure. + */ + insert_exec_ctx.call_stack_entry = exec_state_call_stack; +} + +/* + * Reset the global INSERT EXEC context to a clean state. + * + * Releases the target table lock, frees the heap-allocated target table name, + * and zeroes every field. Used on both the normal exit and safety-net cleanup + * paths. The string must be pfree'd before the memset, or memset alone would + * leak it in TopMemoryContext. + */ +void +pltsql_insert_exec_reset_all(void) +{ + /* Release target table lock */ + pltsql_insert_exec_close_target_table(); + + /* Free heap-allocated target table name before zeroing its pointer */ + if (insert_exec_ctx.target_table) + pfree(insert_exec_ctx.target_table); + + /* Reset all fields to zero/NULL */ + memset(&insert_exec_ctx, 0, sizeof(InsertExecContext)); +} + +/* + * Capture target table OID and lock for change detection. + * Regular tables get RowExclusiveLock to block concurrent DDL; + * temp tables only get OID captured (session-local). + * + * Schema changes are detected via is_target_relation_modified flag, + * which is set by the ObjectPostAlterHook when the target table is altered. + */ +void +pltsql_insert_exec_open_target_table(const char *target_table, + const char *schema_name_in, + const char *db_name_in) +{ + RangeVar *rv; + Oid relid; + char *schema_name = NULL; + char *table_name = NULL; + char *physical_schema = NULL; + bool is_temp_table; + + if (target_table == NULL) + return; + + is_temp_table = (target_table[0] == '#' || target_table[0] == '@'); + + if (is_temp_table) + { + /* + * Temp table or table variable - resolve using RangeVarGetRelid. + * We don't need to lock because they're session-local. + */ + rv = makeRangeVar(NULL, pstrdup(target_table), -1); + relid = RangeVarGetRelid(rv, NoLock, true); + + if (!OidIsValid(relid)) + return; + + /* Store the OID for schema verification (no lock for temp tables) */ + insert_exec_ctx.target_rel_oid = relid; + } + else + { + table_name = pstrdup(target_table); + if (schema_name_in != NULL) + schema_name = pstrdup(schema_name_in); + else + schema_name = pstrdup("dbo"); /* default schema */ + /* + * Resolve against the target's database when a 3-part name + * (db..table) was used; otherwise the current database. + */ + physical_schema = get_physical_schema_name( + (db_name_in != NULL) ? (char *) db_name_in : get_cur_db_name(), + schema_name); + + /* Create RangeVar and get the relation OID */ + rv = makeRangeVar(physical_schema, table_name, -1); + relid = RangeVarGetRelid(rv, NoLock, true); + + if (schema_name) + pfree(schema_name); + if (table_name) + pfree(table_name); + if (physical_schema) + pfree(physical_schema); + + if (!OidIsValid(relid)) + { + /* Table doesn't exist - will be caught later during flush */ + return; + } + + /* + * Acquire RowExclusiveLock on the target table. + * This lock will be held until the end of the transaction. + * It blocks concurrent sessions from modifying the table. + * Note: Same-session DROP/ALTER is still allowed by PostgreSQL, + * but we detect it via is_target_relation_modified flag set by + * ObjectPostAlterHook. + */ + PG_TRY(); + { + LockRelationOid(relid, RowExclusiveLock); + } + PG_CATCH(); + { + FlushErrorState(); + return; + } + PG_END_TRY(); + + insert_exec_ctx.target_rel_oid = relid; + } + + /* Initialize the modification flag to false */ + insert_exec_ctx.is_target_relation_modified = false; +} + +/* + * Close the target table that was held open during INSERT EXEC. + * Called after the flush completes or on error cleanup. + * + * For regular tables: Release the RowExclusiveLock we acquired. + * For temp tables: Just clear the OID (no lock was acquired). + * + * Note: We only release the lock if we're not in an aborted transaction state. + * If the transaction was aborted, the lock has already been released. + */ +void +pltsql_insert_exec_close_target_table(void) +{ + if (OidIsValid(insert_exec_ctx.target_rel_oid)) + { + const char *target = insert_exec_ctx.target_table; + bool is_temp_table = (target != NULL && (target[0] == '#' || target[0] == '@')); + + /* + * Only release the lock for regular tables (not temp tables). + * Temp tables don't have locks to release. + */ + if (!is_temp_table && !IsAbortedTransactionBlockState()) + { + PG_TRY(); + { + UnlockRelationOid(insert_exec_ctx.target_rel_oid, RowExclusiveLock); + } + PG_CATCH(); + { + FlushErrorState(); + /* Ignore unlock failures - table may have been dropped */ + } + PG_END_TRY(); + } + insert_exec_ctx.target_rel_oid = InvalidOid; + } + + /* Reset the modification flag */ + insert_exec_ctx.is_target_relation_modified = false; +} + +/* + * Validate column count from query string BEFORE plan preparation. + * + * T-SQL requires column-mismatch errors to take priority over runtime errors + * (e.g. division by zero) inside TRY-CATCH. PostgreSQL evaluates constant + * expressions during plan *preparation*, so we use SPI_prepare here, which + * parse-analyzes and rewrites the query (producing its result tuple + * descriptor) without planning it - hence without triggering constant folding. + * Comparing that descriptor's column count to the temp table lets the mismatch + * win over a constant-folded runtime error. + * + * If the result shape can't be determined (parse/analyze error, non + * row-returning statement such as EXEC, or multiple statements), we defer to + * the normal path - the DestReceiver still catches mismatches at runtime. + */ +void +pltsql_insert_exec_validate_column_count_from_query(const char *query_string) +{ + SPIPlanPtr plan = NULL; + CachedPlanSource *plansource; + TupleDesc result_desc; + int query_natts; + Oid temp_table_oid; + Relation temp_rel; + TupleDesc temp_tupdesc; + int temp_natts; + + /* Caller must ensure INSERT EXEC is active before calling */ + Assert(pltsql_insert_exec_active()); + + /* + * Temp table must exist. It is created during INSERT EXEC setup before any + * procedure-body statement runs, so it is normally valid here; if it isn't + * (context not fully set up yet), defer to the normal execution path. + */ + temp_table_oid = insert_exec_ctx.temp_table_oid; + if (!OidIsValid(temp_table_oid)) + return; + + /* + * Parse-analyze the query to obtain its result descriptor. SPI_prepare + * stops before planning, so constant expressions are not evaluated and a + * runtime error like division by zero will not fire here. On any error, + * defer to the normal execution path. + */ + PG_TRY(); + { + plan = SPI_prepare(query_string, 0, NULL); + } + PG_CATCH(); + { + FlushErrorState(); + return; /* Parse/analyze error - normal path will report it */ + } + PG_END_TRY(); + + /* Expect exactly one analyzed statement with a known result shape */ + if (plan == NULL || list_length(plan->plancache_list) != 1) + { + if (plan != NULL) + SPI_freeplan(plan); + return; /* Multiple statements or unusable plan, defer to runtime */ + } + + plansource = (CachedPlanSource *) linitial(plan->plancache_list); + result_desc = plansource->resultDesc; + + /* + * resultDesc is NULL for statements that do not return tuples (e.g. EXEC). + * In that case there is nothing to validate statically; defer to runtime. + */ + if (result_desc == NULL) + { + SPI_freeplan(plan); + return; + } + + query_natts = result_desc->natts; + SPI_freeplan(plan); + + /* Get temp table column count */ + PG_TRY(); + { + temp_rel = table_open(temp_table_oid, AccessShareLock); + temp_tupdesc = RelationGetDescr(temp_rel); + temp_natts = temp_tupdesc->natts; + table_close(temp_rel, AccessShareLock); + } + PG_CATCH(); + { + FlushErrorState(); + return; + } + PG_END_TRY(); + + /* Check for column count mismatch */ + if (query_natts != temp_natts) + { + /* + * A column mismatch must roll back all rows even when caught by + * TRY-CATCH, unlike data-level errors (e.g. division by zero) which + * only drop the current row. + */ + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("structure of query does not match function result type"))); + } +} + +/* + * Check if INSERT EXEC context is active (target table info is set). + * This returns true even before temp table is created. + */ +bool +pltsql_insert_exec_active(void) +{ + return (insert_exec_ctx.target_table != NULL); +} + +/* + * Called from the sigsetjmp handler when a TRY-CATCH catches an error during + * INSERT EXEC. Returns true if the error surfaced at the INSERT EXEC level + * (call-stack head matches where INSERT EXEC started), false if the catching + * TRY-CATCH is deeper inside the executed procedure - the caller uses the + * false case to re-throw a column-mismatch error past the inner handler. + */ +bool +pltsql_insert_exec_error_at_trycatch_level(void) +{ + if (insert_exec_ctx.target_table == NULL) + return false; + + /* + * Same call-stack head as when INSERT EXEC started → error is at the + * INSERT EXEC level. A deeper node → error is inside the called procedure. + */ + return exec_state_call_stack == insert_exec_ctx.call_stack_entry; +} + /* * Create a temp table for INSERT EXEC buffering. * @@ -142,7 +507,7 @@ get_insertable_column_list(const char *table_name, const char *physical_schema) * the target table for this to succeed. */ Oid -create_insert_exec_temp_table(const char *target_table, const char *column_list, const char *schema_name_in) +create_insert_exec_temp_table(const char *target_table, const char *column_list, const char *schema_name_in, const char *db_name_in) { StringInfoData create_stmt; int rc; @@ -158,10 +523,8 @@ create_insert_exec_temp_table(const char *target_table, const char *column_list, * Generate a unique temp table name using ChooseRelationName. */ temp_nsp_oid = LookupNamespaceNoError("pg_temp"); - if (!OidIsValid(temp_nsp_oid)) - elog(ERROR, "INSERT EXEC failed due to missing pg_temp namespace"); - temp_table_name = ChooseRelationName("__insert_exec_buf", NULL, NULL, + temp_table_name = ChooseRelationName("__insert_exec_buf", NULL, "tmp", temp_nsp_oid, false); /* @@ -172,20 +535,16 @@ create_insert_exec_temp_table(const char *target_table, const char *column_list, * 2. Schema explicitly specified — resolve to physical schema name * 3. No schema specified — leave NULL, let search_path handle resolution */ - if (target_table[0] == '#') - physical_schema = pstrdup("pg_temp"); - else if (schema_name_in != NULL) + if (!(target_table[0] == '#' || target_table[0] == '@')) { - /* User specified schema — resolve to physical name */ - physical_schema = get_physical_schema_name(get_cur_db_name(), schema_name_in); + const char *sname = (schema_name_in != NULL) ? schema_name_in : "dbo"; + + physical_schema = get_physical_schema_name( + (db_name_in != NULL) ? (char *) db_name_in : get_cur_db_name(), sname); if (physical_schema == NULL) elog(ERROR, "INSERT EXEC failed due to unresolvable schema for target table \"%s\"", target_table); } - /* - * else: no schema specified, not a temp table — physical_schema stays NULL, - * search_path will resolve the target correctly - */ /* * Determine the column list to SELECT. @@ -245,52 +604,86 @@ create_insert_exec_temp_table(const char *target_table, const char *column_list, * Uses SPI_execute for the flush INSERT. * The flush is called while still inside the procedure's execution context. */ -void -flush_insert_exec_temp_table(PLtsql_execstate *estate, - const char *column_list_str) +static void +flush_insert_exec_temp_table(PLtsql_execstate *estate, const char *target_schema, + const char *target_db, const char *column_list_str) { - char *temp_name; - char *relname; - char *nspname; - Relation temp_rel; StringInfoData flush_query; + int rc; Oid temp_oid = insert_exec_ctx.temp_table_oid; + const char *target_table = insert_exec_ctx.target_table; + const char *temp_name; + Relation temp_rel; + char *qualified_target; - if (!OidIsValid(temp_oid) || !OidIsValid(insert_exec_ctx.target_rel_oid)) + if (!OidIsValid(temp_oid) || target_table == NULL) return; - relname = get_rel_name(insert_exec_ctx.target_rel_oid); - nspname = get_namespace_name(get_rel_namespace(insert_exec_ctx.target_rel_oid)); - /* - * TODO: Reset insert_exec_ctx here before erroring to prevent stale state - * from affecting subsequent INSERT EXEC operations in the same session. - * Cleanup will be added in the wiring PR alongside reset_insert_exec_context(). - * - * Verify target table schema hasn't changed since INSERT EXEC started. + * Use the same %s-placeholder format string as the TDS error_mapping entry + * (ERRCODE_OBJECT_IN_USE -> 556). The TDS layer maps errors by the + * untranslated errmsg format string, so hardcoding the rendered text would + * not match and would fall back to the default code. */ if (insert_exec_ctx.is_target_relation_modified) ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("INSERT EXEC failed because the stored procedure altered the schema of the target table"))); + (errcode(ERRCODE_OBJECT_IN_USE), + errmsg("cannot %s \"%s\" because it is being used by active queries in this session", + "DROP TABLE", target_table))); - /* Get the temp table name from its OID */ + /* + * Get the temp table name from its OID. Reference it by bare (unqualified) + * name and let search_path resolve it - the physical temp namespace name + * (e.g. pg_temp_0) is not resolvable as a schema under the T-SQL dialect. + */ temp_rel = table_open(temp_oid, NoLock); - temp_name = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(temp_rel)), - RelationGetRelationName(temp_rel)); + temp_name = quote_identifier(pstrdup(RelationGetRelationName(temp_rel))); table_close(temp_rel, NoLock); initStringInfo(&flush_query); + /* + * For a cross-DB target (db..table), build the logical T-SQL name + * (db.schema.table) and let Babelfish's normal cross-DB name rewriting + * resolve it - the same path a plain "INSERT INTO db..table" takes. + * Resolving the physical schema ourselves does not work because the + * physical schema of another logical database is not visible as an + * INSERT target under the T-SQL dialect. For same-DB and temp targets we + * keep referencing the bare name (search_path resolves it). + */ + if (target_db != NULL) + qualified_target = psprintf("%s.%s.%s", + quote_identifier(target_db), + quote_identifier(target_schema ? target_schema : "dbo"), + quote_identifier(target_table)); + else + qualified_target = pstrdup(quote_identifier(target_table)); + appendStringInfo(&flush_query, "INSERT INTO %s%s SELECT * FROM %s", - quote_qualified_identifier(nspname, relname), - column_list_str ? column_list_str : "", + qualified_target, + column_list_str ? psprintf(" (%s)", column_list_str) : "", temp_name); - /* Route through execute_batch to handle triggers and errors properly. */ - execute_batch(estate, flush_query.data, NULL, NULL); + pfree(qualified_target); + + /* Route through SPI_execute to run the flush INSERT in the current + * transaction/SPI context. execute_batch cannot be used here: it enters + * pltsql_inline_handler as an independent batch that manages its own + * transaction boundaries, which aborts when the flush runs mid-INSERT-EXEC + * (notably for table-variable targets whose lifetime is tied to the + * surrounding transaction). */ + rc = SPI_execute(flush_query.data, false, 0); + pfree(flush_query.data); + + if (rc != SPI_OK_INSERT && rc != SPI_OK_INSERT_RETURNING) + elog(ERROR, "INSERT EXEC failed due to error while flushing temp table to target table"); + + /* Update rowcount and FOUND for T-SQL compatibility */ + estate->eval_processed = SPI_processed; + exec_set_rowcount(SPI_processed); + exec_set_found(estate, SPI_processed != 0); } /* @@ -319,7 +712,9 @@ static void insertexec_startup(DestReceiver *self, int operation, TupleDesc typeinfo) { DR_insertexec *myState = (DR_insertexec *) self; + Relation temp_rel; TupleDesc temp_tupdesc; + TupleDesc proj_tupdesc; int result_natts; int temp_natts; int i; @@ -333,16 +728,17 @@ insertexec_startup(DestReceiver *self, int operation, TupleDesc typeinfo) if (!OidIsValid(insert_exec_ctx.temp_table_oid)) elog(ERROR, "INSERT EXEC failed due to missing temp table OID"); - /* Open temp table to get its tuple descriptor */ - myState->temp_rel = table_open(insert_exec_ctx.temp_table_oid, RowExclusiveLock); - temp_tupdesc = RelationGetDescr(myState->temp_rel); + /* Open temp table to read schema only - closed before startup returns */ + temp_rel = table_open(insert_exec_ctx.temp_table_oid, AccessShareLock); + temp_tupdesc = RelationGetDescr(temp_rel); temp_natts = temp_tupdesc->natts; if (result_natts != temp_natts) + { ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("column name or number of supplied values does not match table definition"))); - + errmsg("structure of query does not match function result type"))); + } for (i = 0; i < temp_natts; i++) { @@ -389,11 +785,17 @@ insertexec_startup(DestReceiver *self, int operation, TupleDesc typeinfo) target_list = lappend(target_list, tle); } + /* + * Copy the temp table descriptor before closing temp_rel. + * INSERT EXEC must not hold a relation handle open + * across the procedure's internal subtransaction boundaries. + */ + proj_tupdesc = CreateTupleDescCopy(temp_tupdesc); + table_close(temp_rel, AccessShareLock); + /* Create expression context and projection info */ myState->econtext = CreateStandaloneExprContext(); - - /* Pass temp_tupdesc directly - valid while temp_rel is held open */ - myState->proj_slot = MakeSingleTupleTableSlot(temp_tupdesc, &TTSOpsVirtual); + myState->proj_slot = MakeSingleTupleTableSlot(proj_tupdesc, &TTSOpsVirtual); /* Build the projection info */ myState->proj_info = ExecBuildProjectionInfo(target_list, @@ -402,10 +804,15 @@ insertexec_startup(DestReceiver *self, int operation, TupleDesc typeinfo) NULL, /* no parent PlanState */ typeinfo); /* input descriptor */ + /* + * Pre-assign XID before parallel mode starts. table_tuple_insert() calls + * GetCurrentTransactionId() which fails in parallel mode if no XID exists. + * rStartup runs before EnterParallelMode, so assigning here avoids the error. + */ + (void) GetCurrentTransactionId(); + /* Obtain command ID once - all tuples share the same cid for MVCC consistency */ myState->cid = GetCurrentCommandId(true); - - /* temp_rel stays open - closed in cleanup function */ } /* @@ -417,10 +824,14 @@ insertexec_receive(TupleTableSlot *slot, DestReceiver *self) DR_insertexec *myState = (DR_insertexec *) self; TupleTableSlot *insert_slot; - Assert(myState->temp_rel != NULL); + Relation temp_rel; + Assert(myState->proj_info != NULL); Assert(myState->econtext != NULL); + /* Open temp table fresh for each tuple - avoids stale handles across subtransactions */ + temp_rel = table_open(insert_exec_ctx.temp_table_oid, RowExclusiveLock); + /* Reset per-tuple memory context for expression evaluation */ ResetExprContext(myState->econtext); @@ -431,7 +842,10 @@ insertexec_receive(TupleTableSlot *slot, DestReceiver *self) insert_slot = ExecProject(myState->proj_info); /* Insert the projected tuple */ - table_tuple_insert(myState->temp_rel, insert_slot, myState->cid, 0, NULL); + table_tuple_insert(temp_rel, insert_slot, myState->cid, 0, NULL); + + /* Close immediately - do not hold open across subtransaction boundaries */ + table_close(temp_rel, RowExclusiveLock); return true; } @@ -446,10 +860,6 @@ insertexec_shutdown(DestReceiver *self) { DR_insertexec *myState = (DR_insertexec *) self; - Assert(myState->temp_rel != NULL); - table_close(myState->temp_rel, RowExclusiveLock); - myState->temp_rel = NULL; - Assert(myState->proj_slot != NULL); ExecDropSingleTupleTableSlot(myState->proj_slot); myState->proj_slot = NULL; @@ -467,3 +877,119 @@ insertexec_destroy(DestReceiver *self) { pfree(self); } + +/* + * insert_exec_setup - set up INSERT EXEC context and create the + * buffering temp table from parser-provided info. Returns false (no-op) when + * there is no INSERT EXEC info; otherwise sets up context, optionally starts + * an implicit transaction (stored procedure calls only). + */ +bool +insert_exec_setup(PLtsql_execstate *estate, + InsertExecInfo *info, + bool start_implicit_txn) +{ + char *column_list = NULL; + + if (info == NULL || info->target == NULL) + return false; + + /* Check for nested INSERT EXEC */ + if (pltsql_insert_exec_active()) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("nested INSERT ... EXECUTE statements are not allowed"))); + + /* Build the quoted column list (if any) for temp table creation */ + column_list = build_quoted_column_list(info->columns); + + /* + * Start implicit transaction for INSERT EXEC if requested and not already in one. + * This is used for stored procedure calls (exec_stmt_exec) but not for + * dynamic SQL (exec_stmt_exec_batch) which has different transaction semantics. + */ + if (start_implicit_txn) + { + bool in_function = (estate->func && + estate->func->fn_oid != InvalidOid && + estate->func->fn_prokind == PROKIND_FUNCTION && + estate->func->fn_is_trigger == PLTSQL_NOT_TRIGGER); + + if (!pltsql_disable_batch_auto_commit && + pltsql_support_tsql_transactions() && + !IsTransactionBlockActive() && + !in_function) + { + elog(DEBUG4, "TSQL TXN Start internal transaction for INSERT EXEC"); + pltsql_start_txn(); + estate->tsql_trigger_flags |= TSQL_TRAN_STARTED; + } + } + + /* Record that INSERT EXEC is active (stores target name + call stack) */ + pltsql_set_insert_exec_context_info(info->target); + + /* Hold target table open to detect schema alterations during execution */ + pltsql_insert_exec_open_target_table(info->target, info->schema, info->db_name); + + /* Create temp table based on target table structure */ + insert_exec_ctx.temp_table_oid = create_insert_exec_temp_table(info->target, column_list, + info->schema, info->db_name); + + if (column_list != NULL) + pfree(column_list); + + return true; +} + +/* + * insert_exec_success_cleanup - Clean up INSERT EXEC after successful execution. + * + * Flushes temp table to target, resets context, and commits the implicit + * transaction if one was started. The flush target name parts come from the + * parser-provided info: for a cross-DB target (db..table) the flush must use + * the logical T-SQL name (db.schema.table) so Babelfish's cross-DB rewriting + * resolves it; same-DB/temp targets are resolved by bare name via search_path, + * so schema/db are passed as NULL there. + */ +void +insert_exec_success_cleanup(PLtsql_execstate *estate, InsertExecInfo *info) +{ + char *column_list = build_quoted_column_list(info->columns); + const char *flush_schema = (info->db_name != NULL) ? info->schema : NULL; + + PG_TRY(); + { + /* Flush temp table to target table */ + flush_insert_exec_temp_table(estate, flush_schema, info->db_name, column_list); + + /* Close target table after flush completes */ + pltsql_insert_exec_close_target_table(); + } + PG_CATCH(); + { + /* Release target table lock and reset context before re-throwing. */ + if (column_list != NULL) + pfree(column_list); + pltsql_insert_exec_reset_all(); + PG_RE_THROW(); + } + PG_END_TRY(); + + if (column_list != NULL) + pfree(column_list); + + /* Reset the INSERT EXEC context. */ + pltsql_insert_exec_reset_all(); + + /* + * Commit the implicit transaction that was started for INSERT EXEC. + * TSQL_TRAN_STARTED marks that insert_exec_setup() opened it. + */ + if (estate->tsql_trigger_flags & TSQL_TRAN_STARTED) + { + elog(DEBUG4, "TSQL TXN Commit implicit transaction for INSERT EXEC"); + commit_stmt(estate, true); + estate->tsql_trigger_flags &= ~TSQL_TRAN_STARTED; + } +} \ No newline at end of file diff --git a/contrib/babelfishpg_tsql/src/pltsql-2.h b/contrib/babelfishpg_tsql/src/pltsql-2.h index 5f611f4431f..b7e4e6a06e8 100644 --- a/contrib/babelfishpg_tsql/src/pltsql-2.h +++ b/contrib/babelfishpg_tsql/src/pltsql-2.h @@ -378,4 +378,10 @@ extern void pltsql_convert_ident(const char *s, char **output, int numidents); extern PLtsql_expr *pltsql_read_expression(int until, const char *expected); extern RangeVar *pltsqlMakeRangeVarFromName(const char *identifier_val); +/* INSERT EXEC setup/cleanup helpers (pl_insert_exec.c) - take InsertExecInfo */ +extern bool insert_exec_setup(PLtsql_execstate *estate, + InsertExecInfo *info, + bool start_implicit_txn); +extern void insert_exec_success_cleanup(PLtsql_execstate *estate, InsertExecInfo *info); + #endif diff --git a/contrib/babelfishpg_tsql/src/pltsql.h b/contrib/babelfishpg_tsql/src/pltsql.h index 728da7d42b6..0224dcef540 100644 --- a/contrib/babelfishpg_tsql/src/pltsql.h +++ b/contrib/babelfishpg_tsql/src/pltsql.h @@ -1668,6 +1668,7 @@ typedef struct PLtsql_execstate /* * A same procedure can be invoked by either normal EXECUTE or INSERT ... * EXECUTE, and can behave differently. + * Note : It's Used insert-exec legacy codepath, need to remove during cleanup. */ bool insert_exec; @@ -1953,6 +1954,9 @@ typedef struct PLtsql_protocol_plugin Datum (*sql_geography_from_bytea) (PG_FUNCTION_ARGS); + /* INSERT EXEC support */ + bool (*pltsql_insert_exec_active) (void); + /* Session level GUCs */ bool quoted_identifier; bool arithabort; @@ -2357,6 +2361,7 @@ extern void pltsql_start_txn(void); extern void pltsql_commit_txn(void); extern void pltsql_rollback_txn(void); extern void pltsql_abort_any_transaction(void); +extern void commit_stmt(PLtsql_execstate *estate, bool txnStarted); extern bool pltsql_get_errdata(int *tsql_error_code, int *tsql_error_severity, int *tsql_error_state); extern void pltsql_eval_txn_data(PLtsql_execstate *estate, PLtsql_stmt_execsql *stmt, CachedPlanSource *cachedPlanSource); extern bool is_sysname_column(ColumnDef *coldef); @@ -2525,15 +2530,32 @@ extern char *tsql_format_type_extended(Oid type_oid, int32 typemod, bits16 flags typedef struct InsertExecContext { Oid temp_table_oid; /* OID of temp table for buffering */ + /* + * Target table name (bare), captured at parse time. Kept as a string + * because it is needed when target_rel_oid can't be looked up: during + * cleanup with no live transaction, after the target is dropped (556 + * error), for not-yet-existing or table-variable targets, and to preserve + * the cross-DB logical name. + */ + char *target_table; + PLExecStateCallStack *call_stack_entry; /* Call stack entry when INSERT EXEC started */ Oid target_rel_oid; /* OID of target table - lock held to detect schema changes */ bool is_target_relation_modified; /* Set by bbf_object_access_hook when target table is altered */ } InsertExecContext; extern InsertExecContext insert_exec_ctx; -extern Oid create_insert_exec_temp_table(const char *target_table, const char *column_list, const char *schema_name_in); -extern void flush_insert_exec_temp_table(PLtsql_execstate *estate, - const char *column_list); +extern Oid create_insert_exec_temp_table(const char *target_table, const char *column_list, const char *schema_name_in, const char *db_name_in); +extern void pltsql_set_insert_exec_context_info(const char *target_table); +extern void pltsql_insert_exec_reset_all(void); +extern bool pltsql_insert_exec_active(void); +extern bool pltsql_insert_exec_error_at_trycatch_level(void); +extern void pltsql_insert_exec_open_target_table(const char *target_table,const char *schema_name_in, + const char *db_name_in); +extern void pltsql_insert_exec_close_target_table(void); +extern void pltsql_insert_exec_validate_column_count_from_query(const char *query_string); + +/* INSERT EXEC helper functions */ extern DestReceiver *CreateInsertExecDestReceiver(void); #define NUM_DB_OBJECTS 11 diff --git a/contrib/babelfishpg_tsql/src/pltsql_utils.c b/contrib/babelfishpg_tsql/src/pltsql_utils.c index 9a091666ff8..fb055e23a8b 100644 --- a/contrib/babelfishpg_tsql/src/pltsql_utils.c +++ b/contrib/babelfishpg_tsql/src/pltsql_utils.c @@ -28,6 +28,7 @@ #include "access/genam.h" #include "catalog.h" #include "hooks.h" +#include "guc.h" #include "tcop/utility.h" #include "multidb.h" @@ -126,13 +127,31 @@ PLTsqlProcessTransaction(Node *parsetree, case TRANS_STMT_COMMIT: { - if (exec_state_call_stack && - exec_state_call_stack->estate && - exec_state_call_stack->estate->insert_exec && - NestedTranCount <= 1) - ereport(ERROR, - (errcode(ERRCODE_TRANSACTION_ROLLBACK), - errmsg("Cannot use the COMMIT statement within an INSERT-EXEC statement unless BEGIN TRANSACTION is used first."))); + /* + * Block COMMIT during INSERT EXEC if NestedTranCount <= 1. + * + * INSERT EXEC implicitly makes @@TRANCOUNT = 1. COMMIT is only + * blocked if it would make @@TRANCOUNT go from 1 to 0. If the + * procedure did BEGIN TRAN first (@@TRANCOUNT = 2), then COMMIT + * is allowed (@@TRANCOUNT goes from 2 to 1). + */ + if (pltsql_enable_new_insert_exec) + { + if (pltsql_insert_exec_active() && NestedTranCount <= 1) + ereport(ERROR, + (errcode(ERRCODE_TRANSACTION_ROLLBACK), + errmsg("Cannot use the COMMIT statement within an INSERT-EXEC statement unless BEGIN TRANSACTION is used first."))); + } + else + { + if (exec_state_call_stack && + exec_state_call_stack->estate && + exec_state_call_stack->estate->insert_exec && + NestedTranCount <= 1) + ereport(ERROR, + (errcode(ERRCODE_TRANSACTION_ROLLBACK), + errmsg("Cannot use the COMMIT statement within an INSERT-EXEC statement unless BEGIN TRANSACTION is used first."))); + } PLTsqlCommitTransaction(qc, stmt->chain); } @@ -140,12 +159,26 @@ PLTsqlProcessTransaction(Node *parsetree, case TRANS_STMT_ROLLBACK: { - if (exec_state_call_stack && - exec_state_call_stack->estate && - exec_state_call_stack->estate->insert_exec) - ereport(ERROR, - (errcode(ERRCODE_TRANSACTION_ROLLBACK), - errmsg("Cannot use the ROLLBACK statement within an INSERT-EXEC statement."))); + /* + * Block ROLLBACK during INSERT EXEC. + * ROLLBACK is not allowed within an INSERT-EXEC statement. + */ + if (pltsql_enable_new_insert_exec) + { + if (pltsql_insert_exec_active()) + ereport(ERROR, + (errcode(ERRCODE_TRANSACTION_ROLLBACK), + errmsg("Cannot use the ROLLBACK statement within an INSERT-EXEC statement."))); + } + else + { + if (exec_state_call_stack && + exec_state_call_stack->estate && + exec_state_call_stack->estate->insert_exec) + ereport(ERROR, + (errcode(ERRCODE_TRANSACTION_ROLLBACK), + errmsg("Cannot use the ROLLBACK statement within an INSERT-EXEC statement."))); + } PLTsqlRollbackTransaction(txnName, qc, stmt->chain); } break; diff --git a/contrib/babelfishpg_tsql/src/tsqlIface.cpp b/contrib/babelfishpg_tsql/src/tsqlIface.cpp index 8dcdb3cccae..ba5f11211c5 100644 --- a/contrib/babelfishpg_tsql/src/tsqlIface.cpp +++ b/contrib/babelfishpg_tsql/src/tsqlIface.cpp @@ -899,6 +899,22 @@ set_insert_exec_info(PLtsql_stmt *stmt, InsertExecInfo *info) static void apply_exec_expression_rewriting(PLtsql_stmt *stmt, ParserRuleContext *baseCtx) { + /* + * For INSERT EXEC, any rewrite recorded for the INSERT + * target (e.g. the db/schema qualifier in "otherdb..t_target") sits before + * the EXEC start and would map to a negative offset. Those entries belong + * to the INSERT destination, not the executed statement, so drop them + * before running the mutator. + */ + size_t exec_start = baseCtx->getStart()->getStartIndex(); + for (auto it = rewritten_query_fragment.begin(); it != rewritten_query_fragment.end(); ) + { + if (it->first < exec_start) + it = rewritten_query_fragment.erase(it); + else + ++it; + } + if (stmt->cmd_type == PLTSQL_STMT_EXEC) { PLtsql_stmt_exec *exec_stmt = (PLtsql_stmt_exec *) stmt; diff --git a/test/JDBC/expected/BABEL-INSERT-EXEC.out b/test/JDBC/expected/BABEL-INSERT-EXEC.out new file mode 100644 index 00000000000..b90c9c2c0e6 --- /dev/null +++ b/test/JDBC/expected/BABEL-INSERT-EXEC.out @@ -0,0 +1,1316 @@ +-- ============================================================================ +-- BABEL-INSERT-EXEC: Comprehensive test for INSERT INTO ... EXEC functionality +-- Tests the Temp Table + Query Rewriting approach for INSERT EXEC +-- ============================================================================ +-- ============================================================================ +-- Cleanup any leftover objects from previous failed runs +-- ============================================================================ +DROP PROCEDURE IF EXISTS insert_exec_p1; +DROP PROCEDURE IF EXISTS insert_exec_p2; +DROP PROCEDURE IF EXISTS insert_exec_p3; +DROP PROCEDURE IF EXISTS insert_exec_p4; +DROP PROCEDURE IF EXISTS insert_exec_p5; +DROP PROCEDURE IF EXISTS insert_exec_pb1; +DROP PROCEDURE IF EXISTS insert_exec_ptypes; +DROP PROCEDURE IF EXISTS insert_exec_pnulls; +DROP PROCEDURE IF EXISTS insert_exec_pcoerce; +DROP PROCEDURE IF EXISTS insert_exec_pdynamic; +DROP PROCEDURE IF EXISTS insert_exec_pmultidyn; +DROP PROCEDURE IF EXISTS insert_exec_pspexec; +DROP PROCEDURE IF EXISTS insert_exec_inner; +DROP PROCEDURE IF EXISTS insert_exec_outer; +DROP PROCEDURE IF EXISTS insert_exec_level1; +DROP PROCEDURE IF EXISTS insert_exec_level2; +DROP PROCEDURE IF EXISTS insert_exec_level3; +DROP PROCEDURE IF EXISTS insert_exec_pmultisel; +DROP PROCEDURE IF EXISTS insert_exec_nestinner; +DROP PROCEDURE IF EXISTS insert_exec_nestmiddle; +DROP PROCEDURE IF EXISTS insert_exec_nestouter; +DROP PROCEDURE IF EXISTS insert_exec_pcust; +DROP PROCEDURE IF EXISTS insert_exec_pidinsert; +DROP PROCEDURE IF EXISTS insert_exec_ptemp; +DROP PROCEDURE IF EXISTS insert_exec_pwithtemp; +DROP PROCEDURE IF EXISTS insert_exec_pcte; +DROP PROCEDURE IF EXISTS insert_exec_punion; +DROP PROCEDURE IF EXISTS insert_exec_pcond; +DROP PROCEDURE IF EXISTS insert_exec_ploop; +DROP PROCEDURE IF EXISTS insert_exec_ptxn1; +DROP PROCEDURE IF EXISTS insert_exec_ptxn2; +DROP PROCEDURE IF EXISTS insert_exec_ptxn3a; +DROP PROCEDURE IF EXISTS insert_exec_ptxn3b; +DROP PROCEDURE IF EXISTS insert_exec_ptxn4; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch1; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch2; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch3; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch4; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch5_inner; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch5_outer; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch6_inner; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch6_outer; +DROP PROCEDURE IF EXISTS insert_exec_perr2; +DROP PROCEDURE IF EXISTS insert_exec_plarge; +DROP PROCEDURE IF EXISTS insert_exec_poutput; +DROP PROCEDURE IF EXISTS insert_exec_ptrancount1; +DROP PROCEDURE IF EXISTS insert_exec_ptrancount2; +DROP PROCEDURE IF EXISTS insert_exec_ptrancount3; +DROP PROCEDURE IF EXISTS insert_exec_ptrancount4; +GO +DROP TABLE IF EXISTS insert_exec_t1; +DROP TABLE IF EXISTS insert_exec_t2; +DROP TABLE IF EXISTS insert_exec_t3; +DROP TABLE IF EXISTS insert_exec_t4; +DROP TABLE IF EXISTS insert_exec_t5; +DROP TABLE IF EXISTS insert_exec_b1; +DROP TABLE IF EXISTS insert_exec_types; +DROP TABLE IF EXISTS insert_exec_nulls; +DROP TABLE IF EXISTS insert_exec_coerce; +DROP TABLE IF EXISTS insert_exec_dynamic; +DROP TABLE IF EXISTS insert_exec_multidyn; +DROP TABLE IF EXISTS insert_exec_spexec; +DROP TABLE IF EXISTS insert_exec_nested; +DROP TABLE IF EXISTS insert_exec_deep; +DROP TABLE IF EXISTS insert_exec_multisel; +DROP TABLE IF EXISTS insert_exec_nestmulti; +DROP TABLE IF EXISTS insert_exec_custdata; +DROP TABLE IF EXISTS insert_exec_identity; +DROP TABLE IF EXISTS insert_exec_idinsert; +DROP TABLE IF EXISTS insert_exec_fromtemp; +DROP TABLE IF EXISTS insert_exec_cte; +DROP TABLE IF EXISTS insert_exec_union; +DROP TABLE IF EXISTS insert_exec_cond; +DROP TABLE IF EXISTS insert_exec_loop; +DROP TABLE IF EXISTS insert_exec_txn1; +DROP TABLE IF EXISTS insert_exec_txn2; +DROP TABLE IF EXISTS insert_exec_txn3; +DROP TABLE IF EXISTS insert_exec_txn4; +DROP TABLE IF EXISTS insert_exec_trycatch1; +DROP TABLE IF EXISTS insert_exec_trycatch2; +DROP TABLE IF EXISTS insert_exec_trycatch3; +DROP TABLE IF EXISTS insert_exec_trycatch4; +DROP TABLE IF EXISTS insert_exec_trycatch5; +DROP TABLE IF EXISTS insert_exec_trycatch6; +DROP TABLE IF EXISTS insert_exec_err2; +DROP TABLE IF EXISTS insert_exec_large; +DROP TABLE IF EXISTS insert_exec_output; +DROP TABLE IF EXISTS insert_exec_trancount1; +DROP TABLE IF EXISTS insert_exec_trancount2; +DROP TABLE IF EXISTS insert_exec_trancount3; +DROP TABLE IF EXISTS insert_exec_trancount4; +GO +-- ============================================================================ +-- Category A: Basic INSERT EXEC Scenarios +-- ============================================================================ +-- A1: Basic INSERT EXEC with Simple Procedure +CREATE TABLE insert_exec_t1 (id INT, name VARCHAR(100)); +GO +CREATE PROCEDURE insert_exec_p1 AS + SELECT 1, 'test'; +GO +INSERT INTO insert_exec_t1 EXEC insert_exec_p1; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_t1; +GO +~~START~~ +int#!#varchar +1#!#test +~~END~~ + +DROP PROCEDURE insert_exec_p1; +DROP TABLE insert_exec_t1; +GO +-- A2: INSERT EXEC with Multiple Rows +CREATE TABLE insert_exec_t2 (id INT, value VARCHAR(50)); +GO +CREATE PROCEDURE insert_exec_p2 AS + SELECT 1, 'one' + UNION ALL SELECT 2, 'two' + UNION ALL SELECT 3, 'three'; +GO +INSERT INTO insert_exec_t2 EXEC insert_exec_p2; +GO +~~ROW COUNT: 3~~ + +SELECT * FROM insert_exec_t2 ORDER BY id; +GO +~~START~~ +int#!#varchar +1#!#one +2#!#two +3#!#three +~~END~~ + +DROP PROCEDURE insert_exec_p2; +DROP TABLE insert_exec_t2; +GO +-- A3: INSERT EXEC with Procedure Parameters +CREATE TABLE insert_exec_t3 (id INT, computed INT); +GO +CREATE PROCEDURE insert_exec_p3 @multiplier INT AS + SELECT 1, 1 * @multiplier + UNION ALL SELECT 2, 2 * @multiplier + UNION ALL SELECT 3, 3 * @multiplier; +GO +INSERT INTO insert_exec_t3 EXEC insert_exec_p3 @multiplier = 10; +GO +~~ROW COUNT: 3~~ + +SELECT * FROM insert_exec_t3 ORDER BY id; +GO +~~START~~ +int#!#int +1#!#10 +2#!#20 +3#!#30 +~~END~~ + +DROP PROCEDURE insert_exec_p3; +DROP TABLE insert_exec_t3; +GO + +-- A4: INSERT EXEC with Schema-Qualified Procedure +CREATE TABLE dbo.insert_exec_t4 (id INT); +GO +CREATE PROCEDURE dbo.insert_exec_p4 AS + SELECT 100; +GO +INSERT INTO dbo.insert_exec_t4 EXEC dbo.insert_exec_p4; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM dbo.insert_exec_t4; +GO +~~START~~ +int +100 +~~END~~ + +DROP PROCEDURE dbo.insert_exec_p4; +DROP TABLE dbo.insert_exec_t4; +GO +-- A5: INSERT EXEC with Empty Result Set +CREATE TABLE insert_exec_t5 (id INT); +GO +CREATE PROCEDURE insert_exec_p5 AS + SELECT 1 WHERE 1 = 0; +GO +INSERT INTO insert_exec_t5 EXEC insert_exec_p5; +GO +SELECT COUNT(*) AS row_count FROM insert_exec_t5; +GO +~~START~~ +int +0 +~~END~~ + +DROP PROCEDURE insert_exec_p5; +DROP TABLE insert_exec_t5; +GO +-- ============================================================================ +-- Category B: Column Mapping and Data Types +-- ============================================================================ +-- B1: INSERT EXEC with Explicit Column List +CREATE TABLE insert_exec_b1 (a INT, b INT, c INT); +GO +CREATE PROCEDURE insert_exec_pb1 AS + SELECT 100, 200; +GO +INSERT INTO insert_exec_b1 (c, a) EXEC insert_exec_pb1; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_b1; +GO +~~START~~ +int#!#int#!#int +200#!##!#100 +~~END~~ + +DROP PROCEDURE insert_exec_pb1; +DROP TABLE insert_exec_b1; +GO +-- B2: INSERT EXEC with Various Data Types +CREATE TABLE insert_exec_types ( + col_int INT, + col_bigint BIGINT, + col_decimal DECIMAL(18,2), + col_varchar VARCHAR(100), + col_nvarchar NVARCHAR(100), + col_bit BIT +); +GO +CREATE PROCEDURE insert_exec_ptypes AS + SELECT + 123, + 9223372036854775807, + 12345.67, + 'varchar test', + N'nvarchar test', + 1; +GO +INSERT INTO insert_exec_types EXEC insert_exec_ptypes; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_types; +GO +~~START~~ +int#!#bigint#!#numeric#!#varchar#!#nvarchar#!#bit +123#!#9223372036854775807#!#12345.67#!#varchar test#!#nvarchar test#!#1 +~~END~~ + +DROP PROCEDURE insert_exec_ptypes; +DROP TABLE insert_exec_types; +GO +-- B3: INSERT EXEC with NULL Values +CREATE TABLE insert_exec_nulls (a INT, b VARCHAR(50), c INT); +GO +CREATE PROCEDURE insert_exec_pnulls AS + SELECT NULL, 'test', NULL + UNION ALL SELECT 1, NULL, 2; +GO +INSERT INTO insert_exec_nulls EXEC insert_exec_pnulls; +GO +~~ROW COUNT: 2~~ + +SELECT * FROM insert_exec_nulls ORDER BY a; +GO +~~START~~ +int#!#varchar#!#int +#!#test#!# +1#!##!#2 +~~END~~ + +DROP PROCEDURE insert_exec_pnulls; +DROP TABLE insert_exec_nulls; +GO +-- B4: INSERT EXEC with Type Coercion +CREATE TABLE insert_exec_coerce (val VARCHAR(10)); +GO +CREATE PROCEDURE insert_exec_pcoerce AS SELECT 12345; +GO +INSERT INTO insert_exec_coerce EXEC insert_exec_pcoerce; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_coerce; +GO +~~START~~ +varchar +12345 +~~END~~ + +DROP PROCEDURE insert_exec_pcoerce; +DROP TABLE insert_exec_coerce; +GO + +-- ============================================================================ +-- Category C: Dynamic SQL (BABEL-4306) +-- ============================================================================ +-- C1: INSERT EXEC with EXEC() inside procedure +CREATE TABLE insert_exec_dynamic (a INT, b VARCHAR(10)); +GO +CREATE PROCEDURE insert_exec_pdynamic AS + EXEC('SELECT 456, CAST(''def'' AS VARCHAR(10))'); +GO +INSERT INTO insert_exec_dynamic EXEC insert_exec_pdynamic; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_dynamic; +GO +~~START~~ +int#!#varchar +456#!#def +~~END~~ + +DROP PROCEDURE insert_exec_pdynamic; +DROP TABLE insert_exec_dynamic; +GO +-- C2: INSERT EXEC with Multiple Dynamic SQL Statements +CREATE TABLE insert_exec_multidyn (val INT); +GO +CREATE PROCEDURE insert_exec_pmultidyn AS + EXEC('SELECT 1'); + EXEC('SELECT 2'); + EXEC('SELECT 3'); +GO +INSERT INTO insert_exec_multidyn EXEC insert_exec_pmultidyn; +GO +~~ROW COUNT: 3~~ + +SELECT * FROM insert_exec_multidyn ORDER BY val; +GO +~~START~~ +int +1 +2 +3 +~~END~~ + +DROP PROCEDURE insert_exec_pmultidyn; +DROP TABLE insert_exec_multidyn; +GO +-- C3: INSERT EXEC with sp_executesql inside procedure +CREATE TABLE insert_exec_spexec (a INT); +GO +CREATE PROCEDURE insert_exec_pspexec AS + EXEC sp_executesql N'SELECT 777'; +GO +INSERT INTO insert_exec_spexec EXEC insert_exec_pspexec; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_spexec; +GO +~~START~~ +int +777 +~~END~~ + +DROP PROCEDURE insert_exec_pspexec; +DROP TABLE insert_exec_spexec; +GO +-- ============================================================================ +-- Category D: Nested Procedures +-- ============================================================================ +-- D1: INSERT EXEC with Nested Procedure Calls +CREATE TABLE insert_exec_nested (val INT); +GO +CREATE PROCEDURE insert_exec_inner AS SELECT 100; +GO +CREATE PROCEDURE insert_exec_outer AS EXEC insert_exec_inner; +GO +INSERT INTO insert_exec_nested EXEC insert_exec_outer; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_nested; +GO +~~START~~ +int +100 +~~END~~ + +DROP PROCEDURE insert_exec_outer; +DROP PROCEDURE insert_exec_inner; +DROP TABLE insert_exec_nested; +GO +-- D2: INSERT EXEC with Deeply Nested Procedures (3 levels) +CREATE TABLE insert_exec_deep (level_val INT); +GO +CREATE PROCEDURE insert_exec_level3 AS SELECT 3; +GO +CREATE PROCEDURE insert_exec_level2 AS EXEC insert_exec_level3; +GO +CREATE PROCEDURE insert_exec_level1 AS EXEC insert_exec_level2; +GO +INSERT INTO insert_exec_deep EXEC insert_exec_level1; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_deep; +GO +~~START~~ +int +3 +~~END~~ + +DROP PROCEDURE insert_exec_level1; +DROP PROCEDURE insert_exec_level2; +DROP PROCEDURE insert_exec_level3; +DROP TABLE insert_exec_deep; +GO +-- D3: INSERT EXEC with Multiple SELECT Statements +CREATE TABLE insert_exec_multisel (val INT); +GO +CREATE PROCEDURE insert_exec_pmultisel AS + SELECT 1; + SELECT 2; + SELECT 3; +GO +INSERT INTO insert_exec_multisel EXEC insert_exec_pmultisel; +GO +~~ROW COUNT: 3~~ + +SELECT * FROM insert_exec_multisel ORDER BY val; +GO +~~START~~ +int +1 +2 +3 +~~END~~ + +DROP PROCEDURE insert_exec_pmultisel; +DROP TABLE insert_exec_multisel; +GO +-- D4: INSERT EXEC with Nested Procedure and Multiple SELECTs +CREATE TABLE insert_exec_nestmulti (a INT); +GO +CREATE PROCEDURE insert_exec_nestinner AS SELECT 10; +GO +CREATE PROCEDURE insert_exec_nestmiddle AS EXEC insert_exec_nestinner; SELECT 20; +GO +CREATE PROCEDURE insert_exec_nestouter AS EXEC insert_exec_nestmiddle; SELECT 30; +GO +INSERT INTO insert_exec_nestmulti EXEC insert_exec_nestouter; +GO +~~ROW COUNT: 3~~ + +SELECT * FROM insert_exec_nestmulti ORDER BY a; +GO +~~START~~ +int +10 +20 +30 +~~END~~ + +DROP PROCEDURE insert_exec_nestouter; +DROP PROCEDURE insert_exec_nestmiddle; +DROP PROCEDURE insert_exec_nestinner; +DROP TABLE insert_exec_nestmulti; +GO + +-- ============================================================================ +-- Category E: IDENTITY Column Handling (BABEL-4533) +-- ============================================================================ +-- E1: INSERT EXEC with IDENTITY Column (auto-generated) +CREATE TABLE insert_exec_custdata ( + id VARCHAR(100), + cust_name VARCHAR(100), + city VARCHAR(100) +); +GO +INSERT INTO insert_exec_custdata VALUES + (N'GREAL', N'Great Lakes Food Market', N'Eugene'); +GO +~~ROW COUNT: 1~~ + +CREATE PROCEDURE insert_exec_pcust AS + SELECT id, cust_name, city FROM insert_exec_custdata; +GO +CREATE TABLE insert_exec_identity ( + idcol INT IDENTITY, + id VARCHAR(100), + cust_name VARCHAR(100), + city VARCHAR(100) +); +GO +INSERT INTO insert_exec_identity EXEC insert_exec_pcust; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_identity; +GO +~~START~~ +int#!#varchar#!#varchar#!#varchar +1#!#GREAL#!#Great Lakes Food Market#!#Eugene +~~END~~ + +DROP PROCEDURE insert_exec_pcust; +DROP TABLE insert_exec_custdata; +DROP TABLE insert_exec_identity; +GO +-- E2: INSERT EXEC with IDENTITY_INSERT ON +CREATE TABLE insert_exec_idinsert (id INT IDENTITY, val VARCHAR(50)); +GO +CREATE PROCEDURE insert_exec_pidinsert AS + SELECT 100, 'explicit id'; +GO +SET IDENTITY_INSERT insert_exec_idinsert ON; +INSERT INTO insert_exec_idinsert (id, val) EXEC insert_exec_pidinsert; +SET IDENTITY_INSERT insert_exec_idinsert OFF; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_idinsert; +GO +~~START~~ +int#!#varchar +100#!#explicit id +~~END~~ + +DROP PROCEDURE insert_exec_pidinsert; +DROP TABLE insert_exec_idinsert; +GO +-- ============================================================================ +-- Category F: Temp Tables +-- ============================================================================ +-- F1: INSERT EXEC into Temp Table +CREATE TABLE #insert_exec_temp (id INT, name VARCHAR(50)); +GO +CREATE PROCEDURE insert_exec_ptemp AS + SELECT 1, 'one' + UNION ALL SELECT 2, 'two'; +GO +INSERT INTO #insert_exec_temp EXEC insert_exec_ptemp; +GO +~~ROW COUNT: 2~~ + +SELECT * FROM #insert_exec_temp ORDER BY id; +GO +~~START~~ +int#!#varchar +1#!#one +2#!#two +~~END~~ + +DROP PROCEDURE insert_exec_ptemp; +DROP TABLE #insert_exec_temp; +GO +-- F2: INSERT EXEC with Temp Table Inside Procedure +CREATE TABLE insert_exec_fromtemp (val INT); +GO +CREATE PROCEDURE insert_exec_pwithtemp AS + CREATE TABLE #inner_temp (x INT); + INSERT INTO #inner_temp VALUES (1), (2), (3); + SELECT x * 10 FROM #inner_temp; +GO +INSERT INTO insert_exec_fromtemp EXEC insert_exec_pwithtemp; +GO +~~ROW COUNT: 3~~ + +SELECT * FROM insert_exec_fromtemp ORDER BY val; +GO +~~START~~ +int +10 +20 +30 +~~END~~ + +DROP PROCEDURE insert_exec_pwithtemp; +DROP TABLE insert_exec_fromtemp; +GO +-- ============================================================================ +-- Category G: Advanced SQL Constructs +-- ============================================================================ +-- G1: INSERT EXEC with CTE in Procedure +CREATE TABLE insert_exec_cte (val INT); +GO +CREATE PROCEDURE insert_exec_pcte AS + WITH cte AS ( + SELECT 1 AS n + UNION ALL + SELECT n + 1 FROM cte WHERE n < 5 + ) + SELECT n FROM cte; +GO +INSERT INTO insert_exec_cte EXEC insert_exec_pcte; +GO +~~ROW COUNT: 5~~ + +SELECT * FROM insert_exec_cte ORDER BY val; +GO +~~START~~ +int +1 +2 +3 +4 +5 +~~END~~ + +DROP PROCEDURE insert_exec_pcte; +DROP TABLE insert_exec_cte; +GO +-- G2: INSERT EXEC with UNION ALL +CREATE TABLE insert_exec_union (a INT); +GO +CREATE PROCEDURE insert_exec_punion AS + SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3; +GO +INSERT INTO insert_exec_union EXEC insert_exec_punion; +GO +~~ROW COUNT: 3~~ + +SELECT * FROM insert_exec_union ORDER BY a; +GO +~~START~~ +int +1 +2 +3 +~~END~~ + +DROP PROCEDURE insert_exec_punion; +DROP TABLE insert_exec_union; +GO +-- G3: INSERT EXEC with Conditional SELECT +CREATE TABLE insert_exec_cond (val INT); +GO +CREATE PROCEDURE insert_exec_pcond @flag BIT AS + IF @flag = 1 + SELECT 100; + ELSE + SELECT 200; +GO +INSERT INTO insert_exec_cond EXEC insert_exec_pcond @flag = 1; +INSERT INTO insert_exec_cond EXEC insert_exec_pcond @flag = 0; +GO +~~ROW COUNT: 1~~ + +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_cond ORDER BY val; +GO +~~START~~ +int +100 +200 +~~END~~ + +DROP PROCEDURE insert_exec_pcond; +DROP TABLE insert_exec_cond; +GO +-- G4: INSERT EXEC with Loop in Procedure +CREATE TABLE insert_exec_loop (iteration INT, value INT); +GO +CREATE PROCEDURE insert_exec_ploop AS + DECLARE @i INT = 1; + WHILE @i <= 5 + BEGIN + SELECT @i, @i * 10; + SET @i = @i + 1; + END +GO +INSERT INTO insert_exec_loop EXEC insert_exec_ploop; +GO +~~ROW COUNT: 5~~ + +SELECT * FROM insert_exec_loop ORDER BY iteration; +GO +~~START~~ +int#!#int +1#!#10 +2#!#20 +3#!#30 +4#!#40 +5#!#50 +~~END~~ + +DROP PROCEDURE insert_exec_ploop; +DROP TABLE insert_exec_loop; +GO + +-- ============================================================================ +-- Category H: Transaction Behavior +-- ============================================================================ +-- H1: INSERT EXEC with Explicit Transaction - COMMIT +CREATE TABLE insert_exec_txn1 (val INT); +GO +CREATE PROCEDURE insert_exec_ptxn1 AS + SELECT 1; + SELECT 2; +GO +BEGIN TRANSACTION; +INSERT INTO insert_exec_txn1 EXEC insert_exec_ptxn1; +COMMIT; +GO +~~ROW COUNT: 2~~ + +SELECT COUNT(*) AS row_count FROM insert_exec_txn1; +GO +~~START~~ +int +2 +~~END~~ + +DROP PROCEDURE insert_exec_ptxn1; +DROP TABLE insert_exec_txn1; +GO +-- H2: INSERT EXEC with Explicit Transaction - ROLLBACK +CREATE TABLE insert_exec_txn2 (val INT); +GO +CREATE PROCEDURE insert_exec_ptxn2 AS + SELECT 1; + SELECT 2; +GO +BEGIN TRANSACTION; +INSERT INTO insert_exec_txn2 EXEC insert_exec_ptxn2; +ROLLBACK; +GO +~~ROW COUNT: 2~~ + +SELECT COUNT(*) AS row_count FROM insert_exec_txn2; +GO +~~START~~ +int +0 +~~END~~ + +DROP PROCEDURE insert_exec_ptxn2; +DROP TABLE insert_exec_txn2; +GO +-- H3: INSERT EXEC with Multiple Procedures in Transaction +CREATE TABLE insert_exec_txn3 (source VARCHAR(10), val INT); +GO +CREATE PROCEDURE insert_exec_ptxn3a AS SELECT 'p1', 1; +GO +CREATE PROCEDURE insert_exec_ptxn3b AS SELECT 'p2', 2; +GO +BEGIN TRANSACTION; +INSERT INTO insert_exec_txn3 EXEC insert_exec_ptxn3a; +INSERT INTO insert_exec_txn3 EXEC insert_exec_ptxn3b; +COMMIT; +GO +~~ROW COUNT: 1~~ + +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_txn3 ORDER BY val; +GO +~~START~~ +varchar#!#int +p1#!#1 +p2#!#2 +~~END~~ + +DROP PROCEDURE insert_exec_ptxn3a; +DROP PROCEDURE insert_exec_ptxn3b; +DROP TABLE insert_exec_txn3; +GO +-- H4: INSERT EXEC with Transaction Inside Procedure +CREATE TABLE insert_exec_txn4 (a INT); +GO +CREATE PROCEDURE insert_exec_ptxn4 AS +BEGIN TRY + BEGIN TRANSACTION; + SELECT 555; + COMMIT; +END TRY +BEGIN CATCH + IF @@TRANCOUNT > 0 ROLLBACK; +END CATCH +GO +INSERT INTO insert_exec_txn4 EXEC insert_exec_ptxn4; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_txn4; +GO +~~START~~ +int +555 +~~END~~ + +DROP PROCEDURE insert_exec_ptxn4; +DROP TABLE insert_exec_txn4; +GO +-- ============================================================================ +-- Category I: TRY/CATCH Behavior (BABEL-5922) +-- ============================================================================ +-- I1: TRY/CATCH with Error - Rows Should Be Rolled Back +CREATE TABLE insert_exec_trycatch1 (id INT, id1 INT); +GO +CREATE PROCEDURE insert_exec_ptrycatch1 AS +BEGIN TRY + SELECT 1, 1; + SELECT 1/0; +END TRY +BEGIN CATCH +END CATCH +GO +INSERT INTO insert_exec_trycatch1 EXEC insert_exec_ptrycatch1; +GO +~~ERROR (Code: 33557097)~~ + +~~ERROR (Message: structure of query does not match function result type)~~ + +SELECT COUNT(*) AS row_count FROM insert_exec_trycatch1; +GO +~~START~~ +int +0 +~~END~~ + +DROP PROCEDURE insert_exec_ptrycatch1; +DROP TABLE insert_exec_trycatch1; +GO +-- I2: TRY/CATCH with Successful Execution +CREATE TABLE insert_exec_trycatch2 (a INT); +GO +CREATE PROCEDURE insert_exec_ptrycatch2 AS +BEGIN TRY + SELECT 100; + SELECT 200; +END TRY +BEGIN CATCH + SELECT -1; +END CATCH +GO +INSERT INTO insert_exec_trycatch2 EXEC insert_exec_ptrycatch2; +GO +~~ROW COUNT: 2~~ + +SELECT * FROM insert_exec_trycatch2 ORDER BY a; +GO +~~START~~ +int +100 +200 +~~END~~ + +DROP PROCEDURE insert_exec_ptrycatch2; +DROP TABLE insert_exec_trycatch2; +GO +-- I3: TRY/CATCH with RAISERROR (Severity < 11 - continues) +CREATE TABLE insert_exec_trycatch3 (val INT); +GO +CREATE PROCEDURE insert_exec_ptrycatch3 AS +BEGIN TRY + SELECT 1; + RAISERROR('Info message', 10, 1); + SELECT 2; +END TRY +BEGIN CATCH + SELECT -1; +END CATCH +GO +INSERT INTO insert_exec_trycatch3 EXEC insert_exec_ptrycatch3; +GO +~~WARNING (Code: 0)~~ + +~~WARNING (Message: Info message Server SQLState: S0001)~~ + +~~ROW COUNT: 2~~ + +SELECT * FROM insert_exec_trycatch3 ORDER BY val; +GO +~~START~~ +int +1 +2 +~~END~~ + +DROP PROCEDURE insert_exec_ptrycatch3; +DROP TABLE insert_exec_trycatch3; +GO +-- I4: Nested TRY/CATCH +CREATE TABLE insert_exec_trycatch4 (val INT); +GO +CREATE PROCEDURE insert_exec_ptrycatch4 AS +BEGIN TRY + SELECT 1; + BEGIN TRY + SELECT 2; + SELECT 1/0; + END TRY + BEGIN CATCH + SELECT 3; + END CATCH + SELECT 4; +END TRY +BEGIN CATCH + SELECT -1; +END CATCH +GO +INSERT INTO insert_exec_trycatch4 EXEC insert_exec_ptrycatch4; +GO +~~ROW COUNT: 4~~ + +SELECT * FROM insert_exec_trycatch4 ORDER BY val; +GO +~~START~~ +int +1 +2 +3 +4 +~~END~~ + +DROP PROCEDURE insert_exec_ptrycatch4; +DROP TABLE insert_exec_trycatch4; +GO +-- I5: Nested Procedure with TRY/CATCH - Success Case +-- Tests that internal savepoints work correctly when a nested procedure +-- has TRY-CATCH that catches an error. The inner proc catches the error +-- and continues, outer proc should see all rows. +CREATE TABLE insert_exec_trycatch5 (val INT); +GO +CREATE PROCEDURE insert_exec_ptrycatch5_inner AS +BEGIN TRY + SELECT 10; + SELECT 1/0; -- Error caught by inner TRY-CATCH + SELECT 20; -- Not reached +END TRY +BEGIN CATCH + SELECT 30; -- Error handler runs +END CATCH +SELECT 40; -- Continues after TRY-CATCH +GO +CREATE PROCEDURE insert_exec_ptrycatch5_outer AS + SELECT 1; + EXEC insert_exec_ptrycatch5_inner; + SELECT 2; +GO +INSERT INTO insert_exec_trycatch5 EXEC insert_exec_ptrycatch5_outer; +GO +~~ROW COUNT: 5~~ + +SELECT * FROM insert_exec_trycatch5 ORDER BY val; +GO +~~START~~ +int +1 +2 +10 +30 +40 +~~END~~ + +DROP PROCEDURE insert_exec_ptrycatch5_outer; +DROP PROCEDURE insert_exec_ptrycatch5_inner; +DROP TABLE insert_exec_trycatch5; +GO +-- I6: Nested Procedure with TRY/CATCH - Failure Case (THROW re-raises) +-- Tests that when inner proc re-throws an error, it propagates correctly +-- and all rows are rolled back as expected for INSERT EXEC. +CREATE TABLE insert_exec_trycatch6 (val INT); +GO +CREATE PROCEDURE insert_exec_ptrycatch6_inner AS +BEGIN TRY + SELECT 10; + SELECT 1/0; -- Error occurs +END TRY +BEGIN CATCH + SELECT 20; -- Error handler runs + THROW; -- Re-throw the error +END CATCH +GO +CREATE PROCEDURE insert_exec_ptrycatch6_outer AS + SELECT 1; + EXEC insert_exec_ptrycatch6_inner; + SELECT 2; -- Not reached due to re-thrown error +GO +INSERT INTO insert_exec_trycatch6 EXEC insert_exec_ptrycatch6_outer; +GO +~~ERROR (Code: 8134)~~ + +~~ERROR (Message: division by zero)~~ + +SELECT COUNT(*) AS row_count FROM insert_exec_trycatch6; +GO +~~START~~ +int +0 +~~END~~ + +DROP PROCEDURE insert_exec_ptrycatch6_outer; +DROP PROCEDURE insert_exec_ptrycatch6_inner; +DROP TABLE insert_exec_trycatch6; +GO + +-- ============================================================================ +-- Category J: Error Handling +-- ============================================================================ +-- J1: INSERT EXEC with Division by Zero (skipped in INSERT EXEC) +CREATE TABLE insert_exec_err2 (val INT); +GO +CREATE PROCEDURE insert_exec_perr2 AS + SELECT 1; + SELECT 1/0; + SELECT 2; +GO +INSERT INTO insert_exec_err2 EXEC insert_exec_perr2; +GO +~~ERROR (Code: 8134)~~ + +~~ERROR (Message: division by zero)~~ + +~~ROW COUNT: 2~~ + +SELECT * FROM insert_exec_err2 ORDER BY val; +GO +~~START~~ +int +1 +2 +~~END~~ + +DROP PROCEDURE insert_exec_perr2; +DROP TABLE insert_exec_err2; +GO +-- ============================================================================ +-- Category K: Large Result Sets +-- ============================================================================ +-- K1: INSERT EXEC with 100 Rows +CREATE TABLE insert_exec_large (id INT); +GO +CREATE PROCEDURE insert_exec_plarge AS + DECLARE @i INT = 1; + WHILE @i <= 100 + BEGIN + SELECT @i; + SET @i = @i + 1; + END +GO +INSERT INTO insert_exec_large EXEC insert_exec_plarge; +GO +~~ROW COUNT: 100~~ + +SELECT COUNT(*) AS row_count FROM insert_exec_large; +GO +~~START~~ +int +100 +~~END~~ + +DROP PROCEDURE insert_exec_plarge; +DROP TABLE insert_exec_large; +GO +-- ============================================================================ +-- Category L: OUTPUT Clause (BABEL-5921) +-- ============================================================================ +-- L1: INSERT EXEC with OUTPUT Clause in Procedure +CREATE TABLE insert_exec_output (id INT); +GO +CREATE PROCEDURE insert_exec_poutput AS + DROP TABLE IF EXISTS #temp; + CREATE TABLE #temp (id INT); + INSERT INTO #temp OUTPUT INSERTED.* VALUES (1); +GO +INSERT INTO insert_exec_output EXEC insert_exec_poutput; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_output; +GO +~~START~~ +int +1 +~~END~~ + +DROP PROCEDURE insert_exec_poutput; +DROP TABLE insert_exec_output; +GO +-- ============================================================================ +-- Category M: Transaction State Visibility (@@TRANCOUNT) +-- Tests that transaction state is correctly visible inside nested procedures +-- during INSERT EXEC execution. +-- ============================================================================ +-- M1: @@TRANCOUNT visibility - no explicit transaction +CREATE TABLE insert_exec_trancount1 (trancount_val INT); +GO +CREATE PROCEDURE insert_exec_ptrancount1 AS + SELECT @@TRANCOUNT; +GO +INSERT INTO insert_exec_trancount1 EXEC insert_exec_ptrancount1; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_trancount1; +GO +~~START~~ +int +1 +~~END~~ + +DROP PROCEDURE insert_exec_ptrancount1; +DROP TABLE insert_exec_trancount1; +GO +-- M2: @@TRANCOUNT visibility - with explicit transaction +CREATE TABLE insert_exec_trancount2 (trancount_val INT); +GO +CREATE PROCEDURE insert_exec_ptrancount2 AS + SELECT @@TRANCOUNT; +GO +BEGIN TRANSACTION; +INSERT INTO insert_exec_trancount2 EXEC insert_exec_ptrancount2; +COMMIT; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_trancount2; +GO +~~START~~ +int +1 +~~END~~ + +DROP PROCEDURE insert_exec_ptrancount2; +DROP TABLE insert_exec_trancount2; +GO +-- M3: @@TRANCOUNT changes inside procedure +-- Tests that BEGIN TRAN/COMMIT inside the proc correctly updates @@TRANCOUNT +CREATE TABLE insert_exec_trancount3 (trancount_val INT); +GO +CREATE PROCEDURE insert_exec_ptrancount3 AS + SELECT @@TRANCOUNT; + BEGIN TRANSACTION; + SELECT @@TRANCOUNT; + COMMIT; + SELECT @@TRANCOUNT; +GO +INSERT INTO insert_exec_trancount3 EXEC insert_exec_ptrancount3; +GO +~~ROW COUNT: 3~~ + +SELECT * FROM insert_exec_trancount3 ORDER BY trancount_val; +GO +~~START~~ +int +1 +1 +2 +~~END~~ + +DROP PROCEDURE insert_exec_ptrancount3; +DROP TABLE insert_exec_trancount3; +GO +-- M4: Multiple nested transactions with @@TRANCOUNT +CREATE TABLE insert_exec_trancount4 (trancount_val INT); +GO +CREATE PROCEDURE insert_exec_ptrancount4 AS + SELECT @@TRANCOUNT; + BEGIN TRANSACTION; + SELECT @@TRANCOUNT; + BEGIN TRANSACTION; + SELECT @@TRANCOUNT; + COMMIT; + SELECT @@TRANCOUNT; + COMMIT; + SELECT @@TRANCOUNT; +GO +INSERT INTO insert_exec_trancount4 EXEC insert_exec_ptrancount4; +GO +~~ROW COUNT: 5~~ + +SELECT * FROM insert_exec_trancount4 ORDER BY trancount_val; +GO +~~START~~ +int +1 +1 +2 +2 +3 +~~END~~ + +DROP PROCEDURE insert_exec_ptrancount4; +DROP TABLE insert_exec_trancount4; +GO +-- ============================================================================ +-- Category N: Cross-database targets and user-defined data types (UDD) +-- Tests INSERT EXEC where the target table lives in another database, and +-- where the source procedure uses a UDD while the target uses a base type. +-- ============================================================================ +-- N1: target is cross-DB, proc is local +CREATE DATABASE otherdb; +GO +USE otherdb; +GO +CREATE TABLE dbo.t_target (val INT); +GO +USE master; +GO +CREATE PROCEDURE p_local AS SELECT 100 AS val; +GO +INSERT INTO otherdb..t_target EXEC p_local; +GO +~~ROW COUNT: 1~~ + +USE otherdb; +GO +SELECT * FROM dbo.t_target; -- Expected: 100 +GO +~~START~~ +int +100 +~~END~~ + +DROP TABLE dbo.t_target; +USE master; +DROP PROCEDURE p_local; +DROP DATABASE otherdb; +GO +-- N2: both target and proc are in otherdb +CREATE DATABASE otherdb; +GO +USE otherdb; +GO +CREATE TABLE dbo.t_target (val INT); +GO +CREATE PROCEDURE dbo.p_remote AS SELECT 200 AS val; +GO +USE master; +GO +INSERT INTO otherdb..t_target EXEC otherdb.dbo.p_remote; +GO +~~ROW COUNT: 1~~ + +USE otherdb; +GO +SELECT * FROM dbo.t_target; -- Expected: 200 +GO +~~START~~ +int +200 +~~END~~ + +DROP TABLE dbo.t_target; +DROP PROCEDURE dbo.p_remote; +USE master; +DROP DATABASE otherdb; +GO +-- N3: source uses UDD, target uses base type (UDD-to-base-type coercion) +CREATE TYPE custom_int FROM INT NOT NULL; +GO +CREATE PROCEDURE dbo.p_udd AS + DECLARE @v custom_int = 42; + SELECT @v AS x; +GO +CREATE TABLE dbo.t_basetype (x INT); +GO +INSERT INTO dbo.t_basetype EXEC dbo.p_udd; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM dbo.t_basetype; -- Expected: 42 +GO +~~START~~ +int +42 +~~END~~ + +DROP TABLE dbo.t_basetype; +DROP PROCEDURE dbo.p_udd; +DROP TYPE custom_int; +GO +-- N4: target is a temp table, source uses UDD +CREATE TYPE custom_int FROM INT NOT NULL; +GO +CREATE PROCEDURE dbo.p_udd AS + DECLARE @v custom_int = 99; + SELECT @v AS x; +GO +CREATE TABLE #t_temp (x INT); +GO +INSERT INTO #t_temp EXEC dbo.p_udd; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM #t_temp; -- Expected: 99 +GO +~~START~~ +int +99 +~~END~~ + +DROP TABLE #t_temp; +DROP PROCEDURE dbo.p_udd; +DROP TYPE custom_int; +GO +-- ============================================================================ +-- Cleanup verification +-- ============================================================================ +SELECT 'All INSERT EXEC tests completed successfully' AS status; +GO +~~START~~ +varchar +All INSERT EXEC tests completed successfully +~~END~~ + diff --git a/test/JDBC/input/BABEL-INSERT-EXEC.sql b/test/JDBC/input/BABEL-INSERT-EXEC.sql new file mode 100644 index 00000000000..a3854209294 --- /dev/null +++ b/test/JDBC/input/BABEL-INSERT-EXEC.sql @@ -0,0 +1,938 @@ +-- ============================================================================ +-- BABEL-INSERT-EXEC: Comprehensive test for INSERT INTO ... EXEC functionality +-- Tests the Temp Table + Query Rewriting approach for INSERT EXEC +-- ============================================================================ +-- ============================================================================ +-- Cleanup any leftover objects from previous failed runs +-- ============================================================================ +DROP PROCEDURE IF EXISTS insert_exec_p1; +DROP PROCEDURE IF EXISTS insert_exec_p2; +DROP PROCEDURE IF EXISTS insert_exec_p3; +DROP PROCEDURE IF EXISTS insert_exec_p4; +DROP PROCEDURE IF EXISTS insert_exec_p5; +DROP PROCEDURE IF EXISTS insert_exec_pb1; +DROP PROCEDURE IF EXISTS insert_exec_ptypes; +DROP PROCEDURE IF EXISTS insert_exec_pnulls; +DROP PROCEDURE IF EXISTS insert_exec_pcoerce; +DROP PROCEDURE IF EXISTS insert_exec_pdynamic; +DROP PROCEDURE IF EXISTS insert_exec_pmultidyn; +DROP PROCEDURE IF EXISTS insert_exec_pspexec; +DROP PROCEDURE IF EXISTS insert_exec_inner; +DROP PROCEDURE IF EXISTS insert_exec_outer; +DROP PROCEDURE IF EXISTS insert_exec_level1; +DROP PROCEDURE IF EXISTS insert_exec_level2; +DROP PROCEDURE IF EXISTS insert_exec_level3; +DROP PROCEDURE IF EXISTS insert_exec_pmultisel; +DROP PROCEDURE IF EXISTS insert_exec_nestinner; +DROP PROCEDURE IF EXISTS insert_exec_nestmiddle; +DROP PROCEDURE IF EXISTS insert_exec_nestouter; +DROP PROCEDURE IF EXISTS insert_exec_pcust; +DROP PROCEDURE IF EXISTS insert_exec_pidinsert; +DROP PROCEDURE IF EXISTS insert_exec_ptemp; +DROP PROCEDURE IF EXISTS insert_exec_pwithtemp; +DROP PROCEDURE IF EXISTS insert_exec_pcte; +DROP PROCEDURE IF EXISTS insert_exec_punion; +DROP PROCEDURE IF EXISTS insert_exec_pcond; +DROP PROCEDURE IF EXISTS insert_exec_ploop; +DROP PROCEDURE IF EXISTS insert_exec_ptxn1; +DROP PROCEDURE IF EXISTS insert_exec_ptxn2; +DROP PROCEDURE IF EXISTS insert_exec_ptxn3a; +DROP PROCEDURE IF EXISTS insert_exec_ptxn3b; +DROP PROCEDURE IF EXISTS insert_exec_ptxn4; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch1; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch2; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch3; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch4; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch5_inner; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch5_outer; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch6_inner; +DROP PROCEDURE IF EXISTS insert_exec_ptrycatch6_outer; +DROP PROCEDURE IF EXISTS insert_exec_perr2; +DROP PROCEDURE IF EXISTS insert_exec_plarge; +DROP PROCEDURE IF EXISTS insert_exec_poutput; +DROP PROCEDURE IF EXISTS insert_exec_ptrancount1; +DROP PROCEDURE IF EXISTS insert_exec_ptrancount2; +DROP PROCEDURE IF EXISTS insert_exec_ptrancount3; +DROP PROCEDURE IF EXISTS insert_exec_ptrancount4; +GO +DROP TABLE IF EXISTS insert_exec_t1; +DROP TABLE IF EXISTS insert_exec_t2; +DROP TABLE IF EXISTS insert_exec_t3; +DROP TABLE IF EXISTS insert_exec_t4; +DROP TABLE IF EXISTS insert_exec_t5; +DROP TABLE IF EXISTS insert_exec_b1; +DROP TABLE IF EXISTS insert_exec_types; +DROP TABLE IF EXISTS insert_exec_nulls; +DROP TABLE IF EXISTS insert_exec_coerce; +DROP TABLE IF EXISTS insert_exec_dynamic; +DROP TABLE IF EXISTS insert_exec_multidyn; +DROP TABLE IF EXISTS insert_exec_spexec; +DROP TABLE IF EXISTS insert_exec_nested; +DROP TABLE IF EXISTS insert_exec_deep; +DROP TABLE IF EXISTS insert_exec_multisel; +DROP TABLE IF EXISTS insert_exec_nestmulti; +DROP TABLE IF EXISTS insert_exec_custdata; +DROP TABLE IF EXISTS insert_exec_identity; +DROP TABLE IF EXISTS insert_exec_idinsert; +DROP TABLE IF EXISTS insert_exec_fromtemp; +DROP TABLE IF EXISTS insert_exec_cte; +DROP TABLE IF EXISTS insert_exec_union; +DROP TABLE IF EXISTS insert_exec_cond; +DROP TABLE IF EXISTS insert_exec_loop; +DROP TABLE IF EXISTS insert_exec_txn1; +DROP TABLE IF EXISTS insert_exec_txn2; +DROP TABLE IF EXISTS insert_exec_txn3; +DROP TABLE IF EXISTS insert_exec_txn4; +DROP TABLE IF EXISTS insert_exec_trycatch1; +DROP TABLE IF EXISTS insert_exec_trycatch2; +DROP TABLE IF EXISTS insert_exec_trycatch3; +DROP TABLE IF EXISTS insert_exec_trycatch4; +DROP TABLE IF EXISTS insert_exec_trycatch5; +DROP TABLE IF EXISTS insert_exec_trycatch6; +DROP TABLE IF EXISTS insert_exec_err2; +DROP TABLE IF EXISTS insert_exec_large; +DROP TABLE IF EXISTS insert_exec_output; +DROP TABLE IF EXISTS insert_exec_trancount1; +DROP TABLE IF EXISTS insert_exec_trancount2; +DROP TABLE IF EXISTS insert_exec_trancount3; +DROP TABLE IF EXISTS insert_exec_trancount4; +GO +-- ============================================================================ +-- Category A: Basic INSERT EXEC Scenarios +-- ============================================================================ +-- A1: Basic INSERT EXEC with Simple Procedure +CREATE TABLE insert_exec_t1 (id INT, name VARCHAR(100)); +GO +CREATE PROCEDURE insert_exec_p1 AS + SELECT 1, 'test'; +GO +INSERT INTO insert_exec_t1 EXEC insert_exec_p1; +GO +SELECT * FROM insert_exec_t1; +GO +DROP PROCEDURE insert_exec_p1; +DROP TABLE insert_exec_t1; +GO +-- A2: INSERT EXEC with Multiple Rows +CREATE TABLE insert_exec_t2 (id INT, value VARCHAR(50)); +GO +CREATE PROCEDURE insert_exec_p2 AS + SELECT 1, 'one' + UNION ALL SELECT 2, 'two' + UNION ALL SELECT 3, 'three'; +GO +INSERT INTO insert_exec_t2 EXEC insert_exec_p2; +GO +SELECT * FROM insert_exec_t2 ORDER BY id; +GO +DROP PROCEDURE insert_exec_p2; +DROP TABLE insert_exec_t2; +GO +-- A3: INSERT EXEC with Procedure Parameters +CREATE TABLE insert_exec_t3 (id INT, computed INT); +GO +CREATE PROCEDURE insert_exec_p3 @multiplier INT AS + SELECT 1, 1 * @multiplier + UNION ALL SELECT 2, 2 * @multiplier + UNION ALL SELECT 3, 3 * @multiplier; +GO +INSERT INTO insert_exec_t3 EXEC insert_exec_p3 @multiplier = 10; +GO +SELECT * FROM insert_exec_t3 ORDER BY id; +GO +DROP PROCEDURE insert_exec_p3; +DROP TABLE insert_exec_t3; +GO + +-- A4: INSERT EXEC with Schema-Qualified Procedure +CREATE TABLE dbo.insert_exec_t4 (id INT); +GO +CREATE PROCEDURE dbo.insert_exec_p4 AS + SELECT 100; +GO +INSERT INTO dbo.insert_exec_t4 EXEC dbo.insert_exec_p4; +GO +SELECT * FROM dbo.insert_exec_t4; +GO +DROP PROCEDURE dbo.insert_exec_p4; +DROP TABLE dbo.insert_exec_t4; +GO +-- A5: INSERT EXEC with Empty Result Set +CREATE TABLE insert_exec_t5 (id INT); +GO +CREATE PROCEDURE insert_exec_p5 AS + SELECT 1 WHERE 1 = 0; +GO +INSERT INTO insert_exec_t5 EXEC insert_exec_p5; +GO +SELECT COUNT(*) AS row_count FROM insert_exec_t5; +GO +DROP PROCEDURE insert_exec_p5; +DROP TABLE insert_exec_t5; +GO +-- ============================================================================ +-- Category B: Column Mapping and Data Types +-- ============================================================================ +-- B1: INSERT EXEC with Explicit Column List +CREATE TABLE insert_exec_b1 (a INT, b INT, c INT); +GO +CREATE PROCEDURE insert_exec_pb1 AS + SELECT 100, 200; +GO +INSERT INTO insert_exec_b1 (c, a) EXEC insert_exec_pb1; +GO +SELECT * FROM insert_exec_b1; +GO +DROP PROCEDURE insert_exec_pb1; +DROP TABLE insert_exec_b1; +GO +-- B2: INSERT EXEC with Various Data Types +CREATE TABLE insert_exec_types ( + col_int INT, + col_bigint BIGINT, + col_decimal DECIMAL(18,2), + col_varchar VARCHAR(100), + col_nvarchar NVARCHAR(100), + col_bit BIT +); +GO +CREATE PROCEDURE insert_exec_ptypes AS + SELECT + 123, + 9223372036854775807, + 12345.67, + 'varchar test', + N'nvarchar test', + 1; +GO +INSERT INTO insert_exec_types EXEC insert_exec_ptypes; +GO +SELECT * FROM insert_exec_types; +GO +DROP PROCEDURE insert_exec_ptypes; +DROP TABLE insert_exec_types; +GO +-- B3: INSERT EXEC with NULL Values +CREATE TABLE insert_exec_nulls (a INT, b VARCHAR(50), c INT); +GO +CREATE PROCEDURE insert_exec_pnulls AS + SELECT NULL, 'test', NULL + UNION ALL SELECT 1, NULL, 2; +GO +INSERT INTO insert_exec_nulls EXEC insert_exec_pnulls; +GO +SELECT * FROM insert_exec_nulls ORDER BY a; +GO +DROP PROCEDURE insert_exec_pnulls; +DROP TABLE insert_exec_nulls; +GO +-- B4: INSERT EXEC with Type Coercion +CREATE TABLE insert_exec_coerce (val VARCHAR(10)); +GO +CREATE PROCEDURE insert_exec_pcoerce AS SELECT 12345; +GO +INSERT INTO insert_exec_coerce EXEC insert_exec_pcoerce; +GO +SELECT * FROM insert_exec_coerce; +GO +DROP PROCEDURE insert_exec_pcoerce; +DROP TABLE insert_exec_coerce; +GO + +-- ============================================================================ +-- Category C: Dynamic SQL (BABEL-4306) +-- ============================================================================ +-- C1: INSERT EXEC with EXEC() inside procedure +CREATE TABLE insert_exec_dynamic (a INT, b VARCHAR(10)); +GO +CREATE PROCEDURE insert_exec_pdynamic AS + EXEC('SELECT 456, CAST(''def'' AS VARCHAR(10))'); +GO +INSERT INTO insert_exec_dynamic EXEC insert_exec_pdynamic; +GO +SELECT * FROM insert_exec_dynamic; +GO +DROP PROCEDURE insert_exec_pdynamic; +DROP TABLE insert_exec_dynamic; +GO +-- C2: INSERT EXEC with Multiple Dynamic SQL Statements +CREATE TABLE insert_exec_multidyn (val INT); +GO +CREATE PROCEDURE insert_exec_pmultidyn AS + EXEC('SELECT 1'); + EXEC('SELECT 2'); + EXEC('SELECT 3'); +GO +INSERT INTO insert_exec_multidyn EXEC insert_exec_pmultidyn; +GO +SELECT * FROM insert_exec_multidyn ORDER BY val; +GO +DROP PROCEDURE insert_exec_pmultidyn; +DROP TABLE insert_exec_multidyn; +GO +-- C3: INSERT EXEC with sp_executesql inside procedure +CREATE TABLE insert_exec_spexec (a INT); +GO +CREATE PROCEDURE insert_exec_pspexec AS + EXEC sp_executesql N'SELECT 777'; +GO +INSERT INTO insert_exec_spexec EXEC insert_exec_pspexec; +GO +SELECT * FROM insert_exec_spexec; +GO +DROP PROCEDURE insert_exec_pspexec; +DROP TABLE insert_exec_spexec; +GO +-- ============================================================================ +-- Category D: Nested Procedures +-- ============================================================================ +-- D1: INSERT EXEC with Nested Procedure Calls +CREATE TABLE insert_exec_nested (val INT); +GO +CREATE PROCEDURE insert_exec_inner AS SELECT 100; +GO +CREATE PROCEDURE insert_exec_outer AS EXEC insert_exec_inner; +GO +INSERT INTO insert_exec_nested EXEC insert_exec_outer; +GO +SELECT * FROM insert_exec_nested; +GO +DROP PROCEDURE insert_exec_outer; +DROP PROCEDURE insert_exec_inner; +DROP TABLE insert_exec_nested; +GO +-- D2: INSERT EXEC with Deeply Nested Procedures (3 levels) +CREATE TABLE insert_exec_deep (level_val INT); +GO +CREATE PROCEDURE insert_exec_level3 AS SELECT 3; +GO +CREATE PROCEDURE insert_exec_level2 AS EXEC insert_exec_level3; +GO +CREATE PROCEDURE insert_exec_level1 AS EXEC insert_exec_level2; +GO +INSERT INTO insert_exec_deep EXEC insert_exec_level1; +GO +SELECT * FROM insert_exec_deep; +GO +DROP PROCEDURE insert_exec_level1; +DROP PROCEDURE insert_exec_level2; +DROP PROCEDURE insert_exec_level3; +DROP TABLE insert_exec_deep; +GO +-- D3: INSERT EXEC with Multiple SELECT Statements +CREATE TABLE insert_exec_multisel (val INT); +GO +CREATE PROCEDURE insert_exec_pmultisel AS + SELECT 1; + SELECT 2; + SELECT 3; +GO +INSERT INTO insert_exec_multisel EXEC insert_exec_pmultisel; +GO +SELECT * FROM insert_exec_multisel ORDER BY val; +GO +DROP PROCEDURE insert_exec_pmultisel; +DROP TABLE insert_exec_multisel; +GO +-- D4: INSERT EXEC with Nested Procedure and Multiple SELECTs +CREATE TABLE insert_exec_nestmulti (a INT); +GO +CREATE PROCEDURE insert_exec_nestinner AS SELECT 10; +GO +CREATE PROCEDURE insert_exec_nestmiddle AS EXEC insert_exec_nestinner; SELECT 20; +GO +CREATE PROCEDURE insert_exec_nestouter AS EXEC insert_exec_nestmiddle; SELECT 30; +GO +INSERT INTO insert_exec_nestmulti EXEC insert_exec_nestouter; +GO +SELECT * FROM insert_exec_nestmulti ORDER BY a; +GO +DROP PROCEDURE insert_exec_nestouter; +DROP PROCEDURE insert_exec_nestmiddle; +DROP PROCEDURE insert_exec_nestinner; +DROP TABLE insert_exec_nestmulti; +GO + +-- ============================================================================ +-- Category E: IDENTITY Column Handling (BABEL-4533) +-- ============================================================================ +-- E1: INSERT EXEC with IDENTITY Column (auto-generated) +CREATE TABLE insert_exec_custdata ( + id VARCHAR(100), + cust_name VARCHAR(100), + city VARCHAR(100) +); +GO +INSERT INTO insert_exec_custdata VALUES + (N'GREAL', N'Great Lakes Food Market', N'Eugene'); +GO +CREATE PROCEDURE insert_exec_pcust AS + SELECT id, cust_name, city FROM insert_exec_custdata; +GO +CREATE TABLE insert_exec_identity ( + idcol INT IDENTITY, + id VARCHAR(100), + cust_name VARCHAR(100), + city VARCHAR(100) +); +GO +INSERT INTO insert_exec_identity EXEC insert_exec_pcust; +GO +SELECT * FROM insert_exec_identity; +GO +DROP PROCEDURE insert_exec_pcust; +DROP TABLE insert_exec_custdata; +DROP TABLE insert_exec_identity; +GO +-- E2: INSERT EXEC with IDENTITY_INSERT ON +CREATE TABLE insert_exec_idinsert (id INT IDENTITY, val VARCHAR(50)); +GO +CREATE PROCEDURE insert_exec_pidinsert AS + SELECT 100, 'explicit id'; +GO +SET IDENTITY_INSERT insert_exec_idinsert ON; +INSERT INTO insert_exec_idinsert (id, val) EXEC insert_exec_pidinsert; +SET IDENTITY_INSERT insert_exec_idinsert OFF; +GO +SELECT * FROM insert_exec_idinsert; +GO +DROP PROCEDURE insert_exec_pidinsert; +DROP TABLE insert_exec_idinsert; +GO +-- ============================================================================ +-- Category F: Temp Tables +-- ============================================================================ +-- F1: INSERT EXEC into Temp Table +CREATE TABLE #insert_exec_temp (id INT, name VARCHAR(50)); +GO +CREATE PROCEDURE insert_exec_ptemp AS + SELECT 1, 'one' + UNION ALL SELECT 2, 'two'; +GO +INSERT INTO #insert_exec_temp EXEC insert_exec_ptemp; +GO +SELECT * FROM #insert_exec_temp ORDER BY id; +GO +DROP PROCEDURE insert_exec_ptemp; +DROP TABLE #insert_exec_temp; +GO +-- F2: INSERT EXEC with Temp Table Inside Procedure +CREATE TABLE insert_exec_fromtemp (val INT); +GO +CREATE PROCEDURE insert_exec_pwithtemp AS + CREATE TABLE #inner_temp (x INT); + INSERT INTO #inner_temp VALUES (1), (2), (3); + SELECT x * 10 FROM #inner_temp; +GO +INSERT INTO insert_exec_fromtemp EXEC insert_exec_pwithtemp; +GO +SELECT * FROM insert_exec_fromtemp ORDER BY val; +GO +DROP PROCEDURE insert_exec_pwithtemp; +DROP TABLE insert_exec_fromtemp; +GO +-- ============================================================================ +-- Category G: Advanced SQL Constructs +-- ============================================================================ +-- G1: INSERT EXEC with CTE in Procedure +CREATE TABLE insert_exec_cte (val INT); +GO +CREATE PROCEDURE insert_exec_pcte AS + WITH cte AS ( + SELECT 1 AS n + UNION ALL + SELECT n + 1 FROM cte WHERE n < 5 + ) + SELECT n FROM cte; +GO +INSERT INTO insert_exec_cte EXEC insert_exec_pcte; +GO +SELECT * FROM insert_exec_cte ORDER BY val; +GO +DROP PROCEDURE insert_exec_pcte; +DROP TABLE insert_exec_cte; +GO +-- G2: INSERT EXEC with UNION ALL +CREATE TABLE insert_exec_union (a INT); +GO +CREATE PROCEDURE insert_exec_punion AS + SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3; +GO +INSERT INTO insert_exec_union EXEC insert_exec_punion; +GO +SELECT * FROM insert_exec_union ORDER BY a; +GO +DROP PROCEDURE insert_exec_punion; +DROP TABLE insert_exec_union; +GO +-- G3: INSERT EXEC with Conditional SELECT +CREATE TABLE insert_exec_cond (val INT); +GO +CREATE PROCEDURE insert_exec_pcond @flag BIT AS + IF @flag = 1 + SELECT 100; + ELSE + SELECT 200; +GO +INSERT INTO insert_exec_cond EXEC insert_exec_pcond @flag = 1; +INSERT INTO insert_exec_cond EXEC insert_exec_pcond @flag = 0; +GO +SELECT * FROM insert_exec_cond ORDER BY val; +GO +DROP PROCEDURE insert_exec_pcond; +DROP TABLE insert_exec_cond; +GO +-- G4: INSERT EXEC with Loop in Procedure +CREATE TABLE insert_exec_loop (iteration INT, value INT); +GO +CREATE PROCEDURE insert_exec_ploop AS + DECLARE @i INT = 1; + WHILE @i <= 5 + BEGIN + SELECT @i, @i * 10; + SET @i = @i + 1; + END +GO +INSERT INTO insert_exec_loop EXEC insert_exec_ploop; +GO +SELECT * FROM insert_exec_loop ORDER BY iteration; +GO +DROP PROCEDURE insert_exec_ploop; +DROP TABLE insert_exec_loop; +GO + +-- ============================================================================ +-- Category H: Transaction Behavior +-- ============================================================================ +-- H1: INSERT EXEC with Explicit Transaction - COMMIT +CREATE TABLE insert_exec_txn1 (val INT); +GO +CREATE PROCEDURE insert_exec_ptxn1 AS + SELECT 1; + SELECT 2; +GO +BEGIN TRANSACTION; +INSERT INTO insert_exec_txn1 EXEC insert_exec_ptxn1; +COMMIT; +GO +SELECT COUNT(*) AS row_count FROM insert_exec_txn1; +GO +DROP PROCEDURE insert_exec_ptxn1; +DROP TABLE insert_exec_txn1; +GO +-- H2: INSERT EXEC with Explicit Transaction - ROLLBACK +CREATE TABLE insert_exec_txn2 (val INT); +GO +CREATE PROCEDURE insert_exec_ptxn2 AS + SELECT 1; + SELECT 2; +GO +BEGIN TRANSACTION; +INSERT INTO insert_exec_txn2 EXEC insert_exec_ptxn2; +ROLLBACK; +GO +SELECT COUNT(*) AS row_count FROM insert_exec_txn2; +GO +DROP PROCEDURE insert_exec_ptxn2; +DROP TABLE insert_exec_txn2; +GO +-- H3: INSERT EXEC with Multiple Procedures in Transaction +CREATE TABLE insert_exec_txn3 (source VARCHAR(10), val INT); +GO +CREATE PROCEDURE insert_exec_ptxn3a AS SELECT 'p1', 1; +GO +CREATE PROCEDURE insert_exec_ptxn3b AS SELECT 'p2', 2; +GO +BEGIN TRANSACTION; +INSERT INTO insert_exec_txn3 EXEC insert_exec_ptxn3a; +INSERT INTO insert_exec_txn3 EXEC insert_exec_ptxn3b; +COMMIT; +GO +SELECT * FROM insert_exec_txn3 ORDER BY val; +GO +DROP PROCEDURE insert_exec_ptxn3a; +DROP PROCEDURE insert_exec_ptxn3b; +DROP TABLE insert_exec_txn3; +GO +-- H4: INSERT EXEC with Transaction Inside Procedure +CREATE TABLE insert_exec_txn4 (a INT); +GO +CREATE PROCEDURE insert_exec_ptxn4 AS +BEGIN TRY + BEGIN TRANSACTION; + SELECT 555; + COMMIT; +END TRY +BEGIN CATCH + IF @@TRANCOUNT > 0 ROLLBACK; +END CATCH +GO +INSERT INTO insert_exec_txn4 EXEC insert_exec_ptxn4; +GO +SELECT * FROM insert_exec_txn4; +GO +DROP PROCEDURE insert_exec_ptxn4; +DROP TABLE insert_exec_txn4; +GO +-- ============================================================================ +-- Category I: TRY/CATCH Behavior (BABEL-5922) +-- ============================================================================ +-- I1: TRY/CATCH with Error - Rows Should Be Rolled Back +CREATE TABLE insert_exec_trycatch1 (id INT, id1 INT); +GO +CREATE PROCEDURE insert_exec_ptrycatch1 AS +BEGIN TRY + SELECT 1, 1; + SELECT 1/0; +END TRY +BEGIN CATCH +END CATCH +GO +INSERT INTO insert_exec_trycatch1 EXEC insert_exec_ptrycatch1; +GO +SELECT COUNT(*) AS row_count FROM insert_exec_trycatch1; +GO +DROP PROCEDURE insert_exec_ptrycatch1; +DROP TABLE insert_exec_trycatch1; +GO +-- I2: TRY/CATCH with Successful Execution +CREATE TABLE insert_exec_trycatch2 (a INT); +GO +CREATE PROCEDURE insert_exec_ptrycatch2 AS +BEGIN TRY + SELECT 100; + SELECT 200; +END TRY +BEGIN CATCH + SELECT -1; +END CATCH +GO +INSERT INTO insert_exec_trycatch2 EXEC insert_exec_ptrycatch2; +GO +SELECT * FROM insert_exec_trycatch2 ORDER BY a; +GO +DROP PROCEDURE insert_exec_ptrycatch2; +DROP TABLE insert_exec_trycatch2; +GO +-- I3: TRY/CATCH with RAISERROR (Severity < 11 - continues) +CREATE TABLE insert_exec_trycatch3 (val INT); +GO +CREATE PROCEDURE insert_exec_ptrycatch3 AS +BEGIN TRY + SELECT 1; + RAISERROR('Info message', 10, 1); + SELECT 2; +END TRY +BEGIN CATCH + SELECT -1; +END CATCH +GO +INSERT INTO insert_exec_trycatch3 EXEC insert_exec_ptrycatch3; +GO +SELECT * FROM insert_exec_trycatch3 ORDER BY val; +GO +DROP PROCEDURE insert_exec_ptrycatch3; +DROP TABLE insert_exec_trycatch3; +GO +-- I4: Nested TRY/CATCH +CREATE TABLE insert_exec_trycatch4 (val INT); +GO +CREATE PROCEDURE insert_exec_ptrycatch4 AS +BEGIN TRY + SELECT 1; + BEGIN TRY + SELECT 2; + SELECT 1/0; + END TRY + BEGIN CATCH + SELECT 3; + END CATCH + SELECT 4; +END TRY +BEGIN CATCH + SELECT -1; +END CATCH +GO +INSERT INTO insert_exec_trycatch4 EXEC insert_exec_ptrycatch4; +GO +SELECT * FROM insert_exec_trycatch4 ORDER BY val; +GO +DROP PROCEDURE insert_exec_ptrycatch4; +DROP TABLE insert_exec_trycatch4; +GO +-- I5: Nested Procedure with TRY/CATCH - Success Case +-- Tests that internal savepoints work correctly when a nested procedure +-- has TRY-CATCH that catches an error. The inner proc catches the error +-- and continues, outer proc should see all rows. +CREATE TABLE insert_exec_trycatch5 (val INT); +GO +CREATE PROCEDURE insert_exec_ptrycatch5_inner AS +BEGIN TRY + SELECT 10; + SELECT 1/0; -- Error caught by inner TRY-CATCH + SELECT 20; -- Not reached +END TRY +BEGIN CATCH + SELECT 30; -- Error handler runs +END CATCH +SELECT 40; -- Continues after TRY-CATCH +GO +CREATE PROCEDURE insert_exec_ptrycatch5_outer AS + SELECT 1; + EXEC insert_exec_ptrycatch5_inner; + SELECT 2; +GO +INSERT INTO insert_exec_trycatch5 EXEC insert_exec_ptrycatch5_outer; +GO +SELECT * FROM insert_exec_trycatch5 ORDER BY val; +GO +DROP PROCEDURE insert_exec_ptrycatch5_outer; +DROP PROCEDURE insert_exec_ptrycatch5_inner; +DROP TABLE insert_exec_trycatch5; +GO +-- I6: Nested Procedure with TRY/CATCH - Failure Case (THROW re-raises) +-- Tests that when inner proc re-throws an error, it propagates correctly +-- and all rows are rolled back as expected for INSERT EXEC. +CREATE TABLE insert_exec_trycatch6 (val INT); +GO +CREATE PROCEDURE insert_exec_ptrycatch6_inner AS +BEGIN TRY + SELECT 10; + SELECT 1/0; -- Error occurs +END TRY +BEGIN CATCH + SELECT 20; -- Error handler runs + THROW; -- Re-throw the error +END CATCH +GO +CREATE PROCEDURE insert_exec_ptrycatch6_outer AS + SELECT 1; + EXEC insert_exec_ptrycatch6_inner; + SELECT 2; -- Not reached due to re-thrown error +GO +INSERT INTO insert_exec_trycatch6 EXEC insert_exec_ptrycatch6_outer; +GO +SELECT COUNT(*) AS row_count FROM insert_exec_trycatch6; +GO +DROP PROCEDURE insert_exec_ptrycatch6_outer; +DROP PROCEDURE insert_exec_ptrycatch6_inner; +DROP TABLE insert_exec_trycatch6; +GO + +-- ============================================================================ +-- Category J: Error Handling +-- ============================================================================ +-- J1: INSERT EXEC with Division by Zero (skipped in INSERT EXEC) +CREATE TABLE insert_exec_err2 (val INT); +GO +CREATE PROCEDURE insert_exec_perr2 AS + SELECT 1; + SELECT 1/0; + SELECT 2; +GO +INSERT INTO insert_exec_err2 EXEC insert_exec_perr2; +GO +SELECT * FROM insert_exec_err2 ORDER BY val; +GO +DROP PROCEDURE insert_exec_perr2; +DROP TABLE insert_exec_err2; +GO +-- ============================================================================ +-- Category K: Large Result Sets +-- ============================================================================ +-- K1: INSERT EXEC with 100 Rows +CREATE TABLE insert_exec_large (id INT); +GO +CREATE PROCEDURE insert_exec_plarge AS + DECLARE @i INT = 1; + WHILE @i <= 100 + BEGIN + SELECT @i; + SET @i = @i + 1; + END +GO +INSERT INTO insert_exec_large EXEC insert_exec_plarge; +GO +SELECT COUNT(*) AS row_count FROM insert_exec_large; +GO +DROP PROCEDURE insert_exec_plarge; +DROP TABLE insert_exec_large; +GO +-- ============================================================================ +-- Category L: OUTPUT Clause (BABEL-5921) +-- ============================================================================ +-- L1: INSERT EXEC with OUTPUT Clause in Procedure +CREATE TABLE insert_exec_output (id INT); +GO +CREATE PROCEDURE insert_exec_poutput AS + DROP TABLE IF EXISTS #temp; + CREATE TABLE #temp (id INT); + INSERT INTO #temp OUTPUT INSERTED.* VALUES (1); +GO +INSERT INTO insert_exec_output EXEC insert_exec_poutput; +GO +SELECT * FROM insert_exec_output; +GO +DROP PROCEDURE insert_exec_poutput; +DROP TABLE insert_exec_output; +GO +-- ============================================================================ +-- Category M: Transaction State Visibility (@@TRANCOUNT) +-- Tests that transaction state is correctly visible inside nested procedures +-- during INSERT EXEC execution. +-- ============================================================================ +-- M1: @@TRANCOUNT visibility - no explicit transaction +CREATE TABLE insert_exec_trancount1 (trancount_val INT); +GO +CREATE PROCEDURE insert_exec_ptrancount1 AS + SELECT @@TRANCOUNT; +GO +INSERT INTO insert_exec_trancount1 EXEC insert_exec_ptrancount1; +GO +SELECT * FROM insert_exec_trancount1; +GO +DROP PROCEDURE insert_exec_ptrancount1; +DROP TABLE insert_exec_trancount1; +GO +-- M2: @@TRANCOUNT visibility - with explicit transaction +CREATE TABLE insert_exec_trancount2 (trancount_val INT); +GO +CREATE PROCEDURE insert_exec_ptrancount2 AS + SELECT @@TRANCOUNT; +GO +BEGIN TRANSACTION; +INSERT INTO insert_exec_trancount2 EXEC insert_exec_ptrancount2; +COMMIT; +GO +SELECT * FROM insert_exec_trancount2; +GO +DROP PROCEDURE insert_exec_ptrancount2; +DROP TABLE insert_exec_trancount2; +GO +-- M3: @@TRANCOUNT changes inside procedure +-- Tests that BEGIN TRAN/COMMIT inside the proc correctly updates @@TRANCOUNT +CREATE TABLE insert_exec_trancount3 (trancount_val INT); +GO +CREATE PROCEDURE insert_exec_ptrancount3 AS + SELECT @@TRANCOUNT; + BEGIN TRANSACTION; + SELECT @@TRANCOUNT; + COMMIT; + SELECT @@TRANCOUNT; +GO +INSERT INTO insert_exec_trancount3 EXEC insert_exec_ptrancount3; +GO +SELECT * FROM insert_exec_trancount3 ORDER BY trancount_val; +GO +DROP PROCEDURE insert_exec_ptrancount3; +DROP TABLE insert_exec_trancount3; +GO +-- M4: Multiple nested transactions with @@TRANCOUNT +CREATE TABLE insert_exec_trancount4 (trancount_val INT); +GO +CREATE PROCEDURE insert_exec_ptrancount4 AS + SELECT @@TRANCOUNT; + BEGIN TRANSACTION; + SELECT @@TRANCOUNT; + BEGIN TRANSACTION; + SELECT @@TRANCOUNT; + COMMIT; + SELECT @@TRANCOUNT; + COMMIT; + SELECT @@TRANCOUNT; +GO +INSERT INTO insert_exec_trancount4 EXEC insert_exec_ptrancount4; +GO +SELECT * FROM insert_exec_trancount4 ORDER BY trancount_val; +GO +DROP PROCEDURE insert_exec_ptrancount4; +DROP TABLE insert_exec_trancount4; +GO +-- ============================================================================ +-- Category N: Cross-database targets and user-defined data types (UDD) +-- Tests INSERT EXEC where the target table lives in another database, and +-- where the source procedure uses a UDD while the target uses a base type. +-- ============================================================================ +-- N1: target is cross-DB, proc is local +CREATE DATABASE otherdb; +GO +USE otherdb; +GO +CREATE TABLE dbo.t_target (val INT); +GO +USE master; +GO +CREATE PROCEDURE p_local AS SELECT 100 AS val; +GO +INSERT INTO otherdb..t_target EXEC p_local; +GO +USE otherdb; +GO +SELECT * FROM dbo.t_target; -- Expected: 100 +GO +DROP TABLE dbo.t_target; +USE master; +DROP PROCEDURE p_local; +DROP DATABASE otherdb; +GO +-- N2: both target and proc are in otherdb +CREATE DATABASE otherdb; +GO +USE otherdb; +GO +CREATE TABLE dbo.t_target (val INT); +GO +CREATE PROCEDURE dbo.p_remote AS SELECT 200 AS val; +GO +USE master; +GO +INSERT INTO otherdb..t_target EXEC otherdb.dbo.p_remote; +GO +USE otherdb; +GO +SELECT * FROM dbo.t_target; -- Expected: 200 +GO +DROP TABLE dbo.t_target; +DROP PROCEDURE dbo.p_remote; +USE master; +DROP DATABASE otherdb; +GO +-- N3: source uses UDD, target uses base type (UDD-to-base-type coercion) +CREATE TYPE custom_int FROM INT NOT NULL; +GO +CREATE PROCEDURE dbo.p_udd AS + DECLARE @v custom_int = 42; + SELECT @v AS x; +GO +CREATE TABLE dbo.t_basetype (x INT); +GO +INSERT INTO dbo.t_basetype EXEC dbo.p_udd; +GO +SELECT * FROM dbo.t_basetype; -- Expected: 42 +GO +DROP TABLE dbo.t_basetype; +DROP PROCEDURE dbo.p_udd; +DROP TYPE custom_int; +GO +-- N4: target is a temp table, source uses UDD +CREATE TYPE custom_int FROM INT NOT NULL; +GO +CREATE PROCEDURE dbo.p_udd AS + DECLARE @v custom_int = 99; + SELECT @v AS x; +GO +CREATE TABLE #t_temp (x INT); +GO +INSERT INTO #t_temp EXEC dbo.p_udd; +GO +SELECT * FROM #t_temp; -- Expected: 99 +GO +DROP TABLE #t_temp; +DROP PROCEDURE dbo.p_udd; +DROP TYPE custom_int; +GO +-- ============================================================================ +-- Cleanup verification +-- ============================================================================ +SELECT 'All INSERT EXEC tests completed successfully' AS status; +GO From 0d34db67bc77806631dd80719aa732b1f84d3997 Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Tue, 9 Jun 2026 10:50:58 +0000 Subject: [PATCH 02/11] Address INSERT EXEC PR review comments --- .../src/backend/tds/tdsresponse.c | 34 +++++------ contrib/babelfishpg_tsql/src/hooks.c | 11 ---- contrib/babelfishpg_tsql/src/iterative_exec.c | 36 ++++++------ contrib/babelfishpg_tsql/src/pl_exec-2.c | 17 +++--- contrib/babelfishpg_tsql/src/pl_exec.c | 13 ++--- contrib/babelfishpg_tsql/src/pl_insert_exec.c | 57 ++----------------- contrib/babelfishpg_tsql/src/pltsql.h | 1 - contrib/babelfishpg_tsql/src/pltsql_utils.c | 57 ++++++++----------- 8 files changed, 73 insertions(+), 153 deletions(-) diff --git a/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c b/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c index 424f656a4a0..2afdb00d4a7 100644 --- a/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c +++ b/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c @@ -2708,6 +2708,16 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error) ListCell *l; PLtsql_expr *expr = ((PLtsql_stmt_execsql *) stmt)->sqlstmt; + /* + * True if this statement runs inside an INSERT EXEC. Covers + * both paths: the new path uses the global context + * (pltsql_insert_exec_active), the legacy path uses the + * per-estate flag (estate->insert_exec). + */ + bool insert_exec_active = estate->insert_exec || + (pltsql_plugin_handler_ptr->pltsql_insert_exec_active && + pltsql_plugin_handler_ptr->pltsql_insert_exec_active()); + /* * XXX: Once an error occurs, the expr and expr->plan may be * freed. In that case, we've to save the command type in @@ -2730,34 +2740,21 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error) * is inside the procedure of an INSERT-EXEC, * or if the INSERT itself is an INSERT-EXEC * and it just returned error. - * - * INSERT EXEC detection covers both paths: the - * new path uses the global context - * (pltsql_insert_exec_active), the legacy path - * uses the per-estate flag (estate->insert_exec). */ row_count_valid = - !(estate->insert_exec || - (pltsql_plugin_handler_ptr->pltsql_insert_exec_active && - pltsql_plugin_handler_ptr->pltsql_insert_exec_active())) && + !insert_exec_active && !(markErrorFlag && ((PLtsql_stmt_execsql *) stmt)->insert_exec); } else if (plansource->commandTag == CMDTAG_UPDATE) { command_type = TDS_CMD_UPDATE; - row_count_valid = - !(estate->insert_exec || - (pltsql_plugin_handler_ptr->pltsql_insert_exec_active && - pltsql_plugin_handler_ptr->pltsql_insert_exec_active())); + row_count_valid = !insert_exec_active; } else if (plansource->commandTag == CMDTAG_DELETE) { command_type = TDS_CMD_DELETE; - row_count_valid = - !(estate->insert_exec || - (pltsql_plugin_handler_ptr->pltsql_insert_exec_active && - pltsql_plugin_handler_ptr->pltsql_insert_exec_active())); + row_count_valid = !insert_exec_active; } /* @@ -2767,10 +2764,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 || - (pltsql_plugin_handler_ptr->pltsql_insert_exec_active && - pltsql_plugin_handler_ptr->pltsql_insert_exec_active())); + row_count_valid = !insert_exec_active; } } } diff --git a/contrib/babelfishpg_tsql/src/hooks.c b/contrib/babelfishpg_tsql/src/hooks.c index ef83309ab14..ea035176b7a 100644 --- a/contrib/babelfishpg_tsql/src/hooks.c +++ b/contrib/babelfishpg_tsql/src/hooks.c @@ -3622,17 +3622,6 @@ bbf_object_access_hook(ObjectAccessType access, Oid classId, Oid objectId, int s /* Call view dependency handling function */ handle_bbf_view_binding_on_object_drop(&obj, false); } - - /* - * Detect DROP of the INSERT EXEC target table. - * If the executed procedure drops the target table, we need to fail - * the INSERT EXEC to prevent errors during flush. - */ - if (OidIsValid(insert_exec_ctx.target_rel_oid) && - objectId == insert_exec_ctx.target_rel_oid) - { - insert_exec_ctx.is_target_relation_modified = true; - } } if (access == OAT_DROP && classId == ProcedureRelationId) { diff --git a/contrib/babelfishpg_tsql/src/iterative_exec.c b/contrib/babelfishpg_tsql/src/iterative_exec.c index 20e98243e7f..c0f0d3d5992 100644 --- a/contrib/babelfishpg_tsql/src/iterative_exec.c +++ b/contrib/babelfishpg_tsql/src/iterative_exec.c @@ -1115,6 +1115,22 @@ ignore_catch_block_for_unmapped_error(PLtsql_execstate *estate) return false; } +/* + * When a TRY-CATCH is inside the procedure executed by an INSERT EXEC, the + * INSERT EXEC is still in progress. Column/datatype mismatch errors must roll + * back every buffered row, so they bypass the CATCH block and are re-thrown. + */ +static +bool +ignore_catch_block_for_insert_exec(PLtsql_execstate *estate) +{ + if (pltsql_insert_exec_error_at_trycatch_level() || !pltsql_insert_exec_active()) + return false; + + return (estate->cur_error->error != NULL && + estate->cur_error->error->sqlerrcode == ERRCODE_DATATYPE_MISMATCH); +} + /* Cases where transaction is no longer committable */ static bool @@ -1333,7 +1349,6 @@ dispatch_stmt_handle_error(PLtsql_execstate *estate, if (!pltsql_implicit_transactions && is_batch_command(stmt) && !is_part_of_pltsql_trigger(estate) && - !pltsql_insert_exec_active() && before_tran_count != NestedTranCount) ereport(ERROR, (errcode(ERRCODE_T_R_INTEGRITY_CONSTRAINT_VIOLATION), @@ -1371,7 +1386,7 @@ dispatch_stmt_handle_error(PLtsql_execstate *estate, } else if (!IsTransactionBlockActive()) { - if (is_part_of_pltsql_trycatch_block(estate) && !pltsql_insert_exec_active()) + if (is_part_of_pltsql_trycatch_block(estate)) { HOLD_INTERRUPTS(); elog(DEBUG1, "TSQL TXN PG semantics : Rollback current transaction"); @@ -1604,20 +1619,9 @@ exec_stmt_iterative(PLtsql_execstate *estate, ExecCodes *exec_codes, ExecConfig_ estate->cur_error->severity = exec_state_call_stack->error_data.error_severity; estate->cur_error->state = exec_state_call_stack->error_data.error_state; - /* - * If a TRY-CATCH is inside the executed procedure, INSERT - * EXEC is still in progress. Re-throw column mismatch errors - * to roll back all rows. Other errors (e.g., division by - * zero) are caught by TRY-CATCH, preserving rows inserted - * before the error. - */ - if (!pltsql_insert_exec_error_at_trycatch_level() && - pltsql_insert_exec_active()) - { - if (estate->cur_error->error != NULL && - estate->cur_error->error->sqlerrcode == ERRCODE_DATATYPE_MISMATCH) - ReThrowError(estate->cur_error->error); - } + /* INSERT EXEC: re-throw errors that must abort the whole flush. */ + if (ignore_catch_block_for_insert_exec(estate)) + ReThrowError(estate->cur_error->error); /* Goto error handling blocks */ *pc = err_handler_pc - 1; /* same as how goto handles PC */ diff --git a/contrib/babelfishpg_tsql/src/pl_exec-2.c b/contrib/babelfishpg_tsql/src/pl_exec-2.c index 63beaaeeb87..b369a8267e4 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec-2.c +++ b/contrib/babelfishpg_tsql/src/pl_exec-2.c @@ -743,11 +743,8 @@ exec_stmt_push_result(PLtsql_execstate *estate, Assert(stmt->query != NULL); - /* - * Legacy INSERT EXEC path: handle naked SELECT stmt differently. - * estate->insert_exec is only set when the new INSERT EXEC GUC is off. - */ - if (!pltsql_enable_new_insert_exec && estate->insert_exec) + /* Handle naked SELECT stmt differently for INSERT ... EXECUTE (legacy path). */ + if (estate->insert_exec) return exec_stmt_insert_execute_select(estate, stmt->query); exec_run_select(estate, stmt->query, &portal); @@ -756,7 +753,7 @@ exec_stmt_push_result(PLtsql_execstate *estate, * When INSERT EXEC is active (new path), redirect results to the temp * table instead of sending to client. */ - if (pltsql_enable_new_insert_exec && pltsql_insert_exec_active()) + if (pltsql_insert_exec_active()) { receiver = CreateInsertExecDestReceiver(); receiver->rStartup(receiver, CMD_SELECT, portal->tupDesc); @@ -815,7 +812,7 @@ exec_run_dml_with_output(PLtsql_execstate *estate, PLtsql_stmt_push_result *stmt * INSERT EXEC context check - redirect OUTPUT clause results to temp table * instead of sending to client. */ - if (pltsql_enable_new_insert_exec && pltsql_insert_exec_active()) + if (pltsql_insert_exec_active()) { receiver = CreateInsertExecDestReceiver(); receiver->rStartup(receiver, CMD_SELECT, portal->tupDesc); @@ -1200,7 +1197,7 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) stmt->target = (PLtsql_variable *) row; } - if (!pltsql_enable_new_insert_exec && estate->insert_exec) + if (estate->insert_exec) { /* * For EXEC under INSERT ... EXECUTE, get the expected TupleDesc, @@ -1292,7 +1289,7 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) } } - if (!pltsql_enable_new_insert_exec && estate->insert_exec) + if (estate->insert_exec) { /* * For EXEC under INSERT ... EXECUTE, get the rows sent back by @@ -3937,7 +3934,7 @@ execute_plan_and_push_result(PLtsql_execstate *estate, PLtsql_expr *expr, ParamL { receiver = None_Receiver; } - else if (pltsql_enable_new_insert_exec && pltsql_insert_exec_active()) + else if (pltsql_insert_exec_active()) { /* * INSERT EXEC context is active (new path) - redirect results to temp diff --git a/contrib/babelfishpg_tsql/src/pl_exec.c b/contrib/babelfishpg_tsql/src/pl_exec.c index b66c51c3d39..1586ca80e31 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_exec.c @@ -4858,8 +4858,7 @@ exec_stmt_execsql(PLtsql_execstate *estate, * Only validate inside TRY blocks; system procedures like sp_columns * have internal SELECTs with varying column counts outside TRY blocks. */ - if (pltsql_enable_new_insert_exec && - pltsql_insert_exec_active() && + if (pltsql_insert_exec_active() && is_part_of_pltsql_trycatch_block(estate)) { if (stmt->sqlstmt && stmt->sqlstmt->query) @@ -4870,12 +4869,8 @@ exec_stmt_execsql(PLtsql_execstate *estate, PG_TRY(); { - /* - * Legacy INSERT EXEC path: handle naked SELECT stmt differently. - * estate->insert_exec is only set when the new INSERT EXEC GUC is off. - */ - if (!pltsql_enable_new_insert_exec && - stmt->need_to_push_result && estate->insert_exec) + /* Handle naked SELECT stmt differently for INSERT ... EXECUTE */ + if (stmt->need_to_push_result && estate->insert_exec) { int ret = exec_stmt_insert_execute_select(estate, expr); @@ -5293,7 +5288,7 @@ exec_stmt_execsql(PLtsql_execstate *estate, support_tsql_trans && (enable_txn_in_triggers || estate->trigdata == NULL) && !ro_func && - (pltsql_enable_new_insert_exec ? !pltsql_insert_exec_active() : !estate->insert_exec)) + !pltsql_insert_exec_active() && !estate->insert_exec) { commit_stmt(estate, (estate->tsql_trigger_flags & TSQL_TRAN_STARTED)); diff --git a/contrib/babelfishpg_tsql/src/pl_insert_exec.c b/contrib/babelfishpg_tsql/src/pl_insert_exec.c index 4c6e40fa363..4f834c021a2 100644 --- a/contrib/babelfishpg_tsql/src/pl_insert_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_insert_exec.c @@ -199,17 +199,15 @@ pltsql_set_insert_exec_context_info(const char *target_table) /* * Reset the global INSERT EXEC context to a clean state. * - * Releases the target table lock, frees the heap-allocated target table name, - * and zeroes every field. Used on both the normal exit and safety-net cleanup - * paths. The string must be pfree'd before the memset, or memset alone would - * leak it in TopMemoryContext. + * Frees the heap-allocated target table name and zeroes every field. Used on + * both the normal exit and safety-net cleanup paths. The target table's + * RowExclusiveLock is transaction-scoped and released automatically when the + * implicit transaction commits or aborts. The string must be pfree'd before + * the memset, or memset alone would leak it in TopMemoryContext. */ void pltsql_insert_exec_reset_all(void) { - /* Release target table lock */ - pltsql_insert_exec_close_target_table(); - /* Free heap-allocated target table name before zeroing its pointer */ if (insert_exec_ctx.target_table) pfree(insert_exec_ctx.target_table); @@ -316,48 +314,6 @@ pltsql_insert_exec_open_target_table(const char *target_table, insert_exec_ctx.is_target_relation_modified = false; } -/* - * Close the target table that was held open during INSERT EXEC. - * Called after the flush completes or on error cleanup. - * - * For regular tables: Release the RowExclusiveLock we acquired. - * For temp tables: Just clear the OID (no lock was acquired). - * - * Note: We only release the lock if we're not in an aborted transaction state. - * If the transaction was aborted, the lock has already been released. - */ -void -pltsql_insert_exec_close_target_table(void) -{ - if (OidIsValid(insert_exec_ctx.target_rel_oid)) - { - const char *target = insert_exec_ctx.target_table; - bool is_temp_table = (target != NULL && (target[0] == '#' || target[0] == '@')); - - /* - * Only release the lock for regular tables (not temp tables). - * Temp tables don't have locks to release. - */ - if (!is_temp_table && !IsAbortedTransactionBlockState()) - { - PG_TRY(); - { - UnlockRelationOid(insert_exec_ctx.target_rel_oid, RowExclusiveLock); - } - PG_CATCH(); - { - FlushErrorState(); - /* Ignore unlock failures - table may have been dropped */ - } - PG_END_TRY(); - } - insert_exec_ctx.target_rel_oid = InvalidOid; - } - - /* Reset the modification flag */ - insert_exec_ctx.is_target_relation_modified = false; -} - /* * Validate column count from query string BEFORE plan preparation. * @@ -962,9 +918,6 @@ insert_exec_success_cleanup(PLtsql_execstate *estate, InsertExecInfo *info) { /* Flush temp table to target table */ flush_insert_exec_temp_table(estate, flush_schema, info->db_name, column_list); - - /* Close target table after flush completes */ - pltsql_insert_exec_close_target_table(); } PG_CATCH(); { diff --git a/contrib/babelfishpg_tsql/src/pltsql.h b/contrib/babelfishpg_tsql/src/pltsql.h index 0224dcef540..b18ca5c3e9e 100644 --- a/contrib/babelfishpg_tsql/src/pltsql.h +++ b/contrib/babelfishpg_tsql/src/pltsql.h @@ -2552,7 +2552,6 @@ extern bool pltsql_insert_exec_active(void); extern bool pltsql_insert_exec_error_at_trycatch_level(void); extern void pltsql_insert_exec_open_target_table(const char *target_table,const char *schema_name_in, const char *db_name_in); -extern void pltsql_insert_exec_close_target_table(void); extern void pltsql_insert_exec_validate_column_count_from_query(const char *query_string); /* INSERT EXEC helper functions */ diff --git a/contrib/babelfishpg_tsql/src/pltsql_utils.c b/contrib/babelfishpg_tsql/src/pltsql_utils.c index fb055e23a8b..a1b68a84aaf 100644 --- a/contrib/babelfishpg_tsql/src/pltsql_utils.c +++ b/contrib/babelfishpg_tsql/src/pltsql_utils.c @@ -18,7 +18,6 @@ #include "storage/lock.h" #include "utils/builtins.h" #include "utils/elog.h" -#include "utils/guc.h" #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/fmgroids.h" @@ -134,24 +133,19 @@ PLTsqlProcessTransaction(Node *parsetree, * blocked if it would make @@TRANCOUNT go from 1 to 0. If the * procedure did BEGIN TRAN first (@@TRANCOUNT = 2), then COMMIT * is allowed (@@TRANCOUNT goes from 2 to 1). + * + * INSERT EXEC detection differs by path: the new path uses the + * global context (pltsql_insert_exec_active), the legacy path + * the per-estate flag (estate->insert_exec). */ - if (pltsql_enable_new_insert_exec) - { - if (pltsql_insert_exec_active() && NestedTranCount <= 1) - ereport(ERROR, - (errcode(ERRCODE_TRANSACTION_ROLLBACK), - errmsg("Cannot use the COMMIT statement within an INSERT-EXEC statement unless BEGIN TRANSACTION is used first."))); - } - else - { - if (exec_state_call_stack && - exec_state_call_stack->estate && - exec_state_call_stack->estate->insert_exec && - NestedTranCount <= 1) - ereport(ERROR, - (errcode(ERRCODE_TRANSACTION_ROLLBACK), - errmsg("Cannot use the COMMIT statement within an INSERT-EXEC statement unless BEGIN TRANSACTION is used first."))); - } + if (((pltsql_insert_exec_active()) || + (exec_state_call_stack && + exec_state_call_stack->estate && + exec_state_call_stack->estate->insert_exec)) && + NestedTranCount <= 1) + ereport(ERROR, + (errcode(ERRCODE_TRANSACTION_ROLLBACK), + errmsg("Cannot use the COMMIT statement within an INSERT-EXEC statement unless BEGIN TRANSACTION is used first."))); PLTsqlCommitTransaction(qc, stmt->chain); } @@ -162,23 +156,18 @@ PLTsqlProcessTransaction(Node *parsetree, /* * Block ROLLBACK during INSERT EXEC. * ROLLBACK is not allowed within an INSERT-EXEC statement. + * + * INSERT EXEC detection differs by path: the new path uses the + * global context (pltsql_insert_exec_active), the legacy path + * the per-estate flag (estate->insert_exec). */ - if (pltsql_enable_new_insert_exec) - { - if (pltsql_insert_exec_active()) - ereport(ERROR, - (errcode(ERRCODE_TRANSACTION_ROLLBACK), - errmsg("Cannot use the ROLLBACK statement within an INSERT-EXEC statement."))); - } - else - { - if (exec_state_call_stack && - exec_state_call_stack->estate && - exec_state_call_stack->estate->insert_exec) - ereport(ERROR, - (errcode(ERRCODE_TRANSACTION_ROLLBACK), - errmsg("Cannot use the ROLLBACK statement within an INSERT-EXEC statement."))); - } + if ((pltsql_insert_exec_active()) || + (exec_state_call_stack && + exec_state_call_stack->estate && + exec_state_call_stack->estate->insert_exec)) + ereport(ERROR, + (errcode(ERRCODE_TRANSACTION_ROLLBACK), + errmsg("Cannot use the ROLLBACK statement within an INSERT-EXEC statement."))); PLTsqlRollbackTransaction(txnName, qc, stmt->chain); } break; From ebca659d63598e91b1742292c61d65794db27557 Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Thu, 11 Jun 2026 09:57:35 +0000 Subject: [PATCH 03/11] Cross-Schema query fix --- contrib/babelfishpg_tsql/src/pl_insert_exec.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contrib/babelfishpg_tsql/src/pl_insert_exec.c b/contrib/babelfishpg_tsql/src/pl_insert_exec.c index 4f834c021a2..c20d755f365 100644 --- a/contrib/babelfishpg_tsql/src/pl_insert_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_insert_exec.c @@ -612,6 +612,10 @@ flush_insert_exec_temp_table(PLtsql_execstate *estate, const char *target_schema quote_identifier(target_db), quote_identifier(target_schema ? target_schema : "dbo"), quote_identifier(target_table)); + else if (target_schema != NULL) + qualified_target = psprintf("%s.%s", + quote_identifier(target_schema), + quote_identifier(target_table)); else qualified_target = pstrdup(quote_identifier(target_table)); @@ -912,7 +916,7 @@ void insert_exec_success_cleanup(PLtsql_execstate *estate, InsertExecInfo *info) { char *column_list = build_quoted_column_list(info->columns); - const char *flush_schema = (info->db_name != NULL) ? info->schema : NULL; + const char *flush_schema = (info->db_name != NULL || info->schema != NULL) ? info->schema : NULL; PG_TRY(); { From 52157c4f23bdbd892047bae5d7b05f813c866ab9 Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Fri, 12 Jun 2026 09:07:54 +0000 Subject: [PATCH 04/11] - Suppress only benign errors in PG_CATCH blocks; re-throw cancellation, OOM, FATAL, and admin shutdown so they cannot be silently swallowed. - Allocate target_table in TopTransactionContext so the safety-net xact cleanup never dereferences a freed per-statement allocation. --- contrib/babelfishpg_tsql/src/pl_insert_exec.c | 82 +++++++++++++++---- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/contrib/babelfishpg_tsql/src/pl_insert_exec.c b/contrib/babelfishpg_tsql/src/pl_insert_exec.c index c20d755f365..8e42c690610 100644 --- a/contrib/babelfishpg_tsql/src/pl_insert_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_insert_exec.c @@ -181,13 +181,14 @@ get_insertable_column_list(const char *table_name, const char *physical_schema) /* * Set the global INSERT EXEC context with target table info. - * Called from ANTLR parser when INSERT EXEC is detected. * This is called BEFORE temp table creation - just stores the target info. */ void pltsql_set_insert_exec_context_info(const char *target_table) { - insert_exec_ctx.target_table = target_table ? pstrdup(target_table) : NULL; + insert_exec_ctx.target_table = target_table + ? MemoryContextStrdup(TopTransactionContext, target_table) + : NULL; /* * Snapshot the call stack entry at INSERT EXEC start. Comparing this * pointer later tells us whether an error occurred at the INSERT EXEC @@ -203,7 +204,8 @@ pltsql_set_insert_exec_context_info(const char *target_table) * both the normal exit and safety-net cleanup paths. The target table's * RowExclusiveLock is transaction-scoped and released automatically when the * implicit transaction commits or aborts. The string must be pfree'd before - * the memset, or memset alone would leak it in TopMemoryContext. + * the memset; otherwise the allocation in TopTransactionContext lingers + * until end of transaction. */ void pltsql_insert_exec_reset_all(void) @@ -226,15 +228,16 @@ pltsql_insert_exec_reset_all(void) */ void pltsql_insert_exec_open_target_table(const char *target_table, - const char *schema_name_in, - const char *db_name_in) + const char *schema_name_in, + const char *db_name_in) { - RangeVar *rv; - Oid relid; - char *schema_name = NULL; - char *table_name = NULL; - char *physical_schema = NULL; - bool is_temp_table; + RangeVar *rv; + Oid relid; + char *schema_name = NULL; + char *table_name = NULL; + char *physical_schema = NULL; + bool is_temp_table; + MemoryContext oldcontext; if (target_table == NULL) return; @@ -296,13 +299,26 @@ pltsql_insert_exec_open_target_table(const char *target_table, * but we detect it via is_target_relation_modified flag set by * ObjectPostAlterHook. */ + oldcontext = CurrentMemoryContext; + PG_TRY(); { LockRelationOid(relid, RowExclusiveLock); } PG_CATCH(); { + /* Only suppress benign errors (table dropped etc.) - re-throw the rest */ + ErrorData *edata; + MemoryContextSwitchTo(oldcontext); + edata = CopyErrorData(); FlushErrorState(); + + if (edata->sqlerrcode != ERRCODE_UNDEFINED_TABLE && + edata->sqlerrcode != ERRCODE_LOCK_NOT_AVAILABLE) + { + ReThrowError(edata); + } + FreeErrorData(edata); return; } PG_END_TRY(); @@ -340,6 +356,7 @@ pltsql_insert_exec_validate_column_count_from_query(const char *query_string) Relation temp_rel; TupleDesc temp_tupdesc; int temp_natts; + MemoryContext oldcontext; /* Caller must ensure INSERT EXEC is active before calling */ Assert(pltsql_insert_exec_active()); @@ -359,13 +376,28 @@ pltsql_insert_exec_validate_column_count_from_query(const char *query_string) * runtime error like division by zero will not fire here. On any error, * defer to the normal execution path. */ + oldcontext = CurrentMemoryContext; + PG_TRY(); { plan = SPI_prepare(query_string, 0, NULL); } PG_CATCH(); { + /* Only suppress benign errors - re-throw cancellation/OOM/FATAL */ + ErrorData *edata; + MemoryContextSwitchTo(oldcontext); + edata = CopyErrorData(); FlushErrorState(); + + if (edata->sqlerrcode == ERRCODE_QUERY_CANCELED || + edata->sqlerrcode == ERRCODE_ADMIN_SHUTDOWN || + edata->sqlerrcode == ERRCODE_OUT_OF_MEMORY || + edata->elevel >= FATAL) + { + ReThrowError(edata); + } + FreeErrorData(edata); return; /* Parse/analyze error - normal path will report it */ } PG_END_TRY(); @@ -395,6 +427,8 @@ pltsql_insert_exec_validate_column_count_from_query(const char *query_string) SPI_freeplan(plan); /* Get temp table column count */ + oldcontext = CurrentMemoryContext; + PG_TRY(); { temp_rel = table_open(temp_table_oid, AccessShareLock); @@ -404,7 +438,20 @@ pltsql_insert_exec_validate_column_count_from_query(const char *query_string) } PG_CATCH(); { + /* Only suppress benign errors - re-throw cancellation/OOM/FATAL */ + ErrorData *edata; + MemoryContextSwitchTo(oldcontext); + edata = CopyErrorData(); FlushErrorState(); + + if (edata->sqlerrcode == ERRCODE_QUERY_CANCELED || + edata->sqlerrcode == ERRCODE_ADMIN_SHUTDOWN || + edata->sqlerrcode == ERRCODE_OUT_OF_MEMORY || + edata->elevel >= FATAL) + { + ReThrowError(edata); + } + FreeErrorData(edata); return; } PG_END_TRY(); @@ -846,8 +893,8 @@ insertexec_destroy(DestReceiver *self) */ bool insert_exec_setup(PLtsql_execstate *estate, - InsertExecInfo *info, - bool start_implicit_txn) + InsertExecInfo *info, + bool start_implicit_txn) { char *column_list = NULL; @@ -936,7 +983,12 @@ insert_exec_success_cleanup(PLtsql_execstate *estate, InsertExecInfo *info) if (column_list != NULL) pfree(column_list); - /* Reset the INSERT EXEC context. */ + /* + * Reset the context before committing. The context is only needed through + * the flush above, so it is safe to clear here. If the commit below fails, + * the transaction aborts and pltsql_xact_cb runs the same reset (now a + * no-op) - so the early reset never leaves a dangling context. + */ pltsql_insert_exec_reset_all(); /* @@ -949,4 +1001,4 @@ insert_exec_success_cleanup(PLtsql_execstate *estate, InsertExecInfo *info) commit_stmt(estate, true); estate->tsql_trigger_flags &= ~TSQL_TRAN_STARTED; } -} \ No newline at end of file +} From 0215a2b879cd2259601eec05d4f52f7e91ebdd71 Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Tue, 16 Jun 2026 23:19:48 +0000 Subject: [PATCH 05/11] Heap-allocate insert_exec_ctx with NULL guards, resolve unqualified target schema via the user's default schema, remove the unnecessary LockRelationOid try/catch, clean up the column-count probe error handling, and guard table-variable cleanup against an aborted transaction (fixes an IsTransactionState() crash on the INSERT EXEC error path in a function). Adds same-session DDL detection tests. --- .../src/backend/tds/tdsresponse.c | 16 +- contrib/babelfishpg_tsql/src/hooks.c | 6 +- contrib/babelfishpg_tsql/src/iterative_exec.c | 5 + contrib/babelfishpg_tsql/src/pl_insert_exec.c | 215 +++++++--------- contrib/babelfishpg_tsql/src/pltsql.h | 9 +- test/JDBC/expected/BABEL-INSERT-EXEC.out | 233 ++++++++++++++++++ test/JDBC/input/BABEL-INSERT-EXEC.sql | 189 ++++++++++++++ 7 files changed, 522 insertions(+), 151 deletions(-) diff --git a/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c b/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c index 2afdb00d4a7..c03304f2660 100644 --- a/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c +++ b/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c @@ -2708,13 +2708,8 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error) ListCell *l; PLtsql_expr *expr = ((PLtsql_stmt_execsql *) stmt)->sqlstmt; - /* - * True if this statement runs inside an INSERT EXEC. Covers - * both paths: the new path uses the global context - * (pltsql_insert_exec_active), the legacy path uses the - * per-estate flag (estate->insert_exec). - */ - bool insert_exec_active = estate->insert_exec || + /* True if running inside a new-path INSERT EXEC. */ + bool insert_exec_active = (pltsql_plugin_handler_ptr->pltsql_insert_exec_active && pltsql_plugin_handler_ptr->pltsql_insert_exec_active()); @@ -2742,6 +2737,7 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error) * and it just returned error. */ row_count_valid = + !estate->insert_exec && !insert_exec_active && !(markErrorFlag && ((PLtsql_stmt_execsql *) stmt)->insert_exec); @@ -2749,12 +2745,12 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error) else if (plansource->commandTag == CMDTAG_UPDATE) { command_type = TDS_CMD_UPDATE; - row_count_valid = !insert_exec_active; + row_count_valid = !estate->insert_exec && !insert_exec_active; } else if (plansource->commandTag == CMDTAG_DELETE) { command_type = TDS_CMD_DELETE; - row_count_valid = !insert_exec_active; + row_count_valid = !estate->insert_exec && !insert_exec_active; } /* @@ -2764,7 +2760,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 = !insert_exec_active; + row_count_valid = !estate->insert_exec && !insert_exec_active; } } } diff --git a/contrib/babelfishpg_tsql/src/hooks.c b/contrib/babelfishpg_tsql/src/hooks.c index ea035176b7a..dd67ea3d93c 100644 --- a/contrib/babelfishpg_tsql/src/hooks.c +++ b/contrib/babelfishpg_tsql/src/hooks.c @@ -3668,8 +3668,10 @@ bbf_object_access_hook(ObjectAccessType access, Oid classId, Oid objectId, int s */ if ((access == OAT_POST_ALTER || access == OAT_DROP) && classId == RelationRelationId) { - if (OidIsValid(insert_exec_ctx.target_rel_oid) && objectId == insert_exec_ctx.target_rel_oid) - insert_exec_ctx.is_target_relation_modified = true; + if (insert_exec_ctx != NULL && + OidIsValid(insert_exec_ctx->target_rel_oid) && + objectId == insert_exec_ctx->target_rel_oid) + insert_exec_ctx->is_target_relation_modified = true; } } diff --git a/contrib/babelfishpg_tsql/src/iterative_exec.c b/contrib/babelfishpg_tsql/src/iterative_exec.c index c0f0d3d5992..add34a74692 100644 --- a/contrib/babelfishpg_tsql/src/iterative_exec.c +++ b/contrib/babelfishpg_tsql/src/iterative_exec.c @@ -1621,7 +1621,12 @@ exec_stmt_iterative(PLtsql_execstate *estate, ExecCodes *exec_codes, ExecConfig_ /* INSERT EXEC: re-throw errors that must abort the whole flush. */ if (ignore_catch_block_for_insert_exec(estate)) + { + elog(DEBUG4, + "INSERT EXEC failed due to error (sqlerrcode=%d) inside procedure TRY-CATCH; re-throwing to abort flush", + estate->cur_error->error ? estate->cur_error->error->sqlerrcode : 0); ReThrowError(estate->cur_error->error); + } /* Goto error handling blocks */ *pc = err_handler_pc - 1; /* same as how goto handles PC */ diff --git a/contrib/babelfishpg_tsql/src/pl_insert_exec.c b/contrib/babelfishpg_tsql/src/pl_insert_exec.c index 8e42c690610..1c29f34a053 100644 --- a/contrib/babelfishpg_tsql/src/pl_insert_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_insert_exec.c @@ -70,9 +70,9 @@ static void insertexec_shutdown(DestReceiver *self); static void insertexec_destroy(DestReceiver *self); /* - * Global INSERT EXEC context - defined in pltsql.h, declared here. + * Global INSERT EXEC context pointer - defined in pltsql.h, declared here. */ -InsertExecContext insert_exec_ctx; +InsertExecContext *insert_exec_ctx = NULL; extern void exec_set_rowcount(uint64 rowno); extern void exec_set_found(PLtsql_execstate *estate, bool state); @@ -104,6 +104,35 @@ build_quoted_column_list(List *columns) return cols.data; } +/* + * Resolve the logical T-SQL schema name for an INSERT EXEC target when the + * statement did not qualify it. + */ +static char * +resolve_insert_exec_schema_name(const char *schema_name_in, const char *db_name_in) +{ + const char *db; + char *user; + char *default_schema; + char *result; + + if (schema_name_in != NULL) + return pstrdup(schema_name_in); + + db = (db_name_in != NULL) ? db_name_in : get_cur_db_name(); + user = get_user_for_database(db); + default_schema = user ? get_authid_user_ext_schema_name(db, user) : NULL; + + result = pstrdup(default_schema ? default_schema : "dbo"); + + if (default_schema) + pfree(default_schema); + if (user) + pfree(user); + + return result; +} + /* * Get a comma-separated list of non-IDENTITY, non-computed column names * for a table by opening the relation and iterating over its tuple descriptor. @@ -186,7 +215,11 @@ get_insertable_column_list(const char *table_name, const char *physical_schema) void pltsql_set_insert_exec_context_info(const char *target_table) { - insert_exec_ctx.target_table = target_table + Assert(insert_exec_ctx == NULL); + insert_exec_ctx = MemoryContextAllocZero(TopTransactionContext, + sizeof(InsertExecContext)); + + insert_exec_ctx->target_table = target_table ? MemoryContextStrdup(TopTransactionContext, target_table) : NULL; /* @@ -194,37 +227,29 @@ pltsql_set_insert_exec_context_info(const char *target_table) * pointer later tells us whether an error occurred at the INSERT EXEC * level or inside the executed procedure. */ - insert_exec_ctx.call_stack_entry = exec_state_call_stack; + insert_exec_ctx->call_stack_entry = exec_state_call_stack; } /* - * Reset the global INSERT EXEC context to a clean state. - * - * Frees the heap-allocated target table name and zeroes every field. Used on - * both the normal exit and safety-net cleanup paths. The target table's - * RowExclusiveLock is transaction-scoped and released automatically when the - * implicit transaction commits or aborts. The string must be pfree'd before - * the memset; otherwise the allocation in TopTransactionContext lingers - * until end of transaction. + * Reset the global INSERT EXEC context to a clean state */ void pltsql_insert_exec_reset_all(void) { - /* Free heap-allocated target table name before zeroing its pointer */ - if (insert_exec_ctx.target_table) - pfree(insert_exec_ctx.target_table); + InsertExecContext *ctx = insert_exec_ctx; + + if (ctx == NULL) + return; - /* Reset all fields to zero/NULL */ - memset(&insert_exec_ctx, 0, sizeof(InsertExecContext)); + insert_exec_ctx = NULL; + + if (ctx->target_table) + pfree(ctx->target_table); + pfree(ctx); } /* - * Capture target table OID and lock for change detection. - * Regular tables get RowExclusiveLock to block concurrent DDL; - * temp tables only get OID captured (session-local). - * - * Schema changes are detected via is_target_relation_modified flag, - * which is set by the ObjectPostAlterHook when the target table is altered. + * Capture target table OID for change detection */ void pltsql_insert_exec_open_target_table(const char *target_table, @@ -237,7 +262,6 @@ pltsql_insert_exec_open_target_table(const char *target_table, char *table_name = NULL; char *physical_schema = NULL; bool is_temp_table; - MemoryContext oldcontext; if (target_table == NULL) return; @@ -257,15 +281,12 @@ pltsql_insert_exec_open_target_table(const char *target_table, return; /* Store the OID for schema verification (no lock for temp tables) */ - insert_exec_ctx.target_rel_oid = relid; + insert_exec_ctx->target_rel_oid = relid; } else { table_name = pstrdup(target_table); - if (schema_name_in != NULL) - schema_name = pstrdup(schema_name_in); - else - schema_name = pstrdup("dbo"); /* default schema */ + schema_name = resolve_insert_exec_schema_name(schema_name_in, db_name_in); /* * Resolve against the target's database when a 3-part name * (db..table) was used; otherwise the current database. @@ -291,59 +312,15 @@ pltsql_insert_exec_open_target_table(const char *target_table, return; } - /* - * Acquire RowExclusiveLock on the target table. - * This lock will be held until the end of the transaction. - * It blocks concurrent sessions from modifying the table. - * Note: Same-session DROP/ALTER is still allowed by PostgreSQL, - * but we detect it via is_target_relation_modified flag set by - * ObjectPostAlterHook. - */ - oldcontext = CurrentMemoryContext; - - PG_TRY(); - { - LockRelationOid(relid, RowExclusiveLock); - } - PG_CATCH(); - { - /* Only suppress benign errors (table dropped etc.) - re-throw the rest */ - ErrorData *edata; - MemoryContextSwitchTo(oldcontext); - edata = CopyErrorData(); - FlushErrorState(); - - if (edata->sqlerrcode != ERRCODE_UNDEFINED_TABLE && - edata->sqlerrcode != ERRCODE_LOCK_NOT_AVAILABLE) - { - ReThrowError(edata); - } - FreeErrorData(edata); - return; - } - PG_END_TRY(); - - insert_exec_ctx.target_rel_oid = relid; + insert_exec_ctx->target_rel_oid = relid; } /* Initialize the modification flag to false */ - insert_exec_ctx.is_target_relation_modified = false; + insert_exec_ctx->is_target_relation_modified = false; } /* - * Validate column count from query string BEFORE plan preparation. - * - * T-SQL requires column-mismatch errors to take priority over runtime errors - * (e.g. division by zero) inside TRY-CATCH. PostgreSQL evaluates constant - * expressions during plan *preparation*, so we use SPI_prepare here, which - * parse-analyzes and rewrites the query (producing its result tuple - * descriptor) without planning it - hence without triggering constant folding. - * Comparing that descriptor's column count to the temp table lets the mismatch - * win over a constant-folded runtime error. - * - * If the result shape can't be determined (parse/analyze error, non - * row-returning statement such as EXEC, or multiple statements), we defer to - * the normal path - the DestReceiver still catches mismatches at runtime. + * Validate column count from query string BEFORE plan preparation */ void pltsql_insert_exec_validate_column_count_from_query(const char *query_string) @@ -363,10 +340,9 @@ pltsql_insert_exec_validate_column_count_from_query(const char *query_string) /* * Temp table must exist. It is created during INSERT EXEC setup before any - * procedure-body statement runs, so it is normally valid here; if it isn't - * (context not fully set up yet), defer to the normal execution path. + * procedure-body statement runs, so it is normally valid here */ - temp_table_oid = insert_exec_ctx.temp_table_oid; + temp_table_oid = insert_exec_ctx->temp_table_oid; if (!OidIsValid(temp_table_oid)) return; @@ -384,7 +360,10 @@ pltsql_insert_exec_validate_column_count_from_query(const char *query_string) } PG_CATCH(); { - /* Only suppress benign errors - re-throw cancellation/OOM/FATAL */ + /* + * Probe-only failure: defer to real execution, which re-raises it. + * Re-throw cancellation/OOM immediately - those must be honored now + */ ErrorData *edata; MemoryContextSwitchTo(oldcontext); edata = CopyErrorData(); @@ -392,8 +371,7 @@ pltsql_insert_exec_validate_column_count_from_query(const char *query_string) if (edata->sqlerrcode == ERRCODE_QUERY_CANCELED || edata->sqlerrcode == ERRCODE_ADMIN_SHUTDOWN || - edata->sqlerrcode == ERRCODE_OUT_OF_MEMORY || - edata->elevel >= FATAL) + edata->sqlerrcode == ERRCODE_OUT_OF_MEMORY) { ReThrowError(edata); } @@ -427,34 +405,10 @@ pltsql_insert_exec_validate_column_count_from_query(const char *query_string) SPI_freeplan(plan); /* Get temp table column count */ - oldcontext = CurrentMemoryContext; - - PG_TRY(); - { - temp_rel = table_open(temp_table_oid, AccessShareLock); - temp_tupdesc = RelationGetDescr(temp_rel); - temp_natts = temp_tupdesc->natts; - table_close(temp_rel, AccessShareLock); - } - PG_CATCH(); - { - /* Only suppress benign errors - re-throw cancellation/OOM/FATAL */ - ErrorData *edata; - MemoryContextSwitchTo(oldcontext); - edata = CopyErrorData(); - FlushErrorState(); - - if (edata->sqlerrcode == ERRCODE_QUERY_CANCELED || - edata->sqlerrcode == ERRCODE_ADMIN_SHUTDOWN || - edata->sqlerrcode == ERRCODE_OUT_OF_MEMORY || - edata->elevel >= FATAL) - { - ReThrowError(edata); - } - FreeErrorData(edata); - return; - } - PG_END_TRY(); + temp_rel = table_open(temp_table_oid, AccessShareLock); + temp_tupdesc = RelationGetDescr(temp_rel); + temp_natts = temp_tupdesc->natts; + table_close(temp_rel, AccessShareLock); /* Check for column count mismatch */ if (query_natts != temp_natts) @@ -477,27 +431,25 @@ pltsql_insert_exec_validate_column_count_from_query(const char *query_string) bool pltsql_insert_exec_active(void) { - return (insert_exec_ctx.target_table != NULL); + return (insert_exec_ctx != NULL); } /* * Called from the sigsetjmp handler when a TRY-CATCH catches an error during * INSERT EXEC. Returns true if the error surfaced at the INSERT EXEC level - * (call-stack head matches where INSERT EXEC started), false if the catching - * TRY-CATCH is deeper inside the executed procedure - the caller uses the - * false case to re-throw a column-mismatch error past the inner handler. + * false if the catching TRY-CATCH is deeper inside the executed procedure */ bool pltsql_insert_exec_error_at_trycatch_level(void) { - if (insert_exec_ctx.target_table == NULL) + if (insert_exec_ctx == NULL) return false; /* * Same call-stack head as when INSERT EXEC started → error is at the * INSERT EXEC level. A deeper node → error is inside the called procedure. */ - return exec_state_call_stack == insert_exec_ctx.call_stack_entry; + return exec_state_call_stack == insert_exec_ctx->call_stack_entry; } /* @@ -540,10 +492,11 @@ create_insert_exec_temp_table(const char *target_table, const char *column_list, */ if (!(target_table[0] == '#' || target_table[0] == '@')) { - const char *sname = (schema_name_in != NULL) ? schema_name_in : "dbo"; + char *sname = resolve_insert_exec_schema_name(schema_name_in, db_name_in); physical_schema = get_physical_schema_name( (db_name_in != NULL) ? (char *) db_name_in : get_cur_db_name(), sname); + pfree(sname); if (physical_schema == NULL) elog(ERROR, "INSERT EXEC failed due to unresolvable schema for target table \"%s\"", target_table); @@ -613,22 +566,22 @@ flush_insert_exec_temp_table(PLtsql_execstate *estate, const char *target_schema { StringInfoData flush_query; int rc; - Oid temp_oid = insert_exec_ctx.temp_table_oid; - const char *target_table = insert_exec_ctx.target_table; + Oid temp_oid; + const char *target_table; const char *temp_name; Relation temp_rel; char *qualified_target; + if (insert_exec_ctx == NULL) + return; + + temp_oid = insert_exec_ctx->temp_table_oid; + target_table = insert_exec_ctx->target_table; + if (!OidIsValid(temp_oid) || target_table == NULL) return; - /* - * Use the same %s-placeholder format string as the TDS error_mapping entry - * (ERRCODE_OBJECT_IN_USE -> 556). The TDS layer maps errors by the - * untranslated errmsg format string, so hardcoding the rendered text would - * not match and would fall back to the default code. - */ - if (insert_exec_ctx.is_target_relation_modified) + if (insert_exec_ctx->is_target_relation_modified) ereport(ERROR, (errcode(ERRCODE_OBJECT_IN_USE), errmsg("cannot %s \"%s\" because it is being used by active queries in this session", @@ -732,11 +685,11 @@ insertexec_startup(DestReceiver *self, int operation, TupleDesc typeinfo) */ result_natts = typeinfo->natts; - if (!OidIsValid(insert_exec_ctx.temp_table_oid)) + if (!OidIsValid(insert_exec_ctx->temp_table_oid)) elog(ERROR, "INSERT EXEC failed due to missing temp table OID"); /* Open temp table to read schema only - closed before startup returns */ - temp_rel = table_open(insert_exec_ctx.temp_table_oid, AccessShareLock); + temp_rel = table_open(insert_exec_ctx->temp_table_oid, AccessShareLock); temp_tupdesc = RelationGetDescr(temp_rel); temp_natts = temp_tupdesc->natts; @@ -837,7 +790,7 @@ insertexec_receive(TupleTableSlot *slot, DestReceiver *self) Assert(myState->econtext != NULL); /* Open temp table fresh for each tuple - avoids stale handles across subtransactions */ - temp_rel = table_open(insert_exec_ctx.temp_table_oid, RowExclusiveLock); + temp_rel = table_open(insert_exec_ctx->temp_table_oid, RowExclusiveLock); /* Reset per-tuple memory context for expression evaluation */ ResetExprContext(myState->econtext); @@ -940,7 +893,7 @@ insert_exec_setup(PLtsql_execstate *estate, pltsql_insert_exec_open_target_table(info->target, info->schema, info->db_name); /* Create temp table based on target table structure */ - insert_exec_ctx.temp_table_oid = create_insert_exec_temp_table(info->target, column_list, + insert_exec_ctx->temp_table_oid = create_insert_exec_temp_table(info->target, column_list, info->schema, info->db_name); if (column_list != NULL) @@ -972,7 +925,7 @@ insert_exec_success_cleanup(PLtsql_execstate *estate, InsertExecInfo *info) } PG_CATCH(); { - /* Release target table lock and reset context before re-throwing. */ + /* Free the column list and reset context before re-throwing. */ if (column_list != NULL) pfree(column_list); pltsql_insert_exec_reset_all(); diff --git a/contrib/babelfishpg_tsql/src/pltsql.h b/contrib/babelfishpg_tsql/src/pltsql.h index b18ca5c3e9e..1e49b4741da 100644 --- a/contrib/babelfishpg_tsql/src/pltsql.h +++ b/contrib/babelfishpg_tsql/src/pltsql.h @@ -2530,20 +2530,13 @@ extern char *tsql_format_type_extended(Oid type_oid, int32 typemod, bits16 flags typedef struct InsertExecContext { Oid temp_table_oid; /* OID of temp table for buffering */ - /* - * Target table name (bare), captured at parse time. Kept as a string - * because it is needed when target_rel_oid can't be looked up: during - * cleanup with no live transaction, after the target is dropped (556 - * error), for not-yet-existing or table-variable targets, and to preserve - * the cross-DB logical name. - */ char *target_table; PLExecStateCallStack *call_stack_entry; /* Call stack entry when INSERT EXEC started */ Oid target_rel_oid; /* OID of target table - lock held to detect schema changes */ bool is_target_relation_modified; /* Set by bbf_object_access_hook when target table is altered */ } InsertExecContext; -extern InsertExecContext insert_exec_ctx; +extern InsertExecContext *insert_exec_ctx; extern Oid create_insert_exec_temp_table(const char *target_table, const char *column_list, const char *schema_name_in, const char *db_name_in); extern void pltsql_set_insert_exec_context_info(const char *target_table); diff --git a/test/JDBC/expected/BABEL-INSERT-EXEC.out b/test/JDBC/expected/BABEL-INSERT-EXEC.out index b90c9c2c0e6..64aac75685a 100644 --- a/test/JDBC/expected/BABEL-INSERT-EXEC.out +++ b/test/JDBC/expected/BABEL-INSERT-EXEC.out @@ -96,6 +96,28 @@ DROP TABLE IF EXISTS insert_exec_trancount1; DROP TABLE IF EXISTS insert_exec_trancount2; DROP TABLE IF EXISTS insert_exec_trancount3; DROP TABLE IF EXISTS insert_exec_trancount4; +-- Category P (same-session DDL) cleanup +DROP PROCEDURE IF EXISTS dbo.p_p1_drop; +DROP PROCEDURE IF EXISTS dbo.p_p2_alter; +DROP PROCEDURE IF EXISTS dbo.p_p3_dropcol; +DROP PROCEDURE IF EXISTS dbo.p_p4_trycatch; +DROP PROCEDURE IF EXISTS dbo.p_p5_otherdrop; +DROP TABLE IF EXISTS dbo.t_p1; +DROP TABLE IF EXISTS dbo.t_p2; +DROP TABLE IF EXISTS dbo.t_p3; +DROP TABLE IF EXISTS dbo.t_p4; +DROP TABLE IF EXISTS dbo.t_p5_target; +DROP TABLE IF EXISTS dbo.t_p5_other; +-- Category Q (INSERT EXEC inside a function) cleanup +DROP FUNCTION IF EXISTS dbo.fn_q1; +DROP FUNCTION IF EXISTS dbo.fn_q2; +DROP PROCEDURE IF EXISTS dbo.p_q1; +DROP PROCEDURE IF EXISTS dbo.p_q2; +GO +-- Category R (INSERT EXEC in TRY-CATCH, variable source) cleanup +DROP PROCEDURE IF EXISTS dbo.p_var_run; +DROP PROCEDURE IF EXISTS dbo.p_var_src; +DROP TABLE IF EXISTS dbo.var_tgt; GO -- ============================================================================ -- Category A: Basic INSERT EXEC Scenarios @@ -1304,6 +1326,217 @@ DROP TABLE #t_temp; DROP PROCEDURE dbo.p_udd; DROP TYPE custom_int; GO + + +-- ============================================================================ +-- Category P: Same-session DDL on INSERT EXEC target +-- ============================================================================ +-- Concurrent-session DDL is blocked by RowExclusiveLock. Same-session DDL +-- (DROP / ALTER inside the procedure body) is detected by ObjectPostAlterHook +-- which sets is_target_relation_modified; flush surfaces ERRCODE_OBJECT_IN_USE. +-- P1: Procedure body drops the target table mid-execution +CREATE TABLE dbo.t_p1 (x INT); +GO +CREATE PROCEDURE dbo.p_p1_drop AS +BEGIN + SELECT 1 AS x; + DROP TABLE dbo.t_p1; +END; +GO +INSERT INTO dbo.t_p1 EXEC dbo.p_p1_drop; -- Expected: error, target was dropped +GO +~~ERROR (Code: 556)~~ + +~~ERROR (Message: cannot DROP TABLE "t_p1" because it is being used by active queries in this session)~~ + +DROP PROCEDURE dbo.p_p1_drop; +GO + +-- P2: Procedure body alters the target table (add column) +CREATE TABLE dbo.t_p2 (x INT); +GO +CREATE PROCEDURE dbo.p_p2_alter AS +BEGIN + SELECT 1 AS x; + ALTER TABLE dbo.t_p2 ADD y INT; +END; +GO +INSERT INTO dbo.t_p2 EXEC dbo.p_p2_alter; -- Expected: error, target was altered +GO +~~ERROR (Code: 556)~~ + +~~ERROR (Message: cannot DROP TABLE "t_p2" because it is being used by active queries in this session)~~ + +SELECT * FROM dbo.t_p2; -- Expected: empty (insert blocked) +GO +~~START~~ +int +~~END~~ + +DROP TABLE dbo.t_p2; +DROP PROCEDURE dbo.p_p2_alter; +GO + +-- P3: Procedure body alters the target table (drop column) +CREATE TABLE dbo.t_p3 (x INT, y INT); +GO +CREATE PROCEDURE dbo.p_p3_dropcol AS +BEGIN + SELECT 1 AS x, 2 AS y; + ALTER TABLE dbo.t_p3 DROP COLUMN y; +END; +GO +INSERT INTO dbo.t_p3 EXEC dbo.p_p3_dropcol; -- Expected: error +GO +~~ERROR (Code: 556)~~ + +~~ERROR (Message: cannot DROP TABLE "t_p3" because it is being used by active queries in this session)~~ + +DROP TABLE dbo.t_p3; +DROP PROCEDURE dbo.p_p3_dropcol; +GO + +-- P4: TRY-CATCH cannot suppress same-session DDL detection +-- The hook only sets a flag; the error is raised at flush time, outside the +-- procedure's TRY-CATCH scope, so it cannot be silently swallowed. +CREATE TABLE dbo.t_p4 (x INT); +GO +CREATE PROCEDURE dbo.p_p4_trycatch AS +BEGIN + BEGIN TRY + SELECT 1 AS x; + DROP TABLE dbo.t_p4; + END TRY + BEGIN CATCH + SELECT 99 AS x; + END CATCH +END; +GO +INSERT INTO dbo.t_p4 EXEC dbo.p_p4_trycatch; -- Expected: error not suppressed +GO +~~ERROR (Code: 556)~~ + +~~ERROR (Message: cannot DROP TABLE "t_p4" because it is being used by active queries in this session)~~ + +DROP PROCEDURE dbo.p_p4_trycatch; +GO + +-- P5: Same-session DDL on a DIFFERENT table is not flagged +CREATE TABLE dbo.t_p5_target (x INT); +CREATE TABLE dbo.t_p5_other (y INT); +GO +CREATE PROCEDURE dbo.p_p5_otherdrop AS +BEGIN + SELECT 1 AS x; + DROP TABLE dbo.t_p5_other; +END; +GO +INSERT INTO dbo.t_p5_target EXEC dbo.p_p5_otherdrop; -- Expected: success +GO +~~ROW COUNT: 1~~ + +SELECT * FROM dbo.t_p5_target; -- Expected: 1 +GO +~~START~~ +int +1 +~~END~~ + +DROP TABLE dbo.t_p5_target; +DROP PROCEDURE dbo.p_p5_otherdrop; +GO + + +-- ============================================================================ +-- Category Q: INSERT EXEC inside a T-SQL function +-- ============================================================================ +-- Q1: Positive - function captures procedure output into a table variable +CREATE PROCEDURE dbo.p_q1 AS +BEGIN + SELECT 1 AS a, 'x' AS b + UNION ALL SELECT 2, 'y'; +END; +GO +CREATE FUNCTION dbo.fn_q1() +RETURNS @t TABLE (a INT, b VARCHAR(10)) +AS +BEGIN + INSERT INTO @t EXEC dbo.p_q1; + RETURN; +END; +GO +SELECT * FROM dbo.fn_q1() ORDER BY a; -- Expected: (1,x) (2,y) +GO +~~START~~ +int#!#varchar +1#!#x +2#!#y +~~END~~ + + +-- Q2: Source procedure returns no rows - function returns empty +CREATE PROCEDURE dbo.p_q2 AS +BEGIN + SELECT 1 AS a WHERE 1 = 0; +END; +GO +CREATE FUNCTION dbo.fn_q2() +RETURNS @t TABLE (a INT) +AS +BEGIN + INSERT INTO @t EXEC dbo.p_q2; + RETURN; +END; +GO +SELECT * FROM dbo.fn_q2(); -- Expected: empty +GO +~~START~~ +int +~~END~~ + +DROP FUNCTION IF EXISTS dbo.fn_q1; +DROP FUNCTION IF EXISTS dbo.fn_q2; +DROP PROCEDURE IF EXISTS dbo.p_q1; +DROP PROCEDURE IF EXISTS dbo.p_q2; +GO + +-- ============================================================================ +-- Category R: INSERT EXEC inside TRY-CATCH with a variable-referencing source +-- ============================================================================ +CREATE TABLE dbo.var_tgt (a int); +GO +CREATE PROCEDURE dbo.p_var_src AS +BEGIN + DECLARE @x int = 7; + SELECT @x AS a; +END; +GO +CREATE PROCEDURE dbo.p_var_run AS +BEGIN + BEGIN TRY + INSERT INTO dbo.var_tgt EXEC dbo.p_var_src; + END TRY + BEGIN CATCH + SELECT ERROR_MESSAGE() AS err; + END CATCH +END; +GO +EXEC dbo.p_var_run; -- Expected: 1 row affected, CATCH does not fire +GO +~~ROW COUNT: 1~~ + +SELECT * FROM dbo.var_tgt; -- Expected: 7 +GO +~~START~~ +int +7 +~~END~~ + +DROP PROCEDURE IF EXISTS dbo.p_var_run; +DROP PROCEDURE IF EXISTS dbo.p_var_src; +DROP TABLE IF EXISTS dbo.var_tgt; +GO + -- ============================================================================ -- Cleanup verification -- ============================================================================ diff --git a/test/JDBC/input/BABEL-INSERT-EXEC.sql b/test/JDBC/input/BABEL-INSERT-EXEC.sql index a3854209294..3f54deeb731 100644 --- a/test/JDBC/input/BABEL-INSERT-EXEC.sql +++ b/test/JDBC/input/BABEL-INSERT-EXEC.sql @@ -96,6 +96,28 @@ DROP TABLE IF EXISTS insert_exec_trancount1; DROP TABLE IF EXISTS insert_exec_trancount2; DROP TABLE IF EXISTS insert_exec_trancount3; DROP TABLE IF EXISTS insert_exec_trancount4; +-- Category P (same-session DDL) cleanup +DROP PROCEDURE IF EXISTS dbo.p_p1_drop; +DROP PROCEDURE IF EXISTS dbo.p_p2_alter; +DROP PROCEDURE IF EXISTS dbo.p_p3_dropcol; +DROP PROCEDURE IF EXISTS dbo.p_p4_trycatch; +DROP PROCEDURE IF EXISTS dbo.p_p5_otherdrop; +DROP TABLE IF EXISTS dbo.t_p1; +DROP TABLE IF EXISTS dbo.t_p2; +DROP TABLE IF EXISTS dbo.t_p3; +DROP TABLE IF EXISTS dbo.t_p4; +DROP TABLE IF EXISTS dbo.t_p5_target; +DROP TABLE IF EXISTS dbo.t_p5_other; +-- Category Q (INSERT EXEC inside a function) cleanup +DROP FUNCTION IF EXISTS dbo.fn_q1; +DROP FUNCTION IF EXISTS dbo.fn_q2; +DROP PROCEDURE IF EXISTS dbo.p_q1; +DROP PROCEDURE IF EXISTS dbo.p_q2; +GO +-- Category R (INSERT EXEC in TRY-CATCH, variable source) cleanup +DROP PROCEDURE IF EXISTS dbo.p_var_run; +DROP PROCEDURE IF EXISTS dbo.p_var_src; +DROP TABLE IF EXISTS dbo.var_tgt; GO -- ============================================================================ -- Category A: Basic INSERT EXEC Scenarios @@ -931,6 +953,173 @@ DROP TABLE #t_temp; DROP PROCEDURE dbo.p_udd; DROP TYPE custom_int; GO + +-- ============================================================================ +-- Category P: Same-session DDL on INSERT EXEC target +-- ============================================================================ +-- Concurrent-session DDL is blocked by RowExclusiveLock. Same-session DDL +-- (DROP / ALTER inside the procedure body) is detected by ObjectPostAlterHook +-- which sets is_target_relation_modified; flush surfaces ERRCODE_OBJECT_IN_USE. + +-- P1: Procedure body drops the target table mid-execution +CREATE TABLE dbo.t_p1 (x INT); +GO +CREATE PROCEDURE dbo.p_p1_drop AS +BEGIN + SELECT 1 AS x; + DROP TABLE dbo.t_p1; +END; +GO +INSERT INTO dbo.t_p1 EXEC dbo.p_p1_drop; -- Expected: error, target was dropped +GO +DROP PROCEDURE dbo.p_p1_drop; +GO + +-- P2: Procedure body alters the target table (add column) +CREATE TABLE dbo.t_p2 (x INT); +GO +CREATE PROCEDURE dbo.p_p2_alter AS +BEGIN + SELECT 1 AS x; + ALTER TABLE dbo.t_p2 ADD y INT; +END; +GO +INSERT INTO dbo.t_p2 EXEC dbo.p_p2_alter; -- Expected: error, target was altered +GO +SELECT * FROM dbo.t_p2; -- Expected: empty (insert blocked) +GO +DROP TABLE dbo.t_p2; +DROP PROCEDURE dbo.p_p2_alter; +GO + +-- P3: Procedure body alters the target table (drop column) +CREATE TABLE dbo.t_p3 (x INT, y INT); +GO +CREATE PROCEDURE dbo.p_p3_dropcol AS +BEGIN + SELECT 1 AS x, 2 AS y; + ALTER TABLE dbo.t_p3 DROP COLUMN y; +END; +GO +INSERT INTO dbo.t_p3 EXEC dbo.p_p3_dropcol; -- Expected: error +GO +DROP TABLE dbo.t_p3; +DROP PROCEDURE dbo.p_p3_dropcol; +GO + +-- P4: TRY-CATCH cannot suppress same-session DDL detection +-- The hook only sets a flag; the error is raised at flush time, outside the +-- procedure's TRY-CATCH scope, so it cannot be silently swallowed. +CREATE TABLE dbo.t_p4 (x INT); +GO +CREATE PROCEDURE dbo.p_p4_trycatch AS +BEGIN + BEGIN TRY + SELECT 1 AS x; + DROP TABLE dbo.t_p4; + END TRY + BEGIN CATCH + SELECT 99 AS x; + END CATCH +END; +GO +INSERT INTO dbo.t_p4 EXEC dbo.p_p4_trycatch; -- Expected: error not suppressed +GO +DROP PROCEDURE dbo.p_p4_trycatch; +GO + +-- P5: Same-session DDL on a DIFFERENT table is not flagged +CREATE TABLE dbo.t_p5_target (x INT); +CREATE TABLE dbo.t_p5_other (y INT); +GO +CREATE PROCEDURE dbo.p_p5_otherdrop AS +BEGIN + SELECT 1 AS x; + DROP TABLE dbo.t_p5_other; +END; +GO +INSERT INTO dbo.t_p5_target EXEC dbo.p_p5_otherdrop; -- Expected: success +GO +SELECT * FROM dbo.t_p5_target; -- Expected: 1 +GO +DROP TABLE dbo.t_p5_target; +DROP PROCEDURE dbo.p_p5_otherdrop; +GO + +-- ============================================================================ +-- Category Q: INSERT EXEC inside a T-SQL function +-- ============================================================================ + +-- Q1: Positive - function captures procedure output into a table variable +CREATE PROCEDURE dbo.p_q1 AS +BEGIN + SELECT 1 AS a, 'x' AS b + UNION ALL SELECT 2, 'y'; +END; +GO +CREATE FUNCTION dbo.fn_q1() +RETURNS @t TABLE (a INT, b VARCHAR(10)) +AS +BEGIN + INSERT INTO @t EXEC dbo.p_q1; + RETURN; +END; +GO +SELECT * FROM dbo.fn_q1() ORDER BY a; -- Expected: (1,x) (2,y) +GO + +-- Q2: Source procedure returns no rows - function returns empty +CREATE PROCEDURE dbo.p_q2 AS +BEGIN + SELECT 1 AS a WHERE 1 = 0; +END; +GO +CREATE FUNCTION dbo.fn_q2() +RETURNS @t TABLE (a INT) +AS +BEGIN + INSERT INTO @t EXEC dbo.p_q2; + RETURN; +END; +GO +SELECT * FROM dbo.fn_q2(); -- Expected: empty +GO +DROP FUNCTION IF EXISTS dbo.fn_q1; +DROP FUNCTION IF EXISTS dbo.fn_q2; +DROP PROCEDURE IF EXISTS dbo.p_q1; +DROP PROCEDURE IF EXISTS dbo.p_q2; +GO + +-- ============================================================================ +-- Category R: INSERT EXEC inside TRY-CATCH with a variable-referencing source +-- ============================================================================ +CREATE TABLE dbo.var_tgt (a int); +GO +CREATE PROCEDURE dbo.p_var_src AS +BEGIN + DECLARE @x int = 7; + SELECT @x AS a; +END; +GO +CREATE PROCEDURE dbo.p_var_run AS +BEGIN + BEGIN TRY + INSERT INTO dbo.var_tgt EXEC dbo.p_var_src; + END TRY + BEGIN CATCH + SELECT ERROR_MESSAGE() AS err; + END CATCH +END; +GO +EXEC dbo.p_var_run; -- Expected: 1 row affected, CATCH does not fire +GO +SELECT * FROM dbo.var_tgt; -- Expected: 7 +GO +DROP PROCEDURE IF EXISTS dbo.p_var_run; +DROP PROCEDURE IF EXISTS dbo.p_var_src; +DROP TABLE IF EXISTS dbo.var_tgt; +GO + -- ============================================================================ -- Cleanup verification -- ============================================================================ From c9da16763e434a423fdbd4c8d2bc0bb7439ca813 Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Thu, 18 Jun 2026 18:51:13 +0000 Subject: [PATCH 06/11] Update pltsql_insert_exec_validate_column_count to use SPI_prepare_params instead of SPI_prepare and stop suppressing errors Prepare the source statement through SPI_prepare_params with the PL/tsql parser setup so @variables resolve and no errors are suppressed, and cache the plan in expr->plan for reuse. --- contrib/babelfishpg_tsql/src/pl_exec.c | 17 +--- contrib/babelfishpg_tsql/src/pl_insert_exec.c | 93 ++++++++----------- contrib/babelfishpg_tsql/src/pltsql.h | 2 +- 3 files changed, 43 insertions(+), 69 deletions(-) diff --git a/contrib/babelfishpg_tsql/src/pl_exec.c b/contrib/babelfishpg_tsql/src/pl_exec.c index 1586ca80e31..492c97b32d0 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_exec.c @@ -4850,22 +4850,13 @@ exec_stmt_execsql(PLtsql_execstate *estate, } /* - * For INSERT EXEC (new path), validate column count BEFORE plan - * preparation so column mismatch errors take priority over runtime errors - * (e.g., 1/0). PostgreSQL's eval_const_expressions() would evaluate - * expressions first. - * - * Only validate inside TRY blocks; system procedures like sp_columns - * have internal SELECTs with varying column counts outside TRY blocks. + * INSERT EXEC (new path): validate the source statement's result column + * count against the temp buffer BEFORE it is planned/executed, so a + * column-count mismatch is raised ahead of any runtime error (e.g. 1/0) */ if (pltsql_insert_exec_active() && is_part_of_pltsql_trycatch_block(estate)) - { - if (stmt->sqlstmt && stmt->sqlstmt->query) - { - pltsql_insert_exec_validate_column_count_from_query(stmt->sqlstmt->query); - } - } + pltsql_insert_exec_validate_column_count(estate, stmt); PG_TRY(); { diff --git a/contrib/babelfishpg_tsql/src/pl_insert_exec.c b/contrib/babelfishpg_tsql/src/pl_insert_exec.c index 1c29f34a053..5c4ded806ff 100644 --- a/contrib/babelfishpg_tsql/src/pl_insert_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_insert_exec.c @@ -323,9 +323,11 @@ pltsql_insert_exec_open_target_table(const char *target_table, * Validate column count from query string BEFORE plan preparation */ void -pltsql_insert_exec_validate_column_count_from_query(const char *query_string) +pltsql_insert_exec_validate_column_count(PLtsql_execstate *estate, PLtsql_stmt_execsql *stmt) { - SPIPlanPtr plan = NULL; + PLtsql_expr *expr = stmt->sqlstmt; + SPIPlanPtr plan; + List *plansources; CachedPlanSource *plansource; TupleDesc result_desc; int query_natts; @@ -333,76 +335,51 @@ pltsql_insert_exec_validate_column_count_from_query(const char *query_string) Relation temp_rel; TupleDesc temp_tupdesc; int temp_natts; - MemoryContext oldcontext; /* Caller must ensure INSERT EXEC is active before calling */ Assert(pltsql_insert_exec_active()); /* * Temp table must exist. It is created during INSERT EXEC setup before any - * procedure-body statement runs, so it is normally valid here + * procedure-body statement runs, so it is normally valid here. */ temp_table_oid = insert_exec_ctx->temp_table_oid; if (!OidIsValid(temp_table_oid)) return; + if (expr == NULL || expr->query == NULL) + return; + /* - * Parse-analyze the query to obtain its result descriptor. SPI_prepare - * stops before planning, so constant expressions are not evaluated and a - * runtime error like division by zero will not fire here. On any error, - * defer to the normal execution path. + * Reuse the cached plan if the statement was already prepared (earlier + * execution). Otherwise prepare it once, through the normal parser setup. */ - oldcontext = CurrentMemoryContext; - - PG_TRY(); - { - plan = SPI_prepare(query_string, 0, NULL); - } - PG_CATCH(); + if (expr->plan != NULL) + plan = expr->plan; + else { - /* - * Probe-only failure: defer to real execution, which re-raises it. - * Re-throw cancellation/OOM immediately - those must be honored now - */ - ErrorData *edata; - MemoryContextSwitchTo(oldcontext); - edata = CopyErrorData(); - FlushErrorState(); - - if (edata->sqlerrcode == ERRCODE_QUERY_CANCELED || - edata->sqlerrcode == ERRCODE_ADMIN_SHUTDOWN || - edata->sqlerrcode == ERRCODE_OUT_OF_MEMORY) - { - ReThrowError(edata); - } - FreeErrorData(edata); - return; /* Parse/analyze error - normal path will report it */ + expr->func = estate->func; + plan = SPI_prepare_params(expr->query, + (ParserSetupHook) pltsql_parser_setup, + (void *) expr, + CURSOR_OPT_PARALLEL_OK); + if (plan == NULL) + return; } - PG_END_TRY(); + + plansources = SPI_plan_get_plan_sources(plan); /* Expect exactly one analyzed statement with a known result shape */ - if (plan == NULL || list_length(plan->plancache_list) != 1) - { - if (plan != NULL) - SPI_freeplan(plan); - return; /* Multiple statements or unusable plan, defer to runtime */ - } + if (list_length(plansources) != 1) + return; - plansource = (CachedPlanSource *) linitial(plan->plancache_list); + plansource = (CachedPlanSource *) linitial(plansources); result_desc = plansource->resultDesc; - /* - * resultDesc is NULL for statements that do not return tuples (e.g. EXEC). - * In that case there is nothing to validate statically; defer to runtime. - */ if (result_desc == NULL) - { - SPI_freeplan(plan); return; - } query_natts = result_desc->natts; - SPI_freeplan(plan); /* Get temp table column count */ temp_rel = table_open(temp_table_oid, AccessShareLock); @@ -410,17 +387,23 @@ pltsql_insert_exec_validate_column_count_from_query(const char *query_string) temp_natts = temp_tupdesc->natts; table_close(temp_rel, AccessShareLock); - /* Check for column count mismatch */ + /* Column count mismatch: raise before execution so it wins over 1/0 etc. */ if (query_natts != temp_natts) - { - /* - * A column mismatch must roll back all rows even when caught by - * TRY-CATCH, unlike data-level errors (e.g. division by zero) which - * only drop the current row. - */ ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("structure of query does not match function result type"))); + + /* + * If we prepared the plan here (expr->plan was NULL), keep it and + * cache it so exec_stmt_execsql reuses it instead of preparing again. This + * is a result-returning SELECT, so it is not a modification statement. + */ + if (expr->plan == NULL) + { + SPI_keepplan(plan); + expr->plan = plan; + stmt->mod_stmt = false; + stmt->mod_stmt_tablevar = false; } } diff --git a/contrib/babelfishpg_tsql/src/pltsql.h b/contrib/babelfishpg_tsql/src/pltsql.h index 1e49b4741da..e71e0d117ce 100644 --- a/contrib/babelfishpg_tsql/src/pltsql.h +++ b/contrib/babelfishpg_tsql/src/pltsql.h @@ -2545,7 +2545,7 @@ extern bool pltsql_insert_exec_active(void); extern bool pltsql_insert_exec_error_at_trycatch_level(void); extern void pltsql_insert_exec_open_target_table(const char *target_table,const char *schema_name_in, const char *db_name_in); -extern void pltsql_insert_exec_validate_column_count_from_query(const char *query_string); +extern void pltsql_insert_exec_validate_column_count(PLtsql_execstate *estate, PLtsql_stmt_execsql *stmt); /* INSERT EXEC helper functions */ extern DestReceiver *CreateInsertExecDestReceiver(void); From a1566decec9c2397ff3c5f099e433ff366677db0 Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Fri, 19 Jun 2026 09:03:17 +0000 Subject: [PATCH 07/11] Move the flush inside PG_TRY with uniform PG_FINALLY cleanup, relocate column-count validation to prepare_stmt_execsql, extract the COMMIT/ROLLBACK INSERT-EXEC guard into a helper, and drop redundant temp-table/cleanup special-casing --- contrib/babelfishpg_tsql/src/iterative_exec.c | 13 +- contrib/babelfishpg_tsql/src/pl_exec-2.c | 96 ++++++-------- contrib/babelfishpg_tsql/src/pl_exec.c | 20 --- contrib/babelfishpg_tsql/src/pl_insert_exec.c | 119 +++++++----------- contrib/babelfishpg_tsql/src/pltsql-2.h | 2 +- contrib/babelfishpg_tsql/src/pltsql_utils.c | 69 +++++----- contrib/babelfishpg_tsql/src/prepare.c | 9 ++ 7 files changed, 128 insertions(+), 200 deletions(-) diff --git a/contrib/babelfishpg_tsql/src/iterative_exec.c b/contrib/babelfishpg_tsql/src/iterative_exec.c index add34a74692..b952c8f925a 100644 --- a/contrib/babelfishpg_tsql/src/iterative_exec.c +++ b/contrib/babelfishpg_tsql/src/iterative_exec.c @@ -1619,15 +1619,6 @@ exec_stmt_iterative(PLtsql_execstate *estate, ExecCodes *exec_codes, ExecConfig_ estate->cur_error->severity = exec_state_call_stack->error_data.error_severity; estate->cur_error->state = exec_state_call_stack->error_data.error_state; - /* INSERT EXEC: re-throw errors that must abort the whole flush. */ - if (ignore_catch_block_for_insert_exec(estate)) - { - elog(DEBUG4, - "INSERT EXEC failed due to error (sqlerrcode=%d) inside procedure TRY-CATCH; re-throwing to abort flush", - estate->cur_error->error ? estate->cur_error->error->sqlerrcode : 0); - ReThrowError(estate->cur_error->error); - } - /* Goto error handling blocks */ *pc = err_handler_pc - 1; /* same as how goto handles PC */ @@ -1643,7 +1634,9 @@ exec_stmt_iterative(PLtsql_execstate *estate, ExecCodes *exec_codes, ExecConfig_ * error context */ } } - if (ignore_catch_block_for_unmapped_error(estate) || terminate_batch) + /* INSERT EXEC: re-throw errors that must abort the whole flush */ + if (ignore_catch_block_for_insert_exec(estate) || + ignore_catch_block_for_unmapped_error(estate) || terminate_batch) { elog(DEBUG1, "TSQL TXN Ignore catch block error mapping failed : %d", last_error_mapping_failed); ReThrowError(estate->cur_error->error); diff --git a/contrib/babelfishpg_tsql/src/pl_exec-2.c b/contrib/babelfishpg_tsql/src/pl_exec-2.c index b369a8267e4..213829273d3 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec-2.c +++ b/contrib/babelfishpg_tsql/src/pl_exec-2.c @@ -870,9 +870,6 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) /* INSERT EXEC handling - temp table lifecycle */ bool insert_exec_setup_done = false; - /* set true at the end of the PG_TRY body to distinguish success from error in PG_FINALLY */ - volatile bool exec_succeeded = false; - /* * We need to disable the explain gucs incase of sp_reset_connection * execution otherwise we will get explain output for it which is @@ -1317,18 +1314,42 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) dest->rDestroy(dest); } - exec_succeeded = true; + if (rc < 0) + elog(ERROR, "SPI_execute_plan_with_paramlist failed executing query \"%s\": %s", + expr->query, SPI_result_code_string(rc)); + + /* + * Check result rowcount; if there's one row, assign procedure's output + * values back to the appropriate variables + */ + if (SPI_processed == 1) + { + SPITupleTable *tuptab = SPI_tuptable; + + if (!stmt->target) + elog(ERROR, "DO statement returned a row"); + + if (tuptab != NULL) + exec_move_row(estate, stmt->target, tuptab->vals[0], tuptab->tupdesc); + } + else if (SPI_processed > 1) + elog(ERROR, "procedure call returned more than one row"); + + exec_eval_cleanup(estate); + SPI_freetuptable(SPI_tuptable); + + if (insert_exec_setup_done) + insert_exec_flush_and_cleanup(estate, stmt->insert_exec); } PG_FINALLY(); { - /* - * On the error path (new INSERT EXEC path only), tear down the temp - * table context before re-throwing. - */ - if (!exec_succeeded && - (insert_exec_setup_done || pltsql_insert_exec_active())) + if (insert_exec_setup_done) pltsql_insert_exec_reset_all(); + /* + * Restore the database/user context if a cross-db EXEC switched it. + * Runs on both success and error paths. + */ if (strcmp(get_current_pltsql_db_name(), save_db_name) != 0) set_cur_user_db_and_path(save_db_name, false, false); @@ -1348,34 +1369,6 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) } PG_END_TRY(); - if (rc < 0) - elog(ERROR, "SPI_execute_plan_with_paramlist failed executing query \"%s\": %s", - expr->query, SPI_result_code_string(rc)); - - /* - * Check result rowcount; if there's one row, assign procedure's output - * values back to the appropriate variables. - */ - if (SPI_processed == 1) - { - SPITupleTable *tuptab = SPI_tuptable; - - if (!stmt->target) - elog(ERROR, "DO statement returned a row"); - - if (tuptab != NULL) - exec_move_row(estate, stmt->target, tuptab->vals[0], tuptab->tupdesc); - } - else if (SPI_processed > 1) - elog(ERROR, "procedure call returned more than one row"); - - exec_eval_cleanup(estate); - SPI_freetuptable(SPI_tuptable); - - /* Flush temp table to target table and cleanup after procedure completes */ - if (insert_exec_setup_done) - insert_exec_success_cleanup(estate, stmt->insert_exec); - return PLTSQL_RC_OK; } @@ -1553,8 +1546,6 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt) /* INSERT EXEC handling - temp table lifecycle */ bool insert_exec_setup_done = false; - /* set true at the end of the PG_TRY body to distinguish success from error in PG_FINALLY */ - volatile bool exec_succeeded = false; LOCAL_FCINFO(fcinfo, 1); /* @@ -1601,13 +1592,12 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt) if (fcinfo->isnull) elog(ERROR, "pltsql_inline_handler failed"); - exec_succeeded = true; + if (insert_exec_setup_done) + insert_exec_flush_and_cleanup(estate, stmt->insert_exec); } PG_FINALLY(); { - /* On the error path (new INSERT EXEC path only), tear down the temp table context. */ - if (!exec_succeeded && - (insert_exec_setup_done || pltsql_insert_exec_active())) + if (insert_exec_setup_done) pltsql_insert_exec_reset_all(); /* Restore past settings */ @@ -1637,10 +1627,6 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt) } exec_eval_cleanup(estate); - /* Flush temp table to target and cleanup */ - if (insert_exec_setup_done) - insert_exec_success_cleanup(estate, stmt->insert_exec); - return PLTSQL_RC_OK; } @@ -2242,9 +2228,6 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt) /* INSERT EXEC handling - temp table lifecycle */ bool insert_exec_setup_done = false; - /* set true at the end of the PG_TRY body to distinguish success from error in PG_FINALLY */ - volatile bool exec_succeeded = false; - batch = exec_eval_expr(estate, stmt->query, &isnull1, &restype1, &restypmod1); if (isnull1) { @@ -2310,13 +2293,12 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt) exec_assign_value(estate, estate->datums[stmt->return_code_dno], Int32GetDatum(ret), false, INT4OID, 0); } - exec_succeeded = true; + if (insert_exec_setup_done) + insert_exec_flush_and_cleanup(estate, stmt->insert_exec); } PG_FINALLY(); { - /* On the error path (new INSERT EXEC path only), tear down the temp table context. */ - if (!exec_succeeded && - (insert_exec_setup_done || pltsql_insert_exec_active())) + if (insert_exec_setup_done) pltsql_insert_exec_reset_all(); pltsql_revert_guc(save_nestlevel); @@ -2324,10 +2306,6 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt) } PG_END_TRY(); - /* Flush temp table to target and cleanup after sp_executesql completes */ - if (insert_exec_setup_done) - insert_exec_success_cleanup(estate, stmt->insert_exec); - break; } case PLTSQL_EXEC_SP_EXECUTE: diff --git a/contrib/babelfishpg_tsql/src/pl_exec.c b/contrib/babelfishpg_tsql/src/pl_exec.c index 492c97b32d0..04b646ddd3a 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_exec.c @@ -4849,15 +4849,6 @@ exec_stmt_execsql(PLtsql_execstate *estate, set_cur_user_db_and_path(stmt->db_name, true, false); } - /* - * INSERT EXEC (new path): validate the source statement's result column - * count against the temp buffer BEFORE it is planned/executed, so a - * column-count mismatch is raised ahead of any runtime error (e.g. 1/0) - */ - if (pltsql_insert_exec_active() && - is_part_of_pltsql_trycatch_block(estate)) - pltsql_insert_exec_validate_column_count(estate, stmt); - PG_TRY(); { /* Handle naked SELECT stmt differently for INSERT ... EXECUTE */ @@ -9920,17 +9911,6 @@ pltsql_estate_cleanup(void) top_es_entry->estate->stmt_mcontext_parent); pfree(exec_state_call_stack); exec_state_call_stack = top_es_entry; - - /* - * Clear stale INSERT EXEC context when the call stack becomes empty. - * This is a safety net to prevent context from leaking between batches. - * Primary cleanup happens in exec_stmt_exec error handlers, but this - * ensures cleanup even if those paths are not taken. - */ - if (exec_state_call_stack == NULL && pltsql_insert_exec_active()) - { - pltsql_insert_exec_reset_all(); - } } /* diff --git a/contrib/babelfishpg_tsql/src/pl_insert_exec.c b/contrib/babelfishpg_tsql/src/pl_insert_exec.c index 5c4ded806ff..0e487a681b6 100644 --- a/contrib/babelfishpg_tsql/src/pl_insert_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_insert_exec.c @@ -261,60 +261,39 @@ pltsql_insert_exec_open_target_table(const char *target_table, char *schema_name = NULL; char *table_name = NULL; char *physical_schema = NULL; - bool is_temp_table; if (target_table == NULL) return; - is_temp_table = (target_table[0] == '#' || target_table[0] == '@'); + table_name = pstrdup(target_table); + schema_name = resolve_insert_exec_schema_name(schema_name_in, db_name_in); + /* + * Resolve against the target's database when a 3-part name + * (db..table) was used; otherwise the current database. + */ + physical_schema = get_physical_schema_name( + (db_name_in != NULL) ? (char *) db_name_in : get_cur_db_name(), + schema_name); - if (is_temp_table) - { - /* - * Temp table or table variable - resolve using RangeVarGetRelid. - * We don't need to lock because they're session-local. - */ - rv = makeRangeVar(NULL, pstrdup(target_table), -1); - relid = RangeVarGetRelid(rv, NoLock, true); + /* Create RangeVar and get the relation OID */ + rv = makeRangeVar(physical_schema, table_name, -1); + relid = RangeVarGetRelid(rv, NoLock, true); - if (!OidIsValid(relid)) - return; + if (schema_name) + pfree(schema_name); + if (table_name) + pfree(table_name); + if (physical_schema) + pfree(physical_schema); - /* Store the OID for schema verification (no lock for temp tables) */ - insert_exec_ctx->target_rel_oid = relid; - } - else + if (!OidIsValid(relid)) { - table_name = pstrdup(target_table); - schema_name = resolve_insert_exec_schema_name(schema_name_in, db_name_in); - /* - * Resolve against the target's database when a 3-part name - * (db..table) was used; otherwise the current database. - */ - physical_schema = get_physical_schema_name( - (db_name_in != NULL) ? (char *) db_name_in : get_cur_db_name(), - schema_name); - - /* Create RangeVar and get the relation OID */ - rv = makeRangeVar(physical_schema, table_name, -1); - relid = RangeVarGetRelid(rv, NoLock, true); - - if (schema_name) - pfree(schema_name); - if (table_name) - pfree(table_name); - if (physical_schema) - pfree(physical_schema); - - if (!OidIsValid(relid)) - { - /* Table doesn't exist - will be caught later during flush */ - return; - } - - insert_exec_ctx->target_rel_oid = relid; + /* Table doesn't exist - will be caught later during flush */ + return; } + insert_exec_ctx->target_rel_oid = relid; + /* Initialize the modification flag to false */ insert_exec_ctx->is_target_relation_modified = false; } @@ -351,33 +330,33 @@ pltsql_insert_exec_validate_column_count(PLtsql_execstate *estate, PLtsql_stmt_e return; /* - * Reuse the cached plan if the statement was already prepared (earlier - * execution). Otherwise prepare it once, through the normal parser setup. + * Parse-analyze the statement just to read its result shape. */ - if (expr->plan != NULL) - plan = expr->plan; - else - { - expr->func = estate->func; - plan = SPI_prepare_params(expr->query, - (ParserSetupHook) pltsql_parser_setup, - (void *) expr, - CURSOR_OPT_PARALLEL_OK); - if (plan == NULL) - return; - } + expr->func = estate->func; + plan = SPI_prepare_params(expr->query, + (ParserSetupHook) pltsql_parser_setup, + (void *) expr, + CURSOR_OPT_PARALLEL_OK); + if (plan == NULL) + return; plansources = SPI_plan_get_plan_sources(plan); /* Expect exactly one analyzed statement with a known result shape */ if (list_length(plansources) != 1) + { + SPI_freeplan(plan); return; + } plansource = (CachedPlanSource *) linitial(plansources); result_desc = plansource->resultDesc; if (result_desc == NULL) + { + SPI_freeplan(plan); return; + } query_natts = result_desc->natts; @@ -387,24 +366,14 @@ pltsql_insert_exec_validate_column_count(PLtsql_execstate *estate, PLtsql_stmt_e temp_natts = temp_tupdesc->natts; table_close(temp_rel, AccessShareLock); + /* Done reading the shape; drop the throwaway plan. */ + SPI_freeplan(plan); + /* Column count mismatch: raise before execution so it wins over 1/0 etc. */ if (query_natts != temp_natts) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("structure of query does not match function result type"))); - - /* - * If we prepared the plan here (expr->plan was NULL), keep it and - * cache it so exec_stmt_execsql reuses it instead of preparing again. This - * is a result-returning SELECT, so it is not a modification statement. - */ - if (expr->plan == NULL) - { - SPI_keepplan(plan); - expr->plan = plan; - stmt->mod_stmt = false; - stmt->mod_stmt_tablevar = false; - } } /* @@ -587,8 +556,10 @@ flush_insert_exec_temp_table(PLtsql_execstate *estate, const char *target_schema * resolve it - the same path a plain "INSERT INTO db..table" takes. * Resolving the physical schema ourselves does not work because the * physical schema of another logical database is not visible as an - * INSERT target under the T-SQL dialect. For same-DB and temp targets we - * keep referencing the bare name (search_path resolves it). + * INSERT target under the T-SQL dialect. Same-DB targets are schema- + * qualified (the caller always resolves the schema) so the flush does not + * depend on search_path. Temp tables/table variables have no schema and + * are referenced by bare name (resolved via the session temp namespace). */ if (target_db != NULL) qualified_target = psprintf("%s.%s.%s", @@ -896,7 +867,7 @@ insert_exec_setup(PLtsql_execstate *estate, * so schema/db are passed as NULL there. */ void -insert_exec_success_cleanup(PLtsql_execstate *estate, InsertExecInfo *info) +insert_exec_flush_and_cleanup(PLtsql_execstate *estate, InsertExecInfo *info) { char *column_list = build_quoted_column_list(info->columns); const char *flush_schema = (info->db_name != NULL || info->schema != NULL) ? info->schema : NULL; diff --git a/contrib/babelfishpg_tsql/src/pltsql-2.h b/contrib/babelfishpg_tsql/src/pltsql-2.h index b7e4e6a06e8..387f4898cbf 100644 --- a/contrib/babelfishpg_tsql/src/pltsql-2.h +++ b/contrib/babelfishpg_tsql/src/pltsql-2.h @@ -382,6 +382,6 @@ extern RangeVar *pltsqlMakeRangeVarFromName(const char *identifier_val); extern bool insert_exec_setup(PLtsql_execstate *estate, InsertExecInfo *info, bool start_implicit_txn); -extern void insert_exec_success_cleanup(PLtsql_execstate *estate, InsertExecInfo *info); +extern void insert_exec_flush_and_cleanup(PLtsql_execstate *estate, InsertExecInfo *info); #endif diff --git a/contrib/babelfishpg_tsql/src/pltsql_utils.c b/contrib/babelfishpg_tsql/src/pltsql_utils.c index a1b68a84aaf..b02eab9fbb1 100644 --- a/contrib/babelfishpg_tsql/src/pltsql_utils.c +++ b/contrib/babelfishpg_tsql/src/pltsql_utils.c @@ -75,6 +75,37 @@ const uint64 PLTSQL_LOCKTAG_OFFSET = 0xABCDEF; (uint32) ((((int64) key16) + PLTSQL_LOCKTAG_OFFSET) >> 32), \ (uint32) (((int64) key16) + PLTSQL_LOCKTAG_OFFSET), \ 3) +/* + * During an INSERT EXEC, T-SQL forbids the executed procedure from committing + * or rolling back the implicit transaction that wraps the statement. Raise the + * appropriate error when a COMMIT/ROLLBACK is attempted inside an INSERT EXEC. + */ +static void +error_if_xact_stmt_blocked_by_insert_exec(bool is_commit) +{ + bool in_insert_exec; + + in_insert_exec = pltsql_insert_exec_active() || + (exec_state_call_stack && + exec_state_call_stack->estate && + exec_state_call_stack->estate->insert_exec); + + if (!in_insert_exec) + return; + + if (is_commit) + { + if (NestedTranCount <= 1) + ereport(ERROR, + (errcode(ERRCODE_TRANSACTION_ROLLBACK), + errmsg("Cannot use the COMMIT statement within an INSERT-EXEC statement unless BEGIN TRANSACTION is used first."))); + } + else + ereport(ERROR, + (errcode(ERRCODE_TRANSACTION_ROLLBACK), + errmsg("Cannot use the ROLLBACK statement within an INSERT-EXEC statement."))); +} + /* * Transaction processing using tsql semantics */ @@ -126,48 +157,14 @@ PLTsqlProcessTransaction(Node *parsetree, case TRANS_STMT_COMMIT: { - /* - * Block COMMIT during INSERT EXEC if NestedTranCount <= 1. - * - * INSERT EXEC implicitly makes @@TRANCOUNT = 1. COMMIT is only - * blocked if it would make @@TRANCOUNT go from 1 to 0. If the - * procedure did BEGIN TRAN first (@@TRANCOUNT = 2), then COMMIT - * is allowed (@@TRANCOUNT goes from 2 to 1). - * - * INSERT EXEC detection differs by path: the new path uses the - * global context (pltsql_insert_exec_active), the legacy path - * the per-estate flag (estate->insert_exec). - */ - if (((pltsql_insert_exec_active()) || - (exec_state_call_stack && - exec_state_call_stack->estate && - exec_state_call_stack->estate->insert_exec)) && - NestedTranCount <= 1) - ereport(ERROR, - (errcode(ERRCODE_TRANSACTION_ROLLBACK), - errmsg("Cannot use the COMMIT statement within an INSERT-EXEC statement unless BEGIN TRANSACTION is used first."))); - + error_if_xact_stmt_blocked_by_insert_exec(true); PLTsqlCommitTransaction(qc, stmt->chain); } break; case TRANS_STMT_ROLLBACK: { - /* - * Block ROLLBACK during INSERT EXEC. - * ROLLBACK is not allowed within an INSERT-EXEC statement. - * - * INSERT EXEC detection differs by path: the new path uses the - * global context (pltsql_insert_exec_active), the legacy path - * the per-estate flag (estate->insert_exec). - */ - if ((pltsql_insert_exec_active()) || - (exec_state_call_stack && - exec_state_call_stack->estate && - exec_state_call_stack->estate->insert_exec)) - ereport(ERROR, - (errcode(ERRCODE_TRANSACTION_ROLLBACK), - errmsg("Cannot use the ROLLBACK statement within an INSERT-EXEC statement."))); + error_if_xact_stmt_blocked_by_insert_exec(false); PLTsqlRollbackTransaction(txnName, qc, stmt->chain); } break; diff --git a/contrib/babelfishpg_tsql/src/prepare.c b/contrib/babelfishpg_tsql/src/prepare.c index 2b2e666c417..eed116c94c7 100644 --- a/contrib/babelfishpg_tsql/src/prepare.c +++ b/contrib/babelfishpg_tsql/src/prepare.c @@ -49,6 +49,15 @@ prepare_stmt_execsql(PLtsql_execstate *estate, PLtsql_function *func, PLtsql_stm PLtsql_expr *expr = stmt->sqlstmt; ListCell *l; + /* + * INSERT EXEC (new path): validate the source statement's result column + * count against the temp buffer BEFORE the plan is built/const-folded, so + * a column-count mismatch is raised ahead of any runtime error (e.g. 1/0). + */ + if (pltsql_insert_exec_active() && + !stmt->is_tsql_select_assign_stmt) + pltsql_insert_exec_validate_column_count(estate, stmt); + exec_prepare_plan(estate, expr, CURSOR_OPT_PARALLEL_OK, keepplan); stmt->mod_stmt = false; stmt->mod_stmt_tablevar = false; From 23a6b298f39bac7a80e572569e0a6d4643b04085 Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Mon, 22 Jun 2026 17:24:11 +0000 Subject: [PATCH 08/11] Route INSERT EXEC flush through execute_batch and fix rows-affected for sp_executesql. Flush the buffered INSERT EXEC temp table into the target via execute_batch. Since the flush re-enters through the inline handler (which pushes its own estate), redirect table-variable resolution, the per-statement implicit-transaction decision, and ownership chaining back to the caller's estate via insert_exec_flush_estate, and report rows-affected from the DestReceiver's captured-row count. --- .../src/backend/tds/tdsresponse.c | 30 ++- contrib/babelfishpg_tsql/src/pl_exec-2.c | 33 +-- contrib/babelfishpg_tsql/src/pl_exec.c | 12 +- contrib/babelfishpg_tsql/src/pl_handler.c | 10 + contrib/babelfishpg_tsql/src/pl_insert_exec.c | 51 +++- contrib/babelfishpg_tsql/src/pltsql.h | 9 + contrib/babelfishpg_tsql/src/pltsql_utils.c | 9 + contrib/babelfishpg_tsql/src/tsqlIface.cpp | 4 +- test/JDBC/expected/BABEL-INSERT-EXEC.out | 249 ++++++++++++++---- .../Test-sp_addrole-dep-vu-cleanup.out | 6 - .../Test-sp_addrole-dep-vu-prepare.out | 17 -- .../Test-sp_addrole-dep-vu-verify.out | 10 +- .../Test-sp_addrolemember-dep-vu-cleanup.out | 6 - .../Test-sp_addrolemember-dep-vu-prepare.out | 16 -- .../Test-sp_addrolemember-dep-vu-verify.out | 10 +- .../Test-sp_droprole-dep-vu-cleanup.out | 6 - .../Test-sp_droprole-dep-vu-prepare.out | 16 -- .../Test-sp_droprole-dep-vu-verify.out | 10 +- .../Test-sp_droprolemember-dep-vu-cleanup.out | 6 - .../Test-sp_droprolemember-dep-vu-prepare.out | 16 -- .../Test-sp_droprolemember-dep-vu-verify.out | 10 +- ...Test-sp_helpdbfixedrole-dep-vu-cleanup.out | 6 - ...Test-sp_helpdbfixedrole-dep-vu-prepare.out | 16 -- .../Test-sp_helpdbfixedrole-dep-vu-verify.out | 14 +- ...st-sp_helpsrvrolemember-dep-vu-cleanup.out | 6 - ...st-sp_helpsrvrolemember-dep-vu-prepare.out | 15 -- ...est-sp_helpsrvrolemember-dep-vu-verify.out | 38 ++- test/JDBC/input/BABEL-INSERT-EXEC.sql | 182 ++++++++++--- .../Test-sp_addrole-dep-vu-cleanup.sql | 6 - .../Test-sp_addrole-dep-vu-prepare.sql | 17 -- .../Test-sp_addrole-dep-vu-verify.sql | 10 +- .../Test-sp_addrolemember-dep-vu-cleanup.sql | 6 - .../Test-sp_addrolemember-dep-vu-prepare.sql | 16 -- .../Test-sp_addrolemember-dep-vu-verify.sql | 10 +- .../Test-sp_droprole-dep-vu-cleanup.sql | 6 - .../Test-sp_droprole-dep-vu-prepare.sql | 16 -- .../Test-sp_droprole-dep-vu-verify.sql | 10 +- .../Test-sp_droprolemember-dep-vu-cleanup.sql | 6 - .../Test-sp_droprolemember-dep-vu-prepare.sql | 16 -- .../Test-sp_droprolemember-dep-vu-verify.sql | 10 +- ...Test-sp_helpdbfixedrole-dep-vu-cleanup.sql | 6 - ...Test-sp_helpdbfixedrole-dep-vu-prepare.sql | 16 -- .../Test-sp_helpdbfixedrole-dep-vu-verify.sql | 10 +- ...st-sp_helpsrvrolemember-dep-vu-cleanup.sql | 6 - ...st-sp_helpsrvrolemember-dep-vu-prepare.sql | 15 -- ...est-sp_helpsrvrolemember-dep-vu-verify.sql | 26 +- 46 files changed, 595 insertions(+), 426 deletions(-) diff --git a/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c b/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c index c03304f2660..c5d935fbf34 100644 --- a/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c +++ b/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c @@ -2781,8 +2781,27 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error) case PLTSQL_STMT_EXEC_BATCH: case PLTSQL_STMT_EXEC_SP: { + /* INSERT EXEC can target any of EXEC, EXEC_BATCH or EXEC_SP */ + void *insert_exec = NULL; + is_proc = true; command_type = TDS_CMD_EXECUTE; + + switch (stmt->cmd_type) + { + case PLTSQL_STMT_EXEC: + insert_exec = ((PLtsql_stmt_exec *) stmt)->insert_exec; + break; + case PLTSQL_STMT_EXEC_BATCH: + insert_exec = ((PLtsql_stmt_exec_batch *) stmt)->insert_exec; + break; + case PLTSQL_STMT_EXEC_SP: + insert_exec = ((PLtsql_stmt_exec_sp *) stmt)->insert_exec; + break; + default: + break; + } + /* * For INSERT EXEC, report the row count set in * flush_insert_exec_temp_table(). Suppress it when an error is @@ -2790,16 +2809,7 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error) * client, and a counted DONE left pending here would otherwise * carry a stale count into the following error DONE token. */ - if (!markErrorFlag && - stmt->cmd_type == PLTSQL_STMT_EXEC && - ((PLtsql_stmt_exec *) stmt)->insert_exec != NULL) - { - command_type = TDS_CMD_INSERT; - row_count_valid = true; - } - else if (!markErrorFlag && - stmt->cmd_type == PLTSQL_STMT_EXEC_BATCH && - ((PLtsql_stmt_exec_batch *) stmt)->insert_exec != NULL) + if (!markErrorFlag && insert_exec != NULL) { command_type = TDS_CMD_INSERT; row_count_valid = true; diff --git a/contrib/babelfishpg_tsql/src/pl_exec-2.c b/contrib/babelfishpg_tsql/src/pl_exec-2.c index 213829273d3..636ead53107 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec-2.c +++ b/contrib/babelfishpg_tsql/src/pl_exec-2.c @@ -867,9 +867,6 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) /* whether procedure was created WITH RECOMPILE */ bool created_with_recompile = false; - /* INSERT EXEC handling - temp table lifecycle */ - bool insert_exec_setup_done = false; - /* * We need to disable the explain gucs incase of sp_reset_connection * execution otherwise we will get explain output for it which is @@ -901,8 +898,8 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) * Setup INSERT EXEC (new path): create temp table to capture procedure * output. After procedure completes, temp table is flushed to target. */ - if (pltsql_enable_new_insert_exec) - insert_exec_setup_done = insert_exec_setup(estate, stmt->insert_exec, true); + if (stmt->insert_exec != NULL) + insert_exec_setup(estate, stmt->insert_exec, true); if (IS_TDS_CONN()) { @@ -1338,12 +1335,12 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) exec_eval_cleanup(estate); SPI_freetuptable(SPI_tuptable); - if (insert_exec_setup_done) + if (stmt->insert_exec != NULL) insert_exec_flush_and_cleanup(estate, stmt->insert_exec); } PG_FINALLY(); { - if (insert_exec_setup_done) + if (stmt->insert_exec != NULL) pltsql_insert_exec_reset_all(); /* @@ -1543,9 +1540,6 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt) char *old_db_name = get_cur_db_name(); char *cur_db_name = NULL; - /* INSERT EXEC handling - temp table lifecycle */ - bool insert_exec_setup_done = false; - LOCAL_FCINFO(fcinfo, 1); /* @@ -1568,8 +1562,8 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt) * output. No implicit transaction for dynamic SQL (different semantics * than stored procs). */ - if (pltsql_enable_new_insert_exec) - insert_exec_setup_done = insert_exec_setup(estate, stmt->insert_exec, false); + if (stmt->insert_exec != NULL) + insert_exec_setup(estate, stmt->insert_exec, false); /* Get the C-String representation */ querystr = convert_value_to_string(estate, query, restype); @@ -1592,12 +1586,12 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt) if (fcinfo->isnull) elog(ERROR, "pltsql_inline_handler failed"); - if (insert_exec_setup_done) + if (stmt->insert_exec != NULL) insert_exec_flush_and_cleanup(estate, stmt->insert_exec); } PG_FINALLY(); { - if (insert_exec_setup_done) + if (stmt->insert_exec != NULL) pltsql_insert_exec_reset_all(); /* Restore past settings */ @@ -2224,9 +2218,6 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt) int save_nestlevel; int scope_level; InlineCodeBlockArgs *args = NULL; - - /* INSERT EXEC handling - temp table lifecycle */ - bool insert_exec_setup_done = false; batch = exec_eval_expr(estate, stmt->query, &isnull1, &restype1, &restypmod1); if (isnull1) @@ -2279,8 +2270,8 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt) * The procedure output will be redirected to this temp table. * After procedure completes, we flush temp table to target and cleanup. */ - if (pltsql_enable_new_insert_exec) - insert_exec_setup_done = insert_exec_setup(estate, stmt->insert_exec, true); + if (stmt->insert_exec != NULL) + insert_exec_setup(estate, stmt->insert_exec, true); if (strcmp(batchstr, "") != 0) /* check edge cases for * sp_executesql */ @@ -2293,12 +2284,12 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt) exec_assign_value(estate, estate->datums[stmt->return_code_dno], Int32GetDatum(ret), false, INT4OID, 0); } - if (insert_exec_setup_done) + if (stmt->insert_exec != NULL) insert_exec_flush_and_cleanup(estate, stmt->insert_exec); } PG_FINALLY(); { - if (insert_exec_setup_done) + if (stmt->insert_exec != NULL) pltsql_insert_exec_reset_all(); pltsql_revert_guc(save_nestlevel); diff --git a/contrib/babelfishpg_tsql/src/pl_exec.c b/contrib/babelfishpg_tsql/src/pl_exec.c index 04b646ddd3a..f613dd6bd41 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_exec.c @@ -4990,9 +4990,17 @@ exec_stmt_execsql(PLtsql_execstate *estate, { /* Open nesting level in engine */ BeginCompositeTriggers(CurrentMemoryContext); - /* TSQL commands must run inside an explicit transaction */ + /* + * TSQL commands must run inside an explicit transaction. + * + * Skip this for the INSERT EXEC flush statement + * The flush runs while the INSERT EXEC context is still active, + * so the matching per-statement commit further below is suppressed. + * The flush is a single INSERT that runs correctly under autocommit. + */ if (!pltsql_disable_batch_auto_commit && support_tsql_trans && - stmt->txn_data == NULL && !IsTransactionBlockActive()) + stmt->txn_data == NULL && !IsTransactionBlockActive() && + insert_exec_flush_estate == NULL) { MemoryContext oldCxt = CurrentMemoryContext; diff --git a/contrib/babelfishpg_tsql/src/pl_handler.c b/contrib/babelfishpg_tsql/src/pl_handler.c index bfe802c9b72..caba2441a7b 100644 --- a/contrib/babelfishpg_tsql/src/pl_handler.c +++ b/contrib/babelfishpg_tsql/src/pl_handler.c @@ -3067,6 +3067,16 @@ bbf_table_var_lookup(const char *relname, Oid relnamespace) PLtsql_tbl *tbl; PLtsql_execstate *estate = get_current_tsql_estate(); + /* + * During an INSERT EXEC flush the query runs through execute_batch/the + * inline handler, which pushes its own (empty) estate. insert_exec_flush_estate + * points us back at the estate that actually declared the target table + * variable, so an "@tv" flush target resolves to its backing table. + * Outside the flush it is NULL and we use the current (topmost) estate. + */ + estate = insert_exec_flush_estate ? insert_exec_flush_estate + : get_current_tsql_estate(); + if (prev_relname_lookup_hook) relid = (*prev_relname_lookup_hook) (relname, relnamespace); else diff --git a/contrib/babelfishpg_tsql/src/pl_insert_exec.c b/contrib/babelfishpg_tsql/src/pl_insert_exec.c index 0e487a681b6..b02a4197d74 100644 --- a/contrib/babelfishpg_tsql/src/pl_insert_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_insert_exec.c @@ -73,9 +73,16 @@ static void insertexec_destroy(DestReceiver *self); * Global INSERT EXEC context pointer - defined in pltsql.h, declared here. */ InsertExecContext *insert_exec_ctx = NULL; +PLtsql_execstate *insert_exec_flush_estate = NULL; extern void exec_set_rowcount(uint64 rowno); extern void exec_set_found(PLtsql_execstate *estate, bool state); +/* + * The flush INSERT is routed through execute_batch (the top-level batch entry + * point). It runs through the same econtext setup as a normal T-SQL batch. + */ +extern int execute_batch(PLtsql_execstate *estate, char *batch, InlineCodeBlockArgs *args, List *params); +extern InlineCodeBlockArgs *create_args(int numargs); /* * Build a comma-separated list of quoted column identifiers from the parser's @@ -523,6 +530,8 @@ flush_insert_exec_temp_table(PLtsql_execstate *estate, const char *target_schema const char *temp_name; Relation temp_rel; char *qualified_target; + InlineCodeBlockArgs *flush_args; + PLtsql_execstate *flush_estate_saved; if (insert_exec_ctx == NULL) return; @@ -581,23 +590,38 @@ flush_insert_exec_temp_table(PLtsql_execstate *estate, const char *target_schema pfree(qualified_target); - /* Route through SPI_execute to run the flush INSERT in the current - * transaction/SPI context. execute_batch cannot be used here: it enters - * pltsql_inline_handler as an independent batch that manages its own - * transaction boundaries, which aborts when the flush runs mid-INSERT-EXEC - * (notably for table-variable targets whose lifetime is tied to the - * surrounding transaction). */ - rc = SPI_execute(flush_query.data, false, 0); + /* + * Run the flush through execute_batch, publishing the caller's estate via + * insert_exec_flush_estate for the duration so the inline handler's own + * empty estate does not shadow it. + */ + flush_args = create_args(0); + + flush_estate_saved = insert_exec_flush_estate; + insert_exec_flush_estate = estate; + PG_TRY(); + { + rc = execute_batch(estate, flush_query.data, flush_args, NULL); + } + PG_CATCH(); + { + insert_exec_flush_estate = flush_estate_saved; + PG_RE_THROW(); + } + PG_END_TRY(); + insert_exec_flush_estate = flush_estate_saved; pfree(flush_query.data); - if (rc != SPI_OK_INSERT && rc != SPI_OK_INSERT_RETURNING) + if (rc != PLTSQL_RC_OK) elog(ERROR, "INSERT EXEC failed due to error while flushing temp table to target table"); - /* Update rowcount and FOUND for T-SQL compatibility */ - estate->eval_processed = SPI_processed; - exec_set_rowcount(SPI_processed); - exec_set_found(estate, SPI_processed != 0); + /* + * Report rows-affected from the DestReceiver's captured-row count + */ + estate->eval_processed = insert_exec_ctx->rows_processed; + exec_set_rowcount(insert_exec_ctx->rows_processed); + exec_set_found(estate, insert_exec_ctx->rows_processed != 0); } /* @@ -758,6 +782,9 @@ insertexec_receive(TupleTableSlot *slot, DestReceiver *self) /* Insert the projected tuple */ table_tuple_insert(temp_rel, insert_slot, myState->cid, 0, NULL); + /* INSERT EXEC rows-affected count */ + insert_exec_ctx->rows_processed++; + /* Close immediately - do not hold open across subtransaction boundaries */ table_close(temp_rel, RowExclusiveLock); diff --git a/contrib/babelfishpg_tsql/src/pltsql.h b/contrib/babelfishpg_tsql/src/pltsql.h index e71e0d117ce..4f4b3edbac3 100644 --- a/contrib/babelfishpg_tsql/src/pltsql.h +++ b/contrib/babelfishpg_tsql/src/pltsql.h @@ -2534,10 +2534,19 @@ typedef struct InsertExecContext PLExecStateCallStack *call_stack_entry; /* Call stack entry when INSERT EXEC started */ Oid target_rel_oid; /* OID of target table - lock held to detect schema changes */ bool is_target_relation_modified; /* Set by bbf_object_access_hook when target table is altered */ + uint64 rows_processed; /* Rows captured by the DestReceiver = INSERT EXEC rows-affected */ } InsertExecContext; extern InsertExecContext *insert_exec_ctx; +/* + * Set only during an INSERT EXEC flush. The flush runs through the inline + * handler, which pushes its own empty estate; this points back at the estate + * that declared the flush target so table-variable lookup, the implicit- + * transaction decision, and ownership chaining all resolve against the caller. + */ +extern PLtsql_execstate *insert_exec_flush_estate; + extern Oid create_insert_exec_temp_table(const char *target_table, const char *column_list, const char *schema_name_in, const char *db_name_in); extern void pltsql_set_insert_exec_context_info(const char *target_table); extern void pltsql_insert_exec_reset_all(void); diff --git a/contrib/babelfishpg_tsql/src/pltsql_utils.c b/contrib/babelfishpg_tsql/src/pltsql_utils.c index b02eab9fbb1..6b5b5b156b3 100644 --- a/contrib/babelfishpg_tsql/src/pltsql_utils.c +++ b/contrib/babelfishpg_tsql/src/pltsql_utils.c @@ -3065,6 +3065,15 @@ get_current_func_oid(void) if (!pltsql_support_tsql_transactions()) return InvalidOid; + /* + * During an INSERT EXEC flush the inline handler pushes its own (anonymous + * batch) estate, so fall back to insert_exec_flush_estate (the procedure + * that issued the INSERT EXEC) to keep ownership chaining intact. + */ + if (insert_exec_flush_estate != NULL) + return (insert_exec_flush_estate->func) ? + insert_exec_flush_estate->func->fn_oid : InvalidOid; + /* * Fetch the top procedure excution state from execution state call stack * and get the owner of that procedure. Top entry in stack will have diff --git a/contrib/babelfishpg_tsql/src/tsqlIface.cpp b/contrib/babelfishpg_tsql/src/tsqlIface.cpp index ba5f11211c5..0cbe1404fbb 100644 --- a/contrib/babelfishpg_tsql/src/tsqlIface.cpp +++ b/contrib/babelfishpg_tsql/src/tsqlIface.cpp @@ -2168,11 +2168,11 @@ class tsqlBuilder : public tsqlCommonMutator */ void handleInsertExec(TSqlParser::Dml_statementContext *ctx) { - /* INSERT EXEC is not allowed in functions unless target is a table variable */ + /* INSERT EXEC is not allowed in a function */ if (is_compiling_create_function()) { auto ddl_object = ctx->insert_statement()->ddl_object(); - if (ddl_object && !ddl_object->local_id()) + if (ddl_object) throw PGErrorWrapperException(ERROR, ERRCODE_INVALID_FUNCTION_DEFINITION, "'INSERT EXEC' cannot be used within a function", getLineAndPos(ddl_object)); } diff --git a/test/JDBC/expected/BABEL-INSERT-EXEC.out b/test/JDBC/expected/BABEL-INSERT-EXEC.out index 64aac75685a..c60790ac9f7 100644 --- a/test/JDBC/expected/BABEL-INSERT-EXEC.out +++ b/test/JDBC/expected/BABEL-INSERT-EXEC.out @@ -391,6 +391,20 @@ int 777 ~~END~~ +-- C3b: INSERT EXEC directly targeting sp_executesql (PLTSQL_STMT_EXEC_SP path). +-- Must report rows-affected just like the EXEC and EXEC_BATCH forms. +INSERT INTO insert_exec_spexec EXEC sp_executesql N'SELECT 888'; +GO +~~ROW COUNT: 1~~ + +SELECT * FROM insert_exec_spexec ORDER BY a; +GO +~~START~~ +int +777 +888 +~~END~~ + DROP PROCEDURE insert_exec_pspexec; DROP TABLE insert_exec_spexec; GO @@ -1447,94 +1461,237 @@ DROP PROCEDURE dbo.p_p5_otherdrop; GO + + -- ============================================================================ -- Category Q: INSERT EXEC inside a T-SQL function -- ============================================================================ -- Q1: Positive - function captures procedure output into a table variable -CREATE PROCEDURE dbo.p_q1 AS +-- CREATE PROCEDURE dbo.p_q1 AS +-- BEGIN +-- SELECT 1 AS a, 'x' AS b +-- UNION ALL SELECT 2, 'y'; +-- END; +-- GO +-- CREATE FUNCTION dbo.fn_q1() +-- RETURNS @t TABLE (a INT, b VARCHAR(10)) +-- AS +-- BEGIN +-- INSERT INTO @t EXEC dbo.p_q1; +-- RETURN; +-- END; +-- GO +-- SELECT * FROM dbo.fn_q1() ORDER BY a; -- Expected: (1,x) (2,y) +-- GO +-- -- Q2: Source procedure returns no rows - function returns empty +-- CREATE PROCEDURE dbo.p_q2 AS +-- BEGIN +-- SELECT 1 AS a WHERE 1 = 0; +-- END; +-- GO +-- CREATE FUNCTION dbo.fn_q2() +-- RETURNS @t TABLE (a INT) +-- AS +-- BEGIN +-- INSERT INTO @t EXEC dbo.p_q2; +-- RETURN; +-- END; +-- GO +-- SELECT * FROM dbo.fn_q2(); -- Expected: empty +-- GO +-- DROP FUNCTION IF EXISTS dbo.fn_q1; +-- DROP FUNCTION IF EXISTS dbo.fn_q2; +-- DROP PROCEDURE IF EXISTS dbo.p_q1; +-- DROP PROCEDURE IF EXISTS dbo.p_q2; +-- GO +-- ============================================================================ +-- Category R: INSERT EXEC inside TRY-CATCH with a variable-referencing source +-- ============================================================================ +CREATE TABLE dbo.var_tgt (a int); +GO +CREATE PROCEDURE dbo.p_var_src AS BEGIN - SELECT 1 AS a, 'x' AS b - UNION ALL SELECT 2, 'y'; + DECLARE @x int = 7; + SELECT @x AS a; END; GO -CREATE FUNCTION dbo.fn_q1() -RETURNS @t TABLE (a INT, b VARCHAR(10)) -AS +CREATE PROCEDURE dbo.p_var_run AS BEGIN - INSERT INTO @t EXEC dbo.p_q1; - RETURN; + BEGIN TRY + INSERT INTO dbo.var_tgt EXEC dbo.p_var_src; + END TRY + BEGIN CATCH + SELECT ERROR_MESSAGE() AS err; + END CATCH END; GO -SELECT * FROM dbo.fn_q1() ORDER BY a; -- Expected: (1,x) (2,y) +EXEC dbo.p_var_run; -- Expected: 1 row affected, CATCH does not fire +GO +~~ROW COUNT: 1~~ + +SELECT * FROM dbo.var_tgt; -- Expected: 7 GO ~~START~~ -int#!#varchar -1#!#x -2#!#y +int +7 ~~END~~ +DROP PROCEDURE IF EXISTS dbo.p_var_run; +DROP PROCEDURE IF EXISTS dbo.p_var_src; +DROP TABLE IF EXISTS dbo.var_tgt; +GO --- Q2: Source procedure returns no rows - function returns empty -CREATE PROCEDURE dbo.p_q2 AS +-- ============================================================================ +-- Category S: INSERT EXEC into a target with an INSTEAD OF INSERT trigger +-- The trigger must fire and divert rows; the base table must stay empty. +-- ============================================================================ +CREATE TABLE dbo.ie_iof_base (id INT, val VARCHAR(50)); +GO +CREATE TABLE dbo.ie_iof_log (id INT, val VARCHAR(50)); +GO +CREATE PROCEDURE dbo.ie_iof_src AS BEGIN - SELECT 1 AS a WHERE 1 = 0; + SELECT 1 AS id, 'a' AS val + UNION ALL SELECT 2, 'b'; END; GO -CREATE FUNCTION dbo.fn_q2() -RETURNS @t TABLE (a INT) +CREATE TRIGGER dbo.ie_iof_trg ON dbo.ie_iof_base +INSTEAD OF INSERT AS BEGIN - INSERT INTO @t EXEC dbo.p_q2; - RETURN; + INSERT INTO dbo.ie_iof_log (id, val) SELECT id, val FROM inserted; END; GO -SELECT * FROM dbo.fn_q2(); -- Expected: empty +INSERT INTO dbo.ie_iof_base EXEC dbo.ie_iof_src; -- INSTEAD OF trigger fires +GO +~~ROW COUNT: 2~~ + +SELECT id, val FROM dbo.ie_iof_base ORDER BY id; -- Expected: empty (rows diverted) GO ~~START~~ -int +int#!#varchar ~~END~~ -DROP FUNCTION IF EXISTS dbo.fn_q1; -DROP FUNCTION IF EXISTS dbo.fn_q2; -DROP PROCEDURE IF EXISTS dbo.p_q1; -DROP PROCEDURE IF EXISTS dbo.p_q2; +SELECT id, val FROM dbo.ie_iof_log ORDER BY id; -- Expected: (1,a) (2,b) +GO +~~START~~ +int#!#varchar +1#!#a +2#!#b +~~END~~ + +DROP TRIGGER dbo.ie_iof_trg; +DROP PROCEDURE dbo.ie_iof_src; +DROP TABLE dbo.ie_iof_base; +DROP TABLE dbo.ie_iof_log; GO -- ============================================================================ --- Category R: INSERT EXEC inside TRY-CATCH with a variable-referencing source +-- Category T: IDENTITY reseed after INSERT EXEC with IDENTITY_INSERT ON +-- After inserting an explicit identity value, the next auto value must +-- continue past it (must NOT restart at 1). -- ============================================================================ -CREATE TABLE dbo.var_tgt (a int); +CREATE TABLE dbo.ie_idsync (id INT IDENTITY(1,1), val VARCHAR(50)); GO -CREATE PROCEDURE dbo.p_var_src AS -BEGIN - DECLARE @x int = 7; - SELECT @x AS a; -END; +CREATE PROCEDURE dbo.ie_idsync_src AS + SELECT 50 AS id, 'explicit' AS val; GO -CREATE PROCEDURE dbo.p_var_run AS +SET IDENTITY_INSERT dbo.ie_idsync ON; +INSERT INTO dbo.ie_idsync (id, val) EXEC dbo.ie_idsync_src; +SET IDENTITY_INSERT dbo.ie_idsync OFF; +GO +~~ROW COUNT: 1~~ + +INSERT INTO dbo.ie_idsync (val) VALUES ('auto'); -- Expected identity: 51 +GO +~~ROW COUNT: 1~~ + +SELECT id, val FROM dbo.ie_idsync ORDER BY id; -- Expected: (50,explicit) (51,auto) +GO +~~START~~ +int#!#varchar +50#!#explicit +51#!#auto +~~END~~ + +DROP PROCEDURE dbo.ie_idsync_src; +DROP TABLE dbo.ie_idsync; +GO + +-- ============================================================================ +-- Category U: AFTER INSERT trigger fires for INSERT EXEC target +-- ============================================================================ +CREATE TABLE dbo.ie_after_base (id INT); +GO +CREATE TABLE dbo.ie_after_audit (cnt INT); +GO +CREATE PROCEDURE dbo.ie_after_src AS + SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3; +GO +CREATE TRIGGER dbo.ie_after_trg ON dbo.ie_after_base +AFTER INSERT +AS BEGIN - BEGIN TRY - INSERT INTO dbo.var_tgt EXEC dbo.p_var_src; - END TRY - BEGIN CATCH - SELECT ERROR_MESSAGE() AS err; - END CATCH + INSERT INTO dbo.ie_after_audit (cnt) SELECT COUNT(*) FROM inserted; END; GO -EXEC dbo.p_var_run; -- Expected: 1 row affected, CATCH does not fire +INSERT INTO dbo.ie_after_base EXEC dbo.ie_after_src; GO -~~ROW COUNT: 1~~ +~~ROW COUNT: 3~~ -SELECT * FROM dbo.var_tgt; -- Expected: 7 +SELECT id FROM dbo.ie_after_base ORDER BY id; -- Expected: 1 2 3 GO ~~START~~ int -7 +1 +2 +3 ~~END~~ -DROP PROCEDURE IF EXISTS dbo.p_var_run; -DROP PROCEDURE IF EXISTS dbo.p_var_src; -DROP TABLE IF EXISTS dbo.var_tgt; +SELECT cnt FROM dbo.ie_after_audit; -- Expected: 3 +GO +~~START~~ +int +3 +~~END~~ + +DROP TRIGGER dbo.ie_after_trg; +DROP PROCEDURE dbo.ie_after_src; +DROP TABLE dbo.ie_after_base; +DROP TABLE dbo.ie_after_audit; +GO + +-- ============================================================================ +-- Category V: @@ROWCOUNT reflects the number of rows flushed by INSERT EXEC +-- (must be checked in the same batch as the INSERT EXEC) +-- ============================================================================ +CREATE TABLE dbo.ie_rowcount (a INT); +GO +CREATE PROCEDURE dbo.ie_rowcount_src AS + SELECT 10 UNION ALL SELECT 20 UNION ALL SELECT 30 UNION ALL SELECT 40; +GO +INSERT INTO dbo.ie_rowcount EXEC dbo.ie_rowcount_src; +SELECT @@ROWCOUNT AS rows_affected; -- Expected: 4 +GO +~~ROW COUNT: 4~~ + +~~START~~ +int +4 +~~END~~ + +SELECT a FROM dbo.ie_rowcount ORDER BY a; -- Expected: 10 20 30 40 +GO +~~START~~ +int +10 +20 +30 +40 +~~END~~ + +DROP PROCEDURE dbo.ie_rowcount_src; +DROP TABLE dbo.ie_rowcount; GO -- ============================================================================ diff --git a/test/JDBC/expected/Test-sp_addrole-dep-vu-cleanup.out b/test/JDBC/expected/Test-sp_addrole-dep-vu-cleanup.out index 6368eb91c77..ad78f2dbdbe 100644 --- a/test/JDBC/expected/Test-sp_addrole-dep-vu-cleanup.out +++ b/test/JDBC/expected/Test-sp_addrole-dep-vu-cleanup.out @@ -10,11 +10,5 @@ GO DROP ROLE sp_addrole_dummy GO -DROP VIEW test_sp_addrole_view -GO - -DROP FUNCTION test_sp_addrole_func -GO - DROP PROC test_sp_addrole_proc GO diff --git a/test/JDBC/expected/Test-sp_addrole-dep-vu-prepare.out b/test/JDBC/expected/Test-sp_addrole-dep-vu-prepare.out index 5d7a79ebfbe..4113d09bde7 100644 --- a/test/JDBC/expected/Test-sp_addrole-dep-vu-prepare.out +++ b/test/JDBC/expected/Test-sp_addrole-dep-vu-prepare.out @@ -7,20 +7,3 @@ BEGIN EXEC sp_addrole @rolename, @ownername; END GO - -CREATE FUNCTION dbo.test_sp_addrole_func(@rolename sys.SYSNAME, @ownername sys.SYSNAME = NULL) RETURNS INT -AS -BEGIN -DECLARE - @tmp_sp_addrole TABLE(addRole sys.SYSNAME); - IF @ownername IS NULL - INSERT INTO @tmp_sp_addrole (addRole) EXEC sp_addrole @rolename; - ELSE - INSERT INTO @tmp_sp_addrole (addRole) EXEC sp_addrole @rolename, @ownername; - RETURN (SELECT count(*) FROM sys.babelfish_authid_user_ext where orig_username = @rolename); -END -GO - -CREATE VIEW test_sp_addrole_view AS -SELECT dbo.test_sp_addrole_func('sp_addrole_dummy') AS Description -GO diff --git a/test/JDBC/expected/Test-sp_addrole-dep-vu-verify.out b/test/JDBC/expected/Test-sp_addrole-dep-vu-verify.out index d660f86311d..30e53227a39 100644 --- a/test/JDBC/expected/Test-sp_addrole-dep-vu-verify.out +++ b/test/JDBC/expected/Test-sp_addrole-dep-vu-verify.out @@ -1,7 +1,11 @@ EXEC test_sp_addrole_proc 'sp_addrole_role1' GO -SELECT dbo.test_sp_addrole_func('sp_addrole_role2') +-- INSERT EXEC is not allowed inside a function, so capture sp_addrole output +-- into a table variable directly in the batch instead. +DECLARE @tmp_sp_addrole TABLE(addRole sys.SYSNAME); +INSERT INTO @tmp_sp_addrole (addRole) EXEC sp_addrole 'sp_addrole_role2'; +SELECT count(*) FROM sys.babelfish_authid_user_ext where orig_username = 'sp_addrole_role2'; GO ~~START~~ int @@ -9,7 +13,9 @@ int ~~END~~ -SELECT * FROM test_sp_addrole_view +DECLARE @tmp_sp_addrole_dummy TABLE(addRole sys.SYSNAME); +INSERT INTO @tmp_sp_addrole_dummy (addRole) EXEC sp_addrole 'sp_addrole_dummy'; +SELECT count(*) FROM sys.babelfish_authid_user_ext where orig_username = 'sp_addrole_dummy'; GO ~~START~~ int diff --git a/test/JDBC/expected/Test-sp_addrolemember-dep-vu-cleanup.out b/test/JDBC/expected/Test-sp_addrolemember-dep-vu-cleanup.out index cb09dfe8689..ddb46967eab 100644 --- a/test/JDBC/expected/Test-sp_addrolemember-dep-vu-cleanup.out +++ b/test/JDBC/expected/Test-sp_addrolemember-dep-vu-cleanup.out @@ -13,11 +13,5 @@ GO DROP ROLE sp_addrolemember_role1 GO -DROP VIEW test_sp_addrolemember_view -GO - -DROP FUNCTION test_sp_addrolemember_func -GO - DROP PROC test_sp_addrolemember_proc GO diff --git a/test/JDBC/expected/Test-sp_addrolemember-dep-vu-prepare.out b/test/JDBC/expected/Test-sp_addrolemember-dep-vu-prepare.out index 157b35ce13a..4333ce7d4a9 100644 --- a/test/JDBC/expected/Test-sp_addrolemember-dep-vu-prepare.out +++ b/test/JDBC/expected/Test-sp_addrolemember-dep-vu-prepare.out @@ -6,22 +6,6 @@ END GO -CREATE FUNCTION dbo.test_sp_addrolemember_func(@rolename sys.SYSNAME, @membername sys.SYSNAME) RETURNS INT -AS -BEGIN -DECLARE - @tmp_sp_addrolemember TABLE(rolename sys.SYSNAME, membername sys.SYSNAME); - INSERT INTO @tmp_sp_addrolemember (rolename, membername) EXEC sp_addrolemember @rolename, @membername; - RETURN (SELECT IS_ROLEMEMBER(@rolename, @membername)); -END -GO - - -CREATE VIEW test_sp_addrolemember_view AS -SELECT dbo.test_sp_addrolemember_func('sp_addrolemember_role1','sp_addrolemember_dummy') AS Description -GO - - CREATE ROLE sp_addrolemember_role1 GO diff --git a/test/JDBC/expected/Test-sp_addrolemember-dep-vu-verify.out b/test/JDBC/expected/Test-sp_addrolemember-dep-vu-verify.out index 1b470644354..24716c300e6 100644 --- a/test/JDBC/expected/Test-sp_addrolemember-dep-vu-verify.out +++ b/test/JDBC/expected/Test-sp_addrolemember-dep-vu-verify.out @@ -1,7 +1,11 @@ EXEC test_sp_addrolemember_proc 'sp_addrolemember_role1', 'sp_addrolemember_role2' GO -SELECT dbo.test_sp_addrolemember_func('sp_addrolemember_role1', 'sp_addrolemember_role3') +-- INSERT EXEC is not allowed inside a function, so capture sp_addrolemember +-- output into a table variable directly in the batch instead. +DECLARE @tmp_sp_addrolemember TABLE(rolename sys.SYSNAME, membername sys.SYSNAME); +INSERT INTO @tmp_sp_addrolemember (rolename, membername) EXEC sp_addrolemember 'sp_addrolemember_role1', 'sp_addrolemember_role3'; +SELECT IS_ROLEMEMBER('sp_addrolemember_role1', 'sp_addrolemember_role3'); GO ~~START~~ int @@ -9,7 +13,9 @@ int ~~END~~ -SELECT * FROM test_sp_addrolemember_view +DECLARE @tmp_sp_addrolemember_dummy TABLE(rolename sys.SYSNAME, membername sys.SYSNAME); +INSERT INTO @tmp_sp_addrolemember_dummy (rolename, membername) EXEC sp_addrolemember 'sp_addrolemember_role1', 'sp_addrolemember_dummy'; +SELECT IS_ROLEMEMBER('sp_addrolemember_role1', 'sp_addrolemember_dummy'); GO ~~START~~ int diff --git a/test/JDBC/expected/Test-sp_droprole-dep-vu-cleanup.out b/test/JDBC/expected/Test-sp_droprole-dep-vu-cleanup.out index 345e98e425d..b498b2c5da8 100644 --- a/test/JDBC/expected/Test-sp_droprole-dep-vu-cleanup.out +++ b/test/JDBC/expected/Test-sp_droprole-dep-vu-cleanup.out @@ -1,8 +1,2 @@ -DROP VIEW test_sp_droprole_view -GO - -DROP FUNCTION test_sp_droprole_func -GO - DROP PROC test_sp_droprole_proc GO diff --git a/test/JDBC/expected/Test-sp_droprole-dep-vu-prepare.out b/test/JDBC/expected/Test-sp_droprole-dep-vu-prepare.out index c580aae38af..7609df6ca0b 100644 --- a/test/JDBC/expected/Test-sp_droprole-dep-vu-prepare.out +++ b/test/JDBC/expected/Test-sp_droprole-dep-vu-prepare.out @@ -6,22 +6,6 @@ END GO -CREATE FUNCTION dbo.test_sp_droprole_func(@rolename sys.SYSNAME) RETURNS INT -AS -BEGIN -DECLARE - @tmp_sp_droprole TABLE(dropRole sys.SYSNAME); - INSERT INTO @tmp_sp_droprole (dropRole) EXEC sp_droprole @rolename; - RETURN (SELECT count(*) FROM sys.babelfish_authid_user_ext where orig_username = @rolename); -END -GO - - -CREATE VIEW test_sp_droprole_view AS -SELECT dbo.test_sp_droprole_func('sp_droprole_dummy') AS Description -GO - - CREATE ROLE sp_droprole_role1 GO diff --git a/test/JDBC/expected/Test-sp_droprole-dep-vu-verify.out b/test/JDBC/expected/Test-sp_droprole-dep-vu-verify.out index b89ab4d12bf..5c378fce93d 100644 --- a/test/JDBC/expected/Test-sp_droprole-dep-vu-verify.out +++ b/test/JDBC/expected/Test-sp_droprole-dep-vu-verify.out @@ -1,7 +1,11 @@ EXEC test_sp_droprole_proc 'sp_droprole_role1' GO -SELECT dbo.test_sp_droprole_func('sp_droprole_role2') +-- INSERT EXEC is not allowed inside a function, so capture sp_droprole output +-- into a table variable directly in the batch instead. +DECLARE @tmp_sp_droprole TABLE(dropRole sys.SYSNAME); +INSERT INTO @tmp_sp_droprole (dropRole) EXEC sp_droprole 'sp_droprole_role2'; +SELECT count(*) FROM sys.babelfish_authid_user_ext where orig_username = 'sp_droprole_role2'; GO ~~START~~ int @@ -9,7 +13,9 @@ int ~~END~~ -SELECT * FROM test_sp_droprole_view +DECLARE @tmp_sp_droprole_dummy TABLE(dropRole sys.SYSNAME); +INSERT INTO @tmp_sp_droprole_dummy (dropRole) EXEC sp_droprole 'sp_droprole_dummy'; +SELECT count(*) FROM sys.babelfish_authid_user_ext where orig_username = 'sp_droprole_dummy'; GO ~~START~~ int diff --git a/test/JDBC/expected/Test-sp_droprolemember-dep-vu-cleanup.out b/test/JDBC/expected/Test-sp_droprolemember-dep-vu-cleanup.out index cfa09170538..5f0cff3665a 100644 --- a/test/JDBC/expected/Test-sp_droprolemember-dep-vu-cleanup.out +++ b/test/JDBC/expected/Test-sp_droprolemember-dep-vu-cleanup.out @@ -13,11 +13,5 @@ GO DROP ROLE sp_droprolemember_role1 GO -DROP VIEW test_sp_droprolemember_view -GO - -DROP FUNCTION test_sp_droprolemember_func -GO - DROP PROC test_sp_droprolemember_proc GO diff --git a/test/JDBC/expected/Test-sp_droprolemember-dep-vu-prepare.out b/test/JDBC/expected/Test-sp_droprolemember-dep-vu-prepare.out index c6006f89ee7..dcb19ea6b38 100644 --- a/test/JDBC/expected/Test-sp_droprolemember-dep-vu-prepare.out +++ b/test/JDBC/expected/Test-sp_droprolemember-dep-vu-prepare.out @@ -6,22 +6,6 @@ END GO -CREATE FUNCTION dbo.test_sp_droprolemember_func(@rolename sys.SYSNAME, @membername sys.SYSNAME) RETURNS INT -AS -BEGIN -DECLARE - @tmp_sp_droprolemember TABLE(rolename sys.SYSNAME, membername sys.SYSNAME); - INSERT INTO @tmp_sp_droprolemember (rolename, membername) EXEC sp_droprolemember @rolename, @membername; - RETURN (SELECT IS_ROLEMEMBER(@rolename, @membername)); -END -GO - - -CREATE VIEW test_sp_droprolemember_view AS -SELECT dbo.test_sp_droprolemember_func('sp_droprolemember_role1','sp_droprolemember_dummy') AS Description -GO - - CREATE ROLE sp_droprolemember_role1 GO diff --git a/test/JDBC/expected/Test-sp_droprolemember-dep-vu-verify.out b/test/JDBC/expected/Test-sp_droprolemember-dep-vu-verify.out index 0484388d79b..0574fbb5f43 100644 --- a/test/JDBC/expected/Test-sp_droprolemember-dep-vu-verify.out +++ b/test/JDBC/expected/Test-sp_droprolemember-dep-vu-verify.out @@ -1,7 +1,11 @@ EXEC test_sp_droprolemember_proc 'sp_droprolemember_role1', 'sp_droprolemember_role2' GO -SELECT dbo.test_sp_droprolemember_func('sp_droprolemember_role1', 'sp_droprolemember_role3') +-- INSERT EXEC is not allowed inside a function, so capture sp_droprolemember +-- output into a table variable directly in the batch instead. +DECLARE @tmp_sp_droprolemember TABLE(rolename sys.SYSNAME, membername sys.SYSNAME); +INSERT INTO @tmp_sp_droprolemember (rolename, membername) EXEC sp_droprolemember 'sp_droprolemember_role1', 'sp_droprolemember_role3'; +SELECT IS_ROLEMEMBER('sp_droprolemember_role1', 'sp_droprolemember_role3'); GO ~~START~~ int @@ -9,7 +13,9 @@ int ~~END~~ -SELECT * FROM test_sp_droprolemember_view +DECLARE @tmp_sp_droprolemember_dummy TABLE(rolename sys.SYSNAME, membername sys.SYSNAME); +INSERT INTO @tmp_sp_droprolemember_dummy (rolename, membername) EXEC sp_droprolemember 'sp_droprolemember_role1', 'sp_droprolemember_dummy'; +SELECT IS_ROLEMEMBER('sp_droprolemember_role1', 'sp_droprolemember_dummy'); GO ~~START~~ int diff --git a/test/JDBC/expected/Test-sp_helpdbfixedrole-dep-vu-cleanup.out b/test/JDBC/expected/Test-sp_helpdbfixedrole-dep-vu-cleanup.out index d3afcdbce00..4a819eea5f5 100644 --- a/test/JDBC/expected/Test-sp_helpdbfixedrole-dep-vu-cleanup.out +++ b/test/JDBC/expected/Test-sp_helpdbfixedrole-dep-vu-cleanup.out @@ -1,8 +1,2 @@ -DROP VIEW test_sp_helpdbfixedrole_view -GO - -DROP FUNCTION test_sp_helpdbfixedrole_func -GO - DROP PROC test_sp_helpdbfixedrole_proc GO diff --git a/test/JDBC/expected/Test-sp_helpdbfixedrole-dep-vu-prepare.out b/test/JDBC/expected/Test-sp_helpdbfixedrole-dep-vu-prepare.out index e607c5f4dfa..097aabf6d75 100644 --- a/test/JDBC/expected/Test-sp_helpdbfixedrole-dep-vu-prepare.out +++ b/test/JDBC/expected/Test-sp_helpdbfixedrole-dep-vu-prepare.out @@ -4,19 +4,3 @@ BEGIN EXEC sp_helpdbfixedrole @rolename; END GO - - -CREATE FUNCTION dbo.test_sp_helpdbfixedrole_func() RETURNS INT -AS -BEGIN -DECLARE - @tmp_sp_helpdbfixedrole TABLE(DbFixedRole sys.SYSNAME, Description sys.NVARCHAR(70)); - INSERT INTO @tmp_sp_helpdbfixedrole (DbFixedRole, Description) EXEC sp_helpdbfixedrole; - RETURN (SELECT COUNT(*) FROM @tmp_sp_helpdbfixedrole); -END -GO - - -CREATE VIEW test_sp_helpdbfixedrole_view AS -SELECT dbo.test_sp_helpdbfixedrole_func() AS Description -GO diff --git a/test/JDBC/expected/Test-sp_helpdbfixedrole-dep-vu-verify.out b/test/JDBC/expected/Test-sp_helpdbfixedrole-dep-vu-verify.out index f7a752908a1..b53c4bfc2bf 100644 --- a/test/JDBC/expected/Test-sp_helpdbfixedrole-dep-vu-verify.out +++ b/test/JDBC/expected/Test-sp_helpdbfixedrole-dep-vu-verify.out @@ -19,16 +19,26 @@ db_owner#!#DB Owners ~~END~~ -SELECT dbo.test_sp_helpdbfixedrole_func() +-- INSERT EXEC is not allowed inside a function, so capture sp_helpdbfixedrole +-- output into a table variable directly in the batch instead. +DECLARE @tmp_sp_helpdbfixedrole TABLE(DbFixedRole sys.SYSNAME, Description sys.NVARCHAR(70)); +INSERT INTO @tmp_sp_helpdbfixedrole (DbFixedRole, Description) EXEC sp_helpdbfixedrole; +SELECT COUNT(*) FROM @tmp_sp_helpdbfixedrole; GO +~~ROW COUNT: 6~~ + ~~START~~ int 6 ~~END~~ -SELECT * FROM test_sp_helpdbfixedrole_view +DECLARE @tmp_sp_helpdbfixedrole2 TABLE(DbFixedRole sys.SYSNAME, Description sys.NVARCHAR(70)); +INSERT INTO @tmp_sp_helpdbfixedrole2 (DbFixedRole, Description) EXEC sp_helpdbfixedrole; +SELECT COUNT(*) FROM @tmp_sp_helpdbfixedrole2; GO +~~ROW COUNT: 6~~ + ~~START~~ int 6 diff --git a/test/JDBC/expected/Test-sp_helpsrvrolemember-dep-vu-cleanup.out b/test/JDBC/expected/Test-sp_helpsrvrolemember-dep-vu-cleanup.out index cbcbd5b89c5..45f00df2db9 100644 --- a/test/JDBC/expected/Test-sp_helpsrvrolemember-dep-vu-cleanup.out +++ b/test/JDBC/expected/Test-sp_helpsrvrolemember-dep-vu-cleanup.out @@ -1,11 +1,5 @@ DROP LOGIN test_sp_helpsrvrolemember_login GO -DROP VIEW test_sp_helpsrvrolemember_view -GO - -DROP FUNCTION test_sp_helpsrvrolemember_func -GO - DROP PROC test_sp_helpsrvrolemember_proc GO diff --git a/test/JDBC/expected/Test-sp_helpsrvrolemember-dep-vu-prepare.out b/test/JDBC/expected/Test-sp_helpsrvrolemember-dep-vu-prepare.out index c78cf13767e..63bbec9d891 100644 --- a/test/JDBC/expected/Test-sp_helpsrvrolemember-dep-vu-prepare.out +++ b/test/JDBC/expected/Test-sp_helpsrvrolemember-dep-vu-prepare.out @@ -10,20 +10,5 @@ BEGIN END GO -CREATE FUNCTION dbo.test_sp_helpsrvrolemember_func() RETURNS INT -AS -BEGIN - DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, - MemberName sys.SYSNAME, - MemberSID sys.VARBINARY(85)); - INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; - RETURN (SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember); -END -GO - -CREATE VIEW test_sp_helpsrvrolemember_view AS -SELECT dbo.test_sp_helpsrvrolemember_func() AS num -GO - CREATE LOGIN test_sp_helpsrvrolemember_login WITH PASSWORD='123' GO diff --git a/test/JDBC/expected/Test-sp_helpsrvrolemember-dep-vu-verify.out b/test/JDBC/expected/Test-sp_helpsrvrolemember-dep-vu-verify.out index 460e9fbdaa3..6013e2d020a 100644 --- a/test/JDBC/expected/Test-sp_helpsrvrolemember-dep-vu-verify.out +++ b/test/JDBC/expected/Test-sp_helpsrvrolemember-dep-vu-verify.out @@ -8,16 +8,26 @@ sysadmin#!#jdbc_user#!#1 ~~END~~ -SELECT dbo.test_sp_helpsrvrolemember_func() +-- INSERT EXEC is not allowed inside a function, so capture sp_helpsrvrolemember +-- output into a table variable directly in the batch instead. +DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, MemberName sys.SYSNAME, MemberSID sys.VARBINARY(85)); +INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; +SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember; GO +~~ROW COUNT: 1~~ + ~~START~~ int 1 ~~END~~ -SELECT * FROM test_sp_helpsrvrolemember_view +DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, MemberName sys.SYSNAME, MemberSID sys.VARBINARY(85)); +INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; +SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember; GO +~~ROW COUNT: 1~~ + ~~START~~ int 1 @@ -38,16 +48,24 @@ sysadmin#!#test_sp_helpsrvrolemember_login#!#1 ~~END~~ -SELECT dbo.test_sp_helpsrvrolemember_func() +DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, MemberName sys.SYSNAME, MemberSID sys.VARBINARY(85)); +INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; +SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember; GO +~~ROW COUNT: 2~~ + ~~START~~ int 2 ~~END~~ -SELECT * FROM test_sp_helpsrvrolemember_view +DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, MemberName sys.SYSNAME, MemberSID sys.VARBINARY(85)); +INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; +SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember; GO +~~ROW COUNT: 2~~ + ~~START~~ int 2 @@ -67,16 +85,24 @@ sysadmin#!#jdbc_user#!#1 ~~END~~ -SELECT dbo.test_sp_helpsrvrolemember_func() +DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, MemberName sys.SYSNAME, MemberSID sys.VARBINARY(85)); +INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; +SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember; GO +~~ROW COUNT: 1~~ + ~~START~~ int 1 ~~END~~ -SELECT * FROM test_sp_helpsrvrolemember_view +DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, MemberName sys.SYSNAME, MemberSID sys.VARBINARY(85)); +INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; +SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember; GO +~~ROW COUNT: 1~~ + ~~START~~ int 1 diff --git a/test/JDBC/input/BABEL-INSERT-EXEC.sql b/test/JDBC/input/BABEL-INSERT-EXEC.sql index 3f54deeb731..28b2f789b3a 100644 --- a/test/JDBC/input/BABEL-INSERT-EXEC.sql +++ b/test/JDBC/input/BABEL-INSERT-EXEC.sql @@ -302,6 +302,12 @@ INSERT INTO insert_exec_spexec EXEC insert_exec_pspexec; GO SELECT * FROM insert_exec_spexec; GO +-- C3b: INSERT EXEC directly targeting sp_executesql (PLTSQL_STMT_EXEC_SP path). +-- Must report rows-affected just like the EXEC and EXEC_BATCH forms. +INSERT INTO insert_exec_spexec EXEC sp_executesql N'SELECT 888'; +GO +SELECT * FROM insert_exec_spexec ORDER BY a; +GO DROP PROCEDURE insert_exec_pspexec; DROP TABLE insert_exec_spexec; GO @@ -1051,44 +1057,44 @@ GO -- ============================================================================ -- Q1: Positive - function captures procedure output into a table variable -CREATE PROCEDURE dbo.p_q1 AS -BEGIN - SELECT 1 AS a, 'x' AS b - UNION ALL SELECT 2, 'y'; -END; -GO -CREATE FUNCTION dbo.fn_q1() -RETURNS @t TABLE (a INT, b VARCHAR(10)) -AS -BEGIN - INSERT INTO @t EXEC dbo.p_q1; - RETURN; -END; -GO -SELECT * FROM dbo.fn_q1() ORDER BY a; -- Expected: (1,x) (2,y) -GO +-- CREATE PROCEDURE dbo.p_q1 AS +-- BEGIN +-- SELECT 1 AS a, 'x' AS b +-- UNION ALL SELECT 2, 'y'; +-- END; +-- GO +-- CREATE FUNCTION dbo.fn_q1() +-- RETURNS @t TABLE (a INT, b VARCHAR(10)) +-- AS +-- BEGIN +-- INSERT INTO @t EXEC dbo.p_q1; +-- RETURN; +-- END; +-- GO +-- SELECT * FROM dbo.fn_q1() ORDER BY a; -- Expected: (1,x) (2,y) +-- GO --- Q2: Source procedure returns no rows - function returns empty -CREATE PROCEDURE dbo.p_q2 AS -BEGIN - SELECT 1 AS a WHERE 1 = 0; -END; -GO -CREATE FUNCTION dbo.fn_q2() -RETURNS @t TABLE (a INT) -AS -BEGIN - INSERT INTO @t EXEC dbo.p_q2; - RETURN; -END; -GO -SELECT * FROM dbo.fn_q2(); -- Expected: empty -GO -DROP FUNCTION IF EXISTS dbo.fn_q1; -DROP FUNCTION IF EXISTS dbo.fn_q2; -DROP PROCEDURE IF EXISTS dbo.p_q1; -DROP PROCEDURE IF EXISTS dbo.p_q2; -GO +-- -- Q2: Source procedure returns no rows - function returns empty +-- CREATE PROCEDURE dbo.p_q2 AS +-- BEGIN +-- SELECT 1 AS a WHERE 1 = 0; +-- END; +-- GO +-- CREATE FUNCTION dbo.fn_q2() +-- RETURNS @t TABLE (a INT) +-- AS +-- BEGIN +-- INSERT INTO @t EXEC dbo.p_q2; +-- RETURN; +-- END; +-- GO +-- SELECT * FROM dbo.fn_q2(); -- Expected: empty +-- GO +-- DROP FUNCTION IF EXISTS dbo.fn_q1; +-- DROP FUNCTION IF EXISTS dbo.fn_q2; +-- DROP PROCEDURE IF EXISTS dbo.p_q1; +-- DROP PROCEDURE IF EXISTS dbo.p_q2; +-- GO -- ============================================================================ -- Category R: INSERT EXEC inside TRY-CATCH with a variable-referencing source @@ -1120,6 +1126,108 @@ DROP PROCEDURE IF EXISTS dbo.p_var_src; DROP TABLE IF EXISTS dbo.var_tgt; GO +-- ============================================================================ +-- Category S: INSERT EXEC into a target with an INSTEAD OF INSERT trigger +-- The trigger must fire and divert rows; the base table must stay empty. +-- ============================================================================ +CREATE TABLE dbo.ie_iof_base (id INT, val VARCHAR(50)); +GO +CREATE TABLE dbo.ie_iof_log (id INT, val VARCHAR(50)); +GO +CREATE PROCEDURE dbo.ie_iof_src AS +BEGIN + SELECT 1 AS id, 'a' AS val + UNION ALL SELECT 2, 'b'; +END; +GO +CREATE TRIGGER dbo.ie_iof_trg ON dbo.ie_iof_base +INSTEAD OF INSERT +AS +BEGIN + INSERT INTO dbo.ie_iof_log (id, val) SELECT id, val FROM inserted; +END; +GO +INSERT INTO dbo.ie_iof_base EXEC dbo.ie_iof_src; -- INSTEAD OF trigger fires +GO +SELECT id, val FROM dbo.ie_iof_base ORDER BY id; -- Expected: empty (rows diverted) +GO +SELECT id, val FROM dbo.ie_iof_log ORDER BY id; -- Expected: (1,a) (2,b) +GO +DROP TRIGGER dbo.ie_iof_trg; +DROP PROCEDURE dbo.ie_iof_src; +DROP TABLE dbo.ie_iof_base; +DROP TABLE dbo.ie_iof_log; +GO + +-- ============================================================================ +-- Category T: IDENTITY reseed after INSERT EXEC with IDENTITY_INSERT ON +-- After inserting an explicit identity value, the next auto value must +-- continue past it (must NOT restart at 1). +-- ============================================================================ +CREATE TABLE dbo.ie_idsync (id INT IDENTITY(1,1), val VARCHAR(50)); +GO +CREATE PROCEDURE dbo.ie_idsync_src AS + SELECT 50 AS id, 'explicit' AS val; +GO +SET IDENTITY_INSERT dbo.ie_idsync ON; +INSERT INTO dbo.ie_idsync (id, val) EXEC dbo.ie_idsync_src; +SET IDENTITY_INSERT dbo.ie_idsync OFF; +GO +INSERT INTO dbo.ie_idsync (val) VALUES ('auto'); -- Expected identity: 51 +GO +SELECT id, val FROM dbo.ie_idsync ORDER BY id; -- Expected: (50,explicit) (51,auto) +GO +DROP PROCEDURE dbo.ie_idsync_src; +DROP TABLE dbo.ie_idsync; +GO + +-- ============================================================================ +-- Category U: AFTER INSERT trigger fires for INSERT EXEC target +-- ============================================================================ +CREATE TABLE dbo.ie_after_base (id INT); +GO +CREATE TABLE dbo.ie_after_audit (cnt INT); +GO +CREATE PROCEDURE dbo.ie_after_src AS + SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3; +GO +CREATE TRIGGER dbo.ie_after_trg ON dbo.ie_after_base +AFTER INSERT +AS +BEGIN + INSERT INTO dbo.ie_after_audit (cnt) SELECT COUNT(*) FROM inserted; +END; +GO +INSERT INTO dbo.ie_after_base EXEC dbo.ie_after_src; +GO +SELECT id FROM dbo.ie_after_base ORDER BY id; -- Expected: 1 2 3 +GO +SELECT cnt FROM dbo.ie_after_audit; -- Expected: 3 +GO +DROP TRIGGER dbo.ie_after_trg; +DROP PROCEDURE dbo.ie_after_src; +DROP TABLE dbo.ie_after_base; +DROP TABLE dbo.ie_after_audit; +GO + +-- ============================================================================ +-- Category V: @@ROWCOUNT reflects the number of rows flushed by INSERT EXEC +-- (must be checked in the same batch as the INSERT EXEC) +-- ============================================================================ +CREATE TABLE dbo.ie_rowcount (a INT); +GO +CREATE PROCEDURE dbo.ie_rowcount_src AS + SELECT 10 UNION ALL SELECT 20 UNION ALL SELECT 30 UNION ALL SELECT 40; +GO +INSERT INTO dbo.ie_rowcount EXEC dbo.ie_rowcount_src; +SELECT @@ROWCOUNT AS rows_affected; -- Expected: 4 +GO +SELECT a FROM dbo.ie_rowcount ORDER BY a; -- Expected: 10 20 30 40 +GO +DROP PROCEDURE dbo.ie_rowcount_src; +DROP TABLE dbo.ie_rowcount; +GO + -- ============================================================================ -- Cleanup verification -- ============================================================================ diff --git a/test/JDBC/input/storedProcedures/Test-sp_addrole-dep-vu-cleanup.sql b/test/JDBC/input/storedProcedures/Test-sp_addrole-dep-vu-cleanup.sql index 6368eb91c77..ad78f2dbdbe 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_addrole-dep-vu-cleanup.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_addrole-dep-vu-cleanup.sql @@ -10,11 +10,5 @@ GO DROP ROLE sp_addrole_dummy GO -DROP VIEW test_sp_addrole_view -GO - -DROP FUNCTION test_sp_addrole_func -GO - DROP PROC test_sp_addrole_proc GO diff --git a/test/JDBC/input/storedProcedures/Test-sp_addrole-dep-vu-prepare.sql b/test/JDBC/input/storedProcedures/Test-sp_addrole-dep-vu-prepare.sql index 5d7a79ebfbe..4113d09bde7 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_addrole-dep-vu-prepare.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_addrole-dep-vu-prepare.sql @@ -7,20 +7,3 @@ BEGIN EXEC sp_addrole @rolename, @ownername; END GO - -CREATE FUNCTION dbo.test_sp_addrole_func(@rolename sys.SYSNAME, @ownername sys.SYSNAME = NULL) RETURNS INT -AS -BEGIN -DECLARE - @tmp_sp_addrole TABLE(addRole sys.SYSNAME); - IF @ownername IS NULL - INSERT INTO @tmp_sp_addrole (addRole) EXEC sp_addrole @rolename; - ELSE - INSERT INTO @tmp_sp_addrole (addRole) EXEC sp_addrole @rolename, @ownername; - RETURN (SELECT count(*) FROM sys.babelfish_authid_user_ext where orig_username = @rolename); -END -GO - -CREATE VIEW test_sp_addrole_view AS -SELECT dbo.test_sp_addrole_func('sp_addrole_dummy') AS Description -GO diff --git a/test/JDBC/input/storedProcedures/Test-sp_addrole-dep-vu-verify.sql b/test/JDBC/input/storedProcedures/Test-sp_addrole-dep-vu-verify.sql index 52d9140b799..c657dd8731c 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_addrole-dep-vu-verify.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_addrole-dep-vu-verify.sql @@ -1,10 +1,16 @@ EXEC test_sp_addrole_proc 'sp_addrole_role1' GO -SELECT dbo.test_sp_addrole_func('sp_addrole_role2') +-- INSERT EXEC is not allowed inside a function, so capture sp_addrole output +-- into a table variable directly in the batch instead. +DECLARE @tmp_sp_addrole TABLE(addRole sys.SYSNAME); +INSERT INTO @tmp_sp_addrole (addRole) EXEC sp_addrole 'sp_addrole_role2'; +SELECT count(*) FROM sys.babelfish_authid_user_ext where orig_username = 'sp_addrole_role2'; GO -SELECT * FROM test_sp_addrole_view +DECLARE @tmp_sp_addrole_dummy TABLE(addRole sys.SYSNAME); +INSERT INTO @tmp_sp_addrole_dummy (addRole) EXEC sp_addrole 'sp_addrole_dummy'; +SELECT count(*) FROM sys.babelfish_authid_user_ext where orig_username = 'sp_addrole_dummy'; GO EXEC test_sp_addrole_proc 'sp_addrole_role3' diff --git a/test/JDBC/input/storedProcedures/Test-sp_addrolemember-dep-vu-cleanup.sql b/test/JDBC/input/storedProcedures/Test-sp_addrolemember-dep-vu-cleanup.sql index cb09dfe8689..ddb46967eab 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_addrolemember-dep-vu-cleanup.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_addrolemember-dep-vu-cleanup.sql @@ -13,11 +13,5 @@ GO DROP ROLE sp_addrolemember_role1 GO -DROP VIEW test_sp_addrolemember_view -GO - -DROP FUNCTION test_sp_addrolemember_func -GO - DROP PROC test_sp_addrolemember_proc GO diff --git a/test/JDBC/input/storedProcedures/Test-sp_addrolemember-dep-vu-prepare.sql b/test/JDBC/input/storedProcedures/Test-sp_addrolemember-dep-vu-prepare.sql index 157b35ce13a..4333ce7d4a9 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_addrolemember-dep-vu-prepare.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_addrolemember-dep-vu-prepare.sql @@ -6,22 +6,6 @@ END GO -CREATE FUNCTION dbo.test_sp_addrolemember_func(@rolename sys.SYSNAME, @membername sys.SYSNAME) RETURNS INT -AS -BEGIN -DECLARE - @tmp_sp_addrolemember TABLE(rolename sys.SYSNAME, membername sys.SYSNAME); - INSERT INTO @tmp_sp_addrolemember (rolename, membername) EXEC sp_addrolemember @rolename, @membername; - RETURN (SELECT IS_ROLEMEMBER(@rolename, @membername)); -END -GO - - -CREATE VIEW test_sp_addrolemember_view AS -SELECT dbo.test_sp_addrolemember_func('sp_addrolemember_role1','sp_addrolemember_dummy') AS Description -GO - - CREATE ROLE sp_addrolemember_role1 GO diff --git a/test/JDBC/input/storedProcedures/Test-sp_addrolemember-dep-vu-verify.sql b/test/JDBC/input/storedProcedures/Test-sp_addrolemember-dep-vu-verify.sql index 9f09d5d8823..b79f68bbdaa 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_addrolemember-dep-vu-verify.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_addrolemember-dep-vu-verify.sql @@ -1,10 +1,16 @@ EXEC test_sp_addrolemember_proc 'sp_addrolemember_role1', 'sp_addrolemember_role2' GO -SELECT dbo.test_sp_addrolemember_func('sp_addrolemember_role1', 'sp_addrolemember_role3') +-- INSERT EXEC is not allowed inside a function, so capture sp_addrolemember +-- output into a table variable directly in the batch instead. +DECLARE @tmp_sp_addrolemember TABLE(rolename sys.SYSNAME, membername sys.SYSNAME); +INSERT INTO @tmp_sp_addrolemember (rolename, membername) EXEC sp_addrolemember 'sp_addrolemember_role1', 'sp_addrolemember_role3'; +SELECT IS_ROLEMEMBER('sp_addrolemember_role1', 'sp_addrolemember_role3'); GO -SELECT * FROM test_sp_addrolemember_view +DECLARE @tmp_sp_addrolemember_dummy TABLE(rolename sys.SYSNAME, membername sys.SYSNAME); +INSERT INTO @tmp_sp_addrolemember_dummy (rolename, membername) EXEC sp_addrolemember 'sp_addrolemember_role1', 'sp_addrolemember_dummy'; +SELECT IS_ROLEMEMBER('sp_addrolemember_role1', 'sp_addrolemember_dummy'); GO EXEC test_sp_addrolemember_proc 'sp_addrolemember_role1', 'sp_addrolemember_role4' diff --git a/test/JDBC/input/storedProcedures/Test-sp_droprole-dep-vu-cleanup.sql b/test/JDBC/input/storedProcedures/Test-sp_droprole-dep-vu-cleanup.sql index 345e98e425d..b498b2c5da8 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_droprole-dep-vu-cleanup.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_droprole-dep-vu-cleanup.sql @@ -1,8 +1,2 @@ -DROP VIEW test_sp_droprole_view -GO - -DROP FUNCTION test_sp_droprole_func -GO - DROP PROC test_sp_droprole_proc GO diff --git a/test/JDBC/input/storedProcedures/Test-sp_droprole-dep-vu-prepare.sql b/test/JDBC/input/storedProcedures/Test-sp_droprole-dep-vu-prepare.sql index c580aae38af..7609df6ca0b 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_droprole-dep-vu-prepare.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_droprole-dep-vu-prepare.sql @@ -6,22 +6,6 @@ END GO -CREATE FUNCTION dbo.test_sp_droprole_func(@rolename sys.SYSNAME) RETURNS INT -AS -BEGIN -DECLARE - @tmp_sp_droprole TABLE(dropRole sys.SYSNAME); - INSERT INTO @tmp_sp_droprole (dropRole) EXEC sp_droprole @rolename; - RETURN (SELECT count(*) FROM sys.babelfish_authid_user_ext where orig_username = @rolename); -END -GO - - -CREATE VIEW test_sp_droprole_view AS -SELECT dbo.test_sp_droprole_func('sp_droprole_dummy') AS Description -GO - - CREATE ROLE sp_droprole_role1 GO diff --git a/test/JDBC/input/storedProcedures/Test-sp_droprole-dep-vu-verify.sql b/test/JDBC/input/storedProcedures/Test-sp_droprole-dep-vu-verify.sql index 867440c8375..dec72532468 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_droprole-dep-vu-verify.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_droprole-dep-vu-verify.sql @@ -1,10 +1,16 @@ EXEC test_sp_droprole_proc 'sp_droprole_role1' GO -SELECT dbo.test_sp_droprole_func('sp_droprole_role2') +-- INSERT EXEC is not allowed inside a function, so capture sp_droprole output +-- into a table variable directly in the batch instead. +DECLARE @tmp_sp_droprole TABLE(dropRole sys.SYSNAME); +INSERT INTO @tmp_sp_droprole (dropRole) EXEC sp_droprole 'sp_droprole_role2'; +SELECT count(*) FROM sys.babelfish_authid_user_ext where orig_username = 'sp_droprole_role2'; GO -SELECT * FROM test_sp_droprole_view +DECLARE @tmp_sp_droprole_dummy TABLE(dropRole sys.SYSNAME); +INSERT INTO @tmp_sp_droprole_dummy (dropRole) EXEC sp_droprole 'sp_droprole_dummy'; +SELECT count(*) FROM sys.babelfish_authid_user_ext where orig_username = 'sp_droprole_dummy'; GO EXEC test_sp_droprole_proc 'sp_droprole_role3' diff --git a/test/JDBC/input/storedProcedures/Test-sp_droprolemember-dep-vu-cleanup.sql b/test/JDBC/input/storedProcedures/Test-sp_droprolemember-dep-vu-cleanup.sql index cfa09170538..5f0cff3665a 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_droprolemember-dep-vu-cleanup.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_droprolemember-dep-vu-cleanup.sql @@ -13,11 +13,5 @@ GO DROP ROLE sp_droprolemember_role1 GO -DROP VIEW test_sp_droprolemember_view -GO - -DROP FUNCTION test_sp_droprolemember_func -GO - DROP PROC test_sp_droprolemember_proc GO diff --git a/test/JDBC/input/storedProcedures/Test-sp_droprolemember-dep-vu-prepare.sql b/test/JDBC/input/storedProcedures/Test-sp_droprolemember-dep-vu-prepare.sql index c6006f89ee7..dcb19ea6b38 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_droprolemember-dep-vu-prepare.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_droprolemember-dep-vu-prepare.sql @@ -6,22 +6,6 @@ END GO -CREATE FUNCTION dbo.test_sp_droprolemember_func(@rolename sys.SYSNAME, @membername sys.SYSNAME) RETURNS INT -AS -BEGIN -DECLARE - @tmp_sp_droprolemember TABLE(rolename sys.SYSNAME, membername sys.SYSNAME); - INSERT INTO @tmp_sp_droprolemember (rolename, membername) EXEC sp_droprolemember @rolename, @membername; - RETURN (SELECT IS_ROLEMEMBER(@rolename, @membername)); -END -GO - - -CREATE VIEW test_sp_droprolemember_view AS -SELECT dbo.test_sp_droprolemember_func('sp_droprolemember_role1','sp_droprolemember_dummy') AS Description -GO - - CREATE ROLE sp_droprolemember_role1 GO diff --git a/test/JDBC/input/storedProcedures/Test-sp_droprolemember-dep-vu-verify.sql b/test/JDBC/input/storedProcedures/Test-sp_droprolemember-dep-vu-verify.sql index 819e04b3ccd..81f958fd10c 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_droprolemember-dep-vu-verify.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_droprolemember-dep-vu-verify.sql @@ -1,10 +1,16 @@ EXEC test_sp_droprolemember_proc 'sp_droprolemember_role1', 'sp_droprolemember_role2' GO -SELECT dbo.test_sp_droprolemember_func('sp_droprolemember_role1', 'sp_droprolemember_role3') +-- INSERT EXEC is not allowed inside a function, so capture sp_droprolemember +-- output into a table variable directly in the batch instead. +DECLARE @tmp_sp_droprolemember TABLE(rolename sys.SYSNAME, membername sys.SYSNAME); +INSERT INTO @tmp_sp_droprolemember (rolename, membername) EXEC sp_droprolemember 'sp_droprolemember_role1', 'sp_droprolemember_role3'; +SELECT IS_ROLEMEMBER('sp_droprolemember_role1', 'sp_droprolemember_role3'); GO -SELECT * FROM test_sp_droprolemember_view +DECLARE @tmp_sp_droprolemember_dummy TABLE(rolename sys.SYSNAME, membername sys.SYSNAME); +INSERT INTO @tmp_sp_droprolemember_dummy (rolename, membername) EXEC sp_droprolemember 'sp_droprolemember_role1', 'sp_droprolemember_dummy'; +SELECT IS_ROLEMEMBER('sp_droprolemember_role1', 'sp_droprolemember_dummy'); GO EXEC test_sp_droprolemember_proc 'sp_droprolemember_role1', 'sp_droprolemember_role4' diff --git a/test/JDBC/input/storedProcedures/Test-sp_helpdbfixedrole-dep-vu-cleanup.sql b/test/JDBC/input/storedProcedures/Test-sp_helpdbfixedrole-dep-vu-cleanup.sql index d3afcdbce00..4a819eea5f5 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_helpdbfixedrole-dep-vu-cleanup.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_helpdbfixedrole-dep-vu-cleanup.sql @@ -1,8 +1,2 @@ -DROP VIEW test_sp_helpdbfixedrole_view -GO - -DROP FUNCTION test_sp_helpdbfixedrole_func -GO - DROP PROC test_sp_helpdbfixedrole_proc GO diff --git a/test/JDBC/input/storedProcedures/Test-sp_helpdbfixedrole-dep-vu-prepare.sql b/test/JDBC/input/storedProcedures/Test-sp_helpdbfixedrole-dep-vu-prepare.sql index e607c5f4dfa..097aabf6d75 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_helpdbfixedrole-dep-vu-prepare.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_helpdbfixedrole-dep-vu-prepare.sql @@ -4,19 +4,3 @@ BEGIN EXEC sp_helpdbfixedrole @rolename; END GO - - -CREATE FUNCTION dbo.test_sp_helpdbfixedrole_func() RETURNS INT -AS -BEGIN -DECLARE - @tmp_sp_helpdbfixedrole TABLE(DbFixedRole sys.SYSNAME, Description sys.NVARCHAR(70)); - INSERT INTO @tmp_sp_helpdbfixedrole (DbFixedRole, Description) EXEC sp_helpdbfixedrole; - RETURN (SELECT COUNT(*) FROM @tmp_sp_helpdbfixedrole); -END -GO - - -CREATE VIEW test_sp_helpdbfixedrole_view AS -SELECT dbo.test_sp_helpdbfixedrole_func() AS Description -GO diff --git a/test/JDBC/input/storedProcedures/Test-sp_helpdbfixedrole-dep-vu-verify.sql b/test/JDBC/input/storedProcedures/Test-sp_helpdbfixedrole-dep-vu-verify.sql index 40b8eaf9b97..0220c81a873 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_helpdbfixedrole-dep-vu-verify.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_helpdbfixedrole-dep-vu-verify.sql @@ -4,10 +4,16 @@ GO EXEC test_sp_helpdbfixedrole_proc 'db_owner' GO -SELECT dbo.test_sp_helpdbfixedrole_func() +-- INSERT EXEC is not allowed inside a function, so capture sp_helpdbfixedrole +-- output into a table variable directly in the batch instead. +DECLARE @tmp_sp_helpdbfixedrole TABLE(DbFixedRole sys.SYSNAME, Description sys.NVARCHAR(70)); +INSERT INTO @tmp_sp_helpdbfixedrole (DbFixedRole, Description) EXEC sp_helpdbfixedrole; +SELECT COUNT(*) FROM @tmp_sp_helpdbfixedrole; GO -SELECT * FROM test_sp_helpdbfixedrole_view +DECLARE @tmp_sp_helpdbfixedrole2 TABLE(DbFixedRole sys.SYSNAME, Description sys.NVARCHAR(70)); +INSERT INTO @tmp_sp_helpdbfixedrole2 (DbFixedRole, Description) EXEC sp_helpdbfixedrole; +SELECT COUNT(*) FROM @tmp_sp_helpdbfixedrole2; GO EXEC test_sp_helpdbfixedrole_proc 'DB_securityadmin' diff --git a/test/JDBC/input/storedProcedures/Test-sp_helpsrvrolemember-dep-vu-cleanup.sql b/test/JDBC/input/storedProcedures/Test-sp_helpsrvrolemember-dep-vu-cleanup.sql index cbcbd5b89c5..45f00df2db9 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_helpsrvrolemember-dep-vu-cleanup.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_helpsrvrolemember-dep-vu-cleanup.sql @@ -1,11 +1,5 @@ DROP LOGIN test_sp_helpsrvrolemember_login GO -DROP VIEW test_sp_helpsrvrolemember_view -GO - -DROP FUNCTION test_sp_helpsrvrolemember_func -GO - DROP PROC test_sp_helpsrvrolemember_proc GO diff --git a/test/JDBC/input/storedProcedures/Test-sp_helpsrvrolemember-dep-vu-prepare.sql b/test/JDBC/input/storedProcedures/Test-sp_helpsrvrolemember-dep-vu-prepare.sql index c78cf13767e..63bbec9d891 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_helpsrvrolemember-dep-vu-prepare.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_helpsrvrolemember-dep-vu-prepare.sql @@ -10,20 +10,5 @@ BEGIN END GO -CREATE FUNCTION dbo.test_sp_helpsrvrolemember_func() RETURNS INT -AS -BEGIN - DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, - MemberName sys.SYSNAME, - MemberSID sys.VARBINARY(85)); - INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; - RETURN (SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember); -END -GO - -CREATE VIEW test_sp_helpsrvrolemember_view AS -SELECT dbo.test_sp_helpsrvrolemember_func() AS num -GO - CREATE LOGIN test_sp_helpsrvrolemember_login WITH PASSWORD='123' GO diff --git a/test/JDBC/input/storedProcedures/Test-sp_helpsrvrolemember-dep-vu-verify.sql b/test/JDBC/input/storedProcedures/Test-sp_helpsrvrolemember-dep-vu-verify.sql index 1da580e1512..bd9e7f2ac88 100644 --- a/test/JDBC/input/storedProcedures/Test-sp_helpsrvrolemember-dep-vu-verify.sql +++ b/test/JDBC/input/storedProcedures/Test-sp_helpsrvrolemember-dep-vu-verify.sql @@ -1,10 +1,16 @@ EXEC test_sp_helpsrvrolemember_proc GO -SELECT dbo.test_sp_helpsrvrolemember_func() +-- INSERT EXEC is not allowed inside a function, so capture sp_helpsrvrolemember +-- output into a table variable directly in the batch instead. +DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, MemberName sys.SYSNAME, MemberSID sys.VARBINARY(85)); +INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; +SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember; GO -SELECT * FROM test_sp_helpsrvrolemember_view +DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, MemberName sys.SYSNAME, MemberSID sys.VARBINARY(85)); +INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; +SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember; GO ALTER SERVER ROLE sysadmin ADD MEMBER test_sp_helpsrvrolemember_login @@ -13,10 +19,14 @@ GO EXEC test_sp_helpsrvrolemember_proc 'sysadmin' GO -SELECT dbo.test_sp_helpsrvrolemember_func() +DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, MemberName sys.SYSNAME, MemberSID sys.VARBINARY(85)); +INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; +SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember; GO -SELECT * FROM test_sp_helpsrvrolemember_view +DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, MemberName sys.SYSNAME, MemberSID sys.VARBINARY(85)); +INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; +SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember; GO ALTER SERVER ROLE sysadmin DROP MEMBER test_sp_helpsrvrolemember_login @@ -25,10 +35,14 @@ GO EXEC test_sp_helpsrvrolemember_proc 'sysadmin' GO -SELECT dbo.test_sp_helpsrvrolemember_func() +DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, MemberName sys.SYSNAME, MemberSID sys.VARBINARY(85)); +INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; +SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember; GO -SELECT * FROM test_sp_helpsrvrolemember_view +DECLARE @tmp_sp_helpsrvrolemember TABLE(ServerRole sys.SYSNAME, MemberName sys.SYSNAME, MemberSID sys.VARBINARY(85)); +INSERT INTO @tmp_sp_helpsrvrolemember (ServerRole, MemberName, MemberSID) EXEC sp_helpsrvrolemember; +SELECT COUNT(*) FROM @tmp_sp_helpsrvrolemember; GO EXEC sp_helpsrvrolemember 'error' From cbaf680479435eb34db7743afd9c26b488d783db Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Tue, 23 Jun 2026 08:42:12 +0000 Subject: [PATCH 09/11] Blocking insert-exec inside function at runtime to match tsql semantics --- contrib/babelfishpg_tsql/src/pl_exec.c | 8 +-- contrib/babelfishpg_tsql/src/pl_insert_exec.c | 49 +++++++++---------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/contrib/babelfishpg_tsql/src/pl_exec.c b/contrib/babelfishpg_tsql/src/pl_exec.c index f613dd6bd41..cdce5b9b538 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_exec.c @@ -9964,9 +9964,11 @@ pltsql_xact_cb(XactEvent event, void *arg) ResetTopTransactionName(); /* - * Clean up INSERT EXEC context on transaction end. This is a safety - * net for timeouts, interrupts, and other cases where normal cleanup - * paths are bypassed. On commit, any remaining context is stale. + * Clean up INSERT EXEC context on transaction end. This is a signal that an + * aborted INSERT EXEC has nothing to flush: on abort the buffer temp + * table is gone, so clearing the context here makes the subsequent + * flush a no-op (it early-returns on a NULL context) instead of + * opening a dropped relation. */ if (pltsql_insert_exec_active()) pltsql_insert_exec_reset_all(); diff --git a/contrib/babelfishpg_tsql/src/pl_insert_exec.c b/contrib/babelfishpg_tsql/src/pl_insert_exec.c index b02a4197d74..88146125ceb 100644 --- a/contrib/babelfishpg_tsql/src/pl_insert_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_insert_exec.c @@ -223,11 +223,11 @@ void pltsql_set_insert_exec_context_info(const char *target_table) { Assert(insert_exec_ctx == NULL); - insert_exec_ctx = MemoryContextAllocZero(TopTransactionContext, + insert_exec_ctx = MemoryContextAllocZero(TopMemoryContext, sizeof(InsertExecContext)); insert_exec_ctx->target_table = target_table - ? MemoryContextStrdup(TopTransactionContext, target_table) + ? MemoryContextStrdup(TopMemoryContext, target_table) : NULL; /* * Snapshot the call stack entry at INSERT EXEC start. Comparing this @@ -531,7 +531,6 @@ flush_insert_exec_temp_table(PLtsql_execstate *estate, const char *target_schema Relation temp_rel; char *qualified_target; InlineCodeBlockArgs *flush_args; - PLtsql_execstate *flush_estate_saved; if (insert_exec_ctx == NULL) return; @@ -597,7 +596,9 @@ flush_insert_exec_temp_table(PLtsql_execstate *estate, const char *target_schema */ flush_args = create_args(0); - flush_estate_saved = insert_exec_flush_estate; + if (insert_exec_flush_estate != NULL) + elog(ERROR, "insert_exec_flush_estate is already set; INSERT EXEC flush must not nest"); + insert_exec_flush_estate = estate; PG_TRY(); { @@ -605,11 +606,11 @@ flush_insert_exec_temp_table(PLtsql_execstate *estate, const char *target_schema } PG_CATCH(); { - insert_exec_flush_estate = flush_estate_saved; + insert_exec_flush_estate = NULL; PG_RE_THROW(); } PG_END_TRY(); - insert_exec_flush_estate = flush_estate_saved; + insert_exec_flush_estate = NULL; pfree(flush_query.data); @@ -841,6 +842,19 @@ insert_exec_setup(PLtsql_execstate *estate, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("nested INSERT ... EXECUTE statements are not allowed"))); + /* + * T-SQL does not allow INSERT EXEC inside a function. The parser blocks + * the common cases at CREATE FUNCTION time; this catches anything that + * still reaches runtime (e.g. a table-variable target). + */ + if (estate->func && + estate->func->fn_oid != InvalidOid && + estate->func->fn_prokind == PROKIND_FUNCTION && + estate->func->fn_is_trigger == PLTSQL_NOT_TRIGGER) + ereport(ERROR, + (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), + errmsg("'INSERT EXEC' cannot be used within a function"))); + /* Build the quoted column list (if any) for temp table creation */ column_list = build_quoted_column_list(info->columns); @@ -851,15 +865,9 @@ insert_exec_setup(PLtsql_execstate *estate, */ if (start_implicit_txn) { - bool in_function = (estate->func && - estate->func->fn_oid != InvalidOid && - estate->func->fn_prokind == PROKIND_FUNCTION && - estate->func->fn_is_trigger == PLTSQL_NOT_TRIGGER); - if (!pltsql_disable_batch_auto_commit && pltsql_support_tsql_transactions() && - !IsTransactionBlockActive() && - !in_function) + !IsTransactionBlockActive()) { elog(DEBUG4, "TSQL TXN Start internal transaction for INSERT EXEC"); pltsql_start_txn(); @@ -899,20 +907,7 @@ insert_exec_flush_and_cleanup(PLtsql_execstate *estate, InsertExecInfo *info) char *column_list = build_quoted_column_list(info->columns); const char *flush_schema = (info->db_name != NULL || info->schema != NULL) ? info->schema : NULL; - PG_TRY(); - { - /* Flush temp table to target table */ - flush_insert_exec_temp_table(estate, flush_schema, info->db_name, column_list); - } - PG_CATCH(); - { - /* Free the column list and reset context before re-throwing. */ - if (column_list != NULL) - pfree(column_list); - pltsql_insert_exec_reset_all(); - PG_RE_THROW(); - } - PG_END_TRY(); + flush_insert_exec_temp_table(estate, flush_schema, info->db_name, column_list); if (column_list != NULL) pfree(column_list); From d27323c06a8401e667df07513465e390acf5fc94 Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Tue, 23 Jun 2026 10:25:53 +0000 Subject: [PATCH 10/11] Added pg endpoint tests, also did a fix as the PG endpoint runs without logical-DB, create_insert_exec_temp_table and pltsql_insert_exec_open_target_table now resolve the physical schema only when a DB context exists; with none and no explicit schema, the target is referenced by its bare name and resolved via search_path, matching the flush path and a plain INSERT. The TDS path is unchanged. --- contrib/babelfishpg_tsql/src/pl_exec.c | 8 +- contrib/babelfishpg_tsql/src/pl_handler.c | 2 +- contrib/babelfishpg_tsql/src/pl_insert_exec.c | 49 ++++--- .../BABEL-INSERT-EXEC-pg-endpoint.out | 121 ++++++++++++++++++ .../temp_table_rollback-vu-prepare.out | 2 +- ...le_rollback_isolation_read_uncommitted.out | 2 +- ...temp_table_rollback_isolation_snapshot.out | 2 +- .../temp_table_rollback_xact_abort_on.out | 2 +- .../input/BABEL-INSERT-EXEC-pg-endpoint.mix | 86 +++++++++++++ .../temp_table_rollback-vu-prepare.sql | 2 +- 10 files changed, 245 insertions(+), 31 deletions(-) create mode 100644 test/JDBC/expected/BABEL-INSERT-EXEC-pg-endpoint.out create mode 100644 test/JDBC/input/BABEL-INSERT-EXEC-pg-endpoint.mix diff --git a/contrib/babelfishpg_tsql/src/pl_exec.c b/contrib/babelfishpg_tsql/src/pl_exec.c index cdce5b9b538..5e3781c25ce 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_exec.c @@ -442,9 +442,9 @@ static pltsql_CastHashEntry *get_cast_hashentry(PLtsql_execstate *estate, Oid srctype, int32 srctypmod, Oid dsttype, int32 dsttypmod); static void exec_init_tuple_store(PLtsql_execstate *estate); -void exec_set_found(PLtsql_execstate *estate, bool state); +static void exec_set_found(PLtsql_execstate *estate, bool state); static void exec_set_fetch_status(PLtsql_execstate *estate, int status); -void exec_set_rowcount(uint64 rowno); +static void exec_set_rowcount(uint64 rowno); static void exec_set_error(PLtsql_execstate *estate, int error, int pg_error, bool error_mapping_failed); static void pltsql_create_econtext(PLtsql_execstate *estate); static void pltsql_commit_not_required_impl_txn(PLtsql_execstate *estate); @@ -9702,7 +9702,7 @@ contains_target_param(Node *node, int *target_dno) * exec_set_found Set the global found variable to true/false * ---------- */ -void +static void exec_set_found(PLtsql_execstate *estate, bool state) { PLtsql_var *var; @@ -9729,7 +9729,7 @@ exec_set_fetch_status(PLtsql_execstate *estate, int status) fetch_status_var = status; } -void +static void exec_set_rowcount(uint64 rowno) { rowcount_var = rowno; diff --git a/contrib/babelfishpg_tsql/src/pl_handler.c b/contrib/babelfishpg_tsql/src/pl_handler.c index caba2441a7b..46974f6a679 100644 --- a/contrib/babelfishpg_tsql/src/pl_handler.c +++ b/contrib/babelfishpg_tsql/src/pl_handler.c @@ -3065,7 +3065,7 @@ bbf_table_var_lookup(const char *relname, Oid relnamespace) ListCell *lc; int n; PLtsql_tbl *tbl; - PLtsql_execstate *estate = get_current_tsql_estate(); + PLtsql_execstate *estate; /* * During an INSERT EXEC flush the query runs through execute_batch/the diff --git a/contrib/babelfishpg_tsql/src/pl_insert_exec.c b/contrib/babelfishpg_tsql/src/pl_insert_exec.c index 88146125ceb..1ae889db7f2 100644 --- a/contrib/babelfishpg_tsql/src/pl_insert_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_insert_exec.c @@ -74,9 +74,6 @@ static void insertexec_destroy(DestReceiver *self); */ InsertExecContext *insert_exec_ctx = NULL; PLtsql_execstate *insert_exec_flush_estate = NULL; - -extern void exec_set_rowcount(uint64 rowno); -extern void exec_set_found(PLtsql_execstate *estate, bool state); /* * The flush INSERT is routed through execute_batch (the top-level batch entry * point). It runs through the same econtext setup as a normal T-SQL batch. @@ -268,19 +265,20 @@ pltsql_insert_exec_open_target_table(const char *target_table, char *schema_name = NULL; char *table_name = NULL; char *physical_schema = NULL; + char *db = NULL; if (target_table == NULL) return; table_name = pstrdup(target_table); - schema_name = resolve_insert_exec_schema_name(schema_name_in, db_name_in); - /* - * Resolve against the target's database when a 3-part name - * (db..table) was used; otherwise the current database. - */ - physical_schema = get_physical_schema_name( - (db_name_in != NULL) ? (char *) db_name_in : get_cur_db_name(), - schema_name); + db = (db_name_in != NULL) ? pstrdup(db_name_in) : get_cur_db_name(); + if (db != NULL && db[0] != '\0') + { + schema_name = resolve_insert_exec_schema_name(schema_name_in, db); + physical_schema = get_physical_schema_name(db, schema_name); + } + if (db != NULL) + pfree(db); /* Create RangeVar and get the relation OID */ rv = makeRangeVar(physical_schema, table_name, -1); @@ -451,14 +449,25 @@ create_insert_exec_temp_table(const char *target_table, const char *column_list, */ if (!(target_table[0] == '#' || target_table[0] == '@')) { - char *sname = resolve_insert_exec_schema_name(schema_name_in, db_name_in); - - physical_schema = get_physical_schema_name( - (db_name_in != NULL) ? (char *) db_name_in : get_cur_db_name(), sname); - pfree(sname); - if (physical_schema == NULL) - elog(ERROR, "INSERT EXEC failed due to unresolvable schema for target table \"%s\"", - target_table); + char *db = (db_name_in != NULL) ? pstrdup(db_name_in) : get_cur_db_name(); + /* + * On the PostgreSQL endpoint a T-SQL procedure runs without logical + * database context (fn_dbid is InvalidDbid for non-TDS connections), + * so the current database name can be empty. With no DB context, leave + * physical_schema NULL and reference the target by its bare name, so + * search_path resolves it - exactly as a plain INSERT does. + */ + if (db != NULL && db[0] != '\0') + { + char *sname = resolve_insert_exec_schema_name(schema_name_in, db); + physical_schema = get_physical_schema_name(db, sname); + pfree(sname); + if (physical_schema == NULL) + elog(ERROR, "INSERT EXEC failed due to unresolvable schema for target table \"%s\"", + target_table); + } + if (db != NULL) + pfree(db); } /* @@ -621,8 +630,6 @@ flush_insert_exec_temp_table(PLtsql_execstate *estate, const char *target_schema * Report rows-affected from the DestReceiver's captured-row count */ estate->eval_processed = insert_exec_ctx->rows_processed; - exec_set_rowcount(insert_exec_ctx->rows_processed); - exec_set_found(estate, insert_exec_ctx->rows_processed != 0); } /* diff --git a/test/JDBC/expected/BABEL-INSERT-EXEC-pg-endpoint.out b/test/JDBC/expected/BABEL-INSERT-EXEC-pg-endpoint.out new file mode 100644 index 00000000000..8c6ac48983a --- /dev/null +++ b/test/JDBC/expected/BABEL-INSERT-EXEC-pg-endpoint.out @@ -0,0 +1,121 @@ + +-- tsql +-- This test verifies INSERT EXEC works when the enclosing T-SQL procedure is +-- created on the Babelfish (TDS) endpoint but invoked from the PostgreSQL +-- endpoint. The flush goes through execute_batch regardless of the entry point, +-- so the buffered result set must still land in the target table. +CREATE TABLE babel_ie_pg_src (id INT, val VARCHAR(20)); +GO +INSERT INTO babel_ie_pg_src VALUES (1, 'alpha'), (2, 'beta'), (3, 'gamma'); +GO +~~ROW COUNT: 3~~ + + +CREATE TABLE babel_ie_pg_dst (id INT, val VARCHAR(20)); +GO + +-- Source procedure produces a result set +CREATE PROCEDURE babel_ie_pg_source AS +BEGIN + SELECT id, val FROM babel_ie_pg_src ORDER BY id; +END +GO + +-- Wrapper procedure buffers the result set into the target via INSERT EXEC +CREATE PROCEDURE babel_ie_pg_wrapper AS +BEGIN + INSERT INTO babel_ie_pg_dst EXEC babel_ie_pg_source; +END +GO + +-- Call the wrapper once from the TDS endpoint as a baseline +EXEC babel_ie_pg_wrapper; +GO +~~ROW COUNT: 3~~ + +SELECT id, val FROM babel_ie_pg_dst ORDER BY id; +GO +~~START~~ +int#!#varchar +1#!#alpha +2#!#beta +3#!#gamma +~~END~~ + + +-- Empty the target so the PG-endpoint call starts clean +DELETE FROM babel_ie_pg_dst; +GO +~~ROW COUNT: 3~~ + + +-- psql currentSchema=master_dbo,public +-- Invoke the T-SQL procedure from the PostgreSQL endpoint +CALL master_dbo.babel_ie_pg_wrapper(); +GO + +-- Rows buffered by INSERT EXEC must be present in the target table +SELECT id, val FROM master_dbo.babel_ie_pg_dst ORDER BY id; +GO +~~START~~ +int4#!#"sys"."varchar" +1#!#alpha +2#!#beta +3#!#gamma +~~END~~ + + +-- Clear the target again before the transaction cases +DELETE FROM master_dbo.babel_ie_pg_dst; +GO +~~ROW COUNT: 3~~ + + +-- INSERT EXEC inside an explicit committed transaction on the PG endpoint: +-- the buffered rows must persist after COMMIT. +BEGIN; +GO +CALL master_dbo.babel_ie_pg_wrapper(); +GO +COMMIT; +GO +SELECT id, val FROM master_dbo.babel_ie_pg_dst ORDER BY id; +GO +~~START~~ +int4#!#"sys"."varchar" +1#!#alpha +2#!#beta +3#!#gamma +~~END~~ + + +-- Clear the target before the rollback case +DELETE FROM master_dbo.babel_ie_pg_dst; +GO +~~ROW COUNT: 3~~ + + +-- INSERT EXEC inside an explicit transaction that is rolled back on the PG +-- endpoint: the buffered rows must be discarded, leaving the target empty. +BEGIN; +GO +CALL master_dbo.babel_ie_pg_wrapper(); +GO +ROLLBACK; +GO +SELECT id, val FROM master_dbo.babel_ie_pg_dst ORDER BY id; +GO +~~START~~ +int4#!#"sys"."varchar" +~~END~~ + + +-- tsql +DROP PROCEDURE babel_ie_pg_wrapper; +GO +DROP PROCEDURE babel_ie_pg_source; +GO +DROP TABLE babel_ie_pg_dst; +GO +DROP TABLE babel_ie_pg_src; +GO diff --git a/test/JDBC/expected/temp_table_rollback-vu-prepare.out b/test/JDBC/expected/temp_table_rollback-vu-prepare.out index a20f9215c6c..08ef15f6948 100644 --- a/test/JDBC/expected/temp_table_rollback-vu-prepare.out +++ b/test/JDBC/expected/temp_table_rollback-vu-prepare.out @@ -202,7 +202,7 @@ BEGIN CREATE TABLE #proc_create(id int, name varchar(30)) INSERT INTO #proc_create VALUES (1, 'first') INSERT INTO #proc_create VALUES (2, 'second') - SELECT * FROM #proc_create + SELECT * FROM #proc_create ORDER BY id DROP TABLE #proc_create END GO diff --git a/test/JDBC/expected/temp_table_rollback_isolation_read_uncommitted.out b/test/JDBC/expected/temp_table_rollback_isolation_read_uncommitted.out index ddfcc34b464..0ec1af774b4 100644 --- a/test/JDBC/expected/temp_table_rollback_isolation_read_uncommitted.out +++ b/test/JDBC/expected/temp_table_rollback_isolation_read_uncommitted.out @@ -204,7 +204,7 @@ BEGIN CREATE TABLE #proc_create(id int, name varchar(30)) INSERT INTO #proc_create VALUES (1, 'first') INSERT INTO #proc_create VALUES (2, 'second') - SELECT * FROM #proc_create + SELECT * FROM #proc_create ORDER BY id DROP TABLE #proc_create END GO diff --git a/test/JDBC/expected/temp_table_rollback_isolation_snapshot.out b/test/JDBC/expected/temp_table_rollback_isolation_snapshot.out index 7c4926cf21a..b553826c2fc 100644 --- a/test/JDBC/expected/temp_table_rollback_isolation_snapshot.out +++ b/test/JDBC/expected/temp_table_rollback_isolation_snapshot.out @@ -204,7 +204,7 @@ BEGIN CREATE TABLE #proc_create(id int, name varchar(30)) INSERT INTO #proc_create VALUES (1, 'first') INSERT INTO #proc_create VALUES (2, 'second') - SELECT * FROM #proc_create + SELECT * FROM #proc_create ORDER BY id DROP TABLE #proc_create END GO diff --git a/test/JDBC/expected/temp_table_rollback_xact_abort_on.out b/test/JDBC/expected/temp_table_rollback_xact_abort_on.out index 8ec29ee2943..24bb1913155 100644 --- a/test/JDBC/expected/temp_table_rollback_xact_abort_on.out +++ b/test/JDBC/expected/temp_table_rollback_xact_abort_on.out @@ -204,7 +204,7 @@ BEGIN CREATE TABLE #proc_create(id int, name varchar(30)) INSERT INTO #proc_create VALUES (1, 'first') INSERT INTO #proc_create VALUES (2, 'second') - SELECT * FROM #proc_create + SELECT * FROM #proc_create ORDER BY id DROP TABLE #proc_create END GO diff --git a/test/JDBC/input/BABEL-INSERT-EXEC-pg-endpoint.mix b/test/JDBC/input/BABEL-INSERT-EXEC-pg-endpoint.mix new file mode 100644 index 00000000000..8f88be92e81 --- /dev/null +++ b/test/JDBC/input/BABEL-INSERT-EXEC-pg-endpoint.mix @@ -0,0 +1,86 @@ +-- This test verifies INSERT EXEC works when the enclosing T-SQL procedure is +-- created on the Babelfish (TDS) endpoint but invoked from the PostgreSQL +-- endpoint. The flush goes through execute_batch regardless of the entry point, +-- so the buffered result set must still land in the target table. + +-- tsql +CREATE TABLE babel_ie_pg_src (id INT, val VARCHAR(20)); +GO +INSERT INTO babel_ie_pg_src VALUES (1, 'alpha'), (2, 'beta'), (3, 'gamma'); +GO + +CREATE TABLE babel_ie_pg_dst (id INT, val VARCHAR(20)); +GO + +-- Source procedure produces a result set +CREATE PROCEDURE babel_ie_pg_source AS +BEGIN + SELECT id, val FROM babel_ie_pg_src ORDER BY id; +END +GO + +-- Wrapper procedure buffers the result set into the target via INSERT EXEC +CREATE PROCEDURE babel_ie_pg_wrapper AS +BEGIN + INSERT INTO babel_ie_pg_dst EXEC babel_ie_pg_source; +END +GO + +-- Call the wrapper once from the TDS endpoint as a baseline +EXEC babel_ie_pg_wrapper; +GO +SELECT id, val FROM babel_ie_pg_dst ORDER BY id; +GO + +-- Empty the target so the PG-endpoint call starts clean +DELETE FROM babel_ie_pg_dst; +GO + +-- psql currentSchema=master_dbo,public +-- Invoke the T-SQL procedure from the PostgreSQL endpoint +CALL master_dbo.babel_ie_pg_wrapper(); +GO + +-- Rows buffered by INSERT EXEC must be present in the target table +SELECT id, val FROM master_dbo.babel_ie_pg_dst ORDER BY id; +GO + +-- Clear the target again before the transaction cases +DELETE FROM master_dbo.babel_ie_pg_dst; +GO + +-- INSERT EXEC inside an explicit committed transaction on the PG endpoint: +-- the buffered rows must persist after COMMIT. +BEGIN; +GO +CALL master_dbo.babel_ie_pg_wrapper(); +GO +COMMIT; +GO +SELECT id, val FROM master_dbo.babel_ie_pg_dst ORDER BY id; +GO + +-- Clear the target before the rollback case +DELETE FROM master_dbo.babel_ie_pg_dst; +GO + +-- INSERT EXEC inside an explicit transaction that is rolled back on the PG +-- endpoint: the buffered rows must be discarded, leaving the target empty. +BEGIN; +GO +CALL master_dbo.babel_ie_pg_wrapper(); +GO +ROLLBACK; +GO +SELECT id, val FROM master_dbo.babel_ie_pg_dst ORDER BY id; +GO + +-- tsql +DROP PROCEDURE babel_ie_pg_wrapper; +GO +DROP PROCEDURE babel_ie_pg_source; +GO +DROP TABLE babel_ie_pg_dst; +GO +DROP TABLE babel_ie_pg_src; +GO diff --git a/test/JDBC/input/temp_tables/temp_table_rollback-vu-prepare.sql b/test/JDBC/input/temp_tables/temp_table_rollback-vu-prepare.sql index bfda6cc22b2..98a2a4396b0 100644 --- a/test/JDBC/input/temp_tables/temp_table_rollback-vu-prepare.sql +++ b/test/JDBC/input/temp_tables/temp_table_rollback-vu-prepare.sql @@ -202,7 +202,7 @@ BEGIN CREATE TABLE #proc_create(id int, name varchar(30)) INSERT INTO #proc_create VALUES (1, 'first') INSERT INTO #proc_create VALUES (2, 'second') - SELECT * FROM #proc_create + SELECT * FROM #proc_create ORDER BY id DROP TABLE #proc_create END GO From 53d07ccadc086f4cd26d4fd8ac88fde745e2f9f9 Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Tue, 23 Jun 2026 14:47:06 +0000 Subject: [PATCH 11/11] adding order by in test files to pass the parallel query tests as insert-exec doesn't give ordered results explicitly --- test/JDBC/expected/temp_table_rollback-vu-prepare.out | 2 +- test/JDBC/expected/temp_table_rollback-vu-verify.out | 2 +- .../temp_table_rollback_isolation_read_uncommitted.out | 4 ++-- test/JDBC/expected/temp_table_rollback_isolation_snapshot.out | 4 ++-- test/JDBC/expected/temp_table_rollback_xact_abort_on.out | 4 ++-- .../JDBC/input/temp_tables/temp_table_rollback-vu-prepare.sql | 2 +- test/JDBC/input/temp_tables/temp_table_rollback-vu-verify.sql | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/JDBC/expected/temp_table_rollback-vu-prepare.out b/test/JDBC/expected/temp_table_rollback-vu-prepare.out index 08ef15f6948..a20f9215c6c 100644 --- a/test/JDBC/expected/temp_table_rollback-vu-prepare.out +++ b/test/JDBC/expected/temp_table_rollback-vu-prepare.out @@ -202,7 +202,7 @@ BEGIN CREATE TABLE #proc_create(id int, name varchar(30)) INSERT INTO #proc_create VALUES (1, 'first') INSERT INTO #proc_create VALUES (2, 'second') - SELECT * FROM #proc_create ORDER BY id + SELECT * FROM #proc_create DROP TABLE #proc_create END GO diff --git a/test/JDBC/expected/temp_table_rollback-vu-verify.out b/test/JDBC/expected/temp_table_rollback-vu-verify.out index b7e23bfb49c..4867204822d 100644 --- a/test/JDBC/expected/temp_table_rollback-vu-verify.out +++ b/test/JDBC/expected/temp_table_rollback-vu-verify.out @@ -3894,7 +3894,7 @@ GO ~~ROW COUNT: 2~~ -SELECT * FROM #insert_exec_target +SELECT * FROM #insert_exec_target ORDER BY id GO ~~START~~ int#!#varchar diff --git a/test/JDBC/expected/temp_table_rollback_isolation_read_uncommitted.out b/test/JDBC/expected/temp_table_rollback_isolation_read_uncommitted.out index 0ec1af774b4..893b6824e99 100644 --- a/test/JDBC/expected/temp_table_rollback_isolation_read_uncommitted.out +++ b/test/JDBC/expected/temp_table_rollback_isolation_read_uncommitted.out @@ -204,7 +204,7 @@ BEGIN CREATE TABLE #proc_create(id int, name varchar(30)) INSERT INTO #proc_create VALUES (1, 'first') INSERT INTO #proc_create VALUES (2, 'second') - SELECT * FROM #proc_create ORDER BY id + SELECT * FROM #proc_create DROP TABLE #proc_create END GO @@ -4480,7 +4480,7 @@ GO ~~ROW COUNT: 2~~ -SELECT * FROM #insert_exec_target +SELECT * FROM #insert_exec_target ORDER BY id GO ~~START~~ int#!#varchar diff --git a/test/JDBC/expected/temp_table_rollback_isolation_snapshot.out b/test/JDBC/expected/temp_table_rollback_isolation_snapshot.out index b553826c2fc..5e49f4112fe 100644 --- a/test/JDBC/expected/temp_table_rollback_isolation_snapshot.out +++ b/test/JDBC/expected/temp_table_rollback_isolation_snapshot.out @@ -204,7 +204,7 @@ BEGIN CREATE TABLE #proc_create(id int, name varchar(30)) INSERT INTO #proc_create VALUES (1, 'first') INSERT INTO #proc_create VALUES (2, 'second') - SELECT * FROM #proc_create ORDER BY id + SELECT * FROM #proc_create DROP TABLE #proc_create END GO @@ -4480,7 +4480,7 @@ GO ~~ROW COUNT: 2~~ -SELECT * FROM #insert_exec_target +SELECT * FROM #insert_exec_target ORDER BY id GO ~~START~~ int#!#varchar diff --git a/test/JDBC/expected/temp_table_rollback_xact_abort_on.out b/test/JDBC/expected/temp_table_rollback_xact_abort_on.out index 24bb1913155..8812847cf20 100644 --- a/test/JDBC/expected/temp_table_rollback_xact_abort_on.out +++ b/test/JDBC/expected/temp_table_rollback_xact_abort_on.out @@ -204,7 +204,7 @@ BEGIN CREATE TABLE #proc_create(id int, name varchar(30)) INSERT INTO #proc_create VALUES (1, 'first') INSERT INTO #proc_create VALUES (2, 'second') - SELECT * FROM #proc_create ORDER BY id + SELECT * FROM #proc_create DROP TABLE #proc_create END GO @@ -4481,7 +4481,7 @@ GO ~~ROW COUNT: 2~~ -SELECT * FROM #insert_exec_target +SELECT * FROM #insert_exec_target ORDER BY id GO ~~START~~ int#!#varchar diff --git a/test/JDBC/input/temp_tables/temp_table_rollback-vu-prepare.sql b/test/JDBC/input/temp_tables/temp_table_rollback-vu-prepare.sql index 98a2a4396b0..bfda6cc22b2 100644 --- a/test/JDBC/input/temp_tables/temp_table_rollback-vu-prepare.sql +++ b/test/JDBC/input/temp_tables/temp_table_rollback-vu-prepare.sql @@ -202,7 +202,7 @@ BEGIN CREATE TABLE #proc_create(id int, name varchar(30)) INSERT INTO #proc_create VALUES (1, 'first') INSERT INTO #proc_create VALUES (2, 'second') - SELECT * FROM #proc_create ORDER BY id + SELECT * FROM #proc_create DROP TABLE #proc_create END GO diff --git a/test/JDBC/input/temp_tables/temp_table_rollback-vu-verify.sql b/test/JDBC/input/temp_tables/temp_table_rollback-vu-verify.sql index 720de065962..8ec7e082fb9 100644 --- a/test/JDBC/input/temp_tables/temp_table_rollback-vu-verify.sql +++ b/test/JDBC/input/temp_tables/temp_table_rollback-vu-verify.sql @@ -1883,7 +1883,7 @@ GO INSERT INTO #insert_exec_target EXEC p_insert_exec_basic GO -SELECT * FROM #insert_exec_target +SELECT * FROM #insert_exec_target ORDER BY id GO DROP TABLE #insert_exec_target