How to pass the value of NEW using dollar quoting? - sql

I cannot access the value of the NEW row inside my crosstab() query string.
CREATE OR REPLACE FUNCTION insert_fx()
RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO outputtb (serial,date, judge)
VALUES (NEW.serial, NEW.date, NEW.tjudge) RETURNING serial INTO newserial;
UPDATE outputtb
SET (reading1,
reading2,
reading3) =
(SELECT ct."reading1",
ct."reading2",
ct."reading3"
FROM crosstab( $$
SELECT tb2. serial,tb2. readings,tb2. value
FROM DATA AS tb2
INNER JOIN outputtb AS tb1 USING (serial)
WHERE tb2.serial = $$||NEW.serno||$$
ORDER BY 1 ASC $$, $$
VALUES ('reading1'),('reading2'),('reading3')$$
) ct ("Serial" VARCHAR(50),"Reading1" FLOAT8, "Reading2" FLOAT8, "Reading3" FLOAT8))
WHERE sn = NEW.serno;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
CREATE TRIGGER insert_tg
BEFORE INSERT ON details
FOR EACH ROW EXECUTE PROCEDURE insert_fx();
It returns this error:
ERROR: syntax error at or near "CC1027HCA0GESKN00CC000FT0000"
LINE 6: tb2. serial = 043611007853619CC1027HCA0GESKN00CC000FT...
I think it does not accept characters, it only accepts integers. Maybe the quoting need some modification and I'm not that familiar with pgsql quoting.
I need help to finish my project. I'm stuck on this part.

The immediate cause of the error message is that you concatenated the string NEW.serno without quoting it. To safely fix use format() or quote_literal() or quote_nullable().
...
UPDATE outputtb
SET (reading1, reading2, reading3)
= (SELECT ct.reading1, ct.reading2, ct.reading3
FROM crosstab(
'SELECT serial, t2.readings, t2.value
FROM data t2
JOIN outputtb t1 USING (serial)
WHERE serial = ' || quote_nullable(NEW.serno) || '
ORDER BY 1'
, $$VALUES ('reading1'),('reading2'),('reading3')$$
) ct (serial text, reading1 float8, reading2 float8, reading3 float8))
WHERE sn = NEW.serno;
...
Basics:
Insert text with single quotes in PostgreSQL
In passing I also fixed your incorrect mixed-case identifiers:
Are PostgreSQL column names case-sensitive?
But there are more problems:
newserial has not been declared and is also not used.
outputtb is pointless noise in the query passed to crosstab().
Like #a_horse commented, you shouldn't need an INSERT and an UPDATE, and crosstab() also seems like overkill.
This is a big mess.
Going out on a limb, my educated guess is you want this:
CREATE OR REPLACE FUNCTION insert_fx()
RETURNS TRIGGER AS
$func$
BEGIN
INSERT INTO outputtb (serial, date, judge, reading1, reading2, reading3)
SELECT NEW.serial, NEW.date, NEW.tjudge, ct.*
FROM (SELECT 1) dummy
LEFT JOIN crosstab (
'SELECT serial, readings, value
FROM data
WHERE serial = ' || quote_nullable(NEW.serno) || '
ORDER BY 1'
, $$VALUES ('reading1'),('reading2'),('reading3')$$
) ct (serial text, reading1 float8, reading2 float8, reading3 float8) ON true;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
The LEFT JOIN to a dummy table prevents losing the INSERT when crosstab() comes up empty.
Which can be simplified to:
CREATE OR REPLACE FUNCTION insert_fx()
RETURNS TRIGGER AS
$func$
BEGIN
INSERT INTO outputtb (serial, date, judge, reading1, reading2, reading3)
SELECT NEW.serial, NEW.date, NEW.tjudge
min(value) FILTER (WHERE readings = 'reading1')
min(value) FILTER (WHERE readings = 'reading2')
min(value) FILTER (WHERE readings = 'reading3')
FROM data
WHERE serial = NEW.serno;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
Since we aggregate now, a result row is guaranteed, and we don't have to defend against losing it.
Aside:
"serial" is not a reserved word. But it's the name of a common pseudo data-type, so I still wouldn't use it as column name to avoid confusing error situations.

Related

Dynamic query that uses CTE gets "syntax error at end of input"

I have a table that looks like this:
CREATE TABLE label (
hid UUID PRIMARY KEY DEFAULT UUID_GENERATE_V4(),
name TEXT NOT NULL UNIQUE
);
I want to create a function that takes a list of names and inserts multiple rows into the table, ignoring duplicate names, and returns an array of the IDs generated for the rows it inserted.
This works:
CREATE OR REPLACE FUNCTION insert_label(nms TEXT[])
RETURNS UUID[]
AS $$
DECLARE
ids UUID[];
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
WITH new_names AS (
INSERT INTO label(name)
SELECT tn.name
FROM tmp_names tn
WHERE NOT EXISTS(SELECT 1 FROM label h WHERE h.name = tn.name)
RETURNING hid
)
SELECT ARRAY_AGG(hid) INTO ids
FROM new_names;
DROP TABLE tmp_names;
RETURN ids;
END;
$$ LANGUAGE PLPGSQL;
I have many tables with the exact same columns as the label table, so I would like to have a function that can insert into any of them. I'd like to create a dynamic query to do that. I tried that, but this does not work:
CREATE OR REPLACE FUNCTION insert_label(h_tbl REGCLASS, nms TEXT[])
RETURNS UUID[]
AS $$
DECLARE
ids UUID[];
query_str TEXT;
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
query_str := FORMAT('WITH new_names AS ( INSERT INTO %1$I(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM %1$I h WHERE h.name = tn.name) RETURNING hid)', h_tbl);
EXECUTE query_str;
SELECT ARRAY_AGG(hid) INTO ids FROM new_names;
DROP TABLE tmp_names;
RETURN ids;
END;
$$ LANGUAGE PLPGSQL;
This is the output I get when I run that function:
psql=# select insert_label('label', array['how', 'now', 'brown', 'cow']);
ERROR: syntax error at end of input
LINE 1: ...SELECT 1 FROM label h WHERE h.name = tn.name) RETURNING hid)
^
QUERY: WITH new_names AS ( INSERT INTO label(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM label h WHERE h.name = tn.name) RETURNING hid)
CONTEXT: PL/pgSQL function insert_label(regclass,text[]) line 19 at EXECUTE
The query generated by the dynamic SQL looks like it should be exactly the same as the query from static SQL.
I got the function to work by changing the return value from an array of UUIDs to a table of UUIDs and not using CTE:
CREATE OR REPLACE FUNCTION insert_label(h_tbl REGCLASS, nms TEXT[])
RETURNS TABLE (hid UUID)
AS $$
DECLARE
query_str TEXT;
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
query_str := FORMAT('INSERT INTO %1$I(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM %1$I h WHERE h.name = tn.name) RETURNING hid', h_tbl);
RETURN QUERY EXECUTE query_str;
DROP TABLE tmp_names;
RETURN;
END;
$$ LANGUAGE PLPGSQL;
I don't know if one way is better than the other, returning an array of UUIDs or a table of UUIDs, but at least I got it to work one of those ways. Plus, possibly not using a CTE is more efficient, so it may be better to stick with the version that returns a table of UUIDs.
What I would like to know is why the dynamic query did not work when using a CTE. The query it produced looked like it should have worked.
If anyone can let me know what I did wrong, I would appreciate it.
... why the dynamic query did not work when using a CTE. The query it produced looked like it should have worked.
No, it was only the CTE without (required) outer query. (You had SELECT ARRAY_AGG(hid) INTO ids FROM new_names in the static version.)
There are more problems, but just use this query instead:
INSERT INTO label(name)
SELECT unnest(nms)
ON CONFLICT DO NOTHING
RETURNING hid;
label.name is defined UNIQUE NOT NULL, so this simple UPSERT can replace your function insert_label() completely.
It's much simpler and faster. It also defends against possible duplicates from within your input array that you didn't cover, yet. And it's safe under concurrent write load - as opposed to your original, which might run into race conditions. Related:
How to use RETURNING with ON CONFLICT in PostgreSQL?
I would just use the simple query and replace the table name.
But if you still want a dynamic function:
CREATE OR REPLACE FUNCTION insert_label(_tbl regclass, _nms text[])
RETURNS TABLE (hid uuid)
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY EXECUTE format(
$$
INSERT INTO %s(name)
SELECT unnest($1)
ON CONFLICT DO NOTHING
RETURNING hid
$$, _tbl)
USING _nms;
END
$func$;
If you don't need an array as result, stick with the set (RETURNS TABLE ...). Simpler.
Pass values (_nms) to EXECUTE in a USING clause.
The tablename (_tbl) is type regclass, so the format specifier %I for format() would be wrong. Use %s instead. See:
Table name as a PostgreSQL function parameter

How to add column inside postgres function without saving it to the db?

I have a postgres DB and I want to create a function that returns a copy of my table with a new column that has a value of 1 if its id is inside the array(idds[]) that the function gets as an input.
In the code below I've try to create a temporary table(chosen) that have id if it's in the idds array and to manually add the isChosen column that obviously doesn't work...
CREATE OR REPLACE FUNCTION public.getTableFromArray(idds integer[])
RETURNS table(
id INTEGER,
isChosen INTEGER
)
LANGUAGE 'plpgsql'
AS $BODY$
begin
with chosen AS(SELECT id,isChosen=1 FROM table1 WHERE ARRAY[table1.id] <# idds)
return query
SELECT id FROM table1 LEFT JOIN chosen ON table1.id=chosen.id;
end;
$BODY$;
Or, with a lot less noise, a proper boolean output column, and without the unhelpful CaMeL case identifiers in a plain SQL function:
CREATE OR REPLACE FUNCTION public.get_table_from_array(idds integer[])
RETURNS TABLE(id int, is_chosen bool)
LANGUAGE sql AS
'SELECT t.id, t.id = ANY(idds) FROM table1 t';
Might as well just run the SQL command directly, though:
SELECT id, id = ANY('{1,2,3}'::int[]) AS is_chosen FROM table1;
you can use this query instead :
select * , case when ARRAY[table1.id] <# idds then 1 else 0 end as choosen FROM table1;
so:
CREATE OR REPLACE FUNCTION public.getTableFromArray(idds integer[])
RETURNS table(
id INTEGER,
isChosen INTEGER
)
LANGUAGE 'plpgsql'
AS $BODY$
begin
return query
select id , case when ARRAY[table1.id] <# idds then 1 else 0 end as isChosen FROM table1;
end;
$BODY$;

Return a composite type or multiple columns from a PostgreSQL function

My aim is to write a function that takes in one parameter and returns two values. The query is working perfectly, however, when executed via the function made, I receive an error that a subquery should not return multiple columns.
My function is as follows:
CREATE TYPE double_integer_type AS (p1 integer, p2 integer);
DROP FUNCTION next_dvd_in_queue;
CREATE OR REPLACE FUNCTION next_dvd_in_queue (member_id_p1 integer) RETURNS double_integer_type as $$
BEGIN
RETURN(
select temp2.dvdid,
temp2.movie_title
from
(select temp1.dvdid,
temp1.movie_title,
temp1.customer_priority
from
(select *
from rentalqueue
where rentalqueue.memberid=member_id_p1) temp1
inner join dvd on dvd.dvdid=temp1.dvdid
where dvd.dvdquantityonhand>0) temp2
order by temp2.customer_priority asc
limit 1
);
END; $$ LANGUAGE PLPGSQL
Call:
select dvdid from next_dvd_in_queue(3);
The query, when executed with a hard-coded value, is:
select temp2.dvdid,
temp2.movie_title
from
(select temp1.dvdid,
temp1.movie_title,
temp1.customer_priority
from
(select *
from rentalqueue
where rentalqueue.memberid=3) temp1
inner join dvd on dvd.dvdid=temp1.dvdid
where dvd.dvdquantityonhand>0) temp2
order by temp2.customer_priority asc
limit 1
The above query works fine.
However, when I call the function in the following way:
select * from next_dvd_in_queue(3);
I get the following error:
ERROR: subquery must return only one column
LINE 1: SELECT (
^
QUERY: SELECT (
select temp2.dvdid,
temp2.movie_title
from
(select temp1.dvdid,
temp1.movie_title,
temp1.customer_priority
from
(select *
from rentalqueue
where rentalqueue.memberid=3) temp1
inner join dvd on dvd.dvdid=temp1.dvdid
where dvd.dvdquantityonhand>0) temp2
order by temp2.customer_priority asc
limit 1
)
CONTEXT: PL/pgSQL function next_dvd_in_queue(integer) line 3 at RETURN
You can fix the syntax error with an explicit cast to the composite type:
CREATE OR REPLACE FUNCTION next_dvd_in_queue (member_id_p1 integer)
RETURNS double_integer_type AS
$func$
BEGIN
RETURN (
SELECT ROW(temp2.dvdid, temp2.movie_title)::double_integer_type
FROM ...
);
END
$func$ LANGUAGE plpgsql
But I would remove the needless complication with the composite type and use OUT parameters instead:
CREATE OR REPLACE FUNCTION pg_temp.next_dvd_in_queue (member_id_p1 integer
OUT p1 integer
OUT p2 varchar(100)) AS
$func$
BEGIN
SELECT INTO p1, p2
temp2.dvdid, temp2.movie_title
FROM ...
END
$func$ LANGUAGE plpgsql;
Avoid naming collisions between parameter names and column names. I like to stick to a naming convention where I prefix all parameter names with _, so _member_id_p1, _p1, _p2.
Related:
Returning from a function with OUT parameter
How to return result of a SELECT inside a function in PostgreSQL?

PostgreSQL Function Returning Table or SETOF

Quick background: very new to PostgreSQL, came from SQL Server. I am working on converting stored procedures from SQL Server to PostgreSQL. I have read Postgres 9.3 documentation and looked through numerous examples and questions but still cannot find a solution.
I have this select statement that I execute weekly to return new values
select distinct id, null as charges
, generaldescription as chargedescription, amount as paidamount
from mytable
where id not in (select id from myothertable)
What can I do to turn this into a function where I can just right click and execute on a weekly basis. Eventually I will automate this using a program another developer built. So it will execute with no user involvement and send results in a spreadsheet to the user. Not sure if that last part matters to how the function is written.
This is one of my many failed attempts:
CREATE FUNCTION newids
RETURNS TABLE (id VARCHAR, charges NUMERIC
, chargedescription VARCHAR, paidamount NUMERIC) AS Results
Begin
SELECT DISTINCT id, NULL AS charges
, generaldescription AS chargedescription, amount AS paidamount
FROM mytable
WHERE id NOT IN (SELECT id FROM myothertable)
END;
$$ LANGUAGE plpgsql;
Also I am using Navicat and the error is:
function result type must be specified
You can use PL/pgSQL here and it may even be the better choice.
Difference between language sql and language plpgsql in PostgreSQL functions
But you need to fix a syntax error, match your dollar-quoting, add an explicit cast (NULL::numeric) and use RETURN QUERY:
CREATE FUNCTION newids_plpgsql()
RETURNS TABLE (
id varchar
, charges numeric
, chargedescription varchar
, paidamount numeric
) AS -- Results -- remove this
$func$
BEGIN
RETURN QUERY
SELECT ...;
END;
$func$ LANGUAGE plpgsql;
Or use a simple SQL function:
CREATE FUNCTION newids_sql()
RETURNS TABLE (
id varchar
, charges numeric
, chargedescription varchar
, paidamount numeric
) AS
$func$
SELECT ...;
$func$ LANGUAGE sql;
Either way, the SELECT statement can be more efficient:
SELECT DISTINCT t.id, NULL::numeric AS charges
, t.generaldescription AS chargedescription, t.amount AS paidamount
FROM mytable t
LEFT JOIN myothertable m ON m.id = t.id
WHERE m.id IS NULL;
Select rows which are not present in other table
Of course, all data types must match. I can't tell, table definition is missing.
And are you sure you need DISTINCT?

Custom aggregate function

I am trying to understand aggregate functions and I need help.
So for instance the following sample:
CREATE OR REPLACE FUNCTION array_median(timestamp[])
RETURNS timestamp AS
$$
SELECT CASE WHEN array_upper($1,1) = 0 THEN null ELSE asorted[ceiling(array_upper(asorted,1)/2.0)] END
FROM (SELECT ARRAY(SELECT ($1)[n] FROM
generate_series(1, array_upper($1, 1)) AS n
WHERE ($1)[n] IS NOT NULL
ORDER BY ($1)[n]
) As asorted) As foo ;
$$
LANGUAGE 'sql' IMMUTABLE;
CREATE AGGREGATE median(timestamp) (
SFUNC=array_append,
STYPE=timestamp[],
FINALFUNC=array_median
)
I am not understanding the structure/logic that needs to go into the select statement in the aggregate function itself. Can someone explain what the flow/logic is?
I am writing an aggregate, a strange one, that the return is always the first string it ever sees.
You're showing a median calculation, but want the first text value you see?
Below is how to do that. Assuming you want the first non-null value, that is. If not, you'll need to keep track of if you've got a value already or not.
The accumulator function is written as plpgsql and sql - the plpgsql one lets you use variable names and debug it too. It simply uses COALESCE against the previous accumulated value and the new value and returns the first non-null. So - as soon as you have a non-null in the accumulator everything else gets ignored.
You may also want to consider the "first_value" window function for this sort of thing if you're on a modern (8.4+) version of PostgreSQL.
http://www.postgresql.org/docs/9.1/static/functions-window.html
HTH
BEGIN;
CREATE FUNCTION remember_first(acc text, newval text) RETURNS text AS $$
BEGIN
RAISE NOTICE '% vs % = %', acc, newval, COALESCE(acc, newval);
RETURN COALESCE(acc, newval);
END;
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE FUNCTION remember_first_sql(text,text) RETURNS text AS $$
SELECT COALESCE($1, $2);
$$ LANGUAGE SQL IMMUTABLE;
-- No "initcond" means we start out with null
--
CREATE AGGREGATE first(text) (
sfunc = remember_first,
stype = text
);
CREATE TEMP TABLE tt (t text);
INSERT INTO tt VALUES ('abc'),('def'),('ghi');
SELECT first(t) FROM tt;
ROLLBACK;