postgres stored procedure pl pgSQL - ghdrako/doc_snipets GitHub Wiki
- https://www.postgresql.org/docs/current/plpgsql.html
- https://database.guide/how-pg_sleep-works-in-postgresql/
- https://www.postgresqltutorial.com/postgresql-plpgsql/
- https://www.postgresql.org/docs/current/plpgsql.html
- https://github.com/okbob/plpgsql_check
- https://supabase.com/docs/guides/database/extensions/plpgsql_check
CREATE EXTENSION plpgsql;
\dx
select * from pg_extension where extname='plpgsql';
PostgreSQL allows user-defined functions to be written in other languages besides SQL and C. These other languages are generically called procedural languages (PLs). There are currently four procedural languages available in the standard PostgreSQL distribution: PL/pgSQL (Chapter 43), PL/Tcl (Chapter 44), PL/Perl (Chapter 45), and PL/Python (Chapter 46). There are additional procedural languages available that are not included in the core distribution.
For the languages supplied with the standard distribution, it is only necessary to execute CREATE EXTENSION language_name to install the language into the current database.
language sql | language pl/pgsql |
---|---|
For simple scalar queries. | dynamic SQL, where you build and EXECUTE statements dynamically. |
can only use pure SQL | has loops, variables, error/exception handling, etc. Not all SQL is valid PL/PgSQL - as you discovered, for example, you can't use SELECT without INTO or RETURN QUERY. PL/PgSQL may also be used in DO blocks for one-shot procedures. |
may be inlined | |
single (or very few) calls per session. | called repeatedly - query plans can be cached for all SQL statements inside pl/pgsql function - they are treated like prepared statements, the plan is cached for repeated calls within the same session |
For trigger functions. | |
ll statements in SQL functions are parsed at once | functions plan and execute each statement sequentially (like a prepared statement). |
Compare function procedure
Aspect | Function | Procedure |
---|---|---|
Definition and Purpose | designed to take inputs, perform operations, and return a value, which can be used directly in SQL queries | set of SQL statements that perform a specific task but do not necessarily need to return a value |
Syntax and Declaration | CREATE FUNCTION |
CREATE PROCEDURE |
Return Type | do not return a value per se | must specify a return type (e.g., INT, TEXT, NUMERIC), and they explicitly return a value using the |
RETURN statement. | ||
Invoking | invoked using the CALL statement | invoked as part of a SQL expression |
Transaction Control | execute within the context of a single transaction and do not support transaction control commands directly within their body, making them suitable for atomic operations. | can manage transactions using BEGIN, COMMIT, and ROLLBACK statements within their body, enabling finer control over transaction management |
PL/SQL supports the following:
- Variable declarations
- Expressions
- Control structures as conditional structures or loop structures
- Cursors
We can define the parameters as follows:
-
IN
: Input parameters (if omitted, this is the default option) -
OUT
: Output parameters -
INOUT
: Input/output parameters
Function volatility categories:
- VOLATILE - can do everything, including modifying the database. It can return different results on successive calls with the same arguments. A query using a volatile function will re-evaluate the function at every row where its value is needed. Example
now()
return current date and time - STABLE - cannot modify the database and is guaranteed to return the same results given the same arguments for all rows within a single statement. This category allows the optimizer to optimize multiple calls of the function to a single call. It is safe to use an expression containing such a function in an index scan condition. Example
lower(string_expression)
- IMMUTABLE - cannot modify the database and is guaranteed to return the same results given the same arguments forever. This category allows the optimizer to pre-evaluate the function when a query calls it with constant arguments.
create or replace function f1(istr varchar)
returns text as $func$
select 'hello! '::varchar || istr;
$func$ language sql;
SELECT * FROM f1('ala');
create or replace function f_multiply_by_5(inout p_inout int)
as $$
begin
select p_inout * 5
into p_inout;
end;
$$ language plpgsql;
select f_multiply_by_5(3);
CREATE FUNCTION f2(istr varchar)
RETURNS text AS
$func$
BEGIN
RETURN 'hello! '; -- defaults to type text anyway
END
$func$ LANGUAGE plpgsql;
create or replace function f_multiply_together(variadic p_nums int[])
returns int as $$
declare
v_tot int = 1;
v_ind int;
begin
foreach v_ind in array p_nums loop
v_tot = v_tot * v_ind;
end loop;
return v_tot;
end;
$$ language plpgsql;
select f_multiply_together(3,5,2);
select f_multiply_together(12,4,9,8,1,333,1,87,5,2);
CREATE OR REPLACE FUNCTION investment_calculator(
IN v_amount numeric, IN v_interest numeric,
IN v_years int)
RETURNS numeric AS
$$
DECLARE
v_sum ALIAS FOR $1;
v_result numeric := 0;
BEGIN
v_result := v_amount
* pow(1 + v_interest, v_years);
RETURN v_result;
END;
$$ LANGUAGE 'plpgsql';
SELECT investment_calculator(1000, 0.1, 2);
We can name those parameters passed to a function and use them directly instead of using $1, and $2. What is also possible is to use aliases. v_sum can be used just like v_amount, so the effect of an alias and a named parameter is pretty much the same from a coding standpoint.
- https://www.postgresql.org/docs/current/queries-table-expressions.html#QUERIES-TABLEFUNCTIONS
- https://www.postgresonline.com/journal/index.php?/archives/406-VARIADIC-Unnest.html unnest function is variadic so can take an unlimited number of arguments
Return Type | Structure | Usage |
---|---|---|
returns | Custom record type | returns a single row with a custom structure |
returns setOF | Custom record type | returns a set of rows, each of which is a record with a specific structure |
returns table | Custom table type | returns a set of rows, each of which has columns defined by the table type Out parameters Custom record type |
CREATE OR REPLACE FUNCTION <function_name>
RETURNS TABLE (col1 type, col2...)
AS $$ BEGIN
RETURN QUERY <your select query>;
END;
$$ LANGUAGE plpgsql;
$$ CREATE OR REPLACE FUNCTION get_users_return_TABLE()
RETURNS TABLE (id INTEGER, username VARCHAR)
AS $$ BEGIN
RETURN QUERY SELECT user_id, name FROM users;
END;
$$ LANGUAGE plpgsql;
SELECT * FROM get_users_return_TABLE();
PL/pgSQL runs in same process as PostgreSQL core database engine and it use same data types.
functions (a body of function) can be changed 100% safely (without any unavailability). Invalidation of cached function syntax trees works perfect.
It is not possible to return any value from the unnamed code blocks. If you want to return any value from anonymous blocks, then we might need to use any session-level variables, which need to be set inside the anonymous block, and access them from the outside of the blocks.
- nested BEGIN... END block
DO $o$
BEGIN
RAISE NOTICE $i$
Hello
World
$i$;
END;
$o$;
DO
$$
BEGIN
BEGIN
RAISE NOTICE 'Hello World';
END;
END;
$$;
nested unnamed code block
DO
$o$
BEGIN
DO
$i$
BEGIN
RAISE NOTICE 'Hello World';
END;
$i$;
END;
$o$;
nested block inside an exception
DO $inline$
BEGIN
PERFORM 1/0;
RAISE NOTICE 'Hello World!';
EXCEPTION
WHEN OTHERS THEN
DO $$
BEGIN
RAISE NOTICE 'Got error';
END;
$$;
END;
$inline$;
<<label>>
DECLARE
-- declare variables here
BEGIN
-- Named block's code here
END;
nested code blocks can refer to outer variables by using that label instead of finding the innermost match for the variable name.
Once a named block has been defined, it can be called from within the same function or procedure using the PERFORM statement:
PERFORM block_name;
Named blocks can be called multiple times within the same function or procedure, allowing for reusable and modular code.
CREATE OR REPLACE FUNCTION example_nested_loops()
RETURNS VOID AS $$
BEGIN
<<outer_loop>> -- Label for the outer loop
FOR i IN 1..5 LOOP
<<inner_loop>> -- Label for the inner loop
FOR j IN 1..5 LOOP
IF i * j > 10 THEN
-- Exit both loops when a condition is met
EXIT outer_loop;
END IF;
-- Your inner loop logic here
END LOOP inner_loop;
-- Your outer loop logic here
END LOOP outer_loop;
END;
$$ LANGUAGE plpgsql;
The scope of the declared variables in PL/pgSQL is always local to its current block.
To access the parent v_var1 variable, we should access that variable with the block’s label: parent.v_var1
$$
<<parent>>
DECLARE
v_var1 INT := 1;
BEGIN
DECLARE
v_var1 INT := 10;
BEGIN
RAISE NOTICE 'Parent v_var1 %', parent.v_var1;
RAISE NOTICE 'Local v_var1 %', v_var1;
END;
END;
$$;
-- Escaping single quote inside string
select 'I''m also a string constant';
select E'I\'m also a string constant'; -- in old version postgres
Syntax
$tag$<string_constant>$tag$
select $$I'm a string constant that contains a backslash \$$;
SELECT $message$I'm a string constant that contains a backslash \$message$;
Using dollar-quoted string constant in anonymous blocks
do
$$
declare
film_count integer;
begin
select count(*) into film_count
from film;
raise notice 'The number of films: %', film_count;
end;
$$
Using dollar-quoted string constants in functions
create function find_film_by_id(
id int
) returns film
language sql
as
$$
select * from film
where film_id = id;
$$;
Using dollar-quoted string constants in stored procedures
create procedure proc_name(param_list)
language lang_name
as $$
-- stored procedure body
$$
CREATE FUNCTION repeat(times integer, s text)
RETURNS text
AS $$
DECLARE
result text;
BEGIN
result := '';
FOR i IN 1..times LOOP
result := result || s;
END LOOP;
RETURN result;
END;
$$
LANGUAGE plpgsql
IMMUTABLE;
-- psql> SELECT repeat(10, '*');
-- repeat
-- ------------
-- **********
-- (1 row)
Compared with stored functions, there is only one extra thing that procedures can do – transaction control.
CREATE PROCEDURE check_commit(v integer)
LANGUAGE plpgsql AS $$
BEGIN
IF v % 2 = 0 THEN
COMMIT;
ELSE
ROLLBACK;
END IF;
END $$;
-- call it
CALL check_commit(10);
CREATE PROCEDURE annual_pay_rise (percent numeric)
LANGUAGE plpgsql AS $$
DECLARE
c CURSOR FOR
SELECT * FROM employee
WHERE job_code = 'A2';
BEGIN
FOR r IN c LOOP
UPDATE employee
SET salary = salary * (1 + (percent/100.0))
WHERE empid = r.empid;
IF mod (r.empid, 100) = 0 THEN
COMMIT;
END IF;
END LOOP;
END;
$$;
CALL annual_pay_rise(2);
Implementation of restart mechanism
CREATE TABLE job_status
(id bigserial not null primary key,status text not null,restartdata bigint);
CREATE OR REPLACE FUNCTION job_start_new ()
RETURNS bigint
LANGUAGE plpgsql
AS $$
DECLARE
p_id BIGINT;
BEGIN
INSERT INTO job_status (status, restartdata)
VALUES ('START', 0)
RETURNING id INTO p_id;
RETURN p_id;
END; $$;
CREATE OR REPLACE FUNCTION job_get_status (jobid bigint)
RETURNS bigint
LANGUAGE plpgsql
AS $$
DECLARE
rdata BIGINT;
BEGIN
SELECT restartdata INTO rdata
FROM job_status
WHERE status != 'COMPLETE' AND id = jobid;
IF NOT FOUND THEN
RAISE EXCEPTION 'job id does not exist';
END IF;
RETURN rdata;
END; $$;
CREATE OR REPLACE PROCEDURE
job_update (jobid bigint, rdata bigint)
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE job_status
SET status = 'IN PROGRESS'
,restartdata = rdata
WHERE id = jobid;
END; $$;
CREATE OR REPLACE PROCEDURE job_complete (jobid bigint)
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE job_status SET status = 'COMPLETE'
WHERE id = jobid;
END; $$;
CREATE OR REPLACE PROCEDURE annual_pay_rise (job bigint)
LANGUAGE plpgsql AS $$
DECLARE
job_empid bigint;
c NO SCROLL CURSOR FOR
SELECT * FROM employee
WHERE job_code='A2'
AND empid > job_empid
ORDER BY empid;
BEGIN
SELECT job_get_status(job) INTO job_empid;
FOR r IN c LOOP
UPDATE employee
SET salary = salary * 1.02
WHERE empid = r.empid;
IF mod (r.empid, 100) = 0 THEN
CALL job_update(job, r.empid);
COMMIT;
END IF;
END LOOP;
CALL job_complete(job);
END; $$;
To create a new trigger in PostgreSQL, you follow these steps:
- First, create a trigger function using CREATE FUNCTION statement.
CREATE FUNCTION trigger_function()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
-- trigger logic
END;
$$
- Second, bind the trigger function to a table by using CREATE TRIGGER statement.
CREATE TRIGGER trigger_name
{BEFORE | AFTER} { event }
ON table_name
[FOR [EACH] { ROW | STATEMENT }]
EXECUTE PROCEDURE trigger_function
The event can be INSERT , DELETE, UPDATE or TRUNCATE.
Example
CREATE OR REPLACE FUNCTION log_last_name_changes()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
BEGIN
IF NEW.last_name <> OLD.last_name THEN
INSERT INTO employee_audits(employee_id,last_name,changed_on)
VALUES(OLD.id,OLD.last_name,now());
END IF;
RETURN NEW;
END;
$$
CREATE TRIGGER last_name_changes
BEFORE UPDATE
ON employees
FOR EACH ROW
EXECUTE PROCEDURE log_last_name_changes();
Rename trigger
ALTER TRIGGER trigger_name
ON table_name
RENAME TO new_trigger_name;
\dS <table_name> -- view all triggers associated with a table
PostgreSQL doesn’t support the OR REPLACE statement that allows you to modify the trigger definition like the function that will be executed when the trigger is fired.
In order to do so, you can use the DROP TRIGGER and CREATE TRIGGER statements.
Disable/enable trigger
ALTER TABLE table_name
DISABLE TRIGGER trigger_name | ALL
ALTER TABLE table_name
ENABLE TRIGGER trigger_name | ALL
- unconditional loop, which is repeated infinitely until an explicit EXIT is invoked. EXIT forces the current loop to stop immediately and can be subject to a Boolean condition specified by a WHEN predicate
testdb=> DO $code$
DECLARE
counter int := 0;
BEGIN
LOOP
counter := counter + 1;
RAISE INFO 'This is the % time I say HELLO!', counter;
EXIT WHEN counter > 3;
END LOOP;
RAISE INFO 'Good bye';
END $code$;
INFO: This is the 1 time I say HELLO!
INFO: This is the 2 time I say...
DO $$ BEGIN PERFORM pg_sleep(10); END $$;
SELECT pg_sleep(10);
SELECT pg_sleep_for('10 seconds');
SELECT pg_sleep_until('today 21:45');
SELECT pg_sleep_until('tomorrow 03:00');
\x
SELECT
clock_timestamp(),
pg_sleep(1),
clock_timestamp(),
pg_sleep(1),
clock_timestamp();
SELECT
clock_timestamp(),
pg_sleep(.5),
clock_timestamp(),
pg_sleep(.5),
clock_timestamp();
To prevent users from the insert or update a row that not visible through the view, you use the WITH CHECK OPTION
clause when creating the view. ''WITH CASCADED CHECK OPTION'' - checked the view-defining condition of view and also all the underlying views. To check the view-defining condition of the view that you insert or update, you use the WITH LOCAL CHECK OPTION
.
The rule of thumb for dynamic SQL: pass values/parameters through $x
placeholders with the using
clause. Inject identifiers (table names, column names) using %I
of the format()
function. So your code should be:
execute format('select 1 from schema.%I where id = $1', 'table_'||tablesuffix)
using id --<< passes the value of the variable id as the value for $1
into exists;
Dynamic SQL in pl/pgsql is written using the EXECUTE
statement. The basic syntax of the EXECUTE statement is as follows:
EXECUTE statement_name [ INTO target ] [ USING expression [, ...] ];
The INTO
clause is used to specify the target variable or variables for the result of the query. The USING
clause is used to supply values for any parameters used in the SQL statement.
CREATE OR REPLACE FUNCTION test_func_dynamic_sql(input_table_name text)
RETURNS TABLE(id integer, name text)
AS $$
BEGIN
EXECUTE 'SELECT id, name FROM ' || input_table_name INTO id, name;
RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
SELECT * FROM test_func_dynamic_sql('test_dynamic_sql');
CREATE OR REPLACE FUNCTION create_dynamic_table(table_name text,columns jsonb)
RETURNS void AS $$
DECLARE
column_rec record;
column_def text;
sql_stmt text;
BEGIN
-- Create column definitions
FOR column_rec IN SELECT * FROM jsonb_each_text(columns)
LOOP
column_def := column_rec.key || ' ' || column_rec.value;
IF sql_stmt IS NULL THEN sql_stmt := column_def;
ELSE sql_stmt := sql_stmt || ', ' || column_def;
END IF;
END LOOP;
-- Execute dynamic SQL to create the table
EXECUTE 'CREATE TABLE ' || table_name || '(' || sql_stmt || ')';
END;
$$ LANGUAGE plpgsql;
SELECT create_dynamic_table('customer_data', '{"name": "text","age": "integer"}');
It uses CASE statements to add conditions only if their corresponding input isn’t null.
CREATE OR REPLACE FUNCTION search_records(id int, name text, city text,
state text)
RETURNS TABLE(v_id integer, v_name text, v_city text, v_state text)
AS $$
BEGIN
EXECUTE 'SELECT id, name, city, state FROM records WHERE TRUE' ||
CASE WHEN name IS NOT NULL THEN ' AND name = ' || quote_literal(name) ELSE '' END ||
CASE WHEN city IS NOT NULL THEN ' AND city = ' || quote_literal(city) ELSE '' END ||
CASE WHEN state IS NOT NULL THEN ' AND state = ' || quote_literal(state) ELSE '' END
INTO v_id, v_name, v_city, v_state;
RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
SELECT * FROM search_records(1, 'foo', 'LA', 'California');
Dynamic index creation
CREATE OR REPLACE FUNCTION create_index(table_name text, column_name text,index_name text)
RETURNS void AS $$
BEGIN
EXECUTE 'CREATE INDEX ' || index_name || ' ON ' || table_name || '(' || column_name || ')';
END;
$$ LANGUAGE plpgsql;
Dynamic column selection
CREATE OR REPLACE FUNCTION select_columns(table_name text, column_names text[])
RETURNS record AS $$
DECLARE
rec record;
BEGIN
EXECUTE 'SELECT ' || array_to_string(column_names, ', ') || ' FROM ' || table_name INTO rec;
RETURN rec;
END;
$$ LANGUAGE plpgsql;
SELECT * FROM select_columns('employee_data', ARRAY['first_name',
'salary']) as foo(fname text, sal numeric);
Preventing SQL Injection
EXECUTE 'SELECT * FROM employees WHERE first_name = ' || quote_literal(input_name);
CREATE TABLE test_dynamic_sql (id int, name varchar);
INSERT INTO test_dynamic_sql VALUES(1, 'test1');
– Create a function that uses dynamic SQL:
CREATE OR REPLACE FUNCTION test_func_dynamic_sql(input_table_ name text) RETURNS TABLE(id integer, name text) AS $$ BEGIN EXECUTE 'SELECT id, name FROM ' || input_table_name INTO id, name;
RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
SELECT * FROM test_func_dynamic_sql('test_dynamic_sql');
id | name
----+-------
1 | test1
(1 row)
EXECUTE statement constructs an SQL statement by concatenating the input parameter with a string containing the SELECT statement. The INTO clause is used to store the results of the query into the id and name variables.
CREATE OR REPLACE FUNCTION create_index(table_name text, column_name text, index_name text)
RETURNS void
AS
$$
BEGIN
EXECUTE 'CREATE INDEX ' || index_name || ' ON ' || table_name || '(' || column_name || ')';
END;
$$ LANGUAGE plpgsql;
SELECT create_index('sample_table', 'column1', 'sample_column1_index');
CREATE OR REPLACE FUNCTION select_columns(table_name text, column_ names text[])
RETURNS record
AS
$$
DECLARE
rec record;
BEGIN
EXECUTE 'SELECT ' || array_to_string(column_names, ', ') || ' FROM ' || table_name INTO rec;
RETURN rec;
END;
$$ LANGUAGE plpgsql;
SELECT * FROM select_columns('employee_data', ARRAY['first_name', 'salary']) as foo(fname text, sal numeric);
- Parameterized Queries: Use parameterized queries or prepared statements to separate data from SQL code. This approach ensures that user inputs are treated as values rather than executable code.
- Quoting and Escaping: When constructing dynamic SQL, use proper quoting and escaping functions to handle user inputs securely. PostgreSQL provides functions like quote_literal and quote_ident to escape and quote input values. Also, quote_ nullable is useful when input values are null.
EXECUTE 'SELECT * FROM employees WHERE first_name = ' || quote_ literal(input_name);
CREATE FUNCTION my_func(my_age INT, my_id INT) RETURNS VOID
AS $$
BEGIN
EXECUTE 'UPDATE person SET age = $1 WHERE id = $2' USING my_age, my_id;
END;
$$ LANGUAGE plpgsql
PREPARE dynamic_query(text) AS SELECT * FROM employees WHERE last_name = $1;
EXECUTE dynamic_query(input_last_name);
PREPARE rental_insert (INTEGER, INTEGER, TIMESTAMP, INTEGER) AS
INSERT INTO rental (rental_id, inventory_id, rental_date, customer_id)
VALUES ($1, $2, $3, $4);
EXECUTE rental_insert(10001, 2019, '2023-06-01 09:00:00', 153);
- https://www.postgresql.org/docs/current/plpgsql-statements.html#PLPGSQL-STATEMENTS-EXECUTING-DYN The command string can use parameter values, which are referenced in the command as $1, $2, etc. These symbols refer to values supplied in the USING clause. This method is often preferable to inserting data values into the command string as text: it avoids run-time overhead of converting the values to text and back, and it is much less prone to SQL-injection attacks since there is no need for quoting or escaping. An example is:
EXECUTE 'SELECT count(*) FROM mytable WHERE inserted_by = $1 AND inserted <= $2'
INTO c
USING checked_user, checked_date;
Note that parameter symbols can only be used for data values — if you want to use dynamically determined table or column names, you must insert them into the command string textually. For example, if the preceding query needed to be done against a dynamically selected table, you could do this:
EXECUTE 'SELECT count(*) FROM '
|| quote_ident(tabname)
|| ' WHERE inserted_by = $1 AND inserted <= $2'
INTO c
USING checked_user, checked_date;
A cleaner approach is to use format()'s %I specification to insert table or column names with automatic quoting:
EXECUTE format('SELECT count(*) FROM %I '
'WHERE inserted_by = $1 AND inserted <= $2', tabname)
INTO c
USING checked_user, checked_date;
PL/pgSQL supports cursors, which are a mechanism for traversing through the records of a result set.
FOR LOOP to iterate over the SQL statement, which creates an implicit cursor in PostgreSQL and iterates over the result set.
postgres=# DO
$$
DECLARE v_rec RECORD;
BEGIN
FOR v_rec IN (SELECT * FROM many_rows_table)
LOOP
...
END LOOP;
END;
$$;
Using cursors can help reduce the response time when dealing with large datasets, as it allows the user to fetch a small, manageable number of records at a time. This is particularly useful when rendering data in a dashboard with pagination, where only a certain number of records are displayed at once. Cursors also allow for processing of the data as needed, rather than returning the entire result set at once to the client.
-
FOUND
Attribute returns true if the last operation on the cursor found a row, and false otherwise.
DO
$$
DECLARE
v_cur CURSOR FOR SELECT * FROM many_rows_table;
v_rec many_rows_table%ROWTYPE;
BEGIN
OPEN v_cur;
FETCH v_cur INTO v_rec;
IF FOUND THEN
RAISE NOTICE 'Row found.';
ELSE
RAISE NOTICE 'No rows found.';
END IF;
CLOSE v_cur;
END;
$$;
To opposit You can use IF NOT FOUND THEN
-
ROWCOUNT
Attribute returns the number of rows processed by the last FETCH or MOVE statement. You can use the GET DIAGNOSTICS command’s ROW_COUNT item as an alternative for this
DO
$$
DECLARE
v_cur CURSOR FOR SELECT * FROM many_rows_table;
v_rec many_rows_table%ROWTYPE;
num_rows int;
BEGIN
OPEN v_cur;
MOVE FORWARD ALL FROM v_cur;
GET DIAGNOSTICS num_rows = ROW_COUNT;
RAISE NOTICE 'Number of rows fetched: %', num_rows;
CLOSE v_cur;
END;
$$;
Unlike other database engines where cursors have to be explicitly closed, in PL/pgSQL cursors will be automatically closed when the transaction ends or when there is an exception in the current execution block. The implicit transactions will also be closed when their task is done. To monitor the current active transaction in PostgreSQL, we can use the PostgreSQL system catalog table “pg_cursors” , which provides insight into the details of opened cursors:
postgres=# \d pg_cursors
SELECT name, creation_time FROM pg_cursors;
PL/pgSQL provides holdable cursors, as well as scrollable cursors that must be explicitly set when creating external triggers.
PL/pgSQL provides the option to use scrollable cursors, which allow you to move back and forth through the result set. This is particularly useful for applications that require random access to the results, such as when implementing search functionality. To use a scrollable cursor, you must declare it with the SCROLL
keyword and then open it with the OPEN
statement. Once the cursor is open, you can fetch rows using the FETCH
statement and then move forward or backward through the result set using the MOVE
statement. Finally, you must close the cursor using the CLOSE statement.
postgres# BEGIN WORK;
DO
$$
DECLARE
v_cur SCROLL CURSOR FOR SELECT * FROM scroll_test;
BEGIN
OPEN v_cur;
END;
$$;
postgres# FETCH NEXT FROM v_cur;
postgres# FETCH NEXT FROM v_cur;
postgres# MOVE BACKWARD 1 IN v_cur;
postgres# MOVE BACKWARD 1 IN v_cur;
postgres# FETCH NEXT FROM v_cur;
By using FETCH NEXT
or MOVE BACKWARD
, we can control the amount of result sets, which we retrieve from the cursor.. If it is not possible to keep the transaction open for pagination, then we should use WITH HOLD
cursors. These cursors hold the cursor data pointer even after the transaction is closed.
“SCROLL” cursors should only be used for read-only operations.
With the NO SCROLL cursor, the data pointer cannot be moved backward. This cursor not only traverses the rows but also allows the traversed rows to be updated/ deleted using DML statements like UPDATE/DELETE.
If the SCROLL behavior of the cursor is not defined, PL/pgSQL will determine whether it should be SCROLL or NO SCROLL. If the SQL is executing FOR UPDATE, then the cursor will be set to “NO SCROLL.” If the SQL is read-only, then the cursor will automatically be set to “SCROLL.”
postgres=# BEGIN WORK;
postgres=*# DO
$$
DECLARE
v_cur NO SCROLL CURSOR FOR SELECT * FROM scroll_test FOR UPDATE;
BEGIN
OPEN v_cur;
END;
$$;
postgres=# FETCH NEXT FROM v_cur;
postgres=# DELETE FROM scroll_test WHERE CURRENT OF v_cur;
postgres=# FETCH NEXT FROM v_cur;
postgres=# END
Option “WITH HOLD” cursors allow the cursor data and pointer to be held even after the transaction is closed. This is useful when dealing with pagination or when you need to preserve the cursor state across multiple transactions.
“WITH HOLD” cursors because they won’t close automatically until we close the session.
postgres=# DO
$$
BEGIN
EXECUTE 'DECLARE cur CURSOR WITH HOLD FOR select * from scroll_test;';
END;
$$;
postgres=# FETCH NEXT FROM cur;
Currently, PL/pgSQL anonymous blocks do not support the creation of “WITH HOLD” cursors, unlike SCROLL and NO SCROLL cursors. To create these cursors, you must use named functions or procedures. Alternatively, you can create “WITH HOLD” cursors using a dynamic SQL EXECUTE statement approach in annonymous block.
PL/pgSQL supports reference cursors, which allow you to create a cursor and open it and then pass or return the cursor to another function as an input argument. This means that you can easily pass the cursor data pointer between multiple function calls, which improves code reusability and maintainability. These cursors can be created, opened, and passed or returned as input arguments to other functions.
postgres=# CREATE OR REPLACE FUNCTION test_refcursor(rf REFCURSOR)
RETURNS VOID
AS
$$
DECLARE v_rec RECORD;
BEGIN
LOOP
FETCH rf INTO v_rec;
RAISE NOTICE 'Record %', v_rec;
-- print only one record and exit the loop EXIT WHEN FOUND;
END LOOP;
END;
$$ LANGUAGE PLPGSQL;
postgres=# DO
$$
DECLARE
v_cur REFCURSOR;
BEGIN
OPEN v_cur FOR SELECT * FROM scroll_test;
PERFORM test_refcursor(v_cur);
END;
$$;
-- A function that builds a part of dynamic SQL - return string
CREATE OR REPLACE FUNCTION age_category_dyn (p_age text)
RETURNS text language plpgsql AS
$body$
BEGIN
RETURN ($$CASE
WHEN $$||p_age ||$$ <= 2 THEN 'Infant'
WHEN $$||p_age ||$$<= 12 THEN 'Child'
WHEN $$||p_age ||$$< 65 THEN 'Adult'
ELSE 'Senior'
END$$);
END; $body$;
CREATE TYPE passenger_age_cat_record AS (
passenger_id int,
age_category text
);
CREATE OR REPLACE FUNCTION passenger_age_category_select (p_limit int)
RETURNS setof passenger_age_cat_record
AS
$body$
BEGIN
RETURN QUERY
EXECUTE $$SELECT
passenger_id,
$$||age_category_dyn('age')||$$ AS age_category
FROM passenger LIMIT $$ ||p_limit::text
;
END;
$body$ LANGUAGE plpgsql;
--Now, we can execute the following statement:
SELECT * FROM passenger_age_category_select (5000000)
This will take about 11 seconds to execute, which is more than a
-- A function that builds dynamic SQL to search by different criteria
CREATE OR REPLACE FUNCTION select_booking_dyn (p_email text,
p_dep_airport text,
p_arr_airport text,
p_dep_date date,
p_flight_id int)
returns setof booking_record_basic
as
$func$
DECLARE
v_sql text:='SELECT DISTINCT b.booking_id, b.booking_ref, booking_name,
account_id, email
FROM booking b ';
v_where_booking text;
v_where_booking_leg text;
v_where_flight text;
BEGIN
IF p_email IS NOT NULL then v_where_booking :=$$ lower(email) like $$
||quote_literal(p_email||'%'); END IF;
IF p_flight_id IS NOT NULL then v_where_booking_leg:= $$ flight_id=$$||p_
flight_id::text;
END IF;
IF p_dep_airport IS NOT NULL
THEN v_where_flight:=concat_ws($$ AND $$, v_where_flight, $$departure_
airport=$$||
quote_literal(p_dep_airport));
END IF;
IF p_arr_airport IS NOT NULL
THEN v_where_flight:=concat_ws($$ AND $$,v_where_flight,
$$arrival_airport=$$||quote_literal(p_arr_airport));
END IF;
IF p_dep_date IS NOT NULL
THEN v_where_flight:=concat_ws($$ AND $$,v_where_flight,
$$scheduled_departure BETWEEN $$||
quote_literal(p_dep_date)||$$::date AND $$||quote_literal(p_dep_
date)||$$::date+1$$);
END IF;
IF v_where_flight IS NOT NULL OR v_where_booking_leg IS NOT NULL
THEN v_sql:=v_sql||$$ JOIN booking_leg bl USING (booking_id) $$;
END IF;
IF v_where_flight IS NOT NULL THEN
v_sql:=v_sql ||$$ JOIN flight f USING (flight_id) $$;
END IF;
v_sql:=v_sql ||$$ WHERE $$||
concat_ws($$ AND $$,v_where_booking, v_where_booking_leg, v_where_flight);
--raise notice 'sql:%', v_sql;
return query EXECUTE (v_sql);
END;
$func$ LANGUAGE plpgsql;
pgcrypto is a set of hashing and encryption functions that allow you to do things like hash a password using blowfish (bcrypt)
CREATE OR REPLACE FUNCTION register(login varchar(50), email varchar(50), password varchar(50), ip inet)
returns TABLE (
new_id bigint,
message varchar(255),
email varchar(255),
email_validation_token varchar(36)
)
AS
$$
DECLARE
new_id bigint;
message varchar(255);
hashedpw varchar(255);
validation_token varchar(36);
BEGIN
--hash the password using pgcrypto
SELECT crypt(password, gen_salt('bf', 10)) into hashedpw;
--create a random string for the
select substring(md5(random()::text),0, 36) into validation_token;
--create the member. Email has a unique constraint so this will
--throw. You could wrap this in an IF if you like too
insert into members(email, created_at, email_validation_token)
VALUES(email, now(), validation_token) returning id into new_id;
--set the return message
select 'Successfully registered' into message;
--add login bits to logins
insert into logins(member_id, provider, provider_key, provider_token)
values(new_id, 'local',email,hashedpw);
--add auth token to logins
insert into logins(member_id, provider, provider_key, provider_token)
values(new_id, 'token',null,validation_token);
-- add them to the members role which is 99
insert into members_roles(member_id, role_id)
VALUES(new_id, 99);
--add log entry
insert into logs(subject,entry,member_id, ip, created_at)
values('registration','Added to system, set role to User',new_id, ip, now());
--return out what happened here with relevant data
return query
select new_id, message, new_email, success, validation_token;
END
$$ LANGUAGE plpgsql;
Nie mozna commit wewnatrz procedury
CREATE OR REPLACE PROCEDURE MyProcedure(lot of args..)
LANGUAGE plpgsql
AS $procedure$
DECLARE
.....
.....
COMMIT;
END;
$procedure$
;
ERROR: invalid transaction termination
The documentation says: Transaction control is only possible in CALL or DO invocations from the top level or nested CALL or DO invocations without any other intervening command. For example, if the call stack is CALL proc1() → CALL proc2() → CALL proc3(), then the second and third procedures can perform transaction control actions. But if the call stack is CALL proc1() → SELECT func2() → CALL proc3(), then the last procedure cannot do transaction control, because of the SELECT in between.
There are some other, undocumented, restrictions:
- You cannot start a transaction explicitly with BEGIN and commit it inside a transaction. So the following will fail:
START TRANSACTION;
CALL procedure_with_commit();
This may be improved in future releases.
- All procedures in the call stack must be written in PL/pgSQL:
CREATE PROCEDURE b() LANGUAGE plpgsql
AS 'BEGIN PERFORM 42; COMMIT; END;';
CREATE PROCEDURE a() LANGUAGE sql
AS 'CALL b()';
CALL a();
ERROR: invalid transaction termination
CONTEXT: PL/pgSQL function b() line 1 at COMMIT
SQL function "a" statement 1
As it is, transaction control inside PostgreSQL procedures is somewhat limited.
If you violate any of these rules, you will get the error message you describe in your question. You will probably have to handle transactions in the application rather than in the procedure — perhaps splitting the procedure into smaller parts makes this possible.
IF condition or expression
THEN -- Code block to execute when condition is true
END IF;
PL/pgSQL uses SQL’s SPI interface to perform conditional evaluation. This means that internally it forms a SELECT statement around the given condition, executes it, and returns in the form of boolean. That is, the “1=1” will be converted into the SQL statement “SELECT 1=1” , and the result will be placed in the IF statement.
Statment | conversion |
---|---|
IF (1 = 1) ORDER BY 1 THEN | “SELECT 1=1 ORDER BY 1” |
IF (select count(*) from test) < 1 THEN | SELECT (SELECT count(*) FROM test) < 1 |
Note If conditional statements are placed inside loop statements, there will be a rapid context switch between the pl/pgSQl and SQl engines. this can result in increased resource utilization. If possible, avoid placing conditional expressions inside a huge number of iterations.
IF EXISTS(SELECT * FROM test)
THEN
RAISE NOTICE 'table is not empty';
END IF;
Optimal way of checking the table is empty. Dont scan whole table but (stops returning records from SELECT *) the process whenever it finds any single record in the table.