Add new column with Boolean in PL/SQL - sql

I'm learning PL/SQL right now and I have a doubt.
I have created the following table called tbProducts:
CREATE TABLE tbProducts (
nIDProduct NUMBER(2) NOT NULL,
vDescription VARCHAR2(20 CHAR),
nQuantity NUMBER(3),
nPrice NUMBER(6,2),
dLastDate DATE)
And I have inserted some values so the table is like this:
nIDProduct | vDescription | nQuantity | nPrice | dLastDate
1 | 'Hammer' | 50 | 3.25 | 13-MAY-2021
2 | 'Nails' | 100 | 0.75 | 28-AUG-2021
3 | 'Screws' | 250 | 0.16 | 21-JUL-2021
Now what I'm looking for is a boolean variable that can be called bUpdate that returns FALSE if today's date (26-AUG-2021) is greater than dLastDate and returns TRUE if it's less or equal so the table would look like this:
nIDProduct | vDescription | nQuantity | nPrice | dLastDate | bUpdate
1 | 'Hammer' | 50 | 3.25 | 13-MAY-2021 | FALSE
2 | 'Nails' | 100 | 0.75 | 28-AUG-2021 | TRUE
3 | 'Screws' | 250 | 0.16 | 21-JUL-2021 | FALSE
I am trying doing the following:
DECLARE
bUpdate BOOLEAN;
BEGIN
SELECT t.*, bUpdate(
IF SYSDATE > dLastDate THEN
bUpdate := FALSE;
ELSE
bUpdate := TRUE;
END IF
FROM tbProducts t
;
END;
I get an error saying that a FROM was expected after the SELECT statement.
Since I'm still learning I don't know what it's wrong in this statement, could someone help me? Is there a way to do it with a CURSOR too?
Thank you all!

What you try wouldn't work. If you want an additional column, you need to add the column to the table with an ALTER TABLE command - but in this case you're adding a non-deterministic expression and that cannot be added as a virtual column.
The easiest way to achieve what you want is to create a view on top of the table with the case statement as illustrated below:
CREATE TABLE tbproducts (
nidproduct NUMBER(2) NOT NULL,
vdescription VARCHAR2(20 CHAR),
nquantity NUMBER(3),
nprice NUMBER(6,2),
dlastdate DATE);
INSERT INTO tbproducts (nidproduct,vdescription,nquantity,nprice,dlastdate) VALUES (1,'Hammer', 50,3.25,TO_DATE('13-MAY-2021','DD-MON-YYYY'));
INSERT INTO tbproducts (nidproduct,vdescription,nquantity,nprice,dlastdate) VALUES (2, 'Nails',100,0.75,TO_DATE('28-AUG-2021','DD-MON-YYYY'));
INSERT INTO tbproducts (nidproduct,vdescription,nquantity,nprice,dlastdate) VALUES (3,'Screws',250,0.16,TO_DATE('21-JUL-2021','DD-MON-YYYY'));
CREATE VIEW tbproducts_v
AS
SELECT
nidproduct
,vdescription
,nquantity
,nprice
,dlastdate
,CASE WHEN SYSDATE > dlastdate THEN 'TRUE' ELSE 'FALSE' END as status
FROM tbproducts;
select * from tbproducts_v;
NIDPRODUCT VDESCRIPTION NQUANTITY NPRICE DLASTDATE STATU
---------- -------------------- ---------- ---------- ----------- -----
1 Hammer 50 3.25 13-MAY-2021 TRUE
2 Nails 100 .75 28-AUG-2021 FALSE
3 Screws 250 .16 21-JUL-2021 TRUE
If you insist on adding a new column then this is what you'd do.
ALTER TABLE tbproducts ADD status VARCHAR2(100);
UPDATE tbproducts t
SET (t.status) =
(SELECT
CASE WHEN SYSDATE > dlastdate THEN 'TRUE' ELSE 'FALSE' END
FROM tbproducts st
WHERE st.nidproduct = t.nidproduct);
if you just want to display the true/false in the console with a pl/sql block then this is an option (using implicit cursor for loop):
set serveroutput on size 999999
clear screen
DECLARE
BEGIN
FOR r IN (SELECT * FROM tbproducts) LOOP
dbms_output.put_line('Product '||r.vdescription ||', status: '||CASE WHEN SYSDATE > r.dlastdate THEN 'TRUE' ELSE 'FALSE' END);
IF r.dlastdate < SYSDATE THEN
dbms_output.put_line('Product '||r.vdescription ||' is expired !');
END IF;
END LOOP;
END;
/
note 1 : pl/sql has a boolean datatype, but sql does NOT have one. So in your table you'll need to store a string (1/0, T/F, Y/N, TRUE/FALSE).
note 2 : it's not a good practice to use camelcase in table names or column names. If you're creating them without quotes the names are case insensitive anyway.

Related

How to AUTOMATICALLY update a value of a column based on condition on another table's column?

So basically, I'm using Postgresql and what I want to do is this:
Say, we have 2 tables, the inventory and buyList
create table inventory
(item_id serial primary key,
name text not null,
quantity int not null,
price int not null);
insert into inventory values
(1,'a',44,10000),
(2,'b',12,12000),
(3,'c',11,5000),
(4,'d',6,3000);
create table buyList
(buy_id serial primary key,
item_id not null references inventory(item_id),
quantity int not null);
insert into buyList values
(1,2,4),
(2,2,5),
(3,1,1);
so I want to have the inventory.quantity value to be subtracted by the buyList.quantity of relevant item (based of item_id ofcourse)
for example, when there is someone who buy 4 of item 'a', then the value of item 'a' quantity column in table inventory will be 40 (automatically updated).
EDIT :
THANKS A LOT to krithikaGopalakrisnan for the answer,
so I use the trigger made by krithikaGopalakrisnan (and modified it a little)
CREATE OR REPLACE FUNCTION trigger() RETURNS trigger AS $$
BEGIN
UPDATE inventory SET quantity = quantity-NEW.quantity WHERE inventory.item_id = NEW.item_id ;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
DECLARE
BEGIN
EXECUTE format('CREATE TRIGGER trigger BEFORE INSERT OR UPDATE ON buylist FOR EACH ROW WHEN (pg_trigger_depth() = 0) EXECUTE PROCEDURE trigger()');
END;
$$ LANGUAGE plpgsql;
But now a new problem arises, when the quantity of the item in inventory table (inventory.quantity) is 0, and there is a new purchase of that item in the buylist table, the inventory.quantity of that item becomes a negative number! (of course we can't have that), how do I fix this so that when the item quantity is 0 in the inventory table, the buylist table can't accept another tuple indicating someone buying that item (maybe a function to return error message or something)
thanks in advance, I am still a total novice so I will really appreciate any help and guidance from you guys.
A trigger is what u need..
CREATE FUNCTION trigger() RETURNS trigger AS $$
BEGIN
UPDATE inventory SET quantity = NEW.quantity WHERE inventory.item_id = NEW.item_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
DECLARE
BEGIN
EXECUTE format('CREATE TRIGGER trigger BEFORE INSERT OR UPDATE ON buylist FOR EACH ROW WHEN (pg_trigger_depth() = 0) EXECUTE PROCEDURE trigger()');
END;
$$ LANGUAGE plpgsql;
Sample data:
postgres=# select * from inventory;
item_id | name | quantity | price
---------+------+----------+-------
1 | a | 44 | 10000
2 | b | 12 | 12000
3 | c | 11 | 5000
4 | d | 6 | 3000
(4 rows)
postgres=# select * from buylist;
buy_id | item_id | quantity
--------+---------+----------
1 | 2 | 4
2 | 2 | 5
3 | 1 | 1
(3 rows)
postgres=# update buylist set quantity=4 where item_id=1;
postgres=# select * from inventory;
item_id | name | quantity | price
---------+------+----------+-------
2 | b | 12 | 12000
3 | c | 11 | 5000
4 | d | 6 | 3000
1 | a | 40 | 10000
Hope it helps

Is there a way to TRIM all data in a SELECT * FROM statement?

I am trying to select and trim all the entries from a table using the following statement:
SELECT TRIM(*) FROM TABLE
But I get an error. Is there a way to return all entries selected so they are trimmed for blank characters at the beginning and end of each string?
You need to specify each string column by hand:
SELECT TRIM(col1), --LTRIM(RTRIM(...)) If RDBMS is SQL Server
TRIM(col2),
TRIM(col3),
TRIM(col4)
-- ...
FROM table
There is another problem with your proposal. * is placeholder for each column in table so there will be problem with trimming date/decimal/spatial data ....
Addendum
Using Oracle 18c Polymorphic Table Functions(provided code is just PoC, there is a space for a lot of improvements):
CREATE TABLE tab(id INT, d DATE,
v1 VARCHAR2(100), v2 VARCHAR2(100), v3 VARCHAR2(100) );
INSERT INTO tab(id, d,v1, v2, v3)
VALUES (1, SYSDATE, ' aaaa ', ' b ', ' c');
INSERT INTO tab(id, d,v1, v2, v3)
VALUES (2, SYSDATE+1, ' afasd', ' ', ' d');
COMMIT;
SELECT * FROM tab;
-- Output
.----.-----------.-----------.-----------.-----.
| ID | D | V1 | V2 | V3 |
:----+-----------+-----------+-----------+-----:
| 1 | 02-MAR-18 | aaaa | b | c |
:----+-----------+-----------+-----------+-----:
| 2 | 03-MAR-18 | afasd | | d |
'----'-----------'-----------'-----------'-----'
And table function:
CREATE OR REPLACE PACKAGE ptf AS
FUNCTION describe(tab IN OUT dbms_tf.table_t)RETURN dbms_tf.describe_t;
PROCEDURE FETCH_ROWS;
END ptf;
/
CREATE OR REPLACE PACKAGE BODY ptf AS
FUNCTION describe(tab IN OUT dbms_tf.table_t) RETURN dbms_tf.describe_t AS
new_cols DBMS_TF.COLUMNS_NEW_T;
BEGIN
FOR i IN 1 .. tab.column.count LOOP
IF tab.column(i).description.type IN ( dbms_tf.type_varchar2) THEN
tab.column(i).pass_through:=FALSE;
tab.column(i).for_read:= TRUE;
NEW_COLS(i) :=
DBMS_TF.COLUMN_METADATA_T(name=> tab.column(i).description.name,
type => tab.column(i).description.type);
END IF;
END LOOP;
RETURN DBMS_TF.describe_t(new_columns=>new_cols, row_replication=>true);
END;
PROCEDURE FETCH_ROWS AS
inp_rs DBMS_TF.row_set_t;
out_rs DBMS_TF.row_set_t;
rows PLS_INTEGER;
BEGIN
DBMS_TF.get_row_set(inp_rs, rows);
FOR c IN 1 .. inp_rs.count() LOOP
FOR r IN 1 .. rows LOOP
out_rs(c).tab_varchar2(r) := TRIM(inp_rs(c).tab_varchar2(r));
END LOOP;
END LOOP;
DBMS_TF.put_row_set(out_rs, replication_factor => 1);
END;
END ptf;
And final call:
CREATE OR REPLACE FUNCTION trim_col(tab TABLE)
RETURN TABLE pipelined row polymorphic USING ptf;
SELECT *
FROM trim_col(tab); -- passing table as table function argument
.----.-----------.-------.-----.----.
| ID | D | V1 | V2 | V3 |
:----+-----------+-------+-----+----:
| 1 | 02-MAR-18 | aaaa | b | c |
:----+-----------+-------+-----+----:
| 2 | 03-MAR-18 | afasd | - | d |
'----'-----------'-------'-----'----'
db<>fiddle demo

Audit operations with a trigger

I have a trigger that fires in certain conditions, and when I update some data in EMPLOYEES table (specifically when inserting, deleting and updating comm_pct and salary) the changes that were made are registered into the following table:
CREATE TABLE "HR"."AUDIT_E" ("USR" VARCHAR2(30 BYTE) DEFAULT USER,
"DATE" DATE DEFAULT SYSDATE,
"DML_TYPE" VARCHAR2), -- UPDATE, INSERT, DELETE
"OLD_EMPLOYEE_ID" NUMBER,
"OLD_FIRST_NAME" VARCHAR2,
...,--more fields
"OLD_JOB_ID" VARCHAR2,
"OLD_SALARY" NUMBER,
"OLD_COMMISSION_PCT" NUMBER,
"NEW_FIRST_NAME" VARCHAR2,
..., -- more fields!
"NEW_JOB_ID" VARCHAR2,
"NEW_SALARY" NUMBER,
"NEW_COMMISSION_PCT" NUMBER)
My question is: How can I do an INSERT in AUDIT_E (Because I must register old and new values into it) when updating rows with another values (as email with comm_pct and other fields, besides only updating comm_pct and salary)? Because my trigger has the following structure:
IF DELETING THEN
--some actions
-- Insert into AUDIT_E(...) values...
ELSIF INSERTING THEN
--some actions
-- Insert into AUDIT_E(...) values...
ELSIF UPDATING ('a field') THEN --I have two of these
--some actions
-- Insert into AUDIT_E(...) values...comm_pct/salary
Thank you very much if you can help me and sorry for my english.
EDIT: My trigger runs fine registring changes into audit_e when I am inserting, deleting rows and updating only comm_pct and salary:
AUDIT_E:
ID |Oper|Old_Name|Old_job_id|Old_comm_pct|Old_Salary|New_name|New_job_id|New_comm_pct|New_salary
------------------------------------------------------------------------------------------------
1 |Ins | NULL | NULL | NULL | NULL |Kappa | SA_REP | 0.2 | 4980
2 |Upd | Kappa | SA_REP | 0.2 | 4980 | NULL | NULL | 0.3 | NULL
3 |Upd | Kappa | SA_REP | 0.3 | 4980 | NULL | NULL | NULL | 5000
4 |Del | Kappa | SA_REP | 0.3 | 4980 | NULL | NULL | NULL | NULL
But when I am changing the job_id for example (putting an additional elsif update), the changes are saved into audit_e wrong:
AUDIT_E:
ID |Oper|Old_Name|Old_job_id|Old_comm_pct|Old_Salary|New_name|New_job_id|New_comm_pct|New_salary
------------------------------------------------------------------------------------------------
1 |Upd |Kappa | SA_REP | 0.2 | 4980 | NULL | NULL | NULL | NULL
And I want those changes saved into audit_e table like this:
AUDIT_E:
ID |Oper|Old_Name|Old_job_id|Old_comm_pct|Old_Salary|New_name|New_job_id|New_comm_pct|New_salary
------------------------------------------------------------------------------------------------
1 |Upd |Kappa | SA_REP | 0.2 | 4980 | NULL | IT_PROG | NULL | NULL
I'm still not sure that I understand the question. My guess is that you just want something like
CREATE OR REPLACE TRIGGER trigger_name
AFTER INSERT OR UPDATE OR DELETE
ON employees
FOR EACH ROW
BEGIN
INSERT INTO audit_e( dml_type,
old_employee_id, new_employee_id,
old_first_name, new_first_name,
...
)
VALUES( CASE WHEN deleting
THEN 'D'
WHEN inserting
THEN 'I'
WHEN updating
THEN 'U'
ELSE 'X'
END,
:old.employee_id, :new.employee_id,
:old.first_name, :new.first_name,
...
);
END;
That said, it's not clear to me why you would bother storing the old and new data in your audit table each time. The old_* values are always going to be identical to the new_ values from the prior row. It generally makes sense to just store the new_ values in the audit table.

Get all siblings in SQL tree

I have to handle a table PRODUCTS which is created to accommodate tree structure of products. It is done to handle situations when one product can contain several others (e.g. one package product holds several other positions). So, I'm making a function that takes OrderDetails, and it must iterate through all PRODUCTS and list out the child products for each product listed. I am facing an issue that I have to iterate through tree of unknown depth. Please, give me an idea how to do it.
I've implemented it in the table below with the function listed along with it. But in that solution the depth of listing is limited to 1, and what i want to do is to fetch all depth of the tree.
Here is the code:
CREATE OR REPLACE FUNCTION foo()RETURNS text AS
$body$
DECLARE _row RECORD;
_result text := '';
_child_row RECORD;
_count integer := -1;
_marker integer := 1;
BEGIN
FOR _row IN SELECT * FROM tree_products
LOOP
_result := _result || _marker || ' ' || _row.name;
_count := (SELECT count(product_id) FROM tree_products WHERE parent_id = _row.product_id);
IF _count > 0 THEN
FOR _child_row IN SELECT * FROM tree_products WHERE parent_id = _row.product_id
LOOP
_result := _result || ' ' || _child_row.name;
END LOOP;
END IF;
_marker := _marker =1;
END LOOP;
END;
$body$
LANGUAGE plpgsql
UPD Done this usign WITH CTE, but the groupiing problem occured:
CREATE OR REPLACE FUNCTION public.__foo (
)
RETURNS SETOF refcursor AS
$body$
DECLARE _returnvalue refcursor;
_q text;
BEGIN
_q :='
WITH RECURSIVE r_p (product_id, name, parent_id) AS -- 1
(SELECT t_p.product_id, t_p.name , t_p.parent_id -- 2
FROM tree_products t_p
WHERE t_p.product_id = 1
UNION ALL
SELECT t_c.product_id, t_c.name, t_c.parent_id -- 3
FROM r_p t_p, tree_products t_c
WHERE t_c.parent_id = t_p.product_id)
SELECT product_id, name, parent_id -- 4
FROM r_p;';
OPEN _returnvalue FOR EXECUTE (_q);
RETURN NEXT _returnvalue;
END
$body$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER
COST 100 ROWS 1000;
I want to sibling products be under their respectiveparents, I wonder how to write Grouping statement...
UPD Sorry, the definition of the tree_products is the following:
CREATE TABLE public.tree_products (
product_id INTEGER DEFAULT nextval('ree_products_product_id_seq'::regclass) NOT NULL,
name VARCHAR,
parent_id INTEGER,
CONSTRAINT ree_products_pkey PRIMARY KEY(product_id)
)
WITH (oids = false);
UPD: SAMPLE OUTPUT:
product_id | name | parent_id
---------------------------------------
1 | promo | NULL
3 | fork | 1
4 | spoon | 1
6 | can | 1
10 | big can | 3
11 | small can | 4
12 | large spoon | 6
13 | mega fork | 3
14 | super duper | 6
DESIRED OUTPUT:
product_id | name | parent_id
---------------------------------------
1 | promo | NULL
3 | fork | 1
10 | big can | 3
13 | mega fork | 3
4 | spoon | 1
11 | small can | 4
6 | can | 1
12 | large spoon | 6
14 | super duper | 6
So, the fetched table has structure of the real tree, like the follwing:
- promo
- fork
- big can
- mega fork
- spoon
- small can
- can
- large can
- super duper
This SQLFiddle traverses the tree top-down, keeping an list of parent row numbers in an array, essentially a "parent row position list".
It then sorts the results by the parent-list.
WITH RECURSIVE tree(product_id, name, parentlist) AS (
SELECT product_id, name, ARRAY[ row_number() OVER (ORDER BY product_id) ]
FROM tree_products
WHERE parent_id IS NULL
UNION
SELECT tp.product_id, tp.name, array_append(parentlist, row_number() OVER (ORDER BY tp.product_id))
FROM tree_products tp
INNER JOIN tree t
ON (tp.parent_id = t.product_id)
)
SELECT *
FROM tree
ORDER BY parentlist;

How to substring and join with another table with the substring result

I have 2 tables: errorlookup and errors.
errorlookup has 2 columns: codes and description.
The codes are of length 2.
errors has 2 columns id and errorcodes.
The errorcodes are of length 40 meaning they code store 20 error codes for each id.
I need to display all the description associated with the id by substring the errorcodes and matching with code in errorlookup table.
Sample data for errorlookup:
codes:description
12:Invalid
22:Inactive
21:active
Sample data for errors:
id:errorcodes
1:1221
2:2112
3:1222
I cant use LIKE as it would result in too many errors. I want the errorcodes column to be broken down into strings of length 2 and then joined with the errorlookup.
How can it be done?
If you really cannot alter the tables structure, here's another approach:
Create an auxilary numbers table:
CREATE TABLE numbers
( i INT NOT NULL
, PRIMARY KEY (i)
) ;
INSERT INTO numbers VALUES
( 1 ) ;
INSERT INTO numbers VALUES
( 2 ) ;
--- ...
INSERT INTO numbers VALUES
( 100 ) ;
Then you could use this:
SELECT err.id
, err.errorcodes
, num.i
, look.codes
, look.descriptionid
FROM
( SELECT i, 2*i-1 AS pos --- odd numbers
FROM numbers
WHERE i <= 20 --- 20 pairs
) num
CROSS JOIN
errors err
JOIN
errorlookup look
ON look.codes = SUBSTR(err.errorcodes, pos, 2)
ORDER BY
err.errorcodes
, num.i ;
Test at: SQL-Fiddle
ID ERRORCODES I CODES DESCRIPTIONID
1 1221 1 12 Invalid
1 1221 2 21 Active
3 1222 1 12 Invalid
3 1222 2 22 Inactive
2 2112 1 21 Active
2 2112 2 12 Invalid
I think the cleanest solution is to "normalize" your errocodes table using a PL/SQL function. That way you can keep the current (broken) table design, but still access its content as if it was properly normlized.
create type error_code_type as object (id integer, code varchar(2))
/
create or replace type error_table as table of error_code_type
/
create or replace function unnest_errors
return error_table pipelined
is
codes_l integer;
i integer;
one_row error_code_type := error_code_type(null, null);
begin
for err_rec in (select id, errorcodes from errors) loop
codes_l := length(err_rec.errorcodes);
i := 1;
while i < codes_l loop
one_row.id := err_rec.id;
one_row.code := substr(err_rec.errorcodes, i, 2);
pipe row (one_row);
i := i + 2;
end loop;
end loop;
end;
/
Now with this function you can do something like this:
select er.id, er.code, el.description
from table(unnest_errors) er
join errorlookup el on el.codes = er.code;
You can also create a view based on the function to make the statements a bit easier to read:
create or replace view normalized_errorcodes
as
select *
from table(unnest_errors);
Then you can simply reference the view in the real statement.
(I tested this on 11.2 but I believe it should work on 10.x as well)
I think you're on the right track with LIKE. MySQL has an RLIKE function that allows matching by regular expression (I don't know if it's present in Oracle.) You could use errorlookup.code as a pattern to match against errors.errorcodes. The (..)* pattern is used to prevent things like "1213" from matching, for example, "21".
SELECT *
FROM error
JOIN errorlookup
WHERE errorcodes RLIKE CONCAT('^(..)*',code)
ORDER BY id;
+------+----------+------+
| id | errorcode| code |
+------+----------+------+
| 1 | 11 | 11 |
| 2 | 1121 | 11 |
| 2 | 1121 | 21 |
| 3 | 21313245 | 21 |
| 3 | 21313245 | 31 |
| 3 | 21313245 | 32 |
| 4 | 21 | 21 |
+------+----------+------+