Larry Steinle

September 21, 2013

Microsoft SQL Server Call Stack: Adding The Missing Oracle Feature

Filed under: Oracle,SQL Server — Larry Steinle @ 9:32 am
Tags: ,

A call stack can be an invaluable tool for any developer’s toolbox. The call stack shows from point-of-entry to the currently executing procedure what methods were called giving helpful hints as to where a problem in the logic hides. While the call stack is readily available to an Oracle object via the DBMS_Utility.Format_Call_Stack method, as of Microsoft SQL Server 2012 there is no such capability. In today’s article I will demonstrate how to make your own DBMS_Utility.Format_Call_Stack method in SQL Server.

What is a Call Stack?

Let’s begin by understanding what call stack provides with an example. Say there are three stored procedures in SQL Server: Test1, Test2 and Test3. Test1 calls Test2 which calls Test3. We expect that when we check the call stack from Test1 we only see Test1. Check the call stack from Test2 and we will see Test1 and Test2. Again, check the call stack from Test3 and we will see Test1, Test2 and Test3 in the order by which they were called in the call stack. This behavior is currently unavailable in Microsoft SQL Server.

An example PL/SQL Call Stack from Oracle

----- PL/SQL Call Stack -----
object	line	object
id	number	name
407672500	15	[Sandbox].[Test1]
391672443	8	[Sandbox].[Test2]
375672386	5	[Sandbox].[Test3]

Strategy

I will be the first to admit that this is not going to be an elegant solution…What I am proposing today is a work-around. But when you need the ability to view the call stack, you need it. You do what you have to do to make your life easier for debugging and support.

What we will do is create a table to track the current stored procedure. Each stored procedure instance will be associated with a unique GUID. Whenever a call to a child procedure is made the procedure instances unique GUID will be passed to the child procedure. That’s the only ugly to this strategy. Every procedure you create must have a parameter where the caller’s guid can be passed into the callee’s method. Just before exiting the procedure we remove the record instance from the call stack table.

The Call Stack Table

Let’s begin by creating our call stack table where we will record the in-memory procedure instances.

CREATE SCHEMA Stack;
CREATE TABLE Stack.Trace(
	StackId    UNIQUEIDENTIFIER NOT NULL,
	ParentId   UNIQUEIDENTIFIER NULL,
	ProcId     INT NOT NULL,
	DatabaseId INT NOT NULL
);

We will track these customer administrative tools in a new schema called, Stack. The table will be named Trace and contain a column to record the current procedure instance and the caller parent procedure instance. The @@PROCID value representing the current object will be stored along with the database id value from the DB_ID method in the event that we want to organize this new behavior in a centralized, shared database.

Tracking The Current Running Procedure Instance

When entering a method we will need a routine to add the procedure instance into the table and another to remove the instance before exiting the routine. These routines will be called Push and Pop respectively.

In our push routine if a database id is not provided we will automatically assume the current database. The current instance GUID will be returned to the calling procedure via the output parameter @sID.

CREATE PROCEDURE Stack.Push
  @sID    UNIQUEIDENTIFIER = NULL OUTPUT,
  @ProcId INT,
  @pID    UNIQUEIDENTIFIER = NULL,
  @dbId   INT = NULL
AS BEGIN
  SET NOCOUNT ON;

  SET @sID = NEWID();
  SET @dbId = ISNULL(@dbId, DB_ID());

  IF (NOT EXISTS(SELECT 1 FROM Stack.Trace WHERE StackId = @pID))
    SET @pID = NULL;

  INSERT INTO Stack.Trace (StackId, ParentId, ProcId, DatabaseId)
  VALUES (@sID, @pID, @ProcId, @dbId);
END
GO

The Pop routine will simply remove the current instance from the stack trace table using the StackId returned from the Push routine.

CREATE PROCEDURE Stack.Pop
  @sID UNIQUEIDENTIFIER
AS BEGIN
  SET NOCOUNT ON;
  DELETE
    FROM Stack.Trace
   WHERE StackId = @sID;
END
GO

Building the Call Stack

Now that we have all the information required we can use a Common Table Expression (CTE) to construct the call stack. The CTE provides a very nice, simplified model to construct a recursive query. For additional information about recursive CTEs refer to Microsoft’s article, Recursive Queries Using Common Table Expressions.

  WITH CallStackCTE (RootId, StackId, ParentId, ProcId, ObjectName, Level) AS (
    --Anchor Definition
    SELECT StackId AS RootId,
           StackId,
           ParentId,
           ProcId,
           '[' + DB_NAME(DatabaseId) + '].[' 
               + OBJECT_SCHEMA_NAME(ProcId, DatabaseId) + '].[' 
               + OBJECT_NAME(ProcId, DatabaseId) + 
           ']',
           0 AS Level
      FROM Stack.Trace
     WHERE ParentId IS NULL
    UNION ALL
    --Recursive Definition
    SELECT CallStackCTE.RootId,
           Stack.Trace.StackId,
           Stack.Trace.ParentId,
           Stack.Trace.ProcId,
           '[' + DB_NAME(Stack.Trace.DatabaseId) + '].[' 
               + OBJECT_SCHEMA_NAME(Stack.Trace.ProcId, Stack.Trace.DatabaseId) + '].['
               + OBJECT_NAME(Stack.Trace.ProcId, Stack.Trace.DatabaseId) + 
           ']',
           Level + 1 AS Level
      FROM Stack.Trace
      JOIN CallStackCTE ON CallStackCTE.StackId = Stack.Trace.ParentId
  )

The very first column aliased as RootId tracks the entry routine instance. The RootId column is key to pulling together all the calls up to the current procedure instance without mixing in the call hierarchy from other call chains. The last column, Level, tells us where in the call chain we are located.

Two more methods will be required to get our formatted call stack: the first to identify which root the current call instance is part of and the second to get the call stack for the current call chain:

CREATE PROCEDURE Stack.GetRootId
  @rID UNIQUEIDENTIFIER OUTPUT,
  @sID UNIQUEIDENTIFIER
AS BEGIN
  SET NOCOUNT ON;

  --Use Common Table Expression to Iterate thru Recursive Table Relation
  --REF: http://technet.microsoft.com/en-us/library/ms186243(v=sql.105).aspx
  WITH CallStackCTE (RootId, StackId, ParentId, Level) AS (
    --Anchor Definition
    SELECT StackId AS RootId,
           StackId,
           ParentId,
           0 AS Level
      FROM Stack.Trace
     WHERE ParentId IS NULL
    UNION ALL
    --Recursive Definition
    SELECT CallStackCTE.RootId,
           Stack.Trace.StackId,
           Stack.Trace.ParentId,
           Level + 1 AS Level
      FROM Stack.Trace
      JOIN CallStackCTE ON CallStackCTE.StackId = Stack.Trace.ParentId
  )

  --Get RootId From CTE
  SELECT @rID = CallStackCTE.RootId
    FROM CallStackCTE
   WHERE CallStackCTE.StackId = @sID;
END
GO
CREATE PROCEDURE Stack.GetCallStack
  @stack  VARCHAR(MAX) OUTPUT,
  @sID    UNIQUEIDENTIFIER,
  @indent BIT = 0
AS
  DECLARE @rID UNIQUEIDENTIFIER;
BEGIN
  SET NOCOUNT ON;
  SET @stack = '';
  SET @stack = @stack + '----- SQL Call Stack -----' + CHAR(13) + CHAR(10);
  IF @indent = 0 BEGIN
    SET @stack = @stack + 'proc' + CHAR(9) + 'line' + CHAR(9) + 'proc' + CHAR(13) + CHAR(10);
    SET @stack = @stack + 'id' + CHAR(9) + 'number' + CHAR(9) + 'name' + CHAR(13) + CHAR(10);
  END ELSE BEGIN
    SET @stack = @stack + 'proc' + CHAR(13) + CHAR(10);
    SET @stack = @stack + 'name' + CHAR(13) + CHAR(10);
  END;

  --Need the Root Id to identify all levels of the current stack trace
  EXECUTE Stack.GetRootId @rID = @rID OUTPUT, @sID = @sID;

  --Use Common Table Expression to Iterate thru Recursive Table Relation
  --REF: http://technet.microsoft.com/en-us/library/ms186243(v=sql.105).aspx
  WITH CallStackCTE (RootId, StackId, ParentId, ProcId, ObjectName, Level) AS (
    --Anchor Definition
    SELECT StackId AS RootId,
           StackId,
           ParentId,
           ProcId,
           '[' + DB_NAME(DatabaseId) + '].[' 
               + OBJECT_SCHEMA_NAME(ProcId, DatabaseId) + '].[' 
               + OBJECT_NAME(ProcId, DatabaseId) + 
           ']',
           0 AS Level
      FROM Stack.Trace
     WHERE ParentId IS NULL
    UNION ALL
    --Recursive Definition
    SELECT CallStackCTE.RootId,
           Stack.Trace.StackId,
           Stack.Trace.ParentId,
           Stack.Trace.ProcId,
           '[' + DB_NAME(Stack.Trace.DatabaseId) + '].[' 
               + OBJECT_SCHEMA_NAME(Stack.Trace.ProcId, Stack.Trace.DatabaseId) + '].[' 
               + OBJECT_NAME(Stack.Trace.ProcId, Stack.Trace.DatabaseId) + 
           ']',
           Level + 1 AS Level
      FROM Stack.Trace
      JOIN CallStackCTE ON CallStackCTE.StackId = Stack.Trace.ParentId
  )

  --Get Stack
  SELECT @stack = @stack
                + CASE WHEN @Indent = 0
                    THEN 
                      CAST(CallStackCTE.ProcId AS VARCHAR(255)) + CHAR(9) + '--' + CHAR(9) + CallStackCTE.ObjectName + CHAR(13) + CHAR(10)
                    ELSE
                      CASE
                        WHEN Level = 0 THEN ''
                        ELSE REPLICATE(' ', (CallStackCTE.Level - 1) * 2) + '--> ' 
                      END
                     + CallStackCTE.ObjectName + CHAR(13) + CHAR(10)
                END
    FROM CallStackCTE
   WHERE CallStackCTE.RootId = @rID;
END
GO

In the GetCallStack method I provide an extra argument called @indent which can be used to control how we will format the call stack. When @indent is disabled with a value of 0 we will return the call stack using the same syntax as the Oracle, DBMS_Utility.Format_Call_Stack. When indent is enabled with a value of 1 we will return just the object names in an indented tree like layout.

Note that the only limitation in this example is that the code lacks support for line numbers. However, if you want, you can add another column to the table and an argument to the Push procedure to track the line number for the entry point.

Demonstration

Now all we need is a few routines to demonstrate how the call stack works! Three routines will be created, one calling the other. Each routine will print the call stack so that it is obvious how well our call stack works.

CREATE PROCEDURE Test1
  @pID UNIQUEIDENTIFIER = NULL
AS
  DECLARE @sID       UNIQUEIDENTIFIER;
  DECLARE @stackCall VARCHAR(MAX);
BEGIN
  SET NOCOUNT ON;
  EXECUTE Stack.Push @ProcId = @@PROCID, @pID = @pID, @sID = @sID OUTPUT;

  EXECUTE Stack.GetCallStack @sID = @sID, @stack = @stackCall OUTPUT;
  PRINT 'Stack Trace From Test1:' + CHAR(13) + CHAR(10);
  PRINT @stackCall;
  PRINT CHAR(13) + CHAR(10);

  EXECUTE Test2 @pID = @sID;
  EXECUTE Stack.Pop @sID = @sID;
END
GO
CREATE PROCEDURE Test2
  @pID UNIQUEIDENTIFIER = NULL
AS
  DECLARE @sID       UNIQUEIDENTIFIER;
  DECLARE @stackCall VARCHAR(MAX);
BEGIN
  SET NOCOUNT ON;
  EXECUTE Stack.Push @ProcId = @@PROCID, @pID = @pID, @sID = @sID OUTPUT;

  EXECUTE Stack.GetCallStack @sID = @sID, @stack = @stackCall OUTPUT;
  PRINT 'Stack Trace From Test2:' + CHAR(13) + CHAR(10);
  PRINT @stackCall;
  PRINT CHAR(13) + CHAR(10);

  EXECUTE Test3 @pID = @sID;
  EXECUTE Stack.Pop @sID = @sID;
END
GO
CREATE PROCEDURE Test3
  @pID UNIQUEIDENTIFIER = NULL
AS
  DECLARE @sID       UNIQUEIDENTIFIER;
  DECLARE @stackCall VARCHAR(MAX);
BEGIN
  SET NOCOUNT ON;
  EXECUTE Stack.Push @ProcId = @@PROCID, @pID = @pID, @sID = @sID OUTPUT;

  EXECUTE Stack.GetCallStack @sID = @sID, @stack = @stackCall OUTPUT;
  PRINT 'Stack Trace From Test3:' + CHAR(13) + CHAR(10);
  PRINT @stackCall;
  PRINT CHAR(13) + CHAR(10);

  EXECUTE Stack.Pop @sID = @sID;
END
GO

Now all that we have to do is run the test!

EXECUTE Test1;
GO

The following data is output from our call stack trace:

Stack Trace From Test1:
----- SQL Call Stack -----
proc	line	proc
id	number	name
407672500	--	[Sandbox].[dbo].[Test1]

Stack Trace From Test2:
----- SQL Call Stack -----
proc	line	proc
id	number	name
407672500	--	[Sandbox].[dbo].[Test1]
391672443	--	[Sandbox].[dbo].[Test2]

Stack Trace From Test3:
----- SQL Call Stack -----
proc	line	proc
id	number	name
407672500	--	[Sandbox].[dbo].[Test1]
391672443	--	[Sandbox].[dbo].[Test2]
375672386	--	[Sandbox].[dbo].[Test3]

Enable the @indent argument for the GetCallStack method and you will get the next call stack print:

Stack Trace From Test1:
----- SQL Call Stack -----
proc
name
[Sandbox].[dbo].[Test1]

Stack Trace From Test2:
----- SQL Call Stack -----
proc
name
[Sandbox].[dbo].[Test1]
--> [Sandbox].[dbo].[Test2]

Stack Trace From Test3:
----- SQL Call Stack -----
proc
name
[Sandbox].[dbo].[Test1]
--> [Sandbox].[dbo].[Test2]
    --> [Sandbox].[dbo].[Test3]

Summary

While not the most elegant means by which to get the call stack, we have created a very handy call stack model that emulates the Oracle, DBMS_Utility.Format_Call_Stack method. I did see a forum post titled, Provide Function to Return the Entire Call Stack, in which a Mr. Tomar Verona of the SQL Engine Development team stated that they are considering adding the call stack capability to SQL Server 2014. With any luck we won’t require this workaround much longer!

Happy Coding!

Advertisement

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: