Please start any new threads on our new site at https://forums.sqlteam.com. We've got lots of great SQL Server experts to answer whatever question you can come up with.

 All Forums
 SQL Server 2005 Forums
 Transact-SQL (2005)
 Noob help with syntax linking in a CLR function

Author  Topic 

R
Constraint Violating Yak Guru

328 Posts

Posted - 2012-02-01 : 10:04:41
Following Adam Machanic's code for creating a CSV splitter CLR function, I've used a slightly amended version in Visual Studio and compiled it. The aim is to take a CSV string of integers and return a table of integer values. A shortened version of the .NET code is here...


Imports Microsoft.SqlServer.Server
Imports System.Data.SqlTypes

Public Class CLRSplit
Implements IEnumerator

...

End Class

Public Class SplitString

<Microsoft.SqlServer.Server.SqlFunction(FillRowMethodName:="FillRow", TableDefinition:="output IdVal int")> _
Public Shared Function DoSplit(instr As SqlChars, <SqlFacet(IsFixedLength:=True, MaxSize:=1)> delimiter As SqlString) As IEnumerator
Return (If((instr.IsNull OrElse delimiter.IsNull), New CLRSplit("", ","c), New CLRSplit(instr.ToSqlString().Value, Convert.ToChar(delimiter.Value))))
End Function

Public Shared Sub FillRow(obj As Object, ByRef output As SqlString)
output = DirectCast(obj, SqlInt32)
End Sub

End Class


This is compiled to a DLL. In SSMS I've done the following, but can't achieve the final step of making the function available to use in a query editor. Could anyone help please with the last step? I can't use the VS Deploy function as I need to place the DLL manually in the live environment. Here's what I've done so far...


sp_configure 'clr enabled', 1
GO
RECONFIGURE
GO
Success

CREATE ASSEMBLY mySplitter
FROM 'D:\mySplitter.dll'
WITH PERMISSION_SET = SAFE
GO
Success

CREATE FUNCTION dbo.myfn_ClrSplitter
(
@Ids nvarchar(max),
@separator char(1)
)
RETURNS TABLE
(
IdVal int NOT NULL
)
AS EXTERNAL NAME mySplitter.DoSplit
Msg 102, Level 15, State 1, Procedure myfn_ClrSplitter, Line 11
Incorrect syntax near 'DoSplit'.

sunitabeck
Master Smack Fu Yak Hacker

5155 Posts

Posted - 2012-02-01 : 11:28:36
The syntax is Assembly.Class.Method, so in your example, it should be
AS EXTERNAL NAME mySplitter.SplitString.DoSplit
Go to Top of Page

R
Constraint Violating Yak Guru

328 Posts

Posted - 2012-02-01 : 11:37:53
Okay thanks. Now sadly I'm getting another error that I don't understand...


Msg 6505, Level 16, State 2, Procedure myfn_ClrSplitter, Line 2
Could not find Type 'SplitString' in assembly 'mySplitter'.
Go to Top of Page

robvolk
Most Valuable Yak

15732 Posts

Posted - 2012-02-01 : 13:20:05
Is there any reason you can't use Adam's code verbatim?
Go to Top of Page

sunitabeck
Master Smack Fu Yak Hacker

5155 Posts

Posted - 2012-02-01 : 17:49:43
Looking at your code, I don't see anything wrong either - but I know so little about VB.Net, so I am mentally translating it to C# and comparing.

In Visual Studio, if you use object browser to look at the mysplitter.dll, does it show the class and the method with the correct visibility?
Go to Top of Page

R
Constraint Violating Yak Guru

328 Posts

Posted - 2012-02-02 : 06:47:29
quote:
In Visual Studio, if you use object browser to look at the mysplitter.dll, does it show the class and the method with the correct visibility?


It does show CLRSplit and SplitString class in the browser, then to the right (within the SplitString class) I see the following methods:

  • Public Shared Sub FillRow(obj As Object, ByRef output As System.Data.SqlTypes.SqlString)
    Member of mySplitter.SplitString
  • Public Shared Function DoSplit(instr As System.Data.SqlTypes.SqlChars, delimiter As System.Data.SqlTypes.SqlString) As System.Collections.IEnumerator
    Member of mySplitter.SplitString


From my minimal experience, that appears to look okay...


quote:
Is there any reason you can't use Adam's code verbatim?


It seems that there's a significant lump of code missing from Adam's post. When I read other posts regarding CLR TVF's, all examples seem to use a wrapper class that implements IEnumerable. My code only has IEnumerator implementations, so I wonder if i'm missing something stupid here. The problem I don't have sufficient .NET knowledge to fill in these gaps myself...
I don't understand why SQL interprets SplitString as a type, unless of course its because IEnumerable is not being used?
Go to Top of Page

sunitabeck
Master Smack Fu Yak Hacker

5155 Posts

Posted - 2012-02-02 : 07:31:16
I tried to reproduce the problem that you saw and I was able to.

What I found was that in your CREATE FUNCTION statement, you have to specify the namespace and class. For example:
CREATE FUNCTION SqlDoubleVB (@i INT )
RETURNS INT
AS EXTERNAL NAME TestVBFunctionAssembly.[ProjectVB.ClassVB].DoubleVB
This is creating the CLR function from this VB code:
Public Class ClassVB
<Microsoft.SqlServer.Server.SqlFunction()>
Public Shared Function DoubleVB(i As SqlTypes.SqlInt32) As SqlTypes.SqlInt32
Return 2 * i
End Function
End Class
You can see the name resolution hierarchy if you look in object browser in Visual Studio.
My DoubleVB function appears in ProjectVB assembly under namespace = ProjectVB, class = ClassVB.
ProjectVB happens to be the name of my VB project, which I assume ends up being the default namespace.
In C#, you can see the namespace in the code, but don't know where you look for it in VB, or how you change it if you need to.

Bottom line: if you qualify your class name with the namespace it is contained in, it should stop complaining.

Now, whether there are other issues with the VB code - I am EXCEPTIONALLY UNQUALIFIED to answer that
Go to Top of Page

R
Constraint Violating Yak Guru

328 Posts

Posted - 2012-02-02 : 07:56:47
Hi

Thanks for that. I've spent the morning looking for answers yet again. Many people have a similar and remove the Namespace completely to fix it, so I have done this, but to no avail

I've changed the code to just use nvarchar for now. Once its working in SQL I'll go back and start altering it. I did find a warning on MSDN that stated that VB works differently in the sense that parameters cannot be declared OUT. Instead you have to use System.Runtime.InteropServices and declare like this (notice <out()>)...

Public Shared Sub FillRow(obj As Object, <Out()> ByRef IdVal As SqlString)
IdVal = DirectCast(obj, SqlString)
End Sub


I guess this is more complicated because its not a function that returns a single value, but rather a set of values as table rows. Are you able to produce a test project to achieve that (even in C#!)? I'm going to keep on reading up until I can get around this as its starting to become a headache now...!
Go to Top of Page

sunitabeck
Master Smack Fu Yak Hacker

5155 Posts

Posted - 2012-02-02 : 08:09:51
I don't know if you can completely remove the namespace - because, everything in .Net has to be under some namespace, even static functions.

Since your original posting did not have the whole code, I didn't even try to test that. If you can post the code, I can try to build it as time permits.

Not sure when I will though - I have a day job, and these people expect me to not just show up, but also do some work for them, can you believe it!?! Life is so unfair!! But, if you post the code, someone else on the forum who is more familiar with VB might be able to help, even if I am not able to.
Go to Top of Page

R
Constraint Violating Yak Guru

328 Posts

Posted - 2012-02-02 : 11:15:10
Okay I've made some progress . It turns out that Visual Studio has a property in the assembly whereby additional namespace entries are added. So my namespace needed to be a little longer and actually reference the assembly name twice. Anyway its found the function, although is now returning something a little more devious...



Msg 6552, Level 16, State 3, Procedure e4fn_ClrSplitter, Line 1
CREATE FUNCTION for "myfn_ClrSplitter" failed because T-SQL and CLR types for parameter "@delimiter" do not match.



I can't quite figure out where the difference is in the .NET code that's causing this. I've checked that the data types are the same but it's still not playing...


Imports System.Data
Imports Microsoft.SqlServer.Server
Imports System.Data.SqlTypes
Imports System.Data.SqlClient
Imports System.Collections
Imports System.Runtime.InteropServices

Public Class SplitString

Private Class CLRSplit
Implements IEnumerator

Private lastPos As Integer
Private nextPos As Integer
Private theString As String
Private delimiter As Char

Public Sub New(s As String, d As Char)
Me.theString = s
Me.delimiter = d
Me.lastPos = -1
Me.nextPos = -1
End Sub

Public ReadOnly Property Current() As Object Implements IEnumerator.Current
Get
Return theString.Substring(lastPos, nextPos - lastPos).Trim()
End Get
End Property

Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
If nextPos >= theString.Length Then
Return False
Else
lastPos = nextPos + 1
If lastPos = theString.Length Then
Return False
End If
nextPos = theString.IndexOf(delimiter, lastPos)
If nextPos = -1 Then
nextPos = theString.Length
End If
Return True
End If
End Function

Public Sub Reset() Implements IEnumerator.Reset
Me.lastPos = -1
Me.nextPos = -1
End Sub

End Class

<Microsoft.SqlServer.Server.SqlFunction(FillRowMethodName:="FillRow", TableDefinition:="IdVal INT")> _
Public Shared Function DoSplit(instr As SqlString, <SqlFacet(IsFixedLength:=True, MaxSize:=1)> delimiter As SqlChars) As IEnumerable
Return (If((instr.IsNull OrElse delimiter.IsNull), New CLRSplit("", ","c), New CLRSplit(instr.Value, Convert.ToChar(delimiter.Value))))
End Function

Public Shared Sub FillRow(obj As Object, <Out()> ByRef IdVal As SqlInt32)
IdVal = DirectCast(obj, SqlInt32)
End Sub

End Class
Go to Top of Page

sunitabeck
Master Smack Fu Yak Hacker

5155 Posts

Posted - 2012-02-02 : 13:41:50
I made 2 or three changes to your function and it works correctly for me now. But, you may want to add checks to make sure that non-integers are not passed in, or it will throw a .Net exception. My changes are all in the DoSplit function (or its attributes), highlighted in red below:
    <Microsoft.SqlServer.Server.SqlFunction(FillRowMethodName:="FillRow", TableDefinition:="output INT")> _
Public Shared Function DoSplit(instr As SqlString, <SqlFacet(IsFixedLength:=True, MaxSize:=1)> delimiter As SqlChars) As IEnumerator
Return (If((instr.IsNull OrElse delimiter.IsNull), New CLRSplit("", ","c), New CLRSplit(instr.Value, Convert.ToChar(delimiter.Value))))
End Function

Public Shared Sub FillRow(obj As Object, ByRef IdVal As SqlInt32)
IdVal = Convert.ToInt32(obj)
End Sub
Go to Top of Page

R
Constraint Violating Yak Guru

328 Posts

Posted - 2012-02-03 : 01:35:54
Hi sunitabeck

Thanks for your suggestion. I finally got it working

Just one last point if I may, for clarification...

You made the following change to my code to convert the obj to a .NET Int32:

Public Shared Sub FillRow(obj As Object, ByRef output As SqlString)
output = DirectCast(obj, SqlInt32)
output = Convert.ToInt32(obj)
End Sub


This works, but I was wondering why my original version threw an exception? Surely if the code is passing integers back to SQL, then the SqlInt32 would be the correct format? Indeed, if I try to change the code back it throws an InvalidCastException.
Go to Top of Page

sunitabeck
Master Smack Fu Yak Hacker

5155 Posts

Posted - 2012-02-03 : 08:00:55
SqlTypes including SqlInt32 are all structures, so you cannot assign an object to it. You should be able to do this:
IdVal = New SqlInt32(obj)
The way I was doing it, I was converting the object to an int (which is a value type) and then (implicitly) assigning the value type to the structure. So you should even be able to do this:

Dim intVal As Integer
intVal = obj
IdVal = intVal
I have not tested either approach, so if my theory is not correct, I would not be surprised.
Go to Top of Page

Transact Charlie
Master Smack Fu Yak Hacker

3451 Posts

Posted - 2012-02-03 : 08:14:24
you can return objects into SQL_VARIANT datatypes....

But you probably don't want to.

Charlie
===============================================================
Msg 3903, Level 16, State 1, Line 1736
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION
Go to Top of Page

R
Constraint Violating Yak Guru

328 Posts

Posted - 2012-02-03 : 10:38:47
quote:
IdVal = New SqlInt32(obj)



Ahh yes of course i see. Super, thank you - that's great

Okay last bit of relentless questioning before you get on with some proper work

I would have liked to build in some error checking in the function. The main one was to ensure that a single character is always passed to the delimiter parameter. So I modified the code to check the length of the delimiter string...


<Microsoft.SqlServer.Server.SqlFunction(FillRowMethodName:="FillRow", TableDefinition:="output INT")> _
Public Shared Function SplitToIntegers(instr As SqlString, <SqlFacet(IsFixedLength:=True, MaxSize:=1)> delimiter As SqlChars) As IEnumerator


' Throw New Exception(delimiter.Length.ToString) ' always returns '1' regardless of char
If delimiter.Length <> 1 Then
Throw New ArgumentException("Length: " & delimiter.Length.ToString & " - The delimiter character must be a single character")
Exit Function
End If

Return New CLRSplit(instr.Value, Convert.ToChar(delimiter.Value))
End Function


Strangely, even when a blank delimiter is passed, the exception I added never gets thrown, and FillRow() executes. So if a blank delimiter is passed to the function, the error I actually witness is:


Msg 6260, Level 16, State 1, Line 1
An error occurred while getting new row from user defined Table Valued Function :
System.OverflowException: Arithmetic operation resulted in an overflow.
System.OverflowException:
at Microsoft.VisualBasic.CompilerServices.Conversions.ToInteger(String Value)
at Microsoft.VisualBasic.CompilerServices.Conversions.ToInteger(Object Value)
at mySplitter.SplitString.FillRow(Object obj, SqlInt32& IdVal)


Is it therefore proper practice to put this type of parameter checking into FillRow()? The same error also occurs if I try an alternative approach...


If delimiter.Value.ToString.Trim = String.Empty Then
Throw New ArgumentException...
Go to Top of Page

theboyholty
Posting Yak Master

226 Posts

Posted - 2012-02-03 : 11:16:49
Forgive me for getting back to basics here but why on earth are you using a CLR function and VB.bnet to do something as simple as split a comma separated list into a table?

Try this SQL code below and save yourself a world of pain.

CREATE FUNCTION [dbo].[fnSplitStringList] (@StringList VARCHAR(MAX))
RETURNS @TableList TABLE( StringLiteral VARCHAR(128))
BEGIN
DECLARE @StartPointer INT, @EndPointer INT
SELECT @StartPointer = 1, @EndPointer = CHARINDEX(',', @StringList)
WHILE (@StartPointer < LEN(@StringList) + 1)
BEGIN
IF @EndPointer = 0
SET @EndPointer = LEN(@StringList) + 1
INSERT INTO @TableList (StringLiteral)
VALUES(LTRIM(RTRIM(SUBSTRING(@StringList, @StartPointer,
@EndPointer - @StartPointer))))
SET @StartPointer = @EndPointer + 1
SET @EndPointer = CHARINDEX(',', @StringList, @StartPointer)
END -- WHILE
RETURN
END -- FUNCTION

SELECT * FROM [dbo].[fnSplitStringList]('Andrew,Brian,Charles')


---------------------------------------------------------------------------------
http://www.mannyroadend.co.uk A Bury FC supporters website and forum
Go to Top of Page

Transact Charlie
Master Smack Fu Yak Hacker

3451 Posts

Posted - 2012-02-03 : 11:27:21
quote:

Forgive me for getting back to basics here but why on earth are you using a CLR function and VB.bnet to do something as simple as split a comma separated list into a table?
...


OK but, Gah.... you ruined your point by posting an incredibly slow splitter.

The CLR is the quickest way to do it. You *can* use a tally table approach with native tsql to get a fairly fast one.
http://www.sqlservercentral.com/articles/Tally+Table/72993/

If you are going to use a CLR for anything. This is one of the best things to use it for.


Charlie
===============================================================
Msg 3903, Level 16, State 1, Line 1736
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION
Go to Top of Page

R
Constraint Violating Yak Guru

328 Posts

Posted - 2012-02-03 : 11:39:40
[code]Try this SQL code below and save yourself a world of pain.[/code]

Waiting for SQL to split a 10,000 comma list is pain! The CLR function does it in a few hundredths of a second. Here is the proof that CLR beats SQL hands down... http://sqlblog.com/blogs/aaron_bertrand/archive/2010/07/07/splitting-a-list-of-integers-another-roundup.aspx

Go to Top of Page

sunitabeck
Master Smack Fu Yak Hacker

5155 Posts

Posted - 2012-02-03 : 12:58:32
quote:
Strangely, even when a blank delimiter is passed, the exception I added never gets thrown, and FillRow() executes. So if a blank delimiter is passed to the function, the error I actually witness is:

If the SQL function definition you are using is still the same (with the separator as constant length = 1), the VB code always gets a char array of length 1. If you change it to nvarchar, then an empty separator or separator with more than one character should hit the exception (I hope)
CREATE FUNCTION dbo.myfn_ClrSplitter
(
@Ids nvarchar(max),
@separator char(1) -- Change this to nvarchar(25) to see the exception.
)
Go to Top of Page

theboyholty
Posting Yak Master

226 Posts

Posted - 2012-02-08 : 09:59:52
quote:
Originally posted by R

Try this SQL code below and save yourself a world of pain.

Waiting for SQL to split a 10,000 comma list is pain! The CLR function does it in a few hundredths of a second. Here is the proof that CLR beats SQL hands down... http://sqlblog.com/blogs/aaron_bertrand/archive/2010/07/07/splitting-a-list-of-integers-another-roundup.aspx



That's a fascinating article. If awards were handed out for services to the SQL community, Aaron would get one for a study like that. We live and learn.

---------------------------------------------------------------------------------
http://www.mannyroadend.co.uk A Bury FC supporters website and forum
Go to Top of Page

Transact Charlie
Master Smack Fu Yak Hacker

3451 Posts

Posted - 2012-02-08 : 10:19:46
quote:
Originally posted by theboyholty

quote:
Originally posted by R

Try this SQL code below and save yourself a world of pain.

Waiting for SQL to split a 10,000 comma list is pain! The CLR function does it in a few hundredths of a second. Here is the proof that CLR beats SQL hands down... http://sqlblog.com/blogs/aaron_bertrand/archive/2010/07/07/splitting-a-list-of-integers-another-roundup.aspx



That's a fascinating article. If awards were handed out for services to the SQL community, Aaron would get one for a study like that. We live and learn.

---------------------------------------------------------------------------------
http://www.mannyroadend.co.uk A Bury FC supporters website and forum


Yeah. ++


Charlie
===============================================================
Msg 3903, Level 16, State 1, Line 1736
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION
Go to Top of Page
    Next Page

- Advertisement -