How to query last_value of a sequence using late-binding - postgresql

Is it possible to obtain the last value from a sequence of which name is stored in a variable?
seq_name := 'myschema.mysequence';
SELECT last_value FROM ${seq_name};

If the sequence has already been used in the session, you can simply user the currval function:
DO
$$DECLARE
var text := 'seq';
BEGIN
RAISE NOTICE 'The current value is %', currval(var);
END;$$;
NOTICE: The next value is 1
Otherwise, you need dynamic SQL:
DO
$$DECLARE
var text := 'seq';
value bigint;
BEGIN
EXECUTE format('SELECT last_value FROM %s', var) INTO value;
RAISE NOTICE 'The current value is %', value;
END;$$;
NOTICE: The current value is 1

Related

Variables in Bash postgres loop [duplicate]

Here's what I would like to do:
\set values foo,bar,baz
DO $$
DECLARE
value TEXT;
values TEXT[] := string_to_array(:'values', ',');
BEGIN
FOREACH value IN ARRAY values LOOP
raise notice 'v: %', value;
END LOOP;
END $$ LANGUAGE plpgsql;
Which results in the following error:
ERROR: syntax error at or near ":"
SELECT string_to_array(:'values', ',') INTO values...
^
Here's the solution I have currently, but it feels hacky:
\set values foo,bar,baz
PREPARE get_values AS SELECT string_to_array(:'values', ',');
DO $$
DECLARE
value TEXT;
values TEXT[];
BEGIN
EXECUTE 'EXECUTE get_values' INTO values;
FOREACH value IN ARRAY values LOOP
raise notice 'v: %', value;
END LOOP;
END $$ LANGUAGE plpgsql;
Answer
DO expects a string literal with plpgsql code. Symbols are not substituted inside strings in psql.
You could concatenate the whole string into a psql variable and then execute it.
How to concatenate psql variables?
Pretty multi-line format is not possible, because (per documentation):
But in any case, the arguments of a meta-command cannot continue
beyond the end of the line.
Simple example:
test=# \set value foo
test=# \set do 'BEGIN\n RAISE NOTICE ''v: %'', ' :'value' ';\nEND'
test=# DO :'do';
NOTICE: v: foo
Replace line breaks with \n (or remove them if you don't care for pretty format). Based on this adapted code:
DO
'
DECLARE
_val text;
_vals text[] := string_to_array(>>values<<, '','');
BEGIN
FOREACH _val IN ARRAY _vals
LOOP
RAISE NOTICE ''v: %'', _val;
END LOOP;
END
'
It looks like this:
test=# \set do 'DECLARE\n _val text;\n _vals text[] := string_to_array(' :'values' ', '','');\nBEGIN\n FOREACH _val IN ARRAY _vals\n LOOP\n RAISE NOTICE ''v: %'', _val;\n END LOOP;\nEND'
test=# DO :'do';
NOTICE: v: foo
NOTICE: v: bar
NOTICE: v: baz
DO
I added bold emphasis to the variable to make it easier to spot.
Related answer by #Pavel (ab)using a server session variable:
Referring to session variables (\set var='value') from PL/PGSQL
Alternative solutions
Prepared statement
Your current solution doesn't look that bad. I would simplify:
PREPARE get_values AS SELECT * FROM regexp_split_to_table(:'values', ',');
DO
$do$
DECLARE
_val text;
BEGIN
FOR _val IN EXECUTE
'EXECUTE get_values'
LOOP
RAISE NOTICE 'v: %', _val;
END LOOP;
END
$do$;
Temporary table
Similar solution with a temporary table:
CREATE TEMP TABLE tmp AS SELECT * FROM regexp_split_to_table(:'values', ',') v;
DO
$do$
DECLARE
_val text;
BEGIN
FOR _val IN
TABLE tmp
LOOP
RAISE NOTICE 'v: %', _val;
END LOOP;
END
$do$;
Was able to take advantage of this solution:
Passing argument to a psql procedural script
Where I set the variable as such and retrieve it with current_setting()
\set values foo,bar,baz
SET vars.values TO :'values';
DO $$
DECLARE
value TEXT;
values TEXT[] := string_to_array(current_setting('vars.values'), ',');
BEGIN
FOREACH value IN ARRAY values LOOP
RAISE NOTICE 'v: %', value;
END LOOP;
END $$ LANGUAGE plpgsql

How to assign and extract table values defined in RECORD type

Here is sample code
CREATE TYPE entity AS
(
record_init boolean,
b_modified boolean,
custom_name character varying(8000),
customer_rec customer, --- customer is a table
lang_pref boolean,
)
do
$$
declare
entity_rec entity;
Begin
entity_rec.record_init := TRUE;
entity_rec.customer_rec.customer_id := 100;
raise notice ' Customer Id is %', entity_rec.customer_rec.customer_id;
end;
$$;
getting an error while referring customer_id from entity_rec.customer_rec
PLpgSQL doesn't support complex expressions on left side of assign statement. You should to divide this operation to more operations (this issue is fixed in prepared PostgreSQL 14):
DECLARE
entity_rec entity;
customer_rec customer;
BEGIN
entity_rec.record_init := TRUE;
customer_rec.customer_id := 100;
entity_rec.customer_rec := customer_rec;
raise notice ' Customer Id is %', entity_rec.customer_rec.customer_id;
END;
Second issue can be dereference of nested records. Postgres by default uses schema schema.table.name. When you use nested composite values, then the parenthesis should be used:
postgres=# do $$
DECLARE
entity_rec entity;
customer_rec customer;
BEGIN
entity_rec.record_init := TRUE;
customer_rec.customer_id := 100;
entity_rec.customer_rec := customer_rec;
raise notice ' Customer Id is %', entity_rec.customer_rec.customer_id;
END;
$$;
ERROR: missing FROM-clause entry for table "customer_rec"
LINE 1: SELECT entity_rec.customer_rec.customer_id
^
QUERY: SELECT entity_rec.customer_rec.customer_id
CONTEXT: PL/pgSQL function inline_code_block line 9 at RAISE
Correct:
postgres=# do $$
DECLARE
entity_rec entity;
customer_rec customer;
BEGIN
entity_rec.record_init := TRUE;
customer_rec.customer_id := 100;
entity_rec.customer_rec := customer_rec;
raise notice ' Customer Id is %', (entity_rec).customer_rec.customer_id;
END;
$$;
NOTICE: Customer Id is 100
DO

Auto Table Partitioning -PostgreSQL- ERROR: too few arguments for format()

I'm trying to auto partition my oltpsales table using a function below and a trigger below but then I try to perform in insert on the table I get error code below. I have referenced a few threads below and suggestions are welcomed.
INSERT with dynamic table name in trigger function
Postgres format string using array
ERROR: too few arguments for format() CONTEXT: PL/pgSQL function
testoltpsales_insert_function() line 17 at EXECUTE
CREATE TRIGGER testoltpsales_insert_trg
BEFORE INSERT ON myschema."testoltpsales"
FOR EACH ROW EXECUTE PROCEDURE testoltpsales_insert_function();
CREATE OR REPLACE FUNCTION testoltpsales_insert_function()
RETURNS TRIGGER AS $$
DECLARE
partition_date TEXT;
partition_name TEXT;
start_of_month TEXT;
end_of_next_month TEXT;
BEGIN
partition_date := to_char(NEW."CreateDateTime",'YYYY_MM');
partition_name := 'testoltpsaless_' || partition_date;
start_of_month := to_char((NEW."CreateDateTime"),'YYYY-MM') || '-01';
end_of_next_month := to_char((NEW."CreateDateTime" + interval '1 month'),'YYYY-MM') || '-01';
IF NOT EXISTS
(SELECT 1
FROM information_schema.tables
WHERE table_name = partition_name)
THEN
EXECUTE format(E'CREATE TABLE %I (CHECK ( date_trunc(\'day\', %I.CreateDateTime) >= ''%s'' AND date_trunc(\'day\', %I.CreateDateTime) < ''%s'')) INHERITS (myschema."Testoltpsaless")',
VARIADIC ARRAY [partition_name, start_of_month,end_of_next_month]);
RAISE NOTICE 'A partition has been created %', partition_name;
-- EXECUTE format('GRANT SELECT ON TABLE %I TO readonly', partition_name); -- use this if you use role based permission
END IF;
EXECUTE format('INSERT INTO %I ("OwnerId","DaddyTable","SaleId","RunId","CreateDateTime","SalesetId","Result","Score","NumberOfMatches" ) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)', partition_name)
USING NEW."OwnerId",NEW."DaddyTable",NEW."SaleId",NEW."RunId",NEW."CreateDateTime",NEW."SalesetId",NEW."Result",NEW."Score",NEW."NumberOfMatches";
RETURN NULL;
END
$$
LANGUAGE plpgsql;
EXECUTE format(E'CREATE TABLE %I (CHECK ( date_trunc(\'day\', %I.CreateDateTime) >= ''%s'' AND date_trunc(\'day\',
%I.CreateDateTime) < ''%s'')) INHERITS (myschema."Testoltpsaless")',
VARIADIC ARRAY [partition_name, start_of_month,end_of_next_month]); ```
There are five format specifiers in your format string but you're passing it only three arguments. Unless you're using positional formatting e.g. %1$I, you must supply the same number of args, as they are used sequentially.
https://www.postgresql.org/docs/current/functions-string.html#FUNCTIONS-STRING-FORMAT

Use variable set by psql meta-command inside of DO block

Here's what I would like to do:
\set values foo,bar,baz
DO $$
DECLARE
value TEXT;
values TEXT[] := string_to_array(:'values', ',');
BEGIN
FOREACH value IN ARRAY values LOOP
raise notice 'v: %', value;
END LOOP;
END $$ LANGUAGE plpgsql;
Which results in the following error:
ERROR: syntax error at or near ":"
SELECT string_to_array(:'values', ',') INTO values...
^
Here's the solution I have currently, but it feels hacky:
\set values foo,bar,baz
PREPARE get_values AS SELECT string_to_array(:'values', ',');
DO $$
DECLARE
value TEXT;
values TEXT[];
BEGIN
EXECUTE 'EXECUTE get_values' INTO values;
FOREACH value IN ARRAY values LOOP
raise notice 'v: %', value;
END LOOP;
END $$ LANGUAGE plpgsql;
Answer
DO expects a string literal with plpgsql code. Symbols are not substituted inside strings in psql.
You could concatenate the whole string into a psql variable and then execute it.
How to concatenate psql variables?
Pretty multi-line format is not possible, because (per documentation):
But in any case, the arguments of a meta-command cannot continue
beyond the end of the line.
Simple example:
test=# \set value foo
test=# \set do 'BEGIN\n RAISE NOTICE ''v: %'', ' :'value' ';\nEND'
test=# DO :'do';
NOTICE: v: foo
Replace line breaks with \n (or remove them if you don't care for pretty format). Based on this adapted code:
DO
'
DECLARE
_val text;
_vals text[] := string_to_array(>>values<<, '','');
BEGIN
FOREACH _val IN ARRAY _vals
LOOP
RAISE NOTICE ''v: %'', _val;
END LOOP;
END
'
It looks like this:
test=# \set do 'DECLARE\n _val text;\n _vals text[] := string_to_array(' :'values' ', '','');\nBEGIN\n FOREACH _val IN ARRAY _vals\n LOOP\n RAISE NOTICE ''v: %'', _val;\n END LOOP;\nEND'
test=# DO :'do';
NOTICE: v: foo
NOTICE: v: bar
NOTICE: v: baz
DO
I added bold emphasis to the variable to make it easier to spot.
Related answer by #Pavel (ab)using a server session variable:
Referring to session variables (\set var='value') from PL/PGSQL
Alternative solutions
Prepared statement
Your current solution doesn't look that bad. I would simplify:
PREPARE get_values AS SELECT * FROM regexp_split_to_table(:'values', ',');
DO
$do$
DECLARE
_val text;
BEGIN
FOR _val IN EXECUTE
'EXECUTE get_values'
LOOP
RAISE NOTICE 'v: %', _val;
END LOOP;
END
$do$;
Temporary table
Similar solution with a temporary table:
CREATE TEMP TABLE tmp AS SELECT * FROM regexp_split_to_table(:'values', ',') v;
DO
$do$
DECLARE
_val text;
BEGIN
FOR _val IN
TABLE tmp
LOOP
RAISE NOTICE 'v: %', _val;
END LOOP;
END
$do$;
Was able to take advantage of this solution:
Passing argument to a psql procedural script
Where I set the variable as such and retrieve it with current_setting()
\set values foo,bar,baz
SET vars.values TO :'values';
DO $$
DECLARE
value TEXT;
values TEXT[] := string_to_array(current_setting('vars.values'), ',');
BEGIN
FOREACH value IN ARRAY values LOOP
RAISE NOTICE 'v: %', value;
END LOOP;
END $$ LANGUAGE plpgsql

Passing column names dynamically for a record variable in PostgreSQL

Using PostgreSQL, column values from a table for 1st record are stored in a record variable. for ex: let the variable be: recordvar
recordvar.columnname
gives the value of the column name specified. I will define the columname in a variable:
var := columnname
In place of columnname if I replace with the variable i.e. recordvar.var, it is not working.
Please let me know how to proceed in this situation. Following is the sample code:
CREATE OR REPLACE FUNCTION getrowdata(id numeric, table_name character varying)
RETURNS SETOF void AS
$BODY$
DECLARE
srowdata record;
reqfield character varying;
value numeric;
BEGIN
RAISE NOTICE 'id: %',id;
reqfield:= 'columnname';
EXECUTE 'select * from datas.'||table_name||' WHERE id = '||id into srowdata;
RAISE NOTICE 'srowdata: %',srowdata;
RAISE NOTICE 'srowdatadata.columnname: %',srowdata.columnname;
value:= srowdata.reqfield;
RAISE NOTICE 'value: %',value;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100
ROWS 1000;
Working with this dummy table
CREATE TEMP TABLE foo (id int, my_num numeric);
INSERT INTO foo VALUES (1, 12.34)
First, I simplified and sanitized your example:
Removed some noise that is irrelevant to the question.
RETURNS SETOF void hardly makes sense. I use RETURNS void instead.
I use text instead of character varying, just for the sake of simplicity.
When using dynamic SQL, you have to safeguard against SQL injection, I use format() with %I in this case. There are other ways.
The basic problem is that SQL is very rigid with types and identifiers. You are operating with dynamic table name as well as with dynamic field name of a record - an anonymous record in your original example. Pl/pgSQL is not well equipped to deal with this. Postgres does not know what's inside an anonymous record. Only after you assign the record to a well known type can you reference individual fields.
Here is a closely related question, trying to set a field of a record with dynamic name:
How to set value of composite variable field using dynamic SQL
Basic function
CREATE OR REPLACE FUNCTION getrowdata1(table_name text, id int)
RETURNS void AS
$func$
DECLARE
srowdata record;
reqfield text := 'my_num'; -- assigning at declaration time for convenience
value numeric;
BEGIN
RAISE NOTICE 'id: %', id;
EXECUTE format('SELECT * FROM %I WHERE id = $1', table_name)
USING id
INTO srowdata;
RAISE NOTICE 'srowdata: %', srowdata;
RAISE NOTICE 'srowdatadata.my_num: %', srowdata.my_num;
/* This does not work, even with dynamic SQL
EXECUTE format('SELECT ($1).%I', reqfield)
USING srowdata
INTO value;
RAISE NOTICE 'value: %', value;
*/
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * from getrowdata1('foo', 1);
The commented part would raise an exception:
could not identify column "my_num" in record data type: SELECT * from
getrowdata(1,'foo')
hstore
You need to install the additional module hstore for this. Once per database with:
CREATE EXTENSION hstore;
Then all could work like this:
CREATE OR REPLACE FUNCTION getrowdata2(table_name text, id int)
RETURNS void AS
$func$
DECLARE
hstoredata hstore;
reqfield text := 'my_num';
value numeric;
BEGIN
RAISE NOTICE 'id: %', id;
EXECUTE format('SELECT hstore(t) FROM %I t WHERE id = $1', table_name)
USING id
INTO hstoredata;
RAISE NOTICE 'hstoredata: %', hstoredata;
RAISE NOTICE 'hstoredata.my_num: %', hstoredata -> 'my_num';
value := hstoredata -> reqfield;
RAISE NOTICE 'value: %', value;
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * from getrowdata2('foo', 1);
Polymorphic type
Alternative without installing additional modules.
Since you select a whole row into your record variable, there is a well defined type for it per definition. Use it. The key word is polymorphic types.
CREATE OR REPLACE FUNCTION getrowdata3(_tbl anyelement, id int)
RETURNS void AS
$func$
DECLARE
reqfield text := 'my_num';
value numeric;
BEGIN
RAISE NOTICE 'id: %', id;
EXECUTE format('SELECT * FROM %s WHERE id = $1', pg_typeof(_tbl))
USING id
INTO _tbl;
RAISE NOTICE '_tbl: %', _tbl;
RAISE NOTICE '_tbl.my_num: %', _tbl.my_num;
EXECUTE 'SELECT ($1).' || reqfield -- requfield must be SQLi-safe or escape
USING _tbl
INTO value;
RAISE NOTICE 'value: %', value;
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * from getrowdata3(NULL::foo, 1);
-> SQLfiddle
I (ab-)use the input parameter _tbl for three purposes here:
Provides the well defined type of the record
Provides the name of the table, automatically schema-qualified
Serves as variable.
More explanation in this related answer (last chapter):
Refactor a PL/pgSQL function to return the output of various SELECT queries