6

I have a written a number of SQL CLR functions (UDF) that reads data from an external DB2 database hosted on an IBM iSeries (using IBM DB2 .Net Provider). So that the function has the necessary permissions to read this data, I need to decorate the function with the SqlFunction attribute having the DataAccess property set to DataAccessKind.Read. I also deploy the assembly as UNSAFE.

The time taken to read data from the DB2 database is relatively slow (eg, 3ms for the simplest ExecuteScalar).

I use these UDFs to effectively merge data from the DB2 database into Sql Server views.

For example, suppose my UDF is defined as

[SqlFunction(DataAccess = DataAccessKind.Read, IsDeterministic = true)] 
public static SqlMoney GetCostPrice(SqlString partNumber)
{
    decimal costPrice;
    // open DB2 connection and retrieve cost price for part
    return new SqlMoney(costPrice);
}

and then use this in my SQL View as:

select Parts.PartNumber,
       dbo.GetCostPrice(Parts.PartNumber) as CostPrice
from Parts

The poor performance problems could be impacted significantly if I could run my SQL Views with a parallel query plan.

There are documented techniques on how to force a query plan to run in parallel rather than serial but these techniques have limitations imposed by SQL Server, one of which is that a SQL CLR defined function MUST have DataAccess = DataAccessKind.None.

But if I set DataAcessKind to None then I get an exception when attempting to open any DbConnection within the function.

And that is my problem! How can I run my UDF in a parallel query plan while still allowing it to read data from the external database?

The best idea I have to address this is to hard-code DataAccess = DataAccessKind.None in my SqlFunction attribute and then, at runtime, within the body of the function elevate permissions with Code Access Security so that subsequent code has permissions to open DbConnection objects.

But I can't figure out how to do it? As an experiment, I have tried the following

    [SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic = true)]
    public static SqlMoney TestFunction()
    {
        var sqlPerm = new SqlClientPermission(PermissionState.Unrestricted);
        sqlPerm.Assert();

        using (var conn = new SqlConnection("context connection=true"))
        {
            conn.Open();
        }

        return new SqlMoney();
    }

and invoke from Sql Server Management Studio with:

select dbo.TestFunction()

but I continue to get a security exception...

A .NET Framework error occurred during execution of user-defined routine or aggregate "TestFunction": System.InvalidOperationException: Data access is not allowed in this context. Either the context is a function or method not marked with DataAccessKind.Read or SystemDataAccessKind.Read, is a callback to obtain data from FillRow method of a Table Valued Function, or is a UDT validation method. System.InvalidOperationException: at System.Data.SqlServer.Internal.ClrLevelContext.CheckSqlAccessReturnCode(SqlAccessApiReturnCode eRc) at System.Data.SqlServer.Internal.ClrLevelContext.GetCurrentContext(SmiEventSink sink, Boolean throwIfNotASqlClrThread, Boolean fAllowImpersonation) at Microsoft.SqlServer.Server.InProcLink.GetCurrentContext(SmiEventSink eventSink) at Microsoft.SqlServer.Server.SmiContextFactory.GetCurrentContext() at System.Data.SqlClient.SqlConnectionFactory.GetContextConnection(SqlConnectionString options, Object providerInfo, DbConnection owningConnection) at System.Data.SqlClient.SqlConnectionFactory.CreateConnection(DbConnectionOptions options, Object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection) at System.Data.ProviderBase.DbConnectionFactory.CreateNonPooledConnection(DbConnection owningConnection, DbConnectionPoolGroup poolGroup) at System.Data.ProviderBase.DbConnectionFactory.GetConnection(DbConnection owningConnection) at System.Data.ProviderBase.DbConnectionClosed.OpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory) at System.Data.SqlClient.SqlConnection.Open() at UserDefinedFunctions.UserDefinedFunctions.TestFunction()

Anyone got any ideas?

Thanks in advance.

(btw, I am running on SQL 2008 using .Net 3.5)

Marinos An
  • 9,481
  • 6
  • 63
  • 96
Kev
  • 1,832
  • 1
  • 19
  • 24
  • Why not use a connected database? – Matthew Whited Feb 25 '16 at 21:13
  • What do you mean "connected database"? You mean a linked server??? (Sorry if I'm being thick!) – Kev Feb 25 '16 at 21:17
  • https://msdn.microsoft.com/en-us/library/ms188279.aspx – Matthew Whited Feb 25 '16 at 21:18
  • Yes, I mean linked servers – Matthew Whited Feb 25 '16 at 21:19
  • The Linked Server Db2 Providers (from MS) are unbelievably slow. So have never used them except for occasional SSIS jobs. However, if you are saying that I could use one without requiring UserDataAccess rights in my UDF (?) then the performance gains of parallelism might outweigh this. – Kev Feb 25 '16 at 21:23
  • It's been years since i used DB2 so I have no idea how this will impact performance but my guess is that it would work faster than SQL CLR.... but my guess could be wrong :) – Matthew Whited Feb 25 '16 at 21:26
  • If you are running into performance issues I'd suggest caching in your application or syncing the data from DB2 into this database. – Matthew Whited Feb 25 '16 at 21:28

1 Answers1

2

As far as my testing (against SqlConnection to SQL Server) shows, this can only be accomplished by using a regular / external connection (i.e. not Context Connection = true) and adding the Enlist keyword to the Connection String, set to false:

Server=DB2; Enlist=false;

But there does not seem to be any way to make this work when using Context Connection = true. The Context Connection is automatically part of the current transaction and you cannot specify any other connection string keywords when using the Context Connection. What does the transaction have to do with it? Well, the default for Enlist is true, so even if you do have a regular / external connection, if you don't specify Enlist=false;, then you get the same

Data access is not allowed in this context.

error that you are getting now.

Of course, this is a moot point because there is no purpose in using the Context Connection in this particular case as it would then require using a Linked Server, and it was pointed out in a comment on the Question that the "The Linked Server Db2 Providers (from MS) are unbelievably slow".

It was also pointed out that maybe using TransactionScope with an option of Suppress might work. This can't work because you aren't allowed to instantiate a TransactionScope object (with any of the three options: Required, RequiresNew, or Suppress) if both DataAccess and SystemDataAccess are set to None (which is their default value).

Also, regarding the desire to

elevate the UserDataAccess status of a UDF at runtime.

this is just not possible due to UserDataAccess not being a run-time option. It is determined when the CREATE FUNCTION statement is executed (the one that has AS EXTERNAL NAME [Assembly]... as the definition. The UserDataAccess and SystemDataAccess properties are meta-data that is stored with the Function. You can see the setting of either of these by using the OBJECTPROPERTYEX built-in function:

SELECT OBJECTPROPERTYEX(OBJECT_ID(N'SchemaName.FunctionName'), 'UserDataAccess');

Your two options seem to be:

  1. Use a provider that supports the Enlist keyword so that it can be set to false, or if it does not enlist by default, then doesn't otherwise require DataAccess to be set to Read. According to the suggested documentation to review ( Integrating DB2 Universal Universal Database for iSeries with for iSeries with Microsoft ADO .NET ), the options appear to be:

    • OleDb
    • ODBC
    • IBM DB2 for LUW .NET
  2. Build a middle-tear being a web service that a SQLCLR function can pass the request to, it will use whatever provider to get the info, and it will respond with the info. The SQLCLR function is then not doing any direct data access, and the web service can do its own caching (you said that the source data doesn't change that often) to improve performance (even if only caching the values for 1 - 5 minutes). Yes, this does introduce an external dependency, but it should otherwise work as desired.

Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171
  • Thanks for the response. I left the IsDeterministric property as its default for the sake of conciseness but you are right in that I have misled the reader (I will edit the question in a mo). In reality I set it to true - the source data does not change often and if caching misses a change in the source data (very small chance) then this has no significant impact. Permission set SAFE isn't required, I have proven that I can generate a parallel plan with an UNSAFE assembly. – Kev Feb 23 '16 at 14:38
  • @Kev Interesting about doing this in UNSAFE. I have never seen that. There is no direct way around the DataAccess part; that is even used for T-SQL function (look at `OBJECTPROPERTYEX(object_id, 'UserDataAccess')`). I have one or two ideas that I can try out later and update with the results tonight.. – Solomon Rutzky Feb 23 '16 at 17:22
  • @Kev I have updated my answer after running some tests. – Solomon Rutzky Feb 26 '16 at 06:53
  • @srutsky. Thanks. enlist=false is not supported by the .net provider (see pg 118 of https://www.redbooks.ibm.com/redbooks/pdfs/sg246440.pdf) - by default it does not enlist into DTS transactions anyway. But (slower) MS OLEDB DB2 providers support this option so I will experiment with them. As for context connection, this may still be possible by wrapping the connection open/close inside a "using (new TransactionScope(TransactionScopeOptions.Suppress)" which may achieve the desired result. I will try this out too. (May not get time for this today/tomorrow tho). – Kev Feb 26 '16 at 09:05
  • @Kev Maybe be more explicit about these details in the question? Your example code is _very_ misleading and far off from what you are trying to do. I was wondering why it was `SqlConnection` if going to DB2 but figured you had something worked out. The `Context Connection` made no sense either since it is local only and would require a Linked Server to get to DB2 and you indicated that you aren't using Linked Servers. So forget `Context Connection`, and `TransactionScope` can't help since it can't be used if `DataAccess = None` (I tested all 3 options and it never gets to the Connection line). – Solomon Rutzky Feb 26 '16 at 17:15
  • @Kev I updated my answer with more info. I did look at that IBM documentation (very outdated--only refers to .NET 1.1 ;-), but it does appear that you should be able to specify `enlist=false` on the provider that you are currently using (top of page 119). It is worth trying. Else, then the `LUW` option looks promising. – Solomon Rutzky Feb 28 '16 at 19:40
  • @srutsky enlist=false not an option for my .net provider. ODBC not an option cos its slow, Have been experimenting with left-field approach to launch a separate thread inside CLR function and perform my dataacess on that thread and return result to calling thread. It works, but is not elegant, also does not allow "context conntect=true". Am currently trying alt drivers, such as LUW and Progress datadirect. will post back here once I have some concrete results. – Kev Mar 03 '16 at 21:59
  • @Kev Ok. Is there a reason to avoid the LUW driver? And why do you need context connection? You are using a regular / external connection, not context. – Solomon Rutzky Mar 03 '16 at 22:02
  • @srutsky. no avoidance of LUW, just that perhaps my performance problem issue could just be simply resolved if I found a faster driver. LUW is (on paper) about same performance as current .net provider but not ruling it out tho. Some of the functions would benefit from reading context connection data but I can work around this so not a biggy. Would be nice if could find a solution that required minimal workarounds/hacks that's all. – Kev Mar 03 '16 at 22:23
  • @Kev You can use different providers as necessary. Besides, you need to use `SqlConnection` for context connection and another driver for direct DB2 access. If LUW is about the same performance as the current driver, why not just switch to it since it definitely supports `Enlist=false`, at least according to the doc that you pointed me to. Then you can set `ReadAccess` to `None` and be done with it, right? – Solomon Rutzky Mar 03 '16 at 22:29
  • @srutsky. 100% agree about LUW, just not getting the connection working - think its either a port/firewall issue or a server config - maybe same problem with other providers - will get to the bottom of it soon and then I'll have more concrete feedback for you. signing off now, as its getting late here in UK – Kev Mar 03 '16 at 22:35
  • Hey @Kev Did you ever get any farther on this? – Solomon Rutzky Oct 15 '16 at 19:34
  • @srutsky. I had one solution which, thanks to an email from Adam Machanic, is to spawn another thread from the SQLCLR method and that thread performs the db access, wait for the thread to finish, rejoin to calling thread and return result. Running db access on another thread means that the sqlclr method can be defined as DataAccess = DataAccessKind.None. I experimented with the solution and got it working but decided that the inherent stability issues with multi-threading inside SQLCLR meant it was a lot or work to get a robust solution. So, decided not to go into production with the idea :-( – Kev Oct 30 '16 at 17:46
  • @Kev Interesting. So, then is there any reason to not accept my answer, as it is effectively correct, given that the only way around it is rather dangerous and something that nobody would actually do? If you want, I can add a note to my answer about Adam's proposal, along with a warning to not use such a method as it could easily result in unpredictable behavior. – Solomon Rutzky Oct 31 '16 at 17:56