Search code examples
sql-serversqlclr

SQL CLR Trigger - get source table


I am creating a DB synchronization engine using SQL CLR Triggers in Microsoft SQL Server 2012. These triggers do not call a stored procedure or function (and thereby have access to the INSERTED and DELETED pseudo-tables but do not have access to the @@procid).

Differences here, for reference.

This "sync engine" uses mapping tables to determine what the table and field maps are for this sync job. In order to determine the target table and fields (from my mapping table) I need to get the source table name from the trigger itself. I have come across many answers on Stack Overflow and other sites that say that this isn't possible. But, I've found one website that provides a clue:

Potential Solution:

using (SqlConnection lConnection = new SqlConnection(@"context connection=true")) {
    SqlCommand cmd = new SqlCommand("SELECT object_name(resource_associated_entity_id) FROM sys.dm_tran_locks WHERE request_session_id = @@spid and resource_type = 'OBJECT'", lConnection);
    cmd.CommandType = CommandType.Text;
    var obj = cmd.ExecuteScalar();
}

This does in fact return the correct table name.

Question:

My question is, how reliable is this potential solution? Is the @@spid actually limited to this single trigger execution? Or is it possible that other simultaneous triggers will overlap within this process id? Will it stand up to multiple executions of the same and/or different triggers within the database?

From these sites, it seems the process Id is in fact limited to the open connection, which doesn't overlap: here, here, and here.

Will this be a safe method to get my source table?

Why?

As I've noticed similar questions, but all without a valid answer for my specific situation (except that one). Most of the comments on those sites ask "Why?", and in order to preempt that, here is why:

This synchronization engine operates on a single DB and can push changes to target tables, transforming the data with user-defined transformations, automatic source-to-target type casting and parsing and can even use the CSharpCodeProvider to execute methods also stored in those mapping tables for transforming data. It is already built, quite robust and has good performance metrics for what we are doing. I'm now trying to build it out to allow for 1:n table changes (including extension tables requiring the same Id as the 'master' table) and am trying to "genericise" the code. Previously each trigger had a "target table" definition hard coded in it and I was using my mapping tables to determine the source. Now I'd like to get the source table and use my mapping tables to determine all the target tables. This is used in a medium-load environment and pushes changes to a "Change Order Book" which a separate server process picks up to finish the CRUD operation.

Edit

As mentioned in the comments, the query listed above is quite "iffy". It will often (after a SQL Server restart, for example) return system objects like syscolpars or sysidxstats. But, it seems that in the dm_tran_locks table there's always an associated resource_type of 'RID' (Row ID) with the same object_name. My current query which works reliably so far is the following (will update if this changes or doesn't work under high load testing):

select t1.ObjectName FROM (
    SELECT object_name(resource_associated_entity_id) as ObjectName
    FROM sys.dm_tran_locks WHERE resource_type = 'OBJECT' and request_session_id = @@spid
) t1 inner join (
    SELECT OBJECT_NAME(partitions.OBJECT_ID) as ObjectName
    FROM sys.dm_tran_locks 
    INNER JOIN sys.partitions ON partitions.hobt_id = dm_tran_locks.resource_associated_entity_id
    WHERE resource_type = 'RID'
) t2 on t1.ObjectName = t2.ObjectName

If this is always the case, I'll have to find that out during testing.


Solution

  • How reliable is this potential solution?

    While I do not have time to set up a test case to show it not working, I find this approach (even taking into account the query in the Edit section) "iffy" (i.e. not guaranteed to always be reliable).

    The main concerns are:

    • cascading (whether recursive or not) Trigger executions
    • User (i.e. Explicit / Implicit) transactions
    • Sub-processes (i.e. EXEC and sp_executesql)

    These scenarios allow for multiple objects to be locked, all at the same time.

    Is the @@SPID actually limited to this single trigger execution? Or is it possible that other simultaneous triggers will overlap within this process id?

    and (from a comment on the question):

    I think I can join my query up with the sys.partitions and get a dm_trans_lock that has a type of 'RID' with an object name that will match up to the one in my original query.

    And here is why it shouldn't be entirely reliable: the Session ID (i.e. @@SPID) is constant for all of the requests on that Connection). So all sub-processes (i.e. EXEC calls, sp_executesql, Triggers, etc) will all be on the same @@SPID / session_id. So, between sub-processes and User Transactions, you can very easily get locks on multiple resources, all on the same Session ID.

    The reason I say "resources" instead of "OBJECT" or even "RID" is that locks can occur on: rows, pages, keys, tables, schemas, stored procedures, the database itself, etc. More than one thing can be considered an "OBJECT", and it is possible that you will have page locks instead of row locks.

    Will it stand up to multiple executions of the same and/or different triggers within the database?

    As long as these executions occur in different Sessions, then they are a non-issue.

    ALL THAT BEING SAID, I can see where simple testing would show that your current method is reliable. However, it should also be easy enough to add more detailed tests that include an explicit transaction that first does some DML on another table, or have a trigger on one table do some DML on one of these tables, etc.

    Unfortunately, there is no built-in mechanism that provides the same functionality that @@PROCID does for T-SQL Triggers. I have come up with a scheme that should allow for getting the parent table for a SQLCLR Trigger (that takes into account these various issues), but haven't had a chance to test it out. It requires using a T-SQL trigger, set as the "first" trigger, to set info that can be discovered by the SQLCLR Trigger.

    A simpler form can be constructed using CONTEXT_INFO, if you are not already using it for something else (and if you don't already have a "first" Trigger set). In this approach you would still create a T-SQL Trigger, and then set it as the "first" Trigger using sp_settriggerorder. In this Trigger you SET CONTEXT_INFO to the table name that is the parent of @@PROCID. You can then read CONTEXT_INFO() on a Context Connection in a SQLCLR Trigger. If there are multiple levels of Triggers then the value of CONTEXT INFO will get overwritten, so reading that value must be the first thing you do in each SQLCLR Trigger.