I'm trying to use sql:variable to read data from a XML file. My problem is, I can read the first or n^th line (or node) of the XML, however I can't make iteration in the lines (or nodes). Here is where I used sql:variable:
CAST((SELECT #xmlExpenseItems.value(N'(/ExpenseItem//ExpenseID/node())[sql:variable("#iteratorVarChar")]','int')) AS int),
CAST((SELECT #xmlExpenseItems.value(N'(/ExpenseItem//ExpenseTypeID/node())[sql:variable("#iteratorVarChar")]','int')) AS int),
CAST((SELECT #xmlExpenseItems.value(N'(/ExpenseItem//ExpenseAmount/node())[sql:variable("#iteratorVarChar")]','float')) AS float)
where #iteratorVarChar is a varchar casted from an int.
I get the error "XQuery [value()]: Only 'http://www.w3.org/2001/XMLSchema#decimal?', 'http://www.w3.org/2001/XMLSchema#boolean?' or 'node()*' expressions allowed as predicates, found 'xs:string ?'" at the first line of the code above.
When I switched #iteratorVarChar with #iterator which is already an int, I get "XQuery [value()]: 'value()' requires a singleton (or empty sequence), found operand of type 'xdt:untypedAtomic *'"
As I said, when I replace sql:variable("#iteratorVarChar") with an int, like 1, then the code works with the first node of the xml.
My question is, am I missing something or am I making a fundamental mistake? How to make this work?
My whole code is below (I commented out the CREATE in order to avoid the recreation errors):
DECLARE #xmlExpenseItems XML
SET #xmlExpenseItems = '
<ExpenseItem>
<ExpenseID>5</ExpenseID>
<ExpenseTypeID>5</ExpenseTypeID>
<ExpenseAmount>5</ExpenseAmount>
</ExpenseItem>
<ExpenseItem>
<ExpenseID>3</ExpenseID>
<ExpenseTypeID>5</ExpenseTypeID>
<ExpenseAmount>7</ExpenseAmount>
</ExpenseItem>
'
--CREATE TABLE #ExpenseItems
--(ExpenseItemID int not null identity(1,1),
--ExpenseID int not null,
--ExpenseTypeID int not null,
--ExpenseAmount float not null
--)
DECLARE #iterator int = 1
DECLARE #IDCount int
SELECT #IDCount = (SELECT #xmlExpenseItems.value('count(/ExpenseItem)', 'int') )
DECLARE #iteratorVarChar varchar(3)
WHILE #iterator <= #IDCount
BEGIN
SET #iteratorVarChar = CAST(#iterator AS varchar(3))
INSERT INTO #ExpenseItems
(ExpenseID, ExpenseTypeID, ExpenseAmount)
VALUES
(
CAST((SELECT #xmlExpenseItems.value(N'(/ExpenseItem//ExpenseID/node())[sql:variable("#iteratorVarChar")]','int')) AS int),
CAST((SELECT #xmlExpenseItems.value(N'(/ExpenseItem//ExpenseTypeID/node())[sql:variable("#iteratorVarChar")]','int')) AS int),
CAST((SELECT #xmlExpenseItems.value(N'(/ExpenseItem//ExpenseAmount/node())[sql:variable("#iteratorVarChar")]','float')) AS float)
)
SET #iterator = #iterator + 1
END
select * from #ExpenseItems
Try a set based approach instead of iteration. The nodes() function returns a rowset from an XML document:
insert #ExpenseItems
(ExpenseID, ExpenseTypeID, ExpenseAmount)
select col.value('ExpenseID[1]', 'int')
, col.value('ExpenseTypeID[1]', 'int')
, col.value('ExpenseAmount[1]', 'int')
from #xmlExpenseItems.nodes('/ExpenseItem') doc(col)
Related
I want to remove from my value / string this character " but I can not remove it using my UPDATE statement here:
UPDATE dbo].[Tablename]
SET [columnname] = REPLACE([columnname], '"', '')
Do you have any idea how to remove this character?
Thank you for opinions
You could simply use the ASCII character codes to replace the wildcard characters. There might be other good solution as well. It worked for me!
UPDATE dbo].[Tablename] SET [columnname] = REPLACE([columnname], char(47), '')
Char(47) is the ASCII code for the forward slash. You can find the full list here.
So first make 100% sure of the character you are trying to eliminate.
Just because it 'looks' like a double quote, doesn't mean that it is.
Alter the commented line in the code below, to select a single record from your dataset.
It will spit out the charcode for each character in the source string.
(obv change table and field names also !)
declare
#sample nvarchar(max),
#char nvarchar(1),
#i_idx int,
#temp int
create table #RtnValue(
Id int identity(1,1),
A nvarchar(max),
[uni] int,
[ascii] int
)
-- set #sample to a single value from your dataset here
select #sample = (select top (1) [sample] from test.test)
While len(#sample) > 0
Begin
Set #char = left(#sample,1)
Insert Into #RtnValue (A, uni, [ascii])
Select A = #char, UNI = UNICODE(#char), [ASCII] = ASCII(#char)
Set #sample = RIGHT(#sample,len(#sample)-1)
End
Select * from #RtnValue
It is asked many times, but not this way.
I am on SQL Server 2008, and there is no STRING_SPLIT function (like in 2016).
A query returns with the following row, see below a single example row. What you see below in bald is a single field actually, so one varchar column has it altogether:
Appple|10|admin|845687|Apr|26|11:32:29|2016|AwesomeApplication.zip
which I'd like to be split by the pipe | character.
I cannot write a CTE for this, or a custom function.
I have to extract the individual pipe delimited elements, into different columns, within one select statement using the built in string functions like CHARINDEX, PATINDEX.
Does anybody have any idea?
DECLARE #Result Table(Value varchar(50))
DECLARE #x XML
SELECT #X = CAST('<A>' + REPLACE(#StringList, '|', '</A><A>') + '</A>' AS XML)
INSERT INTO #Result
SELECT t.value('.', 'varchar(50)') as inVal
FROM #X.nodes('/A') AS x(t)
This will create a table with one column (Value). Each split value from your pipe-delimited string will create a new record in this table. Then you can join to it however you'd like. Please let me know if this is unclear or if it doesn't work on SQL 2008.
You can increase the size of the varchar, if needed - and you can modify the query to split on different values (comma-delimited, etc.).
ALTER FUNCTION [dbo].[split]
(
#string varchar(max),
#separator varchar(1) -- longer separator is also possible
)
RETURNS
#result TABLE (keyword varchar(max) )
AS
BEGIN
declare #pos int=0, #res varchar(100)
while len(#string)>0
begin
set #pos=CHARINDEX(#separator, #string+#separator,0)
select #res=left(#string,#pos),#string=SUBSTRING(#string,#pos+1,100)
insert #result values(#res)
end
RETURN
END
It's been a long while since this question was asked, and although the OP wanted a non-function solution, this is the only post where the answer pointed me in the right direction for me to write my code, so I thought I'd share my solution here.
What it does
This code, checks the master db compatibility mode and creates an appropriate _my_string_split function. If string_split exists, this is just a wrapper.
If not it will use the method Stan proposed in the accepted answer
So now, regardless of where I'm running my code, after creating the function, all I need to do is use master.dbo._my_string_split, for example:
SELECT * FROM master.dbo._my_string_split('Hello|World!','|')
The Code
USE master
IF OBJECT_ID('dbo._my_string_split') IS NOT NULL DROP FUNCTION [dbo].[_my_string_split]
GO
DECLARE #sqlCode NVARCHAR(MAX);
IF 130 <= (SELECT compatibility_level FROM sys.databases WHERE name = DB_NAME())
SET #sqlCode = '
--- 130+ string_split exists
CREATE FUNCTION [dbo].[_my_string_split]
(
#string nvarchar(max),
#separator nvarchar(1)
)
RETURNS
#result TABLE ([Value] nvarchar(max) )
BEGIN
INSERT INTO #result
SELECT [Value] FROM string_split(#string,#separator)
RETURN
END
'
ELSE
SET #sqlCode = '
--- before 130: string_split does not exists
CREATE FUNCTION [dbo].[_my_string_split]
(
#string nvarchar(max),
#separator nvarchar(1)
)
RETURNS
#result TABLE ([Value] nvarchar(max) )
BEGIN
INSERT INTO #result
SELECT [Value]=t.value(''.'', ''varchar(max)'')
FROM (SELECT x=(CAST((''<A>'' + REPLACE(#string, #separator, ''</A><A>'') + ''</A>'') AS XML))) a
CROSS APPLY a.x.nodes(''/A'') AS x(t)
RETURN
END
'
PRINT #sqlCode
EXEC sp_sqlexec #sqlCode
Hope others will find this useful.
If I do
Declare #t table(Email xml)
Declare #email varchar(100) = 'xxx&xx#monop.com'
Insert into #t
select '<Emails> <Email>' + #email +'</Email></Emails>'
select * From #t
I will get expected error
Msg 9411, Level 16, State 1, Line 8
XML parsing: line 1, character 27, semicolon expected
One solution which I found almost everywhere(including SO) is to replace '&' with '& and it works
Insert into #t
select CAST('<Emails><Email>' + REPLACE(#email, '&', '&') + '</Email></Emails>' AS XML)
Output
<Emails><Email>xxx&xx#monop.com</Email></Emails>
However, I was trying with CData approach (just another way to approach the problem)
Declare #t table(Email xml)
Declare #email varchar(100) = 'xxx&xx#monop.com'
Insert into #t
Select CAST('<![CDATA[Emails> <Email>' + #email + '</Email> </Emails]]>' AS XML)
select * From #t
When I got the below output
Emails> <Email>xxx&xx#monop.com</Email> </Emails
What I am trying to achieve is to store the data as it is i.e. the desired output should be
<Emails><Email>xxx&xx#monop.com</Email></Emails>
Is it at all possible?
I know that the replace function will fail if any other special character that xml fails to understand will be passed as an input to it e.g. '<' i which case again we need to replace it...
Thanks
Tags are PCDATA, not CDATA, so don't put them in the CDATA section.
When you work with XML you should use XML-related features of SQL Server.
For example:
/* Create xml and add a variable to it */
DECLARE
#xml xml = '<Emails />',
#email varchar(100) = 'xxx&xx#monop.com';
SET #xml.modify ('insert (
element Email {sql:variable("#email")}
) into (/Emails)[1]');
SELECT #xml;
/* Output:
<Emails>
<Email>xxx&xx#monop.com</Email>
</Emails>
*/
/* Extract value from xml */
DECLARE #email_out varchar(200);
SET #email_out = #xml.value ('(/Emails/Email)[1]', 'varchar (200)');
SELECT #email_out; /* Returns xxx&xx#monop.com */
Good luck
Roman
I am having a small problem with the IN SQL statement. I was just wondering if anyone could help me?
#Ids = "1,2,3,4,5"
SELECT * FROM Nav WHERE CONVERT(VARCHAR,NavigationID) IN (CONVERT(VARCHAR,#Ids))
This is coming back with the error below, I am sure this is pretty simple!
Conversion failed when converting the varchar value '1,' to data type int.
The SQL IN clause does not accept a single variable to represent a list of values -- no database does, without using dynamic SQL. Otherwise, you could use a Table Valued Function (SQL Server 2000+) to pull the values out of the list & return them as a table that you can join against.
Dynamic SQL example:
EXEC('SELECT *
FROM Nav
WHERE NavigationID IN ('+ #Ids +')')
I recommend reading The curse and blessings of dynamic SQL before using dynamic SQL on SQL Server.
Jason:
First create a function like this
Create FUNCTION [dbo].[ftDelimitedAsTable](#dlm char, #string varchar(8000))
RETURNS
--------------------------------------------------------------------------*/
/*------------------------------------------------------------------------
declare #dlm char, #string varchar(1000)
set #dlm=','; set #string='t1,t2,t3';
-- tHIS FUNCION RETUNRS IN THE ASCENDING ORDER
-- 19TH Apr 06
------------------------------------------------------------------------*/
--declare
#table_var TABLE
(id int identity(1,1),
r varchar(1000)
)
AS
BEGIN
declare #n int,#i int
set #n=dbo.fnCountChars(#dlm,#string)+1
SET #I =1
while #I <= #N
begin
insert #table_var
select dbo.fsDelimitedString(#dlm,#string,#i)
set #I= #I+1
end
if #n =1 insert #TABLE_VAR VALUES(#STRING)
delete from #table_var where r=''
return
END
And then
set quoted_identifier off
declare #ids varchar(max)
select #Ids = "1,2,3,4,5"
declare #nav table ( navigationid int identity(1,1),theother bigint)
insert #nav(theother) select 10 union select 11 union select 15
SELECT * FROM #Nav WHERE CONVERT(VARCHAR,NavigationID) IN (select id from dbo.ftDelimitedAsTable(',',#Ids))
select * from dbo.ftDelimitedAsTable(',',#Ids)
What you're doing is not possible with the SQL IN statement. You cannot pass a string to it and expect that string to be parsed. IN is for specific, hard-coded values.
There are two ways to do what you want to do here.
One is to create a 'dynamic sql' query and execute it, after substituting in your IN list.
DECLARE #query varchar(max);
SET #query = 'SELECT * FROM Nav WHERE CONVERT(VARCHAR,NavigationID) IN (' + #Ids + ')'
exec (#query)
This can have performance impacts and other complications. Generally I'd try to avoid it.
The other method is to use a User Defined Function (UDF) to split the string into its component parts and then query against that.
There's a post detailing how to create that function here
Once the function exists, it's trivial to join onto it
SELECT * FROM Nav
CROSS APPLY dbo.StringSplit(#Ids) a
WHERE a.s = CONVERT(varchar, Nav.NavigationId)
NB- the 'a.s' field reference is based on the linked function, which stores the split value in a column named 's'. This may differ based on the implementation of your string split function
This is nice because it uses a set based approach to the query rather than an IN subquery, but a CROSS JOIN may be a little complex for the moment, so if you want to maintain the IN syntax then the following should work:
SELECT * FROM Nav
WHERE Nav.NavigationId IN
(SELECT CONVERT(int, a.s) AS Value
FROM dbo.StringSplit(#Ids) a
Does T-SQL allow a variable number of arguments to a stored procedure like params in C#?
EDIT: I'm using SQL Server 2005. That 2008 answer makes me wish we were using it...
In SQL 2008 there's Table-Valued Parameters (TVPs)
Your stored proc can accept lists of parameters..
Finally we're able to do a IN clause without relying on XML!
Mike
No, not for things like UDFs or stored procedures. That's what tables are for. Put the values in a table somewhere (with a common key) and pass the correct key to your procedure.
Typically
CREATE PROCEDURE dbo.sptest
( #xml TEXT )
AS
BEGIN
DECLARE #flag1 INT
DECLARE #flag2 VARCHAR(50)
DECLARE #flag3 DATETIME
DECLARE #idoc INT
exec sp_xml_preparedocument #idoc OUTPUT, #xml
SELECT #flag1 = firstparam, flag2 = secondparam, flag3 = thirdparam
FROM OPENXML(#idoc, '/root', 2) WITH
( firstparam INT, secondparam VARCHAR(50), thirdparam DATETIME) as x
END
exec sptest '<root><firstparam>5</firstparam><secondparam>Joes Bar</secondparam><thirdparam>12/30/2010</thirdparam></root>'
Extend as necessary
Another approach I've seen to passing in params or arrays is to pass in an XML string, dump that to a temporary table/table variable and work with it from that point. Not the easiest when you want to manually run a stored procedure, but it works as a work around to the lack of array/dynamic param support.
I've used a little function to separate a CSV string into a table
That way I could go
SELECT col1, col2
FROM myTable
WHERE myTable.ID IN (SELECT ID FROM dbo.SplitIDs('1,2,3,4,5...'))
My function is below:
CREATE FUNCTION [dbo].[SplitIDs]
(
#IDList varchar(500)
)
RETURNS
#ParsedList table
(
ID int
)
AS
BEGIN
DECLARE #ID varchar(10), #Pos int
SET #IDList = LTRIM(RTRIM(#IDList))+ ','
SET #Pos = CHARINDEX(',', #IDList, 1)
IF REPLACE(#IDList, ',', '') <> ''
BEGIN
WHILE #Pos > 0
BEGIN
SET #ID = LTRIM(RTRIM(LEFT(#IDList, #Pos - 1)))
IF #ID <> ''
BEGIN
INSERT INTO #ParsedList (ID)
VALUES (CAST(#ID AS int)) --Use Appropriate conversion
END
SET #IDList = RIGHT(#IDList, LEN(#IDList) - #Pos)
SET #Pos = CHARINDEX(',', #IDList, 1)
END
END
RETURN
END
I'm sure there are better ways to implement this, this is one way I found online and it works well for what I'm doing. If there are some improvement that can be made please comment.