0

I understand there is a regularly quoted answer that is meant to address this question, but I believe there is not enough explanation on that thread to really answer the question.

Why earlier answers are inadequate

The first (and accepted) answer simply says this is a common problem and talks about having only one active insert-exec at a time (which is only the first half of the question asked there and doesn't address the ROLLBACK error). The given workaround is to use a table-valued function - which does not help my scenario where my stored procedure needs to update data before returning a result set.

The second answer talks about using openrowset but notes you cannot dynamically specify argument values for the stored procedure - which does not help my scenario because different users need to call my procedure with different parameters.

The third answer provides something called "the old single hash table approach" but does not explain whether it is addressing part 1 or 2 of the question, nor how it works, nor why.

No answer explains why the database is giving this error in the first place.

My use case / requirements

To give specifics for my scenario (although simplified and generic), I have procedures something like below.

In a nutshell though - the first procedure will return a result set, but before it does so, it updates a status column. Effectively these records represent records that need to be synchronised somewhere, so when you call this procedure the procedure will flag the records as being "in progress" for sync.

The second stored procedure calls that first one. Of course the second stored procedure wants to take those records and perform inserts and updates on some tables - to keep those tables in sync with whatever data was returned from the first procedure. After performing all the updates, the second procedure then calls a third procedure - within a cursor (ie. row by row on all the rows in the result set that was received from the first procedure) - for the purpose of setting the status on the source data to "in sync". That is, one by one it goes back and says "update the sync status on record id 1, to 'in sync'" ... and then record 2, and then record 3, etc.

The issue I'm having is that calling the second procedure results in the error

Msg 50000, Level 16, State 1, Procedure getValuesOuterCall, Line 484 [Batch Start Line 24]
Cannot use the ROLLBACK statement within an INSERT-EXEC statement.

but calling the first procedure directly causes no error.

Procedure 1

-- Purpose here is to return a result set,
-- but for every record in the set we want to set a status flag
-- to another value as well.

alter procedure getValues @username, @password, @target

as
begin
  set xact_abort on;
  begin try
    begin transaction;
      declare @tableVariable table (
        ...
      );

      update someOtherTable
      set something = somethingElse
      output 
        someColumns
      into @tableVariable
      from someTable
      join someOtherTable
      join etc
      where someCol = @username
      and etc
      ;

      select 
        someCols
      from @tableVariable
      ;
    commit;
  end try

  begin catch
    if @@trancount > 0 rollback;
    declare @msg nvarchar(2048) = error_message() + ' Error line: ' + CAST(ERROR_LINE() AS nvarchar(100));
    raiserror (@msg, 16, 1);
    return 55555
  end catch
end

Procedure 2

-- Purpose here is to obtain the result set from earlier procedure
-- and then do a bunch of data updates based on the result set.
-- Lastly, for each row in the set, call another procedure which will
-- update that status flag to another value.

alter procedure getValuesOuterCall @username, @password, @target

as
begin
  set xact_abort on;
  begin try
    begin transaction;

      declare @anotherTableVariable

      insert into @anotherTableVariable
      exec getValues @username = 'blah', @password = @somePass, @target = ''
      ;

      with CTE as (
        select someCols
        from @anotherTableVariable
        join someOtherTables, etc;
      )
      merge anUnrelatedTable as target
      using CTE as source
      on target.someCol = source.someCol
      when matched then update 
        target.yetAnotherCol = source.yetAnotherCol,
        etc
      when not matched then
        insert (someCols, andMoreCols, etc)
        values ((select someSubquery), source.aColumn, source.etc)
      ;

      declare @myLocalVariable int;
      declare @mySecondLocalVariable int;

      declare lcur_myCursor cursor for
        select keyColumn
        from @anotherTableVariable
        ;

      open lcur_muCursor;

      fetch lcur_myCursor into @myLocalVariable;

      while @@fetch_status = 0
        begin
          select @mySecondLocalVariable = someCol 
          from someTable 
          where someOtherCol = @myLocalVariable;

          exec thirdStoredProcForSettingStatusValues @id = @mySecondLocalVariable, etc
        end

      deallocate lcur_myCursor;
  
    commit;
  end try

  begin catch
    if @@trancount > 0 rollback;
    declare @msg nvarchar(2048) = error_message() + ' Error line: ' + CAST(ERROR_LINE() AS nvarchar(100));
    raiserror (@msg, 16, 1);
    return 55555
  end catch
end

The parts I don't understand

Firstly, I have no explicit 'rollback' (well, except in the catch block) - so I have to presume that an implicit rollback is causing the issue - but it is difficult to understand where the root of this problem is; I am not even entirely sure which stored procedure is causing the issue.

Secondly, I believe the statements to set xact_abort and begin transaction are required - because in procedure 1 I am updating data before returning the result set. In procedure 2 I am updating data before I call a third procedure to update further data.

Thirdly, I don't think procedure 1 can be converted to a table-valued function because the procedure performs a data update (which would not be allowed in a function?)

Things I have tried

I removed the table variable from procedure 2 and actually created a permanent table to store the results coming back from procedure 1. Before calling procedure 1, procedure 2 would truncate the table. I still got the rollback error.

I replaced the table variable in procedure 1 with a temporary table (ie. single #). I read the articles about how such a table persists for the lifetime of the connection, so within procedure 1 I had drop table if exists... and then create table #.... I still got the rollback error.

Lastly

I still don't understand exactly what is the problem - what is Microsoft struggling to accomplish here? Or what is the scenario that SQL Server cannot accommodate for a requirement that appears to be fairly straightforward: One procedure returns a result set. The calling procedure wants to perform actions based on what's in that result set. If the result set is scoped to the first procedure, then why can't SQL Server just create a temporary copy of the result set within the scope of the second procedure so that it can be acted upon?

Or have I missed it completely and the issue has something to do with the final call to a third procedure, or maybe to do with using try ... catch - for example, perhaps the logic is totally fine but for some reason it is hitting the catch block and the rollback there is the problem (ie. so if I fix the underlying reason leading us to the catch block, all will resolve)?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
youcantryreachingme
  • 1,065
  • 11
  • 17
  • If I understand correctly, you are experiencing an error somewhere in the flow but because of the nesting of the Stored Procedure calls you cannot identify what has triggered it? My suspicion is that it will be in the handling of the TRY..CATCH blocks in each procedure. I suggest working through Erland Sommarskog's series on Error and Transaction Handling in SQL Server at https://www.sommarskog.se/error_handling/Part1.html – Martin Cairney Mar 30 '22 at 02:11
  • Thanks Martin. The error handling in the procs is modelled on working through that article previously. The issue I'm having is that calling proc 2 results in the error in the question title. Calling proc 1 directly results in no error. – youcantryreachingme Mar 30 '22 at 03:09
  • Erland addresses this as well at https://www.sommarskog.se/share_data.html#INSERTEXEC Look for the heading `Rollback and Error Handling is Difficult` which explains that the real error is being lost somewhere. – Martin Cairney Mar 30 '22 at 03:30
  • Thanks Martin - ironically I just read that paragraph and came back here to add a comment noting it ... to find your comment saying same. This at least suggests it is the called procedure that is entering the `catch` block. This does imply there may be nothing wrong with my approach, but rather the real problem is being obscured by the error handling. I will try a few things - like removing the `catch` block in the calling proc to see what error bubbles up.. – youcantryreachingme Mar 30 '22 at 03:43
  • That was a quick test - I got the same error - but I think this at least pinpoints how this error is generated. I guess it does make sense and is in the error text. The calling proc is using `insert select` to call the called proc - and the called proc must be executing a `rollback` (which may not be used during `insert select`. Even if I remove the explicit `rollback` from the called proc, the fact that called proc also has an `update` suggests it will begin an implicit transaction anyway (and therefore somehow risk rollback). That, I believe, at least clarifies where the error originates. – youcantryreachingme Mar 30 '22 at 04:20
  • `INSERT EXEC` is a monstrosity anyway, as are cursor. Why not just dump the data into a proper staging table? The error handling you have makes no sense: it doesn't actually handle the error, just re-throws it, and it re-throws using `RAISERROR` instead of `THROW;`. The whole thing is just unnecessary if you have `SET XACT_ABORT ON;` because that will automatically rollback anyway – Charlieface Mar 30 '22 at 12:57
  • Because there is a separation of concerns across different schemas. Procs 1 and 3 are in their own schema with their own permissions. Some users may not directly access the tabular data those procs control but must request data through proc 1 and then can update data through proc 3. Hence proc 1 is, in fact, managing the staging table (if I understand you correctly) where data are ready for export out through the proc and to be flagged as being in sync when that happens, then updated with a status of 'in sync', through proc 3, when complete. Alternate Q) why can't SQL Server handle this? – youcantryreachingme Mar 30 '22 at 21:39

0 Answers0