Oracle Apex - REST data source - extract a nested JSON array - trigger two tables - PLSQL error question - plsql

This question is a follow up to another SO question.
I've followed Carsten's instructions on the previous question. I am now receiving a new error.
Compilation failed, line 9 (08:51:36) The line numbers associated with compilation errors are relative to the first BEGIN statement. This only affects the compilation of database triggers.
PL/SQL: ORA-00904: "J"."PRICE": invalid identifierCompilation failed, line 3 (08:51:36) The line numbers associated with compilation errors are relative to the first BEGIN statement. This only affects the compilation of database triggers.
PL/SQL: SQL Statement ignored
Trigger
create or replace trigger "TR_MAINTAIN_LINES"
AFTER
insert or update or delete on "ORDERS_LOCAL"
for each row
begin
if inserting then
insert into ORDER_ITEMS_LOCAL ( order_id, line_id, line_number, product_id, quantity, price)
( select :new.order_id,
seq_line_id.nextval,
j.line_number,
j.product_id,
j.quantity,
j.price
from json_table(
:new.order_items,
'$[*]' columns (
line_id for ordinality,
line_number number path '$.line_number',
product_id number path '$.product_id',
quantity number path '$.quantity',
price number path '$.price' ) ) );
elsif deleting then
delete ORDER_ITEMS_LOCAL
where order_id = :old.order_id;
elsif updating then
delete ORDER_ITEMS_LOCAL
where order_id = :old.order_id;
--
-- handle the update case here.
-- I would simply delete and re-insert LINES rows.
end if;
end;
What I need help figuring out
The order_id column in ORDER_ITEMS table should be a foreign key to the ORDERS table referring to the order_id. That way each 'order line item' can be traced back to the order_id. Is :new.order_id correct in this case?
insert into ORDER_ITEMS_LOCAL ( order_id, line_id, line_number, product_id, quantity, price)
( select :new.order_id,
seq_line_id.nextval,
The line_id column in ORDER_ITEMS table should be automatically assigned based on the next value (ex: line_id:98, line_id:99, line_id:100) line_id is not in the JSON response. Is seq_line_id.nextval, correct in this case?
I am not sure what the 'j' is referring to. (j.quantity, j.price)
from json_table(
:new.order_items,
'$[*]' columns (
line_id for ordinality,
line_number number path '$.line_number',
product_id number path '$.product_id',
quantity number path '$.quantity',
price number path '$.price' ) ) );
Does :new.order_items grab the order_items from the JSON array?
'$[*]' columns = $– Start with the current object. [] – Look inside an array
Would I include order_id in here as well?
JSON RESPONSE
{
"order_id": "HO9b6-ahMY-B2i9",
"order_number": 34795,
"order_date": "2022-11-02",
"store_id": 2,
"full_name": "Ronda Perfitt",
"email": "rperfitt1#microsoft.com",
"city": "Fresno",
"state": "California",
"zip_code": "93762",
"credit_card": "5108758574719798",
"order_items": [
{
"line_number": 1,
"product_id": 2,
"quantity": 1,
"price": 3418.85
},
{
"line_number": 2,
"product_id": 7,
"quantity": 1,
"price": 4070.12
}
]
},
I've found a few resources for the json_table function online but I'm having difficulty finding one that's within a trigger function similar to Carsten's code. Your help explaining this would be much appreciated.
-----------UPDATE---------
ORDERS_LOCAL Table
ORDERS_LOCAL Table data
ORDER_ITEMS_LOCAL Table
LINE_ID column to be automatically created
ORDER_ID column foreign key to ORDERS_LOCAL table
PRODUCT_ID column foreign key to PRODUCTS table

The JSON_TABLE expression in the FROM clause is missing the alias. The select list uses the "j" prefix for the columns from the JSON_TABLE expression, but the JSON_TABLE is not aliased ...
You might change as follows (note the additional "j" in the last line)
from json_table(
:new.order_items,
'$[*]' columns (
line_id for ordinality,
line_number number path '$.line_number',
product_id number path '$.product_id',
quantity number path '$.quantity',
price number path '$.price' ) ) j );

Related

Oracle Apex - REST data source - nested JSON array - trigger two tables - delete function error ORA-04091

This question is a follow up to another SO question.
I have an app with a REST Data Source taking in JSON responses from an API. There are two tables ORDERS and ORDER_ITEMS. The ORDERS table contains a column ORDER_ITEMS which is a JSON Document type.
I created a trigger on my ORDERS table which runs AFTER INSERT, UPDATE or DELETE on my ORDERS table, and which maintains the ORDER_ITEMS table (Extracts the JSON array from the ORDERS table ORDER_ITEMS column and inserts into ORDER_ITEMS table and each column).
create or replace trigger "TR_MAINTAIN_LINES"
AFTER
insert or update or delete on "ORDERS_LOCAL"
for each row
begin
if inserting or updating then
if updating then
delete ORDER_ITEMS_LOCAL
where order_id = :old.order_id;
end if;
insert into ORDER_ITEMS_LOCAL ( order_id, line_id, line_number, product_id, quantity, price)
( select :new.order_id,
seq_line_id.nextval,
j.line_number,
j.product_id,
j.quantity,
j.price
from json_table(
:new.order_items,
'$[*]' columns (
line_id for ordinality,
line_number number path '$.line_number',
product_id number path '$.product_id',
quantity number path '$.quantity',
price number path '$.price' ) ) j );
elsif deleting then
delete ORDER_ITEMS_LOCAL
where order_id = :old.order_id;
end if;
end;
The trigger works for AFTER INSERT and UPDATE. However, when I try to DELETE a row from the ORDERS table, I receive an error. error ORA-04091: table WKSP_DEMO.ORDER_ITEMS_LOCAL is mutating, trigger/function may not see it
But deleting a row from the ORDER_ITEMS table works as expected.
ORDERS table
enter image description here
ORDERS table data
enter image description here
ORDER_ITEMS table
enter image description here
ORDER_ITEMS table data
enter image description here
Triggers enter image description here
BI_ORDER_ITEMS_LOCAL trigger
create or replace trigger "BI_ORDER_ITEMS_LOCAL"
before insert on "ORDER_ITEMS_LOCAL"
for each row
begin
if :NEW."LINE_ID" is null then
select "ORDER_ITEMS_LOCAL_SEQ".nextval into :NEW."LINE_ID" from sys.dual;
end if;
end;
Error Message enter image description here
---- UPDATE ------
So I took out the delete portion of the trigger and both ORDER and ORDER_LINES table rows get deleted as expected.
create or replace trigger "TR_MAINTAIN_LINES"
AFTER
insert or update on "ORDERS_LOCAL"
for each row
begin
if inserting or updating then
if updating then
delete ORDER_ITEMS_LOCAL
where order_id = :old.order_id;
end if;
insert into ORDER_ITEMS_LOCAL ( order_id, line_id, line_number, product_id, quantity, price)
( select :new.order_id,
seq_line_id.nextval,
j.line_number,
j.product_id,
j.quantity,
j.price
from json_table(
:new.order_items,
'$[*]' columns (
line_id for ordinality,
line_number number path '$.line_number',
product_id number path '$.product_id',
quantity number path '$.quantity',
price number path '$.price' ) ) j );
end if;
end;
you have a conflict between the ON DELETE CASCADE clause and your trigger logic. In the DELETE case, both attempt to delete the child rows. You can ...
remove the DELETE section from your trigger and let the ON DELETE CASCADE clause do the job
remove the ON DELETE CASCADE clause from your foreign key.
I think, I'd recommend the second option, so that all that logic is in one place, i.e. your trigger.

Oracle Apex 22.21 - REST data source - nested JSON array - sync two tables by trigger - PLSQL error question

This question is a follow up to another SO question.
I've actually recreated the tables from the previous question. The updated JSON response can be found at the bottom of this question.
ORDERS_LOCAL table
ORDERS_LOCAL table data. ORDER_ITEMS column is the JSON array that I need to extract into ORDER_ITEMS_LOCAL table.
ORDER_ITEMS_LOCAL table. LINE_ID column should be created automatically. ORDER_ID column is a foreign key to ORDERS_LOCAL table. PRODUCT_ID column is a foreign key to PRODUCTS table. LINE_NUMBER is just the order line number (line 1 = product 1, price, qty | line 2 = product 2, price, qty etc..) I believe it's called a sequence type?
PRODUCTS table
PRODUCTS table data
Per Carsten's answer, I've created a new trigger for the ORDERS table from the Object Browser.
I've then entered Carsten's PLSQL code from the previous question. He did mention that it was pseudo-code. So I tried to update it..
create or replace trigger "TR_MAINTAIN_LINES"
AFTER
insert or update or delete on "ORDERS_LOCAL"
for each row
begin
if inserting then
insert into ORDER_ITEMS_LOCAL ( line_id, order_id, line_number, product_id, quantity, price)
( select :new.id,
seq_lines.nextval,
j.line_number,
j.product_id,
j.quantity,
j.price
from json_table(
:new.order_items,
'$[*]' columns (
line_number for ordinality,
product_id number path '$.product_id',
quantity number path '$.quantity',
price number path '$.price' ) ) );
elsif deleting then
delete ORDER_ITEMS_LOCAL
where order_id = :old.id;
elsif updating then
delete ORDER_ITEMS_LOCAL
where order_id = :old.id;
--
-- handle the update case here.
-- I would simply delete and re-insert LINES rows.
end if;
end;
I am receiving the following errors
Compilation failed, line 4 (08:38:57) The line numbers associated with compilation errors are relative to the first BEGIN statement. This only affects the compilation of database triggers.
PLS-00049: bad bind variable 'NEW.ID'Compilation failed, line 19 (08:38:57) The line numbers associated with compilation errors are relative to the first BEGIN statement. This only affects the compilation of database triggers.
PLS-00049: bad bind variable 'OLD.ID'Compilation failed, line 22 (08:38:57) The line numbers associated with compilation errors are relative to the first BEGIN statement. This only affects the compilation of database triggers.
PLS-00049: bad bind variable 'OLD.ID'
I believe this is due to missing columns in the trigger code but I'm not sure.
I am new to PLSQL and parsing the JSON is kind of confusing.. especially below. Please see my comments.
if inserting then
insert into ORDER_ITEMS_LOCAL ( line_id, order_id, line_number, product_id, quantity, price)
( select :new.id, -- is this new id for `line_id`
order_id -- how to insert order_id foreign key
seq_lines.nextval, -- not sure what this is for?
j.line_number, -- I changed 'lines' to 'order_items' so should this be seq_order_items.nextval, ?
j.product_id,
j.quantity,
j.price
from json_table(
:new.order_items, -- I changed 'lines' to 'order_items' so I changed this from :new.lines,
'$[*]' columns ( -- Would I include 'line_id' and 'order_id' in here as well?
line_number for ordinality,
product_id number path '$.product_id',
quantity number path '$.quantity',
price number path '$.price' ) ) );
Updated JSON response
[
{
"order_id": "HO9b6-ahMY-B2i9",
"order_number": 34795,
"order_date": "2022-11-02",
"store_id": 2,
"full_name": "Ronda Perfitt",
"email": "rperfitt1#microsoft.com",
"city": "Fresno",
"state": "California",
"zip_code": "93762",
"credit_card": "5108758574719798",
"order_items": [
{
"line_number": 1,
"product_id": 2,
"quantity": 1,
"price": 3418.85
},
{
"line_number": 2,
"product_id": 7,
"quantity": 1,
"price": 4070.12
}
]
},
{
"order_id": "RFvUC-sN8Y-icJP",
"order_number": 62835,
"order_date": "2022-10-09",
"store_id": 1,
"full_name": "Wash Rosenfelt",
"email": "wrosenfelt3#wisc.edu",
"city": "Chicago",
"state": "Illinois",
"zip_code": "60646",
"credit_card": "5048372443777103",
"order_items": [
{
"line_number": 1,
"product_id": 1,
"quantity": 1,
"price": 3349.05
},
{
"line_number": 2,
"product_id": 3,
"quantity": 1,
"price": 4241.29
},
{
"line_number": 3,
"product_id": 1,
"quantity": 1,
"price": 3560.03
}
]
},
]
I apologize for making this confusing. I really want to learn how to do this right. Your support is much appreciated. Thank you.
In the trigger code, the :old and :new prefixes reference the row of your table, before and after the trigger operation. So ...
In the UPDATING case, :old.{column-name} references the value of a table column column before the update, :new.{column-name} references the value after the update.
In the INSERTING case, there is no :old.{column-name} (thus that would be NULL); :new.{column-name} references the inserted value.
And in the DELETING case, there is no :new.{column-name} value; only :old.{column-name} is available.
You see the compiler error, as my trigger pseudo-code contained :new.id, but your table does not have a column named ID; it's ORDER_ID in your case. So you need to adjust that code accordingly.
https://docs.oracle.com/en/database/oracle/oracle-database/21/lnpls/plsql-triggers.html#GUID-E76C8044-6942-4573-B7DB-3502FB96CF6F

Oracle Apex - REST data source - nested JSON array - trigger two tables - update function

This question is a follow up to another SO question.
I've followed Carsten's instructions on the previous question. The trigger works as expected. I now just need to figure out how to handle the updating function of the trigger.
create or replace trigger "TR_MAINTAIN_LINES"
AFTER
insert or update or delete on "ORDERS_LOCAL"
for each row
begin
if inserting then
insert into ORDER_ITEMS_LOCAL ( order_id, line_id, line_number, product_id, quantity, price)
( select :new.order_id,
seq_line_id.nextval,
j.line_number,
j.product_id,
j.quantity,
j.price
from json_table(
:new.order_items,
'$[*]' columns (
line_id for ordinality,
line_number number path '$.line_number',
product_id number path '$.product_id',
quantity number path '$.quantity',
price number path '$.price' ) ) j );
elsif deleting then
delete ORDER_ITEMS_LOCAL
where order_id = :old.order_id;
elsif updating then
delete ORDER_ITEMS_LOCAL
where order_id = :old.order_id;
--
-- handle the update case here.
-- I would simply delete and re-insert ORDER_ITEMS rows.
end if;
end;
Carsten did mention in his answer to a previous question:
"In the UPDATING case, :old.{column-name} references the value of a table column column before the update, :new.{column-name} references the value after the update."
Would I leave the delete line? :
elsif updating then
delete ORDER_ITEMS_LOCAL
where order_id = :old.order_id;
--
-- handle the update case here.
-- I would simply delete and re-insert ORDER_ITEMS rows.
Is the pseudo-code below the correct way to handle this? :
elsif updating then
delete ORDER_ITEMS_LOCAL
where order_id = :old.order_id;
insert into ORDER_ITEMS_LOCAL ( order_id, line_id, line_number, product_id, quantity, price)
( select :new.order_id,
seq_line_id.nextval,
:new.line_number,
:new.product_id,
:new.quantity,
:new.price )
I'm assuming that the update could potentially mean a new json value in ORDERS_LOCAL.ORDER_ITEMS. In that case the json needs to be parsed just like for the insert. Combine both inserting and deleting in a single if statement to avoid duplicate code.
create or replace trigger "TR_MAINTAIN_LINES"
AFTER
insert or update or delete on "ORDERS_LOCAL"
for each row
begin
if inserting or updating then
if updating then
delete ORDER_ITEMS_LOCAL
where order_id = :old.order_id;
end if;
insert into ORDER_ITEMS_LOCAL ( order_id, line_id, line_number, product_id, quantity, price)
( select :new.order_id,
seq_line_id.nextval,
j.line_number,
j.product_id,
j.quantity,
j.price
from json_table(
:new.order_items,
'$[*]' columns (
line_id for ordinality,
line_number number path '$.line_number',
product_id number path '$.product_id',
quantity number path '$.quantity',
price number path '$.price' ) ) j );
elsif deleting then
delete ORDER_ITEMS_LOCAL
where order_id = :old.order_id;
end if;
end;

Create composed unique key field with trigger after insert in SQLITE

I have a table with multiple columns and one (unique key) should be a value composed from the values of other two columns.
CREATE TABLE batches (
id TEXT PRIMARY KEY UNIQUE,
name TEXT NOT NULL,
project_id INTEGER);
On each insert, I want to generate the id based on the value of 'name' and 'project_id' (this one can be null):
INSERT INTO batches (name,project_id) VALUES
('21.01',NULL),
('21.01',1),
('21.02',2);
So, I have created a table TRIGGER but doesn't execute.
CREATE TRIGGER create_batches_id
AFTER INSERT ON batches FOR EACH ROW
BEGIN
UPDATE batches
SET id = SELECT quote(name ||"_"|| (CASE project_id
WHEN NULL THEN '' ELSE project_id END )
FROM batches WHERE rowid = (SELECT MAX(rowid) FROM batches))
WHERE rowid = (SELECT MAX(rowid) FROM batches);
END;
Error:
SQL Error [1]: [SQLITE_ERROR] SQL error or missing database (near "SELECT": syntax error)
I expect:
id = 21.01_
id = 21.01_1
id = 21.01_2
What am I doing wrong? If I run only the SELECT/CASE statment it returns ok: '21.01_2'
I have also tried without the quote() function, no success.
UPDATE I:
I have managed to execute the whole create trigger statement (parenthesis were missing):
CREATE TRIGGER create_batch_id
AFTER INSERT ON batches FOR EACH ROW
BEGIN
UPDATE batches
SET id = (SELECT name ||"_"|| (CASE project_id WHEN NULL THEN 0 ELSE project_id END ) FROM batches WHERE rowid = (SELECT MAX(rowid) FROM batches) )
WHERE rowid = (SELECT MAX(rowid) FROM batches);
END;
It seems my editor (DBeaver) has a glitch with the following new line character. If it is inside the selection it runs into this exception (or I am missing something):
SQL Error [1]: [SQLITE_ERROR] SQL error or missing database (incomplete input)
If I manually select only the above lines (from CREATE to ;), the trigger is created, however, not the expected result. If value in project_id is NULL, no id value is created.
Don't add the column id in the table.
Instead define the combination of name and project_id as the PRIMARY KEY of the table, so that it is also UNIQUE:
CREATE TABLE batches (
name TEXT NOT NULL,
project_id INTEGER,
PRIMARY KEY(name, project_id)
)
Then, whenever you need that id you can run a query:
SELECT name || '_' || COALESCE(project_id, '') AS id,
name,
project_id
FROM batches
Or create a view:
CREATE VIEW v_batches AS
SELECT name || '_' || COALESCE(project_id, '') AS id,
name,
project_id
FROM batches
and query the view:
SELECT * FROM v_batches
See the demo.
Or if your version of SQLite is 3.31.0+ you can have the column id as a generated column:
CREATE TABLE batches (
name TEXT NOT NULL,
project_id INTEGER,
id TEXT GENERATED ALWAYS AS (name || '_' || COALESCE(project_id, '')),
PRIMARY KEY(name, project_id)
);

INSERT OR REPLACE WHERE ROWID Causes Error

The following code:
$stm = $sql->prepare('INSERT OR REPLACE INTO "vehicle_service_invoice" (
invoice, "date", unit, odometer, sublet, sub, po, categories
) VALUES (
:invoice, :date, :unit, :odometer, :sublet, :sub, :po, :categories
) WHERE rowid = :rowid;'
);
$stm->bindParam(':invoice', $_POST['invoice']);
$stm->bindParam(':date', $_POST['date']);
$stm->bindParam(':unit', $_POST['unit']);
$stm->bindParam(':odometer', $_POST['odometer']);
$stm->bindParam(':sublet', $_POST['sublet']);
$stm->bindParam(':sub', $_POST['sub']);
$stm->bindParam(':po', $_POST['po']);
$stm->bindParam(':categories', $categories);
$stm->bindParam(':rowid', $_POST['rowid']);
$stm->execute();
Produces the following query:
INSERT OR REPLACE INTO "vehicle_service_invoice" (
invoice,
"date",
unit,
odometer,
sublet,
sub,
po,
categories
) VALUES (
7230,
'2013-02-07',
558,
34863,
0,
0,
1486347,
5
) WHERE rowid = 1
That produces the following error:
ERROR: near "WHERE": syntax error.
What I am trying to do is make a single path for both my INSERT and UPDATE logic to follow, so after I found out that I could do INSERT OR REPLACE, I figured I could just update the information based on the ROWID of each item. To me the syntax looks correct, what am I doing wrong?
It should be noted that I don't care about changing ROWID values as I understand that is a tripping point on doing INSERT OR REPLACE statements. Everything is joined together in other queries based off of the INVOICE column. I only want to use the ROWID to refer to that row.
In an INSERT statement, a WHERE clause does not make sense.
The INSERT OR REPLACE works as follows: a record with the specified values is inserted into the table.
If this would result in a UNIQUE constraint violation, the old record is deleted.
To replace a record that might already exist, the colum(s) that identify that record must be part of the values to be inserted.
In other words, you must insert the rowid value:
INSERT OR REPLACE INTO vehicle_service_invoice(rowid, ...) VALUES (1, ...)

Resources