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..c5d935fbf34 100644 --- a/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c +++ b/contrib/babelfishpg_tds/src/backend/tds/tdsresponse.c @@ -2708,6 +2708,11 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error) ListCell *l; PLtsql_expr *expr = ((PLtsql_stmt_execsql *) stmt)->sqlstmt; + /* 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()); + /* * XXX: Once an error occurs, the expr and expr->plan may be * freed. In that case, we've to save the command type in @@ -2731,19 +2736,21 @@ StatementEnd_Internal(PLtsql_execstate *estate, PLtsql_stmt *stmt, bool error) * or if the INSERT itself is an INSERT-EXEC * and it just returned error. */ - row_count_valid = !estate->insert_exec && + row_count_valid = + !estate->insert_exec && + !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 && !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 && !insert_exec_active; } /* @@ -2753,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 = !estate->insert_exec; + row_count_valid = !estate->insert_exec && !insert_exec_active; } } } @@ -2774,8 +2781,39 @@ 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 + * 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 && 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..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 a31643cf3a7..b952c8f925a 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 @@ -1618,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 db2b18ee701..636ead53107 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec-2.c +++ b/contrib/babelfishpg_tsql/src/pl_exec-2.c @@ -743,14 +743,26 @@ exec_stmt_push_result(PLtsql_execstate *estate, Assert(stmt->query != NULL); - /* Handle naked SELECT stmt differently for INSERT ... EXECUTE */ + /* 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); - 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_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 +808,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_insert_exec_active()) + { + receiver = CreateInsertExecDestReceiver(); + receiver->rStartup(receiver, CMD_SELECT, portal->tupDesc); + } + else + { + receiver = CreateDestReceiver(DestRemote); + SetRemoteDestReceiverParams(receiver, portal); + } success = PortalRun(portal, FETCH_ALL, @@ -866,10 +890,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 (stmt->insert_exec != NULL) + insert_exec_setup(estate, stmt->insert_exec, true); + if (IS_TDS_CONN()) { if (strncmp(stmt->proc_name, "sp_", 3) == 0 && @@ -936,12 +967,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), @@ -1160,6 +1197,7 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) * 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; @@ -1252,6 +1290,7 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) * 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 +1310,43 @@ exec_stmt_exec(PLtsql_execstate *estate, PLtsql_stmt_exec *stmt) dest->rShutdown(dest); dest->rDestroy(dest); } + + 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 (stmt->insert_exec != NULL) + insert_exec_flush_and_cleanup(estate, stmt->insert_exec); } PG_FINALLY(); { + if (stmt->insert_exec != NULL) + 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); @@ -1293,30 +1366,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); - return PLTSQL_RC_OK; } @@ -1508,6 +1557,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 (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); @@ -1528,9 +1585,15 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt) if (fcinfo->isnull) elog(ERROR, "pltsql_inline_handler failed"); + + if (stmt->insert_exec != NULL) + insert_exec_flush_and_cleanup(estate, stmt->insert_exec); } PG_FINALLY(); { + if (stmt->insert_exec != NULL) + pltsql_insert_exec_reset_all(); + /* Restore past settings */ pltsql_revert_guc(save_nestlevel); pltsql_revert_last_scope_identity(scope_level); @@ -1557,6 +1620,7 @@ exec_stmt_exec_batch(PLtsql_execstate *estate, PLtsql_stmt_exec_batch *stmt) pltsql_create_econtext(estate); } exec_eval_cleanup(estate); + return PLTSQL_RC_OK; } @@ -2154,7 +2218,7 @@ exec_stmt_exec_sp(PLtsql_execstate *estate, PLtsql_stmt_exec_sp *stmt) int save_nestlevel; int scope_level; InlineCodeBlockArgs *args = NULL; - + batch = exec_eval_expr(estate, stmt->query, &isnull1, &restype1, &restypmod1); if (isnull1) { @@ -2200,6 +2264,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 (stmt->insert_exec != NULL) + insert_exec_setup(estate, stmt->insert_exec, true); + if (strcmp(batchstr, "") != 0) /* check edge cases for * sp_executesql */ { @@ -2210,13 +2283,20 @@ 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 (stmt->insert_exec != NULL) + insert_exec_flush_and_cleanup(estate, stmt->insert_exec); } PG_FINALLY(); { + if (stmt->insert_exec != NULL) + pltsql_insert_exec_reset_all(); + pltsql_revert_guc(save_nestlevel); pltsql_revert_last_scope_identity(scope_level); } PG_END_TRY(); + break; } case PLTSQL_EXEC_SP_EXECUTE: @@ -3217,6 +3297,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 +3903,15 @@ execute_plan_and_push_result(PLtsql_execstate *estate, PLtsql_expr *expr, ParamL { receiver = None_Receiver; } + else if (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..5e3781c25ce 100644 --- a/contrib/babelfishpg_tsql/src/pl_exec.c +++ b/contrib/babelfishpg_tsql/src/pl_exec.c @@ -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) @@ -4884,28 +4892,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); + } } /* @@ -4974,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; @@ -5082,8 +5106,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 +5265,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_insert_exec_active() && !estate->insert_exec) { commit_stmt(estate, (estate->tsql_trigger_flags & TSQL_TRAN_STARTED)); @@ -9926,6 +9962,16 @@ 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 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_handler.c b/contrib/babelfishpg_tsql/src/pl_handler.c index 5475d0d49ca..46974f6a679 100644 --- a/contrib/babelfishpg_tsql/src/pl_handler.c +++ b/contrib/babelfishpg_tsql/src/pl_handler.c @@ -3065,7 +3065,17 @@ 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 + * 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); @@ -6502,6 +6512,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 +6689,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..1ae889db7f2 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 */ @@ -54,9 +70,72 @@ 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 = NULL; +PLtsql_execstate *insert_exec_flush_estate = NULL; +/* + * 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 + * 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; +} + +/* + * Resolve the logical T-SQL schema name for an INSERT EXEC target when the + * statement did not qualify it. */ -InsertExecContext insert_exec_ctx; +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 @@ -132,6 +211,204 @@ get_insertable_column_list(const char *table_name, const char *physical_schema) } + +/* + * Set the global INSERT EXEC context with target table info. + * This is called BEFORE temp table creation - just stores the target info. + */ +void +pltsql_set_insert_exec_context_info(const char *target_table) +{ + Assert(insert_exec_ctx == NULL); + insert_exec_ctx = MemoryContextAllocZero(TopMemoryContext, + sizeof(InsertExecContext)); + + insert_exec_ctx->target_table = target_table + ? MemoryContextStrdup(TopMemoryContext, 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 + */ +void +pltsql_insert_exec_reset_all(void) +{ + InsertExecContext *ctx = insert_exec_ctx; + + if (ctx == NULL) + return; + + insert_exec_ctx = NULL; + + if (ctx->target_table) + pfree(ctx->target_table); + pfree(ctx); +} + +/* + * Capture target table OID for change detection + */ +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; + char *db = NULL; + + if (target_table == NULL) + return; + + table_name = pstrdup(target_table); + 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); + 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; + + /* Initialize the modification flag to false */ + insert_exec_ctx->is_target_relation_modified = false; +} + +/* + * Validate column count from query string BEFORE plan preparation + */ +void +pltsql_insert_exec_validate_column_count(PLtsql_execstate *estate, PLtsql_stmt_execsql *stmt) +{ + PLtsql_expr *expr = stmt->sqlstmt; + SPIPlanPtr plan; + List *plansources; + 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. + */ + 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 statement just to read its result shape. + */ + 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; + + /* Get temp table column count */ + temp_rel = table_open(temp_table_oid, AccessShareLock); + temp_tupdesc = RelationGetDescr(temp_rel); + 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"))); +} + +/* + * 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 != 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 + * 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 == 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 +419,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 +435,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 +447,28 @@ 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); - 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); } - /* - * 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 +528,108 @@ 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; - Oid temp_oid = insert_exec_ctx.temp_table_oid; + int rc; + Oid temp_oid; + const char *target_table; + const char *temp_name; + Relation temp_rel; + char *qualified_target; + InlineCodeBlockArgs *flush_args; - if (!OidIsValid(temp_oid) || !OidIsValid(insert_exec_ctx.target_rel_oid)) + if (insert_exec_ctx == 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)); + temp_oid = insert_exec_ctx->temp_table_oid; + target_table = insert_exec_ctx->target_table; - /* - * 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. - */ - if (insert_exec_ctx.is_target_relation_modified) + if (!OidIsValid(temp_oid) || target_table == NULL) + return; + + 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. 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", + 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)); + 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); + + /* + * 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); + + 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(); + { + rc = execute_batch(estate, flush_query.data, flush_args, NULL); + } + PG_CATCH(); + { + insert_exec_flush_estate = NULL; + PG_RE_THROW(); + } + PG_END_TRY(); + insert_exec_flush_estate = NULL; + pfree(flush_query.data); + + if (rc != PLTSQL_RC_OK) + elog(ERROR, "INSERT EXEC failed due to error while flushing temp table to target table"); + + /* + * Report rows-affected from the DestReceiver's captured-row count + */ + estate->eval_processed = insert_exec_ctx->rows_processed; } /* @@ -319,7 +658,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; @@ -330,19 +671,20 @@ 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 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 +731,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 +750,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 +770,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 +788,13 @@ 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); + + /* INSERT EXEC rows-affected count */ + insert_exec_ctx->rows_processed++; + + /* Close immediately - do not hold open across subtransaction boundaries */ + table_close(temp_rel, RowExclusiveLock); return true; } @@ -446,10 +809,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 +826,115 @@ 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"))); + + /* + * 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); + + /* + * 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) + { + if (!pltsql_disable_batch_auto_commit && + pltsql_support_tsql_transactions() && + !IsTransactionBlockActive()) + { + 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_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; + + flush_insert_exec_temp_table(estate, flush_schema, info->db_name, column_list); + + if (column_list != NULL) + pfree(column_list); + + /* + * 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(); + + /* + * 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; + } +} diff --git a/contrib/babelfishpg_tsql/src/pltsql-2.h b/contrib/babelfishpg_tsql/src/pltsql-2.h index 5f611f4431f..387f4898cbf 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_flush_and_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..4f4b3edbac3 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,33 @@ 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 */ + 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 */ + uint64 rows_processed; /* Rows captured by the DestReceiver = INSERT EXEC rows-affected */ } InsertExecContext; -extern InsertExecContext insert_exec_ctx; +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); +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(PLtsql_execstate *estate, PLtsql_stmt_execsql *stmt); -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); +/* 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..6b5b5b156b3 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" @@ -28,6 +27,7 @@ #include "access/genam.h" #include "catalog.h" #include "hooks.h" +#include "guc.h" #include "tcop/utility.h" #include "multidb.h" @@ -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,26 +157,14 @@ 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."))); - + error_if_xact_stmt_blocked_by_insert_exec(true); PLTsqlCommitTransaction(qc, stmt->chain); } break; 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."))); + error_if_xact_stmt_blocked_by_insert_exec(false); PLTsqlRollbackTransaction(txnName, qc, stmt->chain); } break; @@ -3046,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/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; diff --git a/contrib/babelfishpg_tsql/src/tsqlIface.cpp b/contrib/babelfishpg_tsql/src/tsqlIface.cpp index 8dcdb3cccae..0cbe1404fbb 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; @@ -2152,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-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/BABEL-INSERT-EXEC.out b/test/JDBC/expected/BABEL-INSERT-EXEC.out new file mode 100644 index 00000000000..c60790ac9f7 --- /dev/null +++ b/test/JDBC/expected/BABEL-INSERT-EXEC.out @@ -0,0 +1,1706 @@ +-- ============================================================================ +-- 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; +-- 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 +-- ============================================================================ +-- 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~~ + +-- 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 +-- ============================================================================ +-- 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 + + +-- ============================================================================ +-- 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 +-- -- 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 +~~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 + +-- ============================================================================ +-- 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 +~~ROW COUNT: 2~~ + +SELECT id, val FROM dbo.ie_iof_base ORDER BY id; -- Expected: empty (rows diverted) +GO +~~START~~ +int#!#varchar +~~END~~ + +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 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 +~~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 + 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 +~~ROW COUNT: 3~~ + +SELECT id FROM dbo.ie_after_base ORDER BY id; -- Expected: 1 2 3 +GO +~~START~~ +int +1 +2 +3 +~~END~~ + +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 + +-- ============================================================================ +-- 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/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/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 ddfcc34b464..893b6824e99 100644 --- a/test/JDBC/expected/temp_table_rollback_isolation_read_uncommitted.out +++ b/test/JDBC/expected/temp_table_rollback_isolation_read_uncommitted.out @@ -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 7c4926cf21a..5e49f4112fe 100644 --- a/test/JDBC/expected/temp_table_rollback_isolation_snapshot.out +++ b/test/JDBC/expected/temp_table_rollback_isolation_snapshot.out @@ -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 8ec29ee2943..8812847cf20 100644 --- a/test/JDBC/expected/temp_table_rollback_xact_abort_on.out +++ b/test/JDBC/expected/temp_table_rollback_xact_abort_on.out @@ -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/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/BABEL-INSERT-EXEC.sql b/test/JDBC/input/BABEL-INSERT-EXEC.sql new file mode 100644 index 00000000000..28b2f789b3a --- /dev/null +++ b/test/JDBC/input/BABEL-INSERT-EXEC.sql @@ -0,0 +1,1235 @@ +-- ============================================================================ +-- 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; +-- 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 +-- ============================================================================ +-- 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 +-- 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 +-- ============================================================================ +-- 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 + +-- ============================================================================ +-- 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 + +-- ============================================================================ +-- 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 +-- ============================================================================ +SELECT 'All INSERT EXEC tests completed successfully' AS status; +GO 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' 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