How can I create a trigger to unpack json data? - sql

This is the simplest reproducible example I can come up with. I would like to create a materialized view which is managed by triggers. This MV extracts some data from a json column, which could have an unbounded number of values in an array. Since we don't yet have json_table I am extracting these in a recursive CTE.
create or replace table base_data(
id int auto_increment not null primary key,
record_data longtext not null check (json_valid(record_data))
);
insert into base_data(record_data)
values
('{"x":1, "y":[2,3,4]}')
;
create or replace table mv_integers as
with recursive array_values as (
select
json_extract(b.record_data, '$.y[*]') val
, b.id
, json_length(json_extract(b.record_data, '$.y[*]')) n
from base_data b
), rec_elems as (
select av.id, 0 as i, json_extract(av.val, '$[0]') elem
from array_values av
where av.n > 0
union all
select av.id, e.i + 1, json_extract(av.val, concat('$[', e.i + 1, ']'))
from array_values av
inner join rec_elems e on av.id = e.id
where (i + 1) < av.n
)
select r.id record_id, r.elem extracted_integer, 'y' as 'comes_from'
from rec_elems r
union all
select b.id, json_value(b.record_data, '$.x'), 'x'
from base_data b
;
create or replace trigger maintain_mv_integers
after insert on base_data for each row
insert mv_integers(record_id, extracted_integer, comes_from)
with recursive array_values as (
select
json_extract(new.record_data, '$.y[*]') val
, new.id
, json_length(json_extract(new.record_data, '$.y[*]')) n
), rec_elems as (
select av.id, 0 as i, json_extract(av.val, '$[0]') elem
from array_values av
where av.n > 0
union all
select av.id, e.i + 1, json_extract(av.val, concat('$[', e.i + 1, ']'))
from array_values av
inner join rec_elems e on av.id = e.id
where (i + 1) < av.n
)
select r.id record_id, r.elem extracted_integer, 'y' as 'comes_from'
from rec_elems r
union all
select new.id, json_value(new.record_data, '$.x'), 'x'
;
insert into base_data(record_data)
values
('{"x":5, "y":[6,7,4]}')
;
What the code does:
Create a table
Populate that table with some data
Create a materialized view from that data
Create a trigger to maintain the materialized view going forward
Insert some new data to see if the trigger works
The code fails on the last step. The error is Unknown column 'record_data' in 'NEW'. If I comment out the bit before the union all and just leave the last bit which doesn't involve a CTE, then the code works. Does this mean that CTEs are not supported in triggers? I hope not and I can't find any documentation that supports this conclusion.
My version is 10.5.8-MariaDB-1:10.5.8+maria~bionic running on WSL2 Ubuntu.
So how to get this to work?

I have come up with a workaround. All I did was put the logic of the trigger into a stored procedure and call it from the trigger. That fact that this works and the original does not, suggests that this is either a bug or undocumented behavior.
UPDATE: this is now a confirmed bug.
create or replace table base_data(
id int auto_increment not null primary key,
record_data longtext not null check (json_valid(record_data))
);
insert into base_data(record_data)
values
('{"x":1, "y":[2,3,4]}')
;
create or replace table mv_integers as
with recursive array_values as (
select
json_extract(b.record_data, '$.y[*]') val
, b.id
, json_length(json_extract(b.record_data, '$.y[*]')) n
from base_data b
), rec_elems as (
select av.id, 0 as i, json_extract(av.val, '$[0]') elem
from array_values av
where av.n > 0
union all
select av.id, e.i + 1, json_extract(av.val, concat('$[', e.i + 1, ']'))
from array_values av
inner join rec_elems e on av.id = e.id
where (i + 1) < av.n
)
select r.id record_id, r.elem extracted_integer, 'y' as 'comes_from'
from rec_elems r
union all
select b.id, json_value(b.record_data, '$.x'), 'x'
from base_data b
;
create or replace procedure sp_maintain_mv_integers(
in record_id int unsigned,
in json_data longtext
)
insert mv_integers(record_id, extracted_integer, comes_from)
with recursive array_values as (
select
json_extract(json_data, '$.y[*]') val
, record_id id
, json_length(json_extract(json_data, '$.y[*]')) n
), rec_elems as (
select av.id, 0 as i, json_extract(av.val, '$[0]') elem
from array_values av
where av.n > 0
union all
select av.id, e.i + 1, json_extract(av.val, concat('$[', e.i + 1, ']'))
from array_values av
inner join rec_elems e on av.id = e.id
where (i + 1) < av.n
)
select r.id record_id, r.elem extracted_integer, 'y' as 'comes_from'
from rec_elems r
union all
select record_id, json_value(json_data, '$.x'), 'x';
create or replace trigger tg_maintain_mv_integers
after insert on base_data for each row
call sp_maintain_mv_integers(new.id, new.record_data);
insert into base_data(record_data)
values
('{"x":5, "y":[6,7,4]}')
;

Related

Delete the same reference multiple columns rows safely?

I need to find all the parent-children relationships, which are all linked to my primary column ID
How I can delete the same reference columns in the table? Let say for example,if I want to delete "Google", I have to delete "HP" and Intel first also the child of HP as well.
I have tried the below thus far, but that works with only one column.
WITH tb (id,Name, Level, Path, Parent)
AS
(
SELECT
id,Name, 1 AS Level,
CAST('/'+Name as nvarchar(max)) as Path,
CAST(NULL as nvarchar(max)) as Parent
FROM krishtest
WHERE parent1 IS NULL
UNION All
SELECT
e.id,e.Name, x.Level + 1 AS Level, x.Path + '/' + e.Name as Path,
REVERSE(SUBSTRING( REVERSE(x.[Path]) ,0 , CHARINDEX( '/', REVERSE(x.[Path])) )) as [Parent]
FROM krishtest e
JOIN tb x ON x.id = e.parent1
)
SELECT Name, Level, Path, Parent FROM tb
is this use full?
declare #tmp table (id int, Name varchar(10),Parent1 int,Parent2 int,Parent3 int,Parent4 int,Parent5 int)
insert into #tmp
SELECT 1,'Microsoft',NULL,NULL,NULL,NULL,NULL
union
SELECT 2,'Google',1,NULL,NULL,NULL,NULL
union
SELECT 3,'HP',NULL,2,NULL,NULL,NULL
union
SELECT 4,'Amazone',NULL,NULL,3,NULL,NULL
union
SELECT 5,'FB',NULL,NULL,NULL,4,NULL
union
SELECT 6,'Yahoo',NULL,NULL,NULL,4,NULL
union
SELECT 7,'Intel',NULL,NULL,2,NULL,NULL
union
SELECT 8,'Apple',7,5,NULL,NULL,NULL
select * from #tmp
;with name_tree as (
select *
from #tmp
where id = 2
union all
select c.*
from #tmp c
join name_tree p on (p.id = c.parent1 or p.id = c.parent2 or p.id = c.parent3 or p.id = c.parent4 or p.id = c.parent5)
)
delete from t
from #tmp t
JOIN name_tree c on t.id=c.id
select * from #tmp
I haven't provided an actual solution to your issue here. But I would recommend investigating recursive Common Table Expressions. That should allow you to find all the parent records, then you can run a delete on them.
https://technet.microsoft.com/en-us/library/ms186243(v=sql.105).aspx
You can simply modify the recursive CTE's where clause like in below query to get all rows that need to be deleted.
See live demo
create table krishtest (id int, name varchar(100), parent1 int, parent2 int)
insert into krishtest values
(1,'Microsoft', NULL, NULL),
(2,'Google',1,NULL),
(3,'HP',NULL,2),
(4,'amazon',3,NULL),
(5,'FB',NULL,4),
(6,'yahoo',3,NULL),
(7,'cisco',6,NULL)
;
WITH tb (id,Name, Level, Path, Parent)
AS
(
SELECT
id,Name, 1 AS Level,
CAST('/'+Name as nvarchar(max)) as Path,
CAST(NULL as nvarchar(max)) as Parent
FROM krishtest
WHERE -- COALESCE(parent1,parent2) IS NULL
name ='HP'
UNION All
SELECT
e.id,e.Name, x.Level + 1 AS Level, x.Path + '/' + e.Name as Path,
REVERSE(SUBSTRING( REVERSE(x.[Path]) ,0 , CHARINDEX( '/', REVERSE(x.[Path])) )) as [Parent]
FROM krishtest e
JOIN tb x ON x.id = COALESCE(e.parent1,e.parent2)
)
--delete FROM krishtest where id in( select id from tb)
--select * from krishtest
SELECT Name, Level, Path, Parent FROM tb

SQL String join to table

Given few strings as
SET #Codes1 = 3,4
SET #Codes2 = 1
SET #Codes3 = --empty
Table -- TblCode
Id Code
1 A
2 B
3 C
4 D
How to convert the #Codes1, #Codes2, #Codes3 with join to the table TblCode so it returns the following output :
1. #Codes1 = CD
2. #Codes2 = A
3. #Codes3 = --empty
Note that the concatenation for the output is without the comma.
PS - This is a small example to a much larger and complex data set. Kindly ignore any wrongful design pattern here.
You can try this. I added the answer just for #Codes1, but it works with #Codes2 and #Codes3 too.
DECLARE #TblCode TABLE (Id INT, Code VARCHAR(2))
INSERT INTO #TblCode
VALUES(1, 'A'),
(2,'B'),
(3,'C'),
(4,'D')
DECLARE #Codes1 VARCHAR(10) = '3,4'
DECLARE #Codes2 VARCHAR(10) = '1'
DECLARE #Codes3 VARCHAR(10) = NULL
DECLARE #CodesOut VARCHAR(10) = ''
;WITH CTE_1 AS (
SELECT CODE= #Codes1 + ','
)
, CTE_2 AS -- It silit text to rows
(
SELECT RIGHT(CTE_1.CODE, LEN(CTE_1.CODE) - CHARINDEX(',',CTE_1.CODE)) CODE , SUBSTRING(CTE_1.CODE, 0, CHARINDEX(',',CTE_1.CODE)) ID, CHARINDEX(',',CTE_1.CODE) AS CI
FROM CTE_1
UNION ALL
SELECT RIGHT(CTE_2.CODE, LEN(CTE_2.CODE) - CHARINDEX(',',CTE_2.CODE)) CODE , SUBSTRING(CTE_2.CODE, 0, CHARINDEX(',',CTE_2.CODE)) ID, CHARINDEX(',',CTE_2.CODE) AS CI
FROM CTE_2 WHERE LEN(CTE_2.CODE) > 0
)
SELECT #CodesOut = #CodesOut + C.Code FROM CTE_2 INNER JOIN #TblCode C ON CTE_2.ID = C.Id
SELECT #CodesOut
Result:
CD
You can use a recursive CTE. Here is one method:
with c as (
select c.*, row_number() over (partition by id) as seqnum
from c
),
cte as (
select cast(#codes as varchar(max)) as str,
replace(#codes, id, code) as newstr,
1 as lev
from c
where seqnum = 1
union all
select str, replace(newstr, id, code), lev + 1
from cte join
c
on c.seqnum = cte.lev + 1
)
select top (1) newstr
from cte
order by lev desc;
If there is an error in the syntax, set up a SQL Fiddle or Rextester or something similar so it can be fixed.

Using CTE to assign level code to item how to retrieve the last item that already in the trail string

I am using the following query to assign level code to item.
;with C ( Ingredient_Item_No,
Lvl,
Trail)
as (
select
Matl.Ingredient_Item_No,
2 as Lvl,
CAST(('/' + Matl.Ingredient_Item_No + '/') as varchar(max)) as Trail
from Materials as Matl
and Matl.Product_Item_No = Lvl.Item_No
where Lvl.Level_Code = 1
union all
select
Matl.Ingredient_Item_No,
C.Lvl + 1,
C.trail + CAST((Matl.Ingredient_Item_No +'/') as varchar(max))
from NVA_Work_Rollup_BOM_Materials as Matl
inner join C
on Matl.Product_Item_No = C.Ingredient_Item_No
where CHARINDEX(CAST((Matl.Ingredient_Item_No + '/') as varchar(max)), C.trail) = 0
)
select * from C
The "Material" table structure is like this:
Product Ingredient
A B
B C
D E
E F
C A
The Level_Code have a list of item that already been assigned with level 1.
I use the trial column to store the ingredient hierarchy. Whenever the ingredient is already in the trail for the item, I will not assign it with another level. But that also means there is bad record that not supposed to be in the material table. For example, if I have A (1) -> B (2) -> C (3) -> A, A could not be the ingredient of the product C since it is a low level product. it also means that the pair (c (product), A (ingredient)) is a wrong record that need to be take out from 'Material' table. My problem is that I could use the trail to keep track the right order. But how could I retrieve the last pair that with wrong order, such as C -> A?
Edit:
Here is what table 'Lvl' Looks like
item_no level_Code
A 1
B 1
Any help will be appreciated!
Updated answer: Sorry, that was rather buggy.
With some horribly named Level i stuff:
-- Sample data.
declare #Material as Table ( Product VarChar(10), Ingredient VarChar(10) );
insert into #Material ( Product, Ingredient ) values
( 'A', 'B' ), ( 'B', 'C' ), ( 'D', 'E' ), ( 'E', 'F' ), ( 'C', 'A' );
--select * from #Material;
declare #Lvl as Table ( ImALevel1Product VarChar(10) );
insert into #Lvl ( ImALevel1Product ) values ( 'A' ), ( 'B' );
select *
from #Material as M left outer join
#Lvl as L on L.ImALevel1Product = M.Product;
declare #False as Bit = 0, #True as Bit = 1;
-- Run through the hierarchy looking for loops.
with Children as (
select Product, Ingredient,
Convert( VarChar(4096), '|' + Convert( VarChar(10), Product ) + '|' ) as Path, #False as Loop
from #Material
where Product in ( select ImALevel1Product from #Lvl )
union all
select Child.Product, Child.Ingredient,
Convert( VarChar(4096), Path + Convert( VarChar(10), Child.Product ) + '|' ),
case when Path like '%|' + Convert( VarChar(10), Child.Ingredient ) + '|%' then #True else #False end
from #Material as Child inner join
Children as Parent on Parent.Ingredient = Child.Product
where Parent.Loop = 0 )
select *
from Children
option ( MaxRecursion 0 )

SQL splitting a word in separate characters

I need to change an application and the first thing I need is to change a field in a database table.
In this table I now have 1 to 6 single characters, i.e. 'abcdef'
I need to change this to '[a][b][c][d][e][f]'
[edit] It is meant to stay in the same field. So before field = 'abcdef' and after field = '[a][b][c][d][e][f]'.
What would be a good way to do this?
rg.
Eric
You can split string to separate characters using following function:
create function ftStringCharacters
(
#str varchar(100)
)
returns table as
return
with v1(N) as (
select 1 union all select 1 union all select 1 union all select 1 union all select 1
union all
select 1 union all select 1 union all select 1 union all select 1 union all select 1
),
v2(N) as (select 1 from v1 a, v1 b),
v3(N) as (select top (isnull(datalength(#str), 0)) row_number() over (order by ##spid) from v2)
select N, substring(#str, N, 1) as C
from v3
GO
And then apply it as:
update t
set t.FieldName = p.FieldModified
from TableName t
cross apply (
select (select quotename(s.C)
from ftStringCharacters(t.FieldName) s
order by s.N
for xml path(''), type).value('text()[1]', 'varchar(20)')
) p(FieldModified)
SQLFiddle sample
DECLARE #text NVARCHAR(50)
SET #text = 'abcdef'
DECLARE #texttable TABLE (value NVARCHAR(1))
WHILE (len(#text) > 0)
BEGIN
INSERT INTO #texttable
SELECT substring(#text, 1, 1)
SET #text = stuff(#text, 1, 1, '')
END
select * from #texttable
Without using a function:
declare #t table(C varchar(18))
insert #t values('abc'), ('1234'), (' 1234a')
;with CTE as
(
select C, '[' + substring(c, a.n, 1) + ']' v, rn from
(select 1 n union all
select 2 union all
select 3 union all
select 4 union all
select 5 union all
select 6) a
cross apply
(select c, row_number() over (order by C) rn from #t group by c) b
where a.n <= len(C)
)
update t3
set C = t4.[value]
FROM #t t3
JOIN
(
select C,
(
select v
from CTE t1
where t1.rn = t2.rn
for xml path(''), type
).value('.', 'varchar(18)') [value]
from CTE t2
group by t2.rn, C
) t4
ON t3.C = t4.C
SELECT * FROM #t

Comma Separated SQL Server Result Set 'JOINED' with other Columns

I have a table say ProjectMaster:
Id ProjectName
1 A
2 B
3 C
another table ProjectMeter
Id ProjectId MeterNumber
1 1 #0001
2 1 #0002
3 1 #0003
4 2 #0004
5 2 #0005
6 3 #0006
I wish to have following output
ProjectName MeterNumbers
A #0001, #0002, #0003
B #0004, #0005
C #0006
I tried this and this, but unable to solve my problem.
I cannot use a table variable.
I have a already written Stored Procedure and it brings data from many joined tables. ProjectMaster also happens to be joined in one of these tables. Now am required to fetch data from ProjectMeter, such that, each row has concatenated ProjectMeter.MeterNumber corresponding to the ProjectId in that column.
right now, I get concatenated list of all meternumbers in all the rows.
I cannot use CURSOR, TABLE variable , Temp TABLE
( I hope still something can be done to my cause)
please help.....
Try this:
SELECT projectname, STUFF((SELECT distinct ', ' + meternumber
from projectmeter m
where p.id = m.projectid
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'') MeterNumbers
from projectmaster p
See SQL Fiddle with Demo
DECLARE #ProjectMaster AS TABLE
(
ID INT IDENTITY(1, 1) ,
ProjectName VARCHAR(2)
)
DECLARE #ProjectMeter AS TABLE
(
ID INT IDENTITY(1, 1) ,
ProjectID INT ,
MeterNumber VARCHAR(50)
)
INSERT INTO #ProjectMaster
( ProjectName )
VALUES ( 'A' )
INSERT INTO #ProjectMeter
( ProjectID, MeterNumber )
VALUES ( 1, '#0001' )
INSERT INTO #ProjectMeter
( ProjectID, MeterNumber )
VALUES ( 1, '#0002' )
SELECT pMaster.ID, STUFF(( SELECT ',' + MeterNumber
FROM #ProjectMeter
FOR
XML PATH('')
), 1, 1, '') AS 'Concat Result'
FROM #ProjectMeter pMeter
INNER JOIN #ProjectMaster pMaster ON pMaster.ID = pMeter.ProjectID
GROUP BY pMaster.ID
I have used table variables here but surely you just need to drop the #'s as I have used the same table names as you have specified? Not sure if this is okay? :)
Also in MS SQL you can do it using recursive query with CTE.
Here is a SQLFiddle demo
;with t1 as (
select t.*,
cast(meternumber as varchar(max)) as m2,
0 as level
from ProjectMeter t
where not exists
(select id
from ProjectMeter l
where l.id<t.id and l.ProjectId=t.ProjectID
)
union all
select b.*,
cast(c.m2+','+b.MeterNumber as varchar(max)) as m2,
c.level+1 as level
from ProjectMeter b
inner join t1 c
on (c.id < b.id) and (b.ProjectID=c.ProjectId)
)
select pm.ProjectName as ProjectName,
t1.m2 as MeterNumbers
from t1
inner join
(select ProjectId,max(level) ml
from t1
group by ProjectId
) t2
on (t1.ProjectId=t2.ProjectID) and (t1.level=t2.ml)
left join ProjectMaster pm
on (t1.ProjectId=pm.Id)
order by t1.ProjectID