Unexpected error when using "cross apply openjson" with a join - sql

If cross apply is used for a function, but the query also uses a join, how does SQL determine which records to use for the function?
I have a table which contains some JSON strings, and I'm using OPENJSON to parse them. I'm also using a join to determine which records to pass to the OPENJSON function. However, it is erroring on records that aren't in the selection
eg in this example, I am adding two json strings to a table, one of which is good, and the other is truncated. I'm then trying to parse the good row using openJSON, but it is failing on the bad row, even though it isn't being selected...
--create table to store JSON strings
declare #jsontable table (id int identity,
JSONstring nvarchar(2000))
--add one good row, and one bad row
INSERT into #jsontable (JSONstring)
values
('{"RowNumber":1,"Field1":"Hello","Field2":"World"}'), --First row good
('{"RowNumber":2,"Field1":"Hello"') --Second row truncated
--create table with ID of the good record
DECLARE #IDsToSelect table (id int)
INSERT into #IDsToSelect (id)
values
(1)
--parse the json in the good record
select * from #jsontable jst
inner join #IDsToSelect its
on jst.id=its.id
cross apply openjson(jst.JSONstring)
The above example gives a 'JSON text is not properly formatted' error, and it fails because of the bad JSON in row 2, even though I'm only joining to row 1.
Does SQL apply the openjson function to the entire #jsontable table, and then apply the inner join to those results? If so, how should I structure this query so that it only parses the JSON of the selected row?

Related

Convert List Of XML Tags in varchar column to comma separated list

I have a table that contains a list of xml tags/values that I need to use to join to another table to retrieve their actual value and display the result as a csv list.
Example varchar data:
<choice id="100"/><choice id="101"/><choice id="102"/>
However, these values actually translate to other values: red, white, blue respectively. I need to convert that list to the following list:
red,white,blue
As a recap, the "source" table column is varchar, and contains a list of xml attribute values, and those values translate to other values by joining to another table. So the other table has a primary key of id (int) with rows for 100,101,102. Each of those rows has values red,white,blue respectively. I hope this makes enough sense.
Here is the ddl to set up the scenario:
create table datatable(
id int,
data nvarchar(449)
primary key (id)
);
insert into datatable(id, data)
values(1,'<choice id="100"/><choice id="101"/><choice id="102"/>')
,(2,'<choice id="100"/>')
,(3,'<choice id="101"/>')
,(4,'<choice id="102"/>');
create table choicetable(
id int,
choicevalue nvarchar(449)
primary key (id)
);
insert into choicetable(id, choicevalue)
values(100,'red')
,(101,'white')
,(102,'blue');
This would be the first time I've tried parsing XML in this manner so I'm a little stumped where to start. Also, I do not have control over the database I am retrieving the data from (3rd party software).
Without proper sample data it's hard to give an exact query. But you would do something like this
Use CROSS APPLY to convert the varchar to xml
Use .nodes to shred the XML into separate rows.
Join using .value to get the id attribute
Group up, and concatenate using STRING_AGG. You may not need GROUP BY depending on your situation.
SELECT
xt.Id,
STRING_AGG(ot.Value, ',')
FROM XmlTable xt
CROSS APPLY (SELECT CAST(xt.XmlColumn AS xml) ) v(XmlData)
CROSS APPLY v.XmlData.nodes('/choice') x1(choice)
JOIN OtherTable ot ON ot.Id = x1.choice.value('#id','int')
GROUP BY
xt.Id;
I would advise you to store XML data in an xml typed column if at all possible.

JSON_QUERY with Column values

One of my tables contains JSON values in each row of a column.
The data is as below (example. one row)
[{"id":"30a66bec-c0aa-4655-a8ef-506e52bfcc14","type":"nps","value":"promoter","decimalValue":"10"},{"id":"37850b3b-1eac-4921-ae22-b2f6d2450897","type":"sentiment","value":"positive","decimalValue":"0.990000009536743"}]
Now I'm trying to retrieve two columns from it. (id, value)
I'm writing the below query using JSON_VALUE but getting NULL values in each row of the new column.
select a.jsondata,JSON_VALUE(a.jsondata,'$.id') from table as a
Your JSON field is an array so you need to specify which element you're after, assuming its always the first you can use:
select a.jsondata,JSON_VALUE(a.jsondata,'$[0].id') from table as a
You need to change the index inside the square brackets to access the id you want from the JSON string
You have a JSON array. If you want to break it into multiple rows you need to use OPENJSON
SELECT j.*
FROM YourTable t
CROSS APPLY OPENJSON (t.Json)
WITH (
id uniqueidentifier,
value varchar(100)
) j;
db<>fiddle

User-Defined Table Type parameter

Code
CREATE TYPE [dbo].[IntListType] AS TABLE([Number] [int] NULL)
DECLARE #ids as IntListType;
DECLARE #TempIds as IntListType;
Situation
I have a long long list of products which need to be filtered in a stored procedure. The filter is given as a User-Defined Table Type and each filter can be empty or full. I've attempted to create a single query for this, but it won't work if one of the filters is empty.
So I decided to create another User-Defined Table Type which only contains the column: Number.
Then, for each Filter type it will check wether it's empty
IF (select filterName from #customtable) is not null
and if so, it will query the product Id's which match that name and add it to the list of numbers.
If the next filter isn't emmpty, it will add the id's to a temp table, which in turn will be combined with the first table.
So far, I found this to be the most effective way to handle whether a filter is null or not. In any way it will get the id's I need!
Now, every filter will eventually perfrom the following code:
IF (select count(*) from #ids) > 0
BEGIN
IF(select count(*) from #TempIds)
BEGIN
delete from #ids
where Number not in
(select C.Number
from #ids C
inner join #TempIds T on C.Number = T.Number)
END
END
ELSE
BEGIN
insert into #ids
select * from #TempIds
END
delete from #TempIds
Problem
I'm looking for a way to implement the last named code into a stored procedure which gives me back a list of IntListType. However, every implementation I have of #ids (declare #ids as IntListType) can't be set. (asinset #ids = #TempIds' > Will complain about 'Must declare the scalar variable #ids' aswell as '#TempIds')
How can I fix this?
Attempts
I've already attempted to skip this all by combining every table with inner join, outer join and other joins.. but in no way will I get the same result when any (or all) of the filters is empty.
As far as I can see, this is the only way to effectively progress every filter type, check if they are null or not, if not; process the results etc etc
In the part of the query you shared, there is no declaration of #ids or #TempIds.
Can you add that part?
Apart from that, I think you should not be looking at a User-Defined Type: Can T-SQL function return user-defined table type?
Have a look at User-Defined Functions:
http://www.sqlteam.com/article/user-defined-functions
http://blog.sqlauthority.com/2007/05/29/sql-server-user-defined-functions-udf-limitations/
tsql returning a table from a function or store procedure

How to use openXML() for Complex XMLs in SQL Server 2005

I have the following complex XML
<Collection>
<VOUCHER>
<DATE TYPE="Date">20110401</DATE>
<NARRATION TYPE="String">MUNNA CONVENT ROAD</NARRATION>
<VOUCHERTYPENAME>RETAIL</VOUCHERTYPENAME>
<VOUCHERNUMBER>R-2-I2-9-6-27751</VOUCHERNUMBER>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>U.S.T. CANTEEN</LEDGERNAME>
<AMOUNT>-2678.9985</AMOUNT>
</ALLLEDGERENTRIES.LIST>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>U.S.T. CANTEEN</LEDGERNAME>
<AMOUNT>-2678.9985</AMOUNT>
</ALLLEDGERENTRIES.LIST>
</VOUCHER>
<VOUCHER>
<DATE TYPE="Date">20110401</DATE>
<NARRATION TYPE="String">MUNNA CONVENT ROAD</NARRATION>
<VOUCHERTYPENAME>RETAIL</VOUCHERTYPENAME>
<VOUCHERNUMBER>R-2-I2-9-6-27751</VOUCHERNUMBER>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>U.S.T. CANTEEN</LEDGERNAME>
<AMOUNT>-2678.9985</AMOUNT>
</ALLLEDGERENTRIES.LIST>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>U.S.T. CANTEEN</LEDGERNAME>
<AMOUNT>-2678.9985</AMOUNT>
</ALLLEDGERENTRIES.LIST>
</VOUCHER>
</Collection>
I'm saving voucher details in 1 table, ALLLEDGERENTRIES.LIST details in another table.
Both tables have relation on VoucherID. For a particular VoucherID the related x3 values should be stored. In my stored procedure I'm using openxml().
Piece of my SP:
INSERT INTO SalesVoucher(AbsID,VoucherNumber,VoucherTypeName,Narration,VoucherDate)
SELECT #AID,VOUCHERNUMBER,VOUCHERTYPENAME,NARRATION,CAST(DATE AS DATETIME)
FROM OPENXML(#XMLHandle,'ENVELOPE/BODY/DATA/COLLECTION/VOUCHER',3)
WITH (
VOUCHERNUMBER nVarchar(200),VOUCHERTYPENAME varchar(100),NARRATION varchar(500),DATE DATETIME
)
SELECT #VID=##IDENTITY
INSERT INTO SalesLedger(VoucherID,LedgerName,Amount)
SELECT #VID,LEDGERNAME,AMOUNT
FROM OPENXML(#XMLHandle,'ENVELOPE/BODY/DATA/COLLECTION/VOUCHER/ALLLEDGERENTRIES.LIST',3)
WITH(
LEDGERNAME varchar(200),AMOUNT decimal(18,0)
)
All values are storing in DB but the column VoucherID in SalesLedger table is same for all the rows (it should not..) as I used ##IDENTITY it is returning last identity value only.
Please someone help me how to store related voucherID in SalesLedger table using openxml() in sql...
I would probably use the native XQuery capabilities of SQL Server to do this. First, grab the items you need for your SalesVoucher table and insert those.
When you come to insert the details, your "parent" info is already stored in the SalesVoucher table - so go grab the necessary info from there.
Your code would be something like this (assuming your XML data is in a SQL variable called #input of type XML):
-- Insert the "parent" info into SalesVoucher
INSERT INTO dbo.SalesVoucher(VoucherNumber, VoucherTypeName, Narration, VoucherDate)
SELECT
v.value('(VOUCHERNUMBER)[1]', 'NVARCHAR(200)'),
v.value('(VOUCHERTYPENAME)[1]', 'VARCHAR(100)'),
v.value('(NARRATION)[1]', 'VARCHAR(500)'),
v.value('(DATE)[1]', 'DATETIME')
FROM
#input.nodes('/Collection/VOUCHER') AS Coll(V)
This inserts the basic info in your SalesVoucher table.
When you want to parse the details, you need to make a reference back to the VoucherNumber of the parent - with that info, you can retrieve the AbsID from SalesVoucher and insert the appropriate value into SalesLedger:
INSERT INTO #SalesLedger (VoucherID, LedgerName, Amount)
SELECT
sv.AbsID,
AL.LS.value('(LEDGERNAME)[1]', 'VARCHAR(200)'),
AL.LS.value('(AMOUNT)[1]', 'DECIMAL(18,4)')
FROM
#input.nodes('/Collection/VOUCHER') AS Coll(V)
INNER JOIN
dbo.SalesVoucher sv
ON sv.VoucherNumber = v.value('(VOUCHERNUMBER)[1]', 'NVARCHAR(200)')
CROSS APPLY
Coll.V.nodes('.//ALLLEDGERENTRIES.LIST') AS AL(LS)
The CROSS APPLY gets the details for that one particular node, and thus "connects" the details to the "parent" info for the VoucherNumber in the XML above.
As as PS: a datatype of DECIMAL(18,0) is not suitable for values like -2678.9985. DECIMAL(18,0) will store a maximum of 18 digits, but 0 of which after the decimal point - so this value would be stored as -2679. I've changed this to a more useful datatype of DECIMAL(18,4) - 18 digits max, 4 of which after the decimal point.

SQL: I need to take two fields I get as a result of a SELECT COUNT statement and populate a temp table with them

So I have a table which has a bunch of information and a bunch of records. But there will be one field in particular I care about, in this case #BegAttField# where only a subset of records have it populated. Many of them have the same value as one another as well.
What I need to do is get a count (minus 1) of all duplicates, then populate the first record in the bunch with that count value in a new field. I have another field I call BegProd that will match #BegAttField# for each "first" record.
I'm just stuck as to how to make this happen. I may have been on the right path, but who knows. The SELECT statement gets me two fields and as many records as their are unique #BegAttField#'s. But once I have them, I haven't been able to work with them.
Here's my whole set of code, trying to use a temporary table and SELECT INTO to try and populate it. (Note: the fields with # around the names are variables for this 3rd party app)
CREATE TABLE #temp (AttCount int, BegProd varchar(255))
SELECT COUNT(d.[#BegAttField#])-1 AS AttCount, d.[#BegAttField#] AS BegProd
INTO [#temp] FROM [Document] d
WHERE d.[#BegAttField#] IS NOT NULL GROUP BY [#BegAttField#]
UPDATE [Document] d SET d.[#NumAttach#] =
SELECT t.[AttCount] FROM [#temp] t INNER JOIN [Document] d1
WHERE t.[BegProd] = d1.[#BegAttField#]
DROP TABLE #temp
Unfortunately I'm running this script through a 3rd party database application that uses SQL as its back-end. So the errors I get are simply: "There is already an object named '#temp' in the database. Incorrect syntax near the keyword 'WHERE'. "
Comment out the CREATE TABLE statement. The SELECT INTO creates that #temp table.