Looping in array to combine jsonb - arrays

I'm unable to loop multiple array and construct it into jsonb.
CREATE TABLE emr.azt_macres (
id serial NOT NULL,
analyzer_test_full_desc varchar(50) NULL,
specimen_id varchar(100) NULL,
data_reading varchar(20) NULL,
data_result varchar(20) NULL,
result_status varchar(20) NULL,
analyzer_message text NULL,
test_result jsonb NULL,
CONSTRAINT azt_macres_pkey PRIMARY KEY (id)
);
The array look like this. Bare in mind there are two Sample in array which is Sample123 and Sample456
1H|*^&|||Mindry^^|||||||PR|1394-97|20210225142532
P|3||QC1||^^||^^|U||||||||||||||||||||||||||
O|3|1^Sample123^1||TBILV^Bilirubin Total (VOX Method)^^
R|23|KA^Bilirubin Total (VOX Method)^^F|17.648074^^^^|µmol/L|
R|24|ATU^Alanine Aminotransferase^^F|58.934098^^^^|U/L|
L|1|N
1H|*^&|||Mindry^^|||||||PR|1394-97|20210225142532
P|3||QC1||^^||^^|U||||||||||||||||||||||||||
O|3|1^Sample456^1||TBILV^Bilirubin Total (VOX Method)
R|23|TBILV^Bilirubin Total (VOX Method)^^F|17.648074^^^^|
R|24|ALT^Alanine Aminotransferase^^F|58.934098^^^^|U/L|
R|25|TTU^Alkaline phosphatase^^F|92.675340^^^^|U/L|^|N||
I'm able to insert if there are only 1 sample barcode, however the second sample_id is not insert. Do I need to loop the array again?
my code:
CREATE OR REPLACE FUNCTION emr.azintin(v_msgar)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
v_cnt INT = 0;
v_msgar text[];
v_msgln text;
v_msgtyp character varying(3);
v_tmp text;
macres azt_macres%rowtype;
BEGIN
macres.analyzer_test_full_desc := 'CHEMO';
SELECT split_part(items[3], '^', 2)
INTO macres.specimen_id
FROM (
SELECT string_to_array(element, '|') as items
FROM unnest(v_msgar) as t(element)) t
WHERE items[1] = 'O';
SELECT jsonb_agg(jsonb_build_object('resultId', split_part(items[3],'^',1),'resultValue',split_part(items[4],'^',1)))
INTO macres.test_result
FROM (
SELECT string_to_array(element, '|') as items
FROM unnest(v_msgar) as t(element)) t
WHERE items[1] = 'R';
v_cnt := v_cnt + 1;
BEGIN
INSERT INTO azt_macres(analyzer_test_full_desc, specimen_id, data_reading ,
data_result,result_status,analyzer_message,test_result)
VALUES (macres.analyzer_test_full_desc, macres.specimen_id,macres.data_reading,
macres.data_result,macres.result_status,macres.analyzer_message, macres.test_result);
END;
END
$function$
;
Currently my output is like this
specimen_id
test_result
sample123
[{"resultId": "KA", "resultValue": "17.648074"}, {"resultId": "ATU", "resultValue":"58.934098"}, {"resultId": "TBILV", "resultValue": "17.648074"}, {"resultId": "ALT","resultValue": "58.934098"}, {"resultId": "TTU", "resultValue": "92.675340"}]
supposely my output should be like this
specimen_id
test_result
sample123
[{"resultId": "KA", "resultValue": "17.648074"}, {"resultId": "ATU","resultValue":"58.934098"}]
sample456
[{"resultId": "TBILV", "resultValue": "17.648074"}, {"resultId": "ALT","resultValue": "58.934098"}, {"resultId": "TTU", "resultValue": "92.675340"}]

1. Creating the output:
As I see it, you need a group of the R elements by the previous O elements. That needs to create a group of connected R elements, which share the same O value:
step-by-step demo:db<>fiddle
Don't be afraid, it looks heavier than it is ;) To understand all described intermediate steps, please have a look at the fiddle where every step is executed separately to demonstrate its impact.
SELECT
specimen_id,
json_agg(result) as test_result -- 9
FROM (
SELECT
code,
first_value( -- 5
split_part(id, '^', 2)
) OVER (PARTITION BY group_id ORDER BY index) as specimen_id,
json_build_object( -- 7
'result_id',
split_part(id, '^', 1), -- 6
'result_value',
split_part(value, '^', 1)
) as result
FROM (
SELECT
parts[1] as code,
parts[3] as id,
parts[4] as value,
elem.index,
SUM((parts[1] = 'O')::int) OVER (ORDER BY elem.index) as group_id -- 4
FROM mytable t,
unnest(myarray) WITH ORDINALITY as elem(value, index), -- 1
regexp_split_to_array(elem.value, '\|') as parts -- 2
WHERE parts[1] IN ('R', 'O') -- 3
) s
) s
WHERE code = 'R' -- 8
GROUP BY specimen_id
Put all array elements into separate records. WITH ORDINALITY adds an index to the records which represents the original position of the element in the original array.
Split the array elements into their parts (delimited by | character)
Filter only the R and O elements.
This is the most interesting part: Creating the group of connected records. The idea: Every O records gets a value 1 (boolean true cast into int is 1, so: (parts[1] = 'O')::int), every R gets a 0 value. The cumulative SUM() window function creates a total sum of the current and every previous record. So every O increases the sum value, the R values (add 0) keep this value. So this generates the same group identifier for every O record and all directly following R records. To ensure the correct record order, we use the index, which was created by the WITH ORDINALITY in step 1.
The first_value() window function gives the first value of a group (= partition), in this case, it adds to every record the first value of the recently created group. This finally associates the O value with each related R record.
split_part() for retrieving the correct values from the strings
build your JSON object for every record
Remove the O records
Group by the specimen_id (which was added in step 5) and aggregate the result values into an array
Now we have the expected output
2. Inserting:
Insert is easy: You can take the result (maybe you need some columns more, just add them to the SELECT statement, we recently created and execute the INSERT statement:
INSERT INTO mytable (col1, col2)
SELECT -- <query from above>
3. Create function:
Here it is not clear to me, what you want to achieve. Do you want to give the array as input parameter, or do you want to query a table? Why are you using a JSON return type although nothing is returned, ...
However, of course, you can put all the stuff into a stored procedure. Here I assume you give the array as input parameter into the procedure (for example, see the answer to your previous question):
CREATE FUNCTION my_function(myarray text[]) RETURNS void AS $$
BEGIN
INSERT INTO ...
SELECT ...
END;
$$ LANGUAGE plpgsql;

Related

MS SQL How to field add auto increment dependent on another field

For example, there is a table
int type
int number
int value
How to make that when inserting a value into a table
indexing started from 1 for different types.
type 1 => number 1,2,3...
type 2 => number 1,2,3...
That is, it will look like this.
type
number
value
1
1
-
1
2
-
1
3
-
2
1
-
1
4
-
2
2
-
3
1
-
6
1
-
1
5
-
2
3
-
6
2
-
Special thanks to #Larnu.
As a result, in my case, the best solution would be to create a table for each type.
As I mentioned in the comments, neither IDENTITY nor SEQUENCE support the use of another column to denote what "identity set" they should use. You can have multiple SEQUENCEs which you could use for a single table, however, this doesn't scale. If you are specific limited to 2 or 3 types, for example, you might choose to create 3 SEQUENCE objects, and then use a stored procedure to handle your INSERT statements. Then, when a user/application wants to INSERT data, they call the procedure and that procedure has logic to use the SEQUENCE based on the value of the parameter for the type column.
As mentioned, however, this doesn't scale well. If you have an undeterminate number of values of type then you can't easily handle getting the right SEQUENCE and handling new values for type would be difficult too. In this case, you would be better off using a IDENTITY and then a VIEW. The VIEW will use ROW_NUMBER to create your identifier, while IDENTITY gives you your always incrementing value.
CREATE TABLE dbo.YourTable (id int IDENTITY(1,1),
[type] int NOT NULL,
number int NULL,
[value] int NOT NULL);
GO
CREATE VIEW dbo.YourTableView AS
SELECT ROW_NUMBER() OVER (PARTITION BY [type] ORDER BY id ASC) AS Identifier,
[type],
number,
[value]
FROM dbo.YourTable;
Then, instead, you query the VIEW, not the TABLE.
If you need consistency of the column (I name identifier) you'll need to also ensure row(s) can't be DELETEd from the table. Most likely by adding an IsDeleted column to the table defined as a bit (with 0 for no deleted, and 1 for deleted), and then you can filter to those rows in the VIEW:
CREATE VIEW dbo.YourTableView AS
WITH CTE AS(
SELECT id,
ROW_NUMBER() OVER (PARTITION BY [type] ORDER BY id ASC) AS Identifier,
[type],
number,
[value],
IsDeleted
FROM dbo.YourTable)
SELECT id,
Identifier,
[type],
number,
[value]
FROM CTE
WHERE IsDeleted = 0;
You could, if you wanted, even handle the DELETEs on the VIEW (the INSERT and UPDATEs would be handled implicitly, as it's an updatable VIEW):
CREATE TRIGGER trg_YourTableView_Delete ON dbo.YourTableView
INSTEAD OF DELETE AS
BEGIN
SET NOCOUNT ON;
UPDATE YT
SET IsDeleted = 1
FROM dbo.YourTable YT
JOIN deleted d ON d.id = YT.id;
END;
GO
db<>fiddle
For completion, if you wanted to use different SEQUENCE object, it would look like this. Notice that this does not scale easily. I have to CREATE a SEQUENCE for every value of Type. As such, for a small, and known, range of values this would be a solution, but if you are going to end up with more value for type or already have a large range, this ends up not being feasible pretty quickly:
CREATE TABLE dbo.YourTable (identifier int NOT NULL,
[type] int NOT NULL,
number int NULL,
[value] int NOT NULL);
CREATE SEQUENCE dbo.YourTable_Type1
START WITH 1 INCREMENT BY 1;
CREATE SEQUENCE dbo.YourTable_Type2
START WITH 1 INCREMENT BY 1;
CREATE SEQUENCE dbo.YourTable_Type3
START WITH 1 INCREMENT BY 1;
GO
CREATE PROC dbo.Insert_YourTable #Type int, #Number int = NULL, #Value int AS
BEGIN
DECLARE #Identifier int;
IF #Type = 1
SELECT #Identifier = NEXT VALUE FOR dbo.YourTable_Type1;
IF #Type = 2
SELECT #Identifier = NEXT VALUE FOR dbo.YourTable_Type2;
IF #Type = 3
SELECT #Identifier = NEXT VALUE FOR dbo.YourTable_Type3;
INSERT INTO dbo.YourTable (identifier,[type],number,[value])
VALUES(#Identifier, #Type, #Number, #Value);
END;

How can I query properties dynamically in SQL Server?

I am currently working on a project, that includes in an automatical flairing part.
Basicly what this does:
I have a table called Fox, with various columns. And some other tables refering to Fox (i.e. CaughtChickens).
I want to have another table, that I can expand anytime, with 3 columns (other than ID ofc.) in my mind FlairName, FlairColor, and FlairStoredProcedure.
I want to have a stored procedure that returns all FlairName where the FlairStoredProcedure returns 1, for a certain FoxID.
This way I can write a stored procedure that checks if a certain Fox caught a chicken and returns 1 if it did, and add a flair Hunter on the User UI.
There are some cons with this:
Every time I want a new flair I have to write a new stored procedure it (yeah I kinda can't short this one out).
The stored procedures needs to have the same amount of in parameters (ie. #FoxID), and needs to return 1 or 0 (or select nothing when false, select the name if true (?))
I need to use dynamicSQL in the stored procedure that collect these flairs, and I kinda don't want to use any dynamicSQL at all.
Isn't there a lot easier way to do this that I am missing?
EDIT:
Example:
I have a table Fox:
FoxID FoxName FoxColor FoxSize Valid
1 Swiper red 12 1
I would have a table Flairs
FlairID FlairName FlairStoredProcedure Valid
1 Big pFlairs_IsFoxBig 1
2 Green pFlairs_IsFoxGreen 1
I would have 3 stored procedures:
pFox_Flairs
#FoxID int
DECLARE #CurrentFlairSP as varchar(100)
DECLARE #CurrentIDIndex as varchar(100) = 1
DECLARE #ResultFlairs as table(FlairName as varchar(50), FlairColor as integer)
WHILE #CurrentIDIndex <= (SELECT MAX(ID) FROM Flairs WHERE Valid <> 0)
BEGIN
IF EXISTS(SELECT * FROM Flairs WHERE ID = #CurrentIDIndex AND VALID <> 0)
BEGIN
SET #CurrentFlairSP = CONCAT((SELECT TOP 1 FlairStoredProcedure FROM Flairs WHERE ID = #CurrentIDIndex AND VALID <> 0), ' #FoxID=#FoxID')
INSERT INTO #ResultFlairs
EXEC (#CurrentFlairSP)
END
#CurrentIDIndex += 1
END
SELECT * FROM #ResultFlairs
pFlairs_IsFoxBig
#FoxID int
SELECT 'Big' WHERE EXISTS( SELECT TOP 1 * FROM Fox WHERE ID = #Fox AND FoxSize > 10)
pFlairs_IsFoxGreen
#FoxID int
SELECT 'Green' WHERE EXISTS( SELECT TOP 1 * FROM Fox WHERE ID = #Fox AND FoxColor = 'green')
You could create a single table valued function that checks all the conditions:
CREATE OR ALTER FUNCTION dbo.GetFlairs ( #FoxID int )
RETURNS TABLE
AS RETURN
SELECT v.FlairName
FROM Fox f
CROSS APPLY (
SELECT 'Big'
WHERE f.FoxSize > 10
UNION ALL
SELECT 'Green'
WHERE f.FoxColor = 'green'
) v(FlairName)
WHERE f.FoxID = #FoxID;
go
Then you can use it like this:
SELECT *
FROM dbo.GetFlairs(123);
If or when you add more attributes or conditions, simply add them into the function as another UNION ALL

How can I specify a specific custom ordering with a SELECT DISTINCT

I've seen all the other answers on this but I haven't seen my specific problem addressed. Basically, I have a table with a bunch of duplicated values, and I need to select exactly one column (to concatenate into a list, ultimately, for output purposes), but with a specific item in that list FIRST. The problem I have is that I can specify the order, or I can specify distinct, but not both. And I CANNOT INCLUDE the order column in the SELECT because that output is going to be used directly, and having two columns in the output breaks everything. This seems like it should be possible, but I can't figure out how.
Here's a contrived example:
DECLARE #List TABLE ([Name] nvarchar(10));
INSERT INTO #List ([Name])
VALUES (N'A'), (N'A'), (N'B'), (N'B'), (N'B'), (N'C'), (N'D'), (N'D'), (N'J'), (N'X'), (N'X'), (N'Y');
-- Has both duplicates, and not in the right order
SELECT * FROM #List;
-- No duplicates, but not in the right order
SELECT DISTINCT * FROM #List;
-- In the right order, but has duplicates
SELECT * FROM #List ORDER BY CASE WHEN [Name] = 'X' THEN '1' WHEN [Name] = 'Y' THEN '2' ELSE [Name] END;
What I want is a simple output like this:
X
Y
A
B
C
D
J
I've tried various approaches, with CTEs, and intermediate steps, but I really just want ONE select, that I can throw into a STUFF, so I get the string "X, Y, A, B, C, D, J", and I can't find any way to preserve the REQUIRED order while doing that. This HAS to be possible, so I'm clearly missing something...
You should use a group by instead of distinct.
SELECT [Name]
FROM #List
group by [Name]
ORDER BY CASE WHEN [Name] = 'X' THEN '1' WHEN [Name] = 'Y' THEN '2' ELSE [Name] END ;
Unfortunately I do not have enough points to post a comment, so try to figure it out in an "answer".
You've mentioned that you should not include ordering column in the result set, so I assume that column defined in the table.
If so, then you can use a window functions for that purpose. Assume that column has a name Ordering_Column, then you may issue this:
select Name
from (
select Name
, row_number() over (partition by Name order by Ordering_Column) as Position
from SomeTable
order by Ordering_Column
) where Position = 1
This would return firstmost Name values from SomeTable sorted by Ordering_Column.

Is there a way to add a logical Operator in a WHERE clause using CASE statements? - T-SQL

I searched the web but cannot find a solution for my problem (but perhaps I am using the wrong keywords ;) ).
I've got a Stored Procedure which does some automatic validation (every night) for a bunch of records. However, sometimes a user wants to do the same validation for a single record manually. I thought about calling the Stored Procedure with a parameter, when set the original SELECT statement (which loops through all the records) should get an AND operator with the specified record ID. I want to do it this way so that I don't have to copy the entire select statement and modify it just for the manual part.
The original statement is as follows:
DECLARE GenerateFacturen CURSOR LOCAL FOR
SELECT TOP 100 PERCENT becode, dtreknr, franchisebecode, franchisenemer, fakgroep, vonummer, vovolgnr, count(*) as nrVerOrd,
FaktuurEindeMaand, FaktuurEindeWeek
FROM (
SELECT becode, vonummer, vovolgnr, FaktuurEindeMaand, FaktuurEindeWeek, uitgestfaktuurdat, levdat, voomschrijving, vonetto,
faktureerperorder, dtreknr, franchisebecode, franchisenemer, fakgroep, levscandat
FROM vwOpenVerOrd WHERE becode=#BecondeIN AND levdat IS NOT NULL AND fakstatus = 0
AND isAllFaktuurStukPrijsChecked = 1 AND IsAllFaktuurVrChecked = 1
AND (uitgestfaktuurdat IS NULL OR uitgestfaktuurdat<=#FactuurDate)
) sub
WHERE faktureerperorder = 1
GROUP BY becode, dtreknr, franchisebecode, franchisenemer, fakgroep, vonummer, vovolgnr,
FaktuurEindeMaand, FaktuurEindeWeek
ORDER BY MIN(levscandat)
At the WHERE faktureerperorder = 1 I came up with something like this:
WHERE faktureerperorder = 1 AND CASE WHEN #myParameterManual = 1 THEN vonummer=#vonummer ELSE 1=1 END
But this doesn't work. The #myParameterManual indicates whether or not it should select only a specific record. The vonummer=#vonummer is the record's ID. I thought by setting 1=1 I would get all the records.
Any ideas how to achieve my goal (perhaps more efficient ideas or better ideas)?
I'm finding it difficult to read your query, but this is hopefully a simple example of what you're trying to achieve.
I've used a WHERE clause with an OR operator to give you 2 options on the filter. Using the same query you will get different outputs depending on the filter value:
CREATE TABLE #test ( id INT, val INT );
INSERT INTO #test
( id, val )
VALUES ( 1, 10 ),
( 2, 20 ),
( 3, 30 );
DECLARE #filter INT;
-- null filter returns all rows
SET #filter = NULL;
SELECT *
FROM #test
WHERE ( #filter IS NULL
AND id < 5
)
OR ( #filter IS NOT NULL
AND id = #filter
);
-- filter a specific record
SET #filter = 2;
SELECT *
FROM #test
WHERE ( #filter IS NULL
AND id < 5
)
OR ( #filter IS NOT NULL
AND id = #filter
);
DROP TABLE #test;
First query returns all:
id val
1 10
2 20
3 30
Second query returns a single row:
id val
2 20

Copy records with dynamic column names

I have two tables with different columns in PostgreSQL 9.3:
CREATE TABLE person1(
NAME TEXT NOT NULL,
AGE INT NOT NULL
);
CREATE TABLE person2(
NAME TEXT NOT NULL,
AGE INT NOT NULL,
ADDRESS CHAR(50),
SALARY REAL
);
INSERT INTO person2 (Name, Age, ADDRESS, SALARY)
VALUES ('Piotr', 20, 'London', 80);
I would like to copy records from person2 to person1, but column names can change in program, so I would like to select joint column names in program. So I create an array containing the intersection of column names. Next I use a function: insert into .... select, but I get an error, when I pass the array variable to the function by name. Like this:
select column_name into name1 from information_schema.columns where table_name = 'person1';
select column_name into name2 from information_schema.columns where table_name = 'person2';
select * into cols from ( select * from name1 intersect select * from name2) as tmp;
-- Create array with name of columns
select array (select column_name::text from cols) into cols2;
CREATE OR REPLACE FUNCTION f_insert_these_columns(VARIADIC _cols text[])
RETURNS void AS
$func$
BEGIN
EXECUTE (
SELECT 'INSERT INTO person1 SELECT '
|| string_agg(quote_ident(col), ', ')
|| ' FROM person2'
FROM unnest(_cols) col
);
END
$func$ LANGUAGE plpgsql;
select * from cols2;
array
------------
{name,age}
(1 row)
SELECT f_insert_these_columns(VARIADIC cols2);
ERROR: column "cols2" does not exist
What's wrong here?
You seem to assume that SELECT INTO in SQL would assign a variable. But that is not so.
It creates a new table and its use is discouraged in Postgres. Use the superior CREATE TABLE AS instead. Not least, because the meaning of SELECT INTO inside plpgsql is different:
Combine two tables into a new one so that select rows from the other one are ignored
Concerning SQL variables:
User defined variables in PostgreSQL
Hence you cannot call the function like this:
SELECT f_insert_these_columns(VARIADIC cols2);
This would work:
SELECT f_insert_these_columns(VARIADIC (TABLE cols2 LIMIT 1));
Or cleaner:
SELECT f_insert_these_columns(VARIADIC array) -- "array" being the unfortunate column name
FROM cols2
LIMIT 1;
About the short TABLE syntax:
Is there a shortcut for SELECT * FROM?
Better solution
To copy all rows with columns sharing the same name between two tables:
CREATE OR REPLACE FUNCTION f_copy_rows_with_shared_cols(
IN _tbl1 regclass
, IN _tbl2 regclass
, OUT rows int
, OUT columns text)
LANGUAGE plpgsql AS
$func$
BEGIN
SELECT INTO columns -- proper use of SELECT INTO!
string_agg(quote_ident(attname), ', ')
FROM (
SELECT attname
FROM pg_attribute
WHERE attrelid IN (_tbl1, _tbl2)
AND NOT attisdropped -- no dropped (dead) columns
AND attnum > 0 -- no system columns
GROUP BY 1
HAVING count(*) = 2
) sub;
EXECUTE format('INSERT INTO %1$s(%2$s) SELECT %2$s FROM %3$s'
, _tbl1, columns, _tbl2);
GET DIAGNOSTICS rows = ROW_COUNT; -- return number of rows copied
END
$func$;
Call:
SELECT * FROM f_copy_rows_with_shared_cols('public.person2', 'public.person1');
Result:
rows | columns
-----+---------
3 | name, age
Major points
Note the proper use of SELECT INTO for assignment inside plpgsql.
Note the use of the data type regclass. This allows to use schema-qualified table names (optionally) and defends against SQL injection attempts:
Table name as a PostgreSQL function parameter
About GET DIAGNOSTICS:
Count rows affected by DELETE
About OUT parameters:
Returning from a function with OUT parameter
The manual about format().
Information schema vs. system catalogs.

Resources