I am creating a function to delete a product category from a database. First it SELECTs all child tables that inherit from the table to be deleted from, and will return JSON object of table names if child categories depend on it. What's the most efficient way to fill array with values from SELECT query?
CREATE OR REPLACE FUNCTION delete_category(catid INT)
RETURNS json AS $$
DECLARE
depend "catalog";
dependlist "catalog"[];
BEGIN
FOR depend IN SELECT * FROM catalog LOOP
dependlist:=dependlist || depend;
END LOOP;
END;
$$ LANGUAGE plpgsql;
Using append || operator is pretty slow way - on some older PostgreSQL versions it is terrible slow. You can use two possibilities - mentioned array_agg or ARRAY(SUBSELECT) constructor:
dependlist := ARRAY(SELECT depend FROM catalog);
or
dependlist := (SELECT array_agg(depend) FROM catalog);
or
SELECT array_agg(depend) FROM catalog INTO dependlist;
The performance should be same for all possibilities.
I used the array_agg() function, and this worked fine. Only thing I am unsure of is why I added depend after table name in FROM?
CREATE OR REPLACE FUNCTION delete_category(catid INT)
RETURNS jsonb AS $$
DECLARE
--depend "catalog";
dependlist "catalog"[];
cnt INT;
BEGIN
--Check no other tables dependent
SELECT array_agg(depend) INTO dependlist FROM catalog depend
WHERE inherit_from=catid;
IF array_length(dependlist,1) IS NOT NULL THEN
RETURN jsonb_build_object('error',
'children','tables',to_jsonb(dependlist));
END IF;
--Check table is empty of products
SELECT COUNT(*) INTO cnt FROM (SELECT tablename
FROM catalog WHERE catalogid=catid) AS tname;
RETURN jsonb_build_object('error','none');
END;
$$ LANGUAGE plpgsql;
Related
I am trying to have a dynamic variable that I can specify different column's with (depending on some if statements). Explained in code, I am trying to replace this:
IF (TG_TABLE_NAME='this') THEN INSERT INTO table1 (name_id) VALUES id.NEW END IF;
IF (TG_TABLE_NAME='that') THEN INSERT INTO table1 (lastname_id) VALUES id.NEW END IF;
IF (TG_TABLE_NAME='another') THEN INSERT INTO table1 (age_id) VALUES id.NEW END IF;
With this:
DECLARE
varName COLUMN;
BEGIN
IF (TG_TABLE_NAME='this') THEN varName = 'name_id';
ELSE IF (TG_TABLE_NAME='that') THEN varName = 'lastname_id';
ELSE (TG_TABLE_NAME='another') THEN varName = 'age_id';
END IF;
INSERT INTO table1 (varName) VALUES id.NEW;
END;
The INSERT string is just an example, it's actually something longer. I am a beginner at pgSQL. I've seen some examples but I'm only getting more confused. If you can provide an answer that is also more safe from SQL injection that would be awesome.
One way to do what you're looking for is to compose your INSERT statement dynamically based on the named table. The following function approximates the logic you laid out in the question:
CREATE OR REPLACE FUNCTION smart_insert(table_name TEXT) RETURNS VOID AS $$
DECLARE
target TEXT;
statement TEXT;
BEGIN
CASE table_name
WHEN 'this' THEN target := 'name_id';
WHEN 'that' THEN target := 'lastname_id';
WHEN 'another' THEN target := 'age_id';
END CASE;
statement :=
'INSERT INTO '||table_name||'('||target||') VALUES (nextval(''id''));';
EXECUTE statement;
END;
$$ LANGUAGE plpgsql;
Note that I'm using a sequence to populate these tables (the call to nextval). I'm not sure if that is your use case, but hopefully this example is extensible enough for you to modify it to fit your scenario. A contrived demo:
postgres=# SELECT smart_insert('this');
smart_insert
--------------
(1 row)
postgres=# SELECT smart_insert('that');
smart_insert
--------------
(1 row)
postgres=# SELECT name_id FROM this;
name_id
---------
101
(1 row)
postgres=# SELECT lastname_id FROM that;
lastname_id
-------------
102
(1 row)
Your example doesn't make a lot of sense. Probably over-simplified. Anyway, here is a trigger function for the requested functionality that inserts the new id in a selected column of a target table, depending on the triggering table:
CREATE OR REPLACE FUNCTION smart_insert(table_name TEXT)
RETURNS trigger AS
$func$
BEGIN
EXECUTE
'INSERT INTO table1 ('
|| CASE TG_TABLE_NAME
WHEN 'this' THEN 'name_id'
WHEN 'that' THEN 'lastname_id'
WHEN 'another' THEN 'age_id'
END CASE
||') VALUES ($1)'
USING NEW.id;
END
$func$ LANGUAGE plpgsql;
To refer to the id column of the new row, use NEW.id not id.NEW.
To pass a value to dynamic code, use the USING clause of EXECUTE. This is faster and more elegant, avoids casting to text and back and also makes SQL injection impossible.
Don't use many variables and assignments in plpgsql, where this is comparatively expensive.
If the listed columns of the target table don't have non-default column defaults, you don't even need dynamic SQL:
CREATE OR REPLACE FUNCTION smart_insert(table_name TEXT)
RETURNS trigger AS
$func$
BEGIN
INSERT INTO table1 (name_id, lastname_id, age_id)
SELECT CASE WHEN TG_TABLE_NAME = 'this' THEN NEW.id END
, CASE WHEN TG_TABLE_NAME = 'that' THEN NEW.id END
, CASE WHEN TG_TABLE_NAME = 'another' THEN NEW.id END;
END
$func$ LANGUAGE plpgsql;
A CASE expression without ELSE clause defaults to NULL, which is the default column default.
Both variants are safe against SQL injection.
I'm trying to load some data from CSV using the postgresql COPY command. The trick is that I'd like to implement multi-tenancy on a userid (which is contained in the CSV). Is there an easy way to tell the postgres copy command to filter based on this userid when loading the csv?
i.e. all rows with userid=x go to schema=x, rows with userid=y go to schema=y.
There is not a way of doing this with just the COPY command, but you could copy all your data into a master table, and then put together a simple PL/PGSQL function that does this for you. Something like this -
CREATE OR REPLACE FUNCTION public.spike()
RETURNS void AS
$BODY$
DECLARE
user_id integer;
destination_schema text;
BEGIN
FOR user_id IN SELECT userid FROM master_table GROUP BY userid LOOP
CASE user_id
WHEN 1 THEN
destination_schema := 'foo';
WHEN 2 THEN
destination_schema := 'bar';
ELSE
destination_schema := 'baz';
END CASE;
EXECUTE 'INSERT INTO '|| destination_schema ||'.my_table SELECT * FROM master_table WHERE userid=$1' USING user_id;
-- EXECUTE 'DELETE FROM master_table WHERE userid=$1' USING user_id;
END LOOP;
TRUNCATE TABLE master_table;
RETURN;
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE
COST 100;
This gets all unique user_ids from the master_table, uses a CASE statement to determine the destination schema, and then executes an INSERT SELECT to move rows, and finally deletes the moved rows.
I have plpgsql function:
CREATE OR REPLACE FUNCTION test() RETURNS VOID AS
$$
DECLARE
my_row my_table%ROWTYPE;
BEGIN
SELECT * INTO my_row FROM my_table WHERE id='1';
my_row.date := now();
END;
$$ LANGUAGE plpgsql;
I would like to know if it's possible to directly UPDATE my_row record.
The only way I've found to do it now is:
UPDATE my_table SET date=now() WHERE id='1';
Note this is only an example function, the real one is far more complex than this.
I'm using PostgreSQL 9.2.
UPDATE:
Sorry for the confusion, what I wanted to say is:
SELECT * INTO my_row FROM my_table INTO my_row WHERE id='1';
make_lots_of_complicated_modifications_to(my_row, other_complex_parameters);
UPDATE my_row;
I.e. Use my_row to persist information in the underlying table. I have lots of parameters to update.
I would like to know if it's possible to directly update "my_row"
record.
It is.
You can update columns of a row or record type in plpgsql - just like you have it. It should be working, obviously?
This would update the underlying table, of course, not the variable!
UPDATE my_table SET date=now() WHERE id='1';
You are confusing two things here ...
Answer to clarification in comment
I don't think there is syntax in PostgreSQL that can UPDATE a whole row. You can UPDATE a column list, though. Consider this demo:
Note how I use thedate instead of date as column name, date is a reserved word in every SQL standard and a type name in PostgreSQL.
CREATE TEMP TABLE my_table (id serial, thedate date);
INSERT INTO my_table(thedate) VALUES (now());
CREATE OR REPLACE FUNCTION test_up()
RETURNS void LANGUAGE plpgsql AS
$func$
DECLARE
_r my_table;
BEGIN
SELECT * INTO _r FROM my_table WHERE id = 1;
_r.thedate := now()::date + 5 ;
UPDATE my_table t
-- explicit list of columns to be to updated
SET (id, thedate) = (_r.id, _r.thedate)
WHERE t.id = 1;
END
$func$;
SELECT test_up();
SELECT * FROM my_table;
However, you can INSERT a whole row easily. Just don't supply a column list for the table (which you normally should, but in this case it is perfectly ok, not to).
As an UPDATE is internally a DELETE followed by an INSERT anyway, and a function automatically encapsulates everything in a transaction, I don't see, why you couldn't use this instead:
CREATE OR REPLACE FUNCTION x.test_ delins()
RETURNS void LANGUAGE plpgsql AS
$func$
DECLARE
_r my_table;
BEGIN
SELECT * INTO _r
FROM my_table WHERE id = 1;
_r.thedate := now()::date + 10;
DELETE FROM my_table t WHERE t.id = 1;
INSERT INTO my_table SELECT _r.*;
END
$func$;
I managed to get this working in PLPGSQL in a couple of lines of code.
Given a table called table in a schema called example, and a record of the same type declared as _record, you can update all the columns in the table to match the record using the following hack:
declare _record example.table;
...
-- get the columns in the correct order, as a string
select string_agg(format('%I', column_name), ',' order by ordinal_position)
into _columns
from information_schema.columns
where table_schema='example' and table_name='table';
execute 'update example.table set (' || _columns || ') = row($1.*) where pkey=$2'
using _record, _record.pkey;
In the above example, of course, _record.pkey is the table's primary key.
Postgresql has not set row in update.
If you wont update full row you should assign value for each column separately
yes, its possible to update / append the row-type variable,
CREATE OR REPLACE FUNCTION test() RETURNS VOID AS $$
DECLARE
my_row my_table%ROWTYPE;
BEGIN
SELECT * INTO my_row FROM my_table WHERE id='1';
my_row.date := now();
raise notice 'date : %; ',my_row.date;
END;
$$ LANGUAGE plpgsql;
here the raise notice will display the today's date only.
but this will not update the column date in my_table.
I have the following script that I want output to the screen from.
CREATE OR REPLACE FUNCTION randomnametest() RETURNS integer AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT * FROM my_table LOOP
SELECT levenshtein('mystring',lower('rec.Name')) ORDER BY levenshtein;
END LOOP;
RETURN 1;
END;
$$ LANGUAGE plpgsql;
I want to get the output of the levenshein() function in a table along with the rec.Name. How would I do that? Also, it is giving me an error about the line where I call levenshtein(), saying that I should use perform instead.
Assuming that you want to insert the function's return value and the rec.name into a different table. Here is what you can do (create the table new_tab first)-
SELECT levenshtein('mystring',lower(rec.Name)) AS L_val;
INSERT INTO new_tab (L_val, rec.name);
The usage above is demonstrated below.
I guess, you can use RAISE INFO 'This is %', rec.name; to view the values.
CREATE OR REPLACE FUNCTION randomnametest() RETURNS integer AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT * FROM my_table LOOP
SELECT levenshtein('mystring',lower(rec.Name))
AS L_val;
RAISE INFO '% - %', L_val, rec.name;
END LOOP;
RETURN 1;
END;
$$ LANGUAGE plpgsql;
Note- the FROM clause is optional in case you select from a function in a select like netxval(sequence_name) and don't have any actual table to select from i.e. like SELECT nextval(sequence_name) AS next_value;, in Oracle terms it would be SELECT sequence_name.nextval FROM dual; or SELECT function() FROM dual;. There is no dual in postgreSQL.
I also think that the ORDER BY is not necessary since my assumption would be that your function levenshtein() will most likely return only one value at any point of time, and hence wouldn't have enough data to ORDER.
If you want the output from a plpgsql function like the title says:
CREATE OR REPLACE FUNCTION randomnametest(_mystring text)
RETURNS TABLE (l_dist int, name text) AS
$BODY$
BEGIN
RETURN QUERY
SELECT levenshtein(_mystring, lower(t.name)), t.name
FROM my_table t
ORDER BY 1;
END;
$$ LANGUAGE plpgsql;
Declare the table with RETURNS TABLE.
Use RETURN QUERY to return records from the function.
Avoid naming conflicts between column names and OUT parameters (from the RETURNS TABLE clause) by table-qualifying column names in queries. OUT parameters are visible everywhere in the function body.
I made the string to compare to a parameter to the function to make this more useful.
There are other ways, but this is the most effective for the task. You need PostgreSQL 8.4 or later.
For a one-time use I would consider to just use a plain query (= function body without the RETURN QUERY above).
Im new to writing stored functions in postgresql and in general . I'm trying to write onw with an input parameter and return a set of results stored in a temporary table.
I do the following in my function .
1) Get a list of all the consumers and store their id's stored in a temp table.
2) Iterate over a particular table and retrieve values corresponding to each value from the above list and store in a temp table.
3)Return the temp table.
Here's the function that I've tried to write by myself ,
create or replace function getPumps(status varchar) returns setof record as $$ (setof record?)
DECLARE
cons_id integer[];
i integer;
temp table tmp_table;--Point B
BEGIN
select consumer_id into cons_id from db_consumer_pump_details;
FOR i in select * from cons_id LOOP
select objectid,pump_id,pump_serial_id,repdate,pumpmake,db_consumer_pump_details.status,db_consumer.consumer_name,db_consumer.wenexa_id,db_consumer.rr_no into tmp_table from db_consumer_pump_details inner join db_consumer on db_consumer.consumer_id=db_consumer_pump_details.consumer_id
where db_consumer_pump_details.consumer_id=i and db_consumer_pump_details.status=$1--Point A
order by db_consumer_pump_details.consumer_id,pump_id,createddate desc limit 2
END LOOP;
return tmp_table
END;
$$
LANGUAGE plpgsql;
However Im not sure about my approach and whether im right at the points A and B as I've marked in the code above.And getting a load of errors while trying to create the temporary table.
EDIT: got the function to work ,but I get the following error when I try to run the function.
ERROR: array value must start with "{" or dimension information
Here's my revised function.
create temp table tmp_table(objectid integer,pump_id integer,pump_serial_id varchar(50),repdate timestamp with time zone,pumpmake varchar(50),status varchar(2),consumer_name varchar(50),wenexa_id varchar(50),rr_no varchar(25));
select consumer_id into cons_id from db_consumer_pump_details;
FOR i in select * from cons_id LOOP
insert into tmp_table
select objectid,pump_id,pump_serial_id,repdate,pumpmake,db_consumer_pump_details.status,db_consumer.consumer_name,db_consumer.wenexa_id,db_consumer.rr_no from db_consumer_pump_details inner join db_consumer on db_consumer.consumer_id=db_consumer_pump_details.consumer_id where db_consumer_pump_details.consumer_id=i and db_consumer_pump_details.status=$1
order by db_consumer_pump_details.consumer_id,pump_id,createddate desc limit 2;
END LOOP;
return query (select * from tmp_table);
drop table tmp_table;
END;
$$
LANGUAGE plpgsql;
AFAIK one can't declare tables as variables in postgres. What you can do is create one in your funcion body and use it thourough (or even outside of function). Beware though as temporary tables aren't dropped until the end of the session or commit.
The way to go is to use RETURN NEXT or RETURN QUERY
As for the function result type I always found RETURNS TABLE to be more readable.
edit:
Your cons_id array is innecessary, just iterate the values returned by select.
Also you can have multiple return query statements in a single function to append result of the query to the result returned by function.
In your case:
CREATE OR REPLACE FUNCTION getPumps(status varchar)
RETURNS TABLE (objectid INTEGER,pump_id INTEGER,pump_serial_id INTEGER....)
AS
$$
BEGIN
FOR i in SELECT consumer_id FROM db_consumer_pump_details LOOP
RETURN QUERY(
SELECT objectid,pump_id,pump_serial_id,repdate,pumpmake,db_consumer_pump_details.status,db_consumer.consumer_name,db_consumer.wenexa_id,db_consumer.rr_no FROM db_consumer_pump_details INNER JOIN db_consumer ON db_consumer.consumer_id=db_consumer_pump_details.consumer_id
WHERE db_consumer_pump_details.consumer_id=i AND db_consumer_pump_details.status=$1
ORDER BY db_consumer_pump_details.consumer_id,pump_id,createddate DESC LIMIT 2
);
END LOOP;
END;
$$
edit2:
You probably want to take a look at this solution for groupwise-k-maximum problem as that's exactly what you're dealing with here.
it might be easier to just return a table (or query)
CREATE FUNCTION extended_sales(p_itemno int)
RETURNS TABLE(quantity int, total numeric) AS $$
BEGIN
RETURN QUERY SELECT quantity, quantity * price FROM sales
WHERE itemno = p_itemno;
END;
$$ LANGUAGE plpgsql;
(copied from postgresql docs)