JSON in SQL Server - json

I have JSON values stored in a column in a SQL Server database:
'[{"attribute":"Name","Age":50,"sort":true,"visible":true},
{"attribute":"Address","Street":"Wilson street","Country":"United states"},
{"attribute":"Work","Designation":"Developer","Experience":15}]'
We want to remove that entire work attribute and save that in the same column, we will have different no of items in that attribute, here we have only two(designation and Experience), but no of items will vary for each row.
I want to change the above JSON in below format.
'[{"attribute":"Name","Age":50,"sort":true,"visible":true},
{"attribute":"Address","Street":"Wilson street","Country":"United states"}]'`
Please suggest me the best way to do that.

If you are using the sql server 2016 or higher then you can use the OpenJson() method.
Example:
DECLARE #json NVARCHAR(MAX)
SET #json='{"Name":"Anurag","age":25,"skills":["C#","As.Net","MVC","Linq"]}';
SELECT *
FROM OPENJSON(#json);

You could try the below string manipulation to achieve your desired output -
DECLARE #info NVARCHAR(1000) = '[{"attribute":"Name","Age":50,"sort":true,"visible":true},
{"attribute":"Address","Street":"Wilson street","Country":"United states"},
{"attribute":"Work","Designation":"Developer","Experience":15}]'
SELECT
REVERSE(SUBSTRING(REVERSE(SUBSTRING(#info, 0, CHARINDEX('"attribute":"Work"',#info)-1)),CHARINDEX(',',REVERSE(SUBSTRING(#info, 0, CHARINDEX('"attribute":"Work"',#info)-1)))+1,
LEN(REVERSE(SUBSTRING(#info, 0, CHARINDEX('"attribute":"Work"',#info)-1)))))+']'
We also have JSON_MODIFY to update or remove a JSON string.

Try below approach, bit complex but do the trick. Read inline comments to understand how it works.
CREATE TABLE #temp(ID INT, JSON varchar(1000))
INSERT INTO #temp VALUES(1,'[{"attribute":"Name","Age":40,"sort":true,"visible":true},
{"attribute":"Address","Street":"Wilson street","Country":"United states"}]')
INSERT INTO #temp VALUES(2,'[{"attribute":"Name","Age":50,"sort":true,"visible":true},
{"attribute":"Address","Street":"Wilson street","Country":"United states"},
{"attribute":"Work","Designation":"Developer","Experience":15}]')
INSERT INTO #temp VALUES(3,'[{"attribute":"Name","Age":30,"sort":true,"visible":true},
{"attribute":"Work","Designation":"Developer","Experience":15},
{"attribute":"Address","Street":"New Wilson street","Country":"United states"}]')
INSERT INTO #temp VALUES(4,'[{"attribute":"Work","Designation":"Developer","Experience":15},
{"attribute":"Name","Age":30,"sort":true,"visible":true},
{"attribute":"Address","Street":"New Wilson street","Country":"United states"}]')
;WITH CTE AS
(
SELECT ID
,JSON
,SUBSTRING(JSON,0,CHARINDEX('"attribute":"Work"',JSON)-1) AS JSON_P1 -- Get string before the "Work" attribute.
,SUBSTRING(JSON,CHARINDEX('}',JSON,CHARINDEX('"attribute":"Work"',JSON))+1,LEN(JSON)) AS JSON_P2 -- Get string after the "Work" attribute.
FROM #temp
WHERE JSON LIKE '%"attribute":"Work"%'
)
SELECT ID
,JSON
-- Remove the Comma(',') character used with "Work" attribute.
,CASE WHEN CHARINDEX(',',REVERSE(JSON_P1)) = 0 -- In reverse order, When there is no Comma(',') in the first part of string.
THEN JSON_P1
WHEN CHARINDEX(',',REVERSE(JSON_P1)) < CHARINDEX('}',REVERSE(JSON_P1)) -- In reverse order, When Comma(',') appears before a closing bracket, remove it.
THEN REVERSE(STUFF(REVERSE(JSON_P1),CHARINDEX(',',REVERSE(JSON_P1)),1,''))
ELSE JSON_P1 -- non of above
END +
CASE WHEN CHARINDEX(',',REVERSE(JSON_P1)) = 0 -- Check only if no Comma(',') found in the first part of string.
AND CHARINDEX(',',JSON_P2) < CHARINDEX('{',JSON_P2) -- When Comma(',') appears before an opening bracket in second part of string, remove it.
THEN STUFF(JSON_P2,CHARINDEX(',',JSON_P2),1,'')
ELSE JSON_P2 -- non of above
END AS JSON_Final
FROM CTE

Related

OPENJSON Convert Value Column to Multiple Rows does not work

I have a JSON file with a simple structure. I try to extract data into rows out of this JSON File.
The JSON File starts with:
[{"result":
[{"country":"Germany",
"parent":"xxxx",
"city":"Reitbrook",
"latitude":"",
I tried this code, all successfully.
Look at the last 3 statement and their results.
I would Expect multiple records at the SELECT last statement.
What am I doing wrong?
DECLARE #details VARCHAR(MAX)
Select #details =BulkColumn FROM OPENROWSET
(BULK 'folder/cmn_location', DATA_SOURCE='blogstorage', SINGLE_CLOB) as JSON;
IF (ISJSON(#details) = 1)
BEGIN PRINT 'Imported JSON is Valid' END
ELSE
BEGIN PRINT 'Invalid JSON Imported' END
SELECT #details as SingleRow_Column
--delivers one row Where
--SingleRow_Column=[{"result":[{country":"Germany","parent":.....
SELECT * FROM OPENJSON(#details, '$')
--delivers one row. Where
--Key=0, value={"result":[{"country":"Germany","parent":"xxx".....
SELECT * FROM OPENJSON(#details, '$.result')
--delivers no row at all
Now error messages, but just no data
Try it like this
Hint: I had to add some closing brackets...
DECLARE #YourJSON NVARCHAR(MAX)=
N'[{"result":
[{"country":"Germany",
"parent":"xxxx",
"city":"Reitbrook",
"latitude":""}]}]';
SELECT B.*
FROM OPENJSON(#YourJson) WITH(result NVARCHAR(MAX) AS JSON) A
CROSS APPLY OPENJSON(A.result) WITH(country NVARCHAR(1000)
,parent NVARCHAR(1000)
,city NVARCHAR(1000) ) B;
The idea in short:
Your JSON is an array, containing at least one object result. (There might be more objects, but you did not show enough).
This object result is an array itself. Therefore we use the WITH in combination with AS JSON and another APPLY OPENJSON using the nested array returned as A.result.

How can i pass multiple values to an array parameter function

i need your help.....how can i pass multi values into single parameter in a function?
The values 'AAA 1','BBB 2', 'CCC 3' 'DDD 4' are to be passed to the same parameter "v_type", the values will be sent based on the selection from the drop down in the front end screen. The user can select one or more values from the list and those values should be passed to the procedure which in turn will be passed to the WHERE clause of the SELECT statement inside the procedure.
My function is somenthing like this:
Example
CREATE OR REPLACE FUNCTION FN_GET_ROWS
(v_date_ini IN DATE,
v_date_end IN DATE,
v_type IN VARCHAR2
)
RETURN TEST_TABTYPE
AS
V_Test_Tabtype Test_TabType;
BEGIN
SELECT TEST_OBJ_TYPE(DATE, NAME, ALERT)
BULK COLLECT INTO V_Test_TabType
FROM (select date, name, alert
from Table
where DATE BETWEEN v_date_ini AND v_date_end
AND Alert in (select REGEXP_SUBSTR (v_type, '[^,]+', 1, level)
from dual
connect by level <= length(regexp_replace(v_type,'[^,]*'))+1)
);
RETURN V_Test_TabType;
END;
Searching internet i found that maybe an Varray works but i dont know how to assign it to the variable :type with the parameters that the user selects on the screen.
I create this types on database, how can i used it? i'm kind a new in plsql.
CREATE TYPE alert_obj AS OBJECT (type_alert VARCHAR2(60));
CREATE TYPE alert_varray_typ AS VARRAY(100) OF alert_obj;
Thanks for your help
Emanuel.
I dont know, if I really understand your problem. But I think, that there is more solutions.
You can use string of VARCHAR2 as parameter and after that parse it with function like that:
PROCEDURE p_parse_into_array (
lv_str IN VARCHAR2,
lt_table IN OUT sys.dbms_debug_vc2coll,
lv_splitter IN VARCHAR2)
IS
ln_position NUMBER := 0;
ln_position_2 NUMBER;
ln_i NUMBER := 1;
BEGIN
ln_position_2 := INSTR(lv_str,lv_splitter,1,1);
WHILE ln_position_2 != 0
LOOP
lt_table.extend(1);
lt_table(ln_i) := SUBSTR(lv_str,ln_position+1,ln_position_2-ln_position-1);
ln_position := INSTR(lv_str,lv_splitter,1,ln_i);
ln_position_2 := INSTR(lv_str,lv_splitter,1,ln_i+1);
ln_i := ln_i + 1;
END LOOP;
END;
where lv_str is string to parse, lt_table is table of varchar(2000) and lv_splitter is character to split (, . ; - etc) and this function return values into lt_table, which you can use in you select menu.
Second solution is to use varray as you say, but there you need to use dynamic sql with command:
execute immediate 'select * from dual where some_value in (select * from table('||my_varray_table||'));
And other solution is to use nested table. It´s your choice, which of this solution you prefer :)

PostgreSQL: preventing sql injection on multiinsertion

I'm looking for the fastest way to parse, validate and insert data in table(Postgresql 9.3).
The data is an json-array which contains 1..N items.
[{"name":"a","value":"1"},{"name":"b","value":"2"}]
The table looks like:
CREATE TABLE logs
(
id serial NOT NULL,
name text ,
value text,
CONSTRAINT "log_Pkey" PRIMARY KEY (id)
);
For that i have stored procedure:
CREATE OR REPLACE FUNCTION insert_logs(v json)
RETURNS integer AS
$BODY$
DECLARE
sql text;
i json;
logs_part_id int;
BEGIN
SELECT INTO logs_part_id id from another_table_with_that_id where some_condition.
sql = '';
FOR i IN SELECT * FROM json_array_elements(v)
LOOP
sql = sql||'insert into logs_'||logs_part_id ||'
(name, value)
values( ' ||quote_literal(i->>'name')||' , ' ||quote_literal(i->>'value')||' );';
END LOOP;
raise notice '%',sql;
EXECUTE sql;
return 1;
END
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
(function returns integer as a response status)
Function call:
select * from insert_logs('[{"name":"a","value":"1"},{"name":"b","value":"2"}]'::json);
Actually the "insert.." statement is quite bigger - 15 columns to insert and aparently some of them should be checked in order to prevent sql injection.
Question:
Is there any way to rewrite this stored procedure in order to improve performance?
Should I use prepared statements?
EDIT.
The reason i build sql string because the table name is unknown because of the tables partitioning. The table name format is: logs_id where id - int which is obtained just before insert.
If you need to speed up your query, json_populate_recordset() does exactly what you need:
insert into logs
select * from json_populate_recordset(null::logs, '[...]')
As, for SQL-injection: you should always use prepared statements, or at least execute your sql with parameters sent separately (f.ex. with PQexecParams() if you use libpq directly).
Why are you building an SQL multi-statement string then EXECUTEing it at all?
Just:
insert into logs (name, value)
values( i->>name , i->>value );
There's no need for explicit quoting because i->>name is a text value that's inserted as a bound parameter into the insert by PL/PgSQL. It's never parsed as SQL.
If you must build the statement dynamically (e.g. varying table name, per comment) use EXECUTE ... USING with format:
EXECUTE format('insert into %I (name, value) values( $1, $2 );', 'logs_'||log_partition_id)
USING i->>name , i->>value;
in your case

ms sql query on count occurence of words in text column

I have a table called webqueries with a column named qQuestion of data type text(sql server 2008). I want to create a count on words used in qQuestion (excluding 'and', 'is' etc). My goal is to see how many times a person has asked a question about a specific product.
You could create a table-valued function to parse words and join it to your query against qQuestion. In your schema, I recommend using varchar(8000) or varchar(max) instead of text. Meanwhile, the following should get you started:
create function [dbo].[fnParseWords](#str varchar(max), #delimiter varchar(30)='%[^a-zA-Z0-9\_]%')
returns #result table(word varchar(max))
begin
if left(#delimiter,1)<>'%' set #delimiter='%'+#delimiter;
if right(#delimiter,1)<>'%' set #delimiter+='%';
set #str=rtrim(#str);
declare #pi int=PATINDEX(#delimiter,#str);
while #pi>0 begin
insert into #result select LEFT(#str,#pi-1) where #pi>1;
set #str=RIGHT(#str,len(#str)-#pi);
set #pi=PATINDEX(#delimiter,#str);
end
insert into #result select #str where LEN(#str)>0;
return;
end
go
select COUNT(*)
from webqueries q
cross apply dbo.fnParseWords(cast(q.qQuestion as varchar(max)),default) pw
where pw.word not in ('and','is','a','the'/* plus whatever else you need to exclude */)

SSRS multi-value parameter using a stored procedure

I am working on a SSRS report that uses a stored procedure containing a few parameters. I am having problems with two of the parameters because I want to have the option of selecting more than one item.
Here's a condensed version of what I have:
CREATE PROCEDURE [dbo].[uspMyStoredProcedure]
(#ReportProductSalesGroupID AS VARCHAR(MAX)
,#ReportProductFamilyID AS VARCHAR(MAX)
,#ReportStartDate AS DATETIME
,#ReportEndDate AS DATETIME)
--THE REST OF MY QUERY HERE WHICH PULLS ALL OF THE NEEDED COLUMNS
WHERE DateInvoicedID BETWEEN #ReportStartDate AND #ReportEndDate
AND ProductSalesGroupID IN (#ReportProductSalesGroupID)
AND ProductFamilyID IN (#ReportProductFamilyID)
When I try to just run the stored procedure I only return values if I enter only 1 value for #ReportProductSalesGroupID and 1 value #ReportProductFamilyID. If I try to enter two SalesGroupID and/or 2 ProductFamilyID it doesn't error, but I return nothing.
-- Returns data
EXEC uspMyStoredProcedure 'G23', 'NOF', '7/1/2009', '7/31/2009'
-- Doesn't return data
EXEC uspMyStoredProcedure 'G23,G22', 'NOF,ALT', '7/1/2009', '7/31/2009'
In SSRS I get an error that says:
Incorrect syntax near ','
It appears that the , separator is being included in the string instead of a delimiter
You need three things:
In the SSRS dataset properties, pass the multi-value param to the stored procedure as a comma-delimited string
=Join(Parameters!TerritoryMulti.Value, ",")
In Sql Server, you need a table-value function that can split a comma-delimited string back out into a mini table (eg see here). edit: Since SQL Server 2016 you can use the built-in function STRING_SPLIT for this
In the stored procedure, have a where clause something like this:
WHERE sometable.TerritoryID in (select Item from dbo.ufnSplit(#TerritoryMulti,','))
... where ufnSplit is your splitting function from step 2.
(Full steps and code in my blog post 'SSRS multi-value parameters with less fail'):
Let us assume that you have a multi value list #param1
Create another Internal Parameter on your SSRS report called #param2 and set the default value to:
=Join(Parameters!param1.value, 'XXX')
XXX can be any delimiter that you want, EXCEPT a comma (see below)
Then, you can pass #param2 to your query or stored procedure.
If you try to do it any other way, it will cause any string function that uses commas to separate arguments, to fail. (e.g. CHARINDEX, REPLACE).
For example Replace(#param2, ',', 'replacement') will not work. You will end up with errors like "Replace function requires 3 arguments".
Finally I was able to get a simple solution for this problem. Below I have provided all (3) steps that I followed.
I hope you guys will like it :)
Step 1 - I have created a Global Temp Table with one column.
CREATE GLOBAL TEMPORARY TABLE TEMP_PARAM_TABLE(
COL_NAME VARCHAR2(255 BYTE)
) ON COMMIT PRESERVE ROWS NOCACHE;
Step 2 - In the split Procedure, I didn't use any array or datatable, I have directly loaded the split values into my global temp table.
CREATE OR REPLACE PROCEDURE split_param(p_string IN VARCHAR2 ,p_separator IN VARCHAR2
)
IS
v_string VARCHAR2(4000);
v_initial_pos NUMBER(9) := 1;
v_position NUMBER(9) := 1;
BEGIN
v_string := p_string || p_separator;
delete from temp_param_policy;
LOOP
v_position :=
INSTR(v_string, p_separator, v_initial_pos, 1);
EXIT WHEN(NVL(v_position, 0) = 0);
INSERT INTO temp_param_table
VALUES (SUBSTR(v_string, v_initial_pos
, v_position - v_initial_pos));
v_initial_pos := v_position + 1;
END LOOP;
commit;
END split_param;
/
Step 3 - In the SSRS dataset parameters, I have used
=Join(Parameters!A_COUNTRY.Value, ",")
Step 4: In the start of your stored procedure executes the Procedure
Exec split_param(A_Country, ‘,’);
Step 5: In your stored procedure sql use the condition like below.
Where country_name in (select * from TEMP_PARAM_TABLE)
When SSRS passes the parameter it is in the form: Param1,Param2,Param3.
In the procedure, you just need to put identifiers around each parameter. And also identifiers around the value that is returned by the dataset. In my case, I used semicolons.
CREATE OR REPLACE PROCEDURE user.parameter_name (
i_multivalue_parameter
)
AS
l_multivalue_parameter varchar2(25555) := ';' || replace(i_multivalue_parameter,',',';') || ';';
BEGIN
select something
from dual
where (
instr(l_multivalue_parameter, ';' || database_value_that_is_singular || ';') > 0
)
END;
i_multivalue_parameter is passed in via SSRS.
l_multivalue_parameter reads the parameter passed in via SSRS and puts identifiers around each value.
database_value_that_is_singular is the value returned for each record.
So if 'Type1,Type2,Type3'is passed in via SSRS:
i_multivalue_parameter is: Type1,Type2,Type3
l_multivalue_parameter is: ;Type1;Type2;Type3;
database_value_that_is_singular is: ;Type1; or ;Type2; or ;Type3;
Instr will return a value over 0 if the parameter matches.
This works even if each parameters are similar. EG: "Type A" and "Type AA". That is "Type A" will not match "Type AA".
I found a simple way for my solution. Define the parameter value in the report as an expression like this
="'" + Join(Parameters!parm.Value,"','") + "'"
(in case you can't read it the first and last literals are double quote, single quote, double quote. The join literal is double quote, single quote, comma, single quote, double quote)
Then in the stored procedure you can use dynamic sql to create your statement. I did this to create a temp table of values to join to in a later query, like this:
CREATE #nametable (name nvarchar(64))
SET #sql = N'SELECT Name from realtable where name in (' + #namelist + ')'
INSERT INTO #nametable exec sp_executesql #sql
#namelist would be the name of the stored procedure parameter.