Update only dynamically chosen columns - sql

I've created a function copy_rows_from_table(_tbl regclass) which copies records in a table and gives them a new primary key value. It returns a hstore containing pairs of old_id => new_id. For example for table books my function would create two additional records and return a hstore.
books initially:
id | title | price | author_id | publisher_id
----+---------------+-------+-----------+--------------
1 | The Cyberiad | 15.00 | 23 | 46
2 | The Trial | 10.00 | 12 | 67
books after evaluation of copy_rows_from_table('books'):
id | title | price | author_id | publisher_id
----+---------------+-------+-----------+--------------
1 | The Cyberiad | 15.00 | 23 | 46
2 | The Trial | 10.00 | 12 | 67
3 | The Cyberiad | 15.00 | 23 | 46
4 | The Trial | 10.00 | 12 | 67
returned hstore:
"1"=>"3", "2"=>"4"
It works fine. Now I would like to create a function that copies records from a few tables (passed in an array) and then updates all foreign keys using returned hstore. For example after copying books and authors I want author_id column to be updated in books table. After using my function on books, authors and publishers, if I had a hstore which contains "1"=>"3", "2"=>"4","23"=>"167","12"=>"98","46"=>"87","67"=>"102", my function should update books table in this way:
id | title | price | author_id | publisher_id
----+---------------+-------+-----------+--------------
1 | The Cyberiad | 15.00 | 23 | 46
2 | The Trial | 10.00 | 12 | 67
3 | The Cyberiad | 15.00 | 167 | 87
4 | The Trial | 10.00 | 98 | 102
I came up with something like this:
CREATE OR REPLACE FUNCTION copy_tables(_tbls regclass[])
RETURNS void AS
$func$
DECLARE
_tbl regclass;
_id_pairs hstore;
_table_id_pairs hstore;
_row record;
BEGIN
FOR _tbl IN SELECT _tbls
LOOP
EXECUTE format('SELECT copy_rows_from_table(''%1$s'')', _tbl)
INTO _table_id_pairs;
SELECT COALESCE(_id_pairs, hstore('')) || COALESCE(_table_id_pairs, hstore('')) INTO _id_pairs;
END LOOP;
FOR _tbl IN SELECT _tbls
LOOP
FOR _row IN EXECUTE format('SELECT * FROM %1$s WHERE id = ANY(''%2$s''::uuid[])', _tbl, avals(_id_pairs))
LOOP
EXECUTE (
SELECT format('UPDATE %1$s SET (%2$s) = (%3$s) WHERE id = %4$s'
, _tbl, string_agg(quote_ident(attname), ', '),
string_agg(COALESCE(_id_pairs -> ('_row.' || quote_ident(attname)), '_row.' || quote_ident(attname)), ', '), _row.id)
FROM pg_attribute
WHERE attrelid = _tbl
AND NOT attisdropped
AND attnum > 0
AND attname LIKE '%_id'
);
END LOOP;
END LOOP;
END
$func$
LANGUAGE plpgsql;
But it doesn't quite work. Is there any possibility to update records in a way I explained?

I have finally found a way to do it in PLpgSQL. I just iterate through every column of every record. Here is my working function:
CREATE OR REPLACE FUNCTION copy_tables(_tbls regclass[])
RETURNS void AS
$func$
DECLARE
_id_pairs hstore;
_table_id_pairs hstore;
_row record;
_hs_row record;
BEGIN
FOR I IN array_lower(_tbls, 1)..array_upper(_tbls, 1)
LOOP
EXECUTE format('SELECT copy_rows_from_table(''%1$s'')', _tbls[I])
INTO _table_id_pairs;
SELECT COALESCE(_id_pairs, hstore('')) || COALESCE(_table_id_pairs, hstore('')) INTO _id_pairs;
END LOOP;
FOR I IN array_lower(_tbls, 1)..array_upper(_tbls, 1)
LOOP
FOR _row IN EXECUTE format('SELECT * FROM %1$s WHERE id = ANY(''%2$s''::uuid[])', _tbls[I], avals(_id_pairs))
LOOP
FOR _hs_row IN SELECT kv."key", kv."value" FROM each(hstore(_row)) kv
LOOP
IF _hs_row."value" = ANY(akeys(_id_pairs)) THEN
EXECUTE format('UPDATE %1$s SET %2$s = ''%3$s'' WHERE id = ''%4$s''',
_tbls[I], _hs_row."key", _id_pairs -> _hs_row."value", _row.id);
END IF;
END LOOP;
END LOOP;
END LOOP;
END
$func$
LANGUAGE plpgsql;

PLpgSQL isn't good tool for dynamic updates. Probably there are some few others possibilities, but no one is trivial. The other ways:
Using some more dynamic PL language - PLPythonu, PLPerl
Using extension https://github.com/okbob/pltoolbox - there are functions record_get_fields and record_set_fields

Related

Query table A or table B based on some condition

in an Oracle DB (12) I have 2 tables:
table: STEP_DETAILS
+-----------+---------+---------------+
| record_id | step_id | material_type |
+===========+=========+===============+
| 1 | 1 | in |
+-----------+---------+---------------+
| 2 | 1 | in |
+-----------+---------+---------------+
| 3 | 1 | out |
+-----------+---------+---------------+
| 4 | 2 | in |
+-----------+---------+---------------+
| 5 | 2 | out |
+-----------+---------+---------------+
| 6 | 2 | out |
+-----------+---------+---------------+
table: ACTIONS_DETAILS
+-----------+-----------+---------------+
| record_id | action_id | material_type |
+===========+===========+===============+
| 1 | 11 | in |
+-----------+-----------+---------------+
| 2 | 11 | out |
+-----------+-----------+---------------+
| 3 | 12 | in |
+-----------+-----------+---------------+
| 4 | 12 | out |
+-----------+-----------+---------------+
all id-columns are of type INTEGER.
I need to count the input materials for both tables.
in a PL/SQL block I have the following functions, each has 'almost' the same query:
--count from step_details:
FUNCTION get_step_input_count(p_step_id step_details.step_id%TYPE)
RETURN INTEGER
IS
l_count INTEGER := 0;
BEGIN
SELECT COUNT(1)
INTO l_count
FROM step_details
WHERE step_id = p_step_id
AND material_type = 'in';
RETURN l_count;
END get_step_input_count;
--count from action_details:
FUNCTION get_action_input_count(p_action_id action_details.action_id%TYPE)
RETURN INTEGER
IS
l_count INTEGER := 0;
BEGIN
SELECT COUNT(1)
INTO l_count
FROM action_details
WHERE action_id = p_action_id
AND material_type = 'in';
RETURN l_count;
END get_action_input_count;
is it possible to write one single SELECT-statement that can query one of the 2 tables each time based on some condition, so I will eventually write one function that uses one query instead of 2 functions, something like:
FUNCTION get_input_count(p_parent_id integer,
p_from varchar2)
RETURN INTEGER
IS
l_count INTEGER := 0;
BEGIN
SELECT COUNT(1)
INTO l_count
FROM (when p_from = 'S' then 'step_details'
when p_from = 'A' then 'action_details')
WHERE (when p_from = 'S' then 'step_id = p_parent_id'
when p_from = 'A' then 'action_id = p_parent_id')
AND material_type = 'in';
RETURN l_count;
END get_input_count;
You can try with something like the following:
select sum(num_rows)
from
(
select count(*) as num_rows
from tab1 /* first table */
where :param = 1
union all
select count(*) as num_rows
from tab2 /* second table */
where :param = 2
)
Here you use a single select that wraps the UNION ALL of queries from all the possible tables; every table gives its contribution or not, depending on the value of some parameter, so that you only get the rows from the table you want, based on the parameter value.
One option would be using dynamic SQL composed of concatenations for table and column names, and a bind variable for the value which will be common(p_parent_id) for querying from each tables.
SQL> CREATE OR REPLACE FUNCTION get_input_count(p_parent_id INT, p_from VARCHAR2) RETURN INT IS
l_count INT;
crs SYS_REFCURSOR;
v_sql VARCHAR2(32767);
v_from VARCHAR2(32);
v_col VARCHAR2(99);
BEGIN
SELECT DECODE(p_from,'A','actions_details','S','step_details'),
DECODE(p_from,'A','action_id','S','step_id')
INTO v_from, v_col
FROM dual;
v_sql := 'SELECT COUNT(*)
FROM '||v_from||'
WHERE material_type = ''in'' AND '||v_col||' = :prt_id';
OPEN crs FOR v_sql USING p_parent_id;
LOOP
FETCH crs INTO l_count;
EXIT WHEN crs%NOTFOUND;
END LOOP;
CLOSE crs;
RETURN l_count;
END;
/
Demo
where
initializing the value of l_count variable with zero is redundant,
since the query will return zero without exception whenever no
matching records found
the keyword FUNCTION should be prepended by CREATE [OR REPLACE]
ending the stored function with its name is optional(might be
neglected)
You could also use dynamic SQL like below.
SQL injection should be cared in this case.
sql_statement := 'select count(*)';
IF table = 'xxx'
THEN
sql_statement := sql_statement || ' from xxx where material_type = ''in''';
ELSIF table = 'yyy'
sql_statement := sql_statement || ' from yyy where yyyy_type = ''in''';
END IF;
sql_statement := sql_statement blur blur;
EXECUTE IMMEDIATE sql_statement INTO l_count USING p_1;

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

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;

Oracle Nested Table predicate in where clause

I have a table that supposed to be searched with multiple columns, which can have multiple values
create table t as select * from all_objects;
create bitmap index IDX_DATA_OBJECT_ID on T (DATA_OBJECT_ID);
create bitmap index IDX_LAST_DDL_TIME on T (LAST_DDL_TIME);
create bitmap index IDX_OBJECT_NAME on T (OBJECT_NAME);
create bitmap index IDX_OBJECT_TYPE on T (OBJECT_TYPE);
create or replace type strarray as table of varchar2(4000)
CREATE OR REPLACE PROCEDURE p_search(op_cursor out SYS_REFCURSOR
,a strarray
,b strarray) IS
ca constant number:= a.count;
cb constant number:= b.count;
BEGIN
OPEN op_cursor FOR
SELECT /*+ gather_plan_statistics asda*/ *
FROM t
WHERE object_name IN (SELECT * FROM TABLE(a))
AND object_type IN (SELECT * FROM TABLE(b));
END;
declare
op_cursor sys_refcursor;
c t%rowtype;
begin
p_search(op_cursor,strarray('ICOL$'),strarray('TABLE'));
loop
fetch op_cursor into c;
exit when op_cursor%notfound;
end loop;
end;
-----------------------------------------------------------------
| Id | Operation | Name |
-----------------------------------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | HASH JOIN SEMI | |
| 2 | NESTED LOOPS | |
| 3 | NESTED LOOPS | |
| 4 | SORT UNIQUE | |
| 5 | COLLECTION ITERATOR PICKLER FETCH| |
| 6 | BITMAP CONVERSION TO ROWIDS | |
|* 7 | BITMAP INDEX SINGLE VALUE | IDX_OBJECT_NAME |
| 8 | TABLE ACCESS BY INDEX ROWID | T |
| 9 | COLLECTION ITERATOR PICKLER FETCH | |
-----------------------------------------------------------------
It looks fine to me as it does index lookup on more selective column.
But I also have a requirement to search for all values if argument is not passed and I am really stuck with that.
The main question I think is how to write sql to search a table by multiple columns with multiple possible values in these columns? I want to be able to take advantage of bitmap indexes.
Should I maybe stick to Dynamic SQL for such task?
UPDATE.
That is how I currently solved it for the moment.
create context my_ctx using p_search;
CREATE OR REPLACE FUNCTION in_list(p_string IN VARCHAR2) RETURN strarray AS
l_string LONG DEFAULT p_string || ',';
l_data strarray := strarray();
n NUMBER;
BEGIN
LOOP
EXIT WHEN l_string IS NULL;
n := instr(l_string, ',');
l_data.extend;
l_data(l_data.count) := ltrim(rtrim(substr(l_string, 1, n - 1)));
l_string := substr(l_string, n + 1);
END LOOP;
RETURN l_data;
END;
CREATE OR REPLACE PROCEDURE p_search(op_cursor OUT SYS_REFCURSOR
,a VARCHAR2
,b VARCHAR2) IS
l VARCHAR2(4000);
BEGIN
l := 'SELECT /*+ gather_plan_statistics lvv3*/
*
FROM t
WHERE 1=1';
IF a IS NOT NULL
THEN
dbms_session.set_context('MY_CTX', 'OBJ_NAME', a);
l := l || ' and t.object_name in (select /*+ cardinality (objn 5)*/ * from table(cast(in_list(sys_context( ''MY_CTX'',''OBJ_NAME'' )) as strarray)
) objn
)';
END IF;
IF b IS NOT NULL
THEN
dbms_session.set_context('MY_CTX', 'OBJ_TYPE', b);
l := l || ' and t.object_type in (select /*+ cardinality (objt 5)*/ * from table(cast(in_list(sys_context( ''MY_CTX'',''OBJ_TYPE'' )) as strarray)
) objt
)';
END IF;
OPEN op_cursor FOR l;
dbms_session.clear_context('MY_CTX');
END;