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.
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.ServerImports System.Data.SqlTypesPublic Class CLRSplit Implements IEnumerator ...End ClassPublic 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 SubEnd 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 GOSuccessCREATE ASSEMBLY mySplitterFROM 'D:\mySplitter.dll'WITH PERMISSION_SET = SAFEGOSuccessCREATE FUNCTION dbo.myfn_ClrSplitter ( @Ids nvarchar(max), @separator char(1) )RETURNS TABLE ( IdVal int NOT NULL )AS EXTERNAL NAME mySplitter.DoSplitMsg 102, Level 15, State 1, Procedure myfn_ClrSplitter, Line 11Incorrect 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 |
 |
|
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 2Could not find Type 'SplitString' in assembly 'mySplitter'. |
 |
|
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? |
 |
|
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? |
 |
|
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? |
 |
|
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 INTAS 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 FunctionEnd 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 |
 |
|
R
Constraint Violating Yak Guru
328 Posts |
Posted - 2012-02-02 : 07:56:47
|
HiThanks 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...! |
 |
|
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. |
 |
|
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 1CREATE 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.DataImports Microsoft.SqlServer.ServerImports System.Data.SqlTypesImports System.Data.SqlClientImports System.CollectionsImports System.Runtime.InteropServicesPublic 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 SubEnd Class |
 |
|
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 |
 |
|
R
Constraint Violating Yak Guru
328 Posts |
Posted - 2012-02-03 : 01:35:54
|
Hi sunitabeckThanks 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. |
 |
|
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 IntegerintVal = objIdVal = intVal I have not tested either approach, so if my theory is not correct, I would not be surprised. |
 |
|
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 1736The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION |
 |
|
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 1An 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... |
 |
|
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 RETURNEND -- FUNCTIONSELECT * FROM [dbo].[fnSplitStringList]('Andrew,Brian,Charles') ---------------------------------------------------------------------------------http://www.mannyroadend.co.uk A Bury FC supporters website and forum |
 |
|
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 1736The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION |
 |
|
R
Constraint Violating Yak Guru
328 Posts |
|
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. ) |
 |
|
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 |
 |
|
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 1736The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION |
 |
|
Next Page
|
|
|
|
|