Database Functions
Postgres has built-in support for SQL functions.
These functions live inside your database, and you can call them from your app with rpc().
Quick demo#
Getting started#
Create a database function from the Dashboard, or write the SQL yourself against a direct connection.
To use the Dashboard:
- Go to the SQL Editor section.
- Click New Query.
- Enter the SQL that creates or replaces your database function.
- Click Run. You can also press
cmd+enterorctrl+enter.
Basic functions #
Create a basic database function that returns the string "hello world".
create or replace function hello_world() -- 1returns text -- 2language sql -- 3as $$ -- 4 select 'hello world'; -- 5$$; --6Show/Hide Details
At its most basic, a function has the following parts:
create or replace function hello_world(): The function declaration, wherehello_worldis the name of the function. Usecreatefor a new function,replacefor one that exists, orcreate or replacewhen the function might not exist yet.returns text: The type of data the function returns. For a function that returns nothing, writereturns void.language sql: The language used inside the function body. This can also be a procedural language:plpgsql,plpython, etc.as $$: The function wrapper. Anything inside the$$symbols is part of the function body.select 'hello world';: A basic function body. The function returns the result of the lastselectstatement in its body.$$;: The closing symbols of the function wrapper.
Overloaded functions aren't supported. Give every function a unique name.
After you create the function, you can run it inside the database with SQL, or with one of the client libraries.
select hello_world();Returning data sets#
A database function can also return a data set from a table or a view.
For example, take a database holding some Star Wars data:
The following function returns all the planets:
create or replace function get_planets()returns setof planetslanguage sqlas $$ select * from planets;$$;Because this function returns a table set, you can apply filters and selectors to it. To get the first planet only:
select *from get_planets()where id = 1;Passing parameters#
Create a function that inserts a new planet into the planets table and returns the new ID. This function uses the plpgsql language.
create or replace function add_planet(name text)returns bigintlanguage plpgsqlas $$declare new_row bigint;begin insert into planets(name) values (add_planet.name) returning id into new_row; return new_row;end;$$;You can run this function inside your database with a select query, or with the client libraries:
select * from add_planet('Jakku');Suggestions#
Database Functions vs Edge Functions#
For data-intensive operations, use database functions. They run inside your database, and you can call them remotely with the REST and GraphQL API.
For use cases that need low latency, use Edge Functions. They're globally distributed and you write them in TypeScript.
Security definer vs invoker#
Postgres runs a function either as the user calling it (invoker) or as its creator (definer). For example:
create function hello_world()returns textlanguage plpgsqlsecurity definer set search_path = ''as $$begin return 'hello world';end;$$;Prefer security invoker, which is also the default. When you use security definer, you must set the search_path.
With an empty search path, search_path = '', name the schema for every relation in the function body, such as from public.table. An empty search path limits the damage when the function can reach a schema you don't want the calling user to reach.
Function privileges#
By default, any role can run a database function. You can restrict execution in two ways:
-
Revoke on a case-by-case basis. Revoke execute for the functions you want to protect, from both
publicand the role you're restricting:revoke execute on function public.hello_world from public;revoke execute on function public.hello_world from anon; -
Restrict execution by default, then grant access to the roles that need each function.
To restrict every function that exists, revoke execute from both
publicand the role you want to restrict:revoke execute on all functions in schema public from public;revoke execute on all functions in schema public from anon, authenticated;To restrict every function created later, change the default privileges for both
publicand the role you want to restrict:alter default privileges in schema public revoke execute on functions from public;alter default privileges in schema public revoke execute on functions from anon, authenticated;You can then regrant permissions for a specific function to a specific role:
grant execute on function public.hello_world to authenticated;
Debugging functions#
Add logs to help you debug a function. Logs matter most in a complex function.
Good targets to log include:
- Values of (non-sensitive) variables
- Returned results from queries
General logging#
Use the raise keyword to write custom logs to the Postgres logs in the Dashboard. Three severity levels appear by default:
logwarningexception(error level)
create function logging_example( log_message text, warning_message text, error_message text)returns voidlanguage plpgsqlas $$begin raise log 'logging message: %', log_message; raise warning 'logging warning: %', warning_message; -- immediately ends function and reverts transaction raise exception 'logging error: %', error_message;end;$$;select logging_example('LOGGED MESSAGE', 'WARNING MESSAGE', 'ERROR MESSAGE');Error handling#
You can create custom errors with the raise exception keywords.
A common pattern is to throw an error when a variable doesn't meet a condition:
create or replace function error_if_null(some_val text)returns textlanguage plpgsqlas $$begin -- error if some_val is null if some_val is null then raise exception 'some_val should not be NULL'; end if; -- return some_val if it is not null return some_val;end;$$;select error_if_null(null);Value checking is common, so Postgres provides the assert keyword as a shorthand. It takes the following format:
-- throw error when condition is falseassert <some condition>, 'message';For example:
create function assert_example(name text)returns uuidlanguage plpgsqlas $$declare student_id uuid;begin -- save a user's id into the user_id variable select id into student_id from attendance_table where student = name; -- throw an error if the student_id is null assert student_id is not null, 'assert_example() ERROR: student not found'; -- otherwise, return the user's id return student_id;end;$$;select assert_example('Harry Potter');You can also capture and modify an error message with the exception keyword:
create function error_example()returns voidlanguage plpgsqlas $$begin -- fails: cannot read from nonexistent table select * from table_that_does_not_exist; exception when others then raise exception 'An error occurred in function <function name>: %', sqlerrm;end;$$;Advanced logging#
For a more complex function, or for harder debugging, log the following:
- Formatted variables
- Individual rows
- Start and end of function calls
create or replace function advanced_example(num int default 10)returns textlanguage plpgsqlas $$declare var1 int := 20; var2 text;begin -- Logging start of function raise log 'logging start of function call: (%)', (select now()); -- Logging a variable from a SELECT query select col_1 into var1 from some_table limit 1; raise log 'logging a variable (%)', var1; -- It is also possible to avoid using variables, by returning the values of your query to the log raise log 'logging a query with a single return value(%)', (select col_1 from some_table limit 1); -- If necessary, you can even log an entire row as JSON raise log 'logging an entire row as JSON (%)', (select to_jsonb(some_table.*) from some_table limit 1); -- When using INSERT or UPDATE, the new value(s) can be returned -- into a variable. -- When using DELETE, the deleted value(s) can be returned. -- All three operations use "RETURNING value(s) INTO variable(s)" syntax insert into some_table (col_2) values ('new val') returning col_2 into var2; raise log 'logging a value from an INSERT (%)', var2; return var1 || ',' || var2;exception -- Handle exceptions here if needed when others then raise exception 'An error occurred in function <advanced_example>: %', sqlerrm;end;$$;select advanced_example();Resources#
- Official Client libraries: JavaScript and Flutter
- Community client libraries: github.com/supabase-community
- Postgres Official Docs: Chapter 9. Functions and Operators
- Postgres Reference: CREATE FUNCTION