mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
Inline rewording only. Nothing moves and no claim changes. Addresses reader-facing "we", UI labels in quotes rather than bold, "allows you to", "e.g.", future tense, and title-case common nouns in body prose.
660 lines
15 KiB
Plaintext
660 lines
15 KiB
Plaintext
---
|
|
id: 'functions'
|
|
title: 'Database Functions'
|
|
description: 'Creating and using Postgres functions.'
|
|
video: 'https://www.youtube.com/v/MJZCCpCYEqk'
|
|
---
|
|
|
|
Postgres has built-in support for [SQL functions](https://www.postgresql.org/docs/current/sql-createfunction.html).
|
|
These functions live inside your database, and you can call them from your app with [`rpc()`](../../reference/javascript/rpc).
|
|
|
|
## Quick demo
|
|
|
|
<YouTube id="MJZCCpCYEqk" title="Creating and calling Postgres functions" />
|
|
|
|
## Getting started
|
|
|
|
Create a database function from the Dashboard, or write the SQL yourself against a
|
|
[direct connection](../../guides/database/connecting-to-postgres).
|
|
|
|
To use the Dashboard:
|
|
|
|
1. Go to the **SQL Editor** section.
|
|
2. Click **New Query**.
|
|
3. Enter the SQL that creates or replaces your database function.
|
|
4. Click **Run**. You can also press `cmd+enter` or `ctrl+enter`.
|
|
|
|
## Basic functions [#simple-functions]
|
|
|
|
Create a basic database function that returns the string "hello world".
|
|
|
|
```sql
|
|
create or replace function hello_world() -- 1
|
|
returns text -- 2
|
|
language sql -- 3
|
|
as $$ -- 4
|
|
select 'hello world'; -- 5
|
|
$$; --6
|
|
|
|
```
|
|
|
|
<details>
|
|
<summary>Show/Hide Details</summary>
|
|
|
|
At its most basic, a function has the following parts:
|
|
|
|
1. `create or replace function hello_world()`: The function declaration, where `hello_world` is the name of the function. Use `create` for a new function, `replace` for one that exists, or `create or replace` when the function might not exist yet.
|
|
2. `returns text`: The type of data the function returns. For a function that returns nothing, write `returns void`.
|
|
3. `language sql`: The language used inside the function body. This can also be a procedural language: `plpgsql`, `plpython`, etc.
|
|
4. `as $$`: The function wrapper. Anything inside the `$$` symbols is part of the function body.
|
|
5. `select 'hello world';`: A basic function body. The function returns the result of the last `select` statement in its body.
|
|
6. `$$;`: The closing symbols of the function wrapper.
|
|
|
|
</details>
|
|
|
|
<Admonition type="caution">
|
|
|
|
Overloaded functions aren't supported. Give every function a unique name.
|
|
|
|
</Admonition>
|
|
|
|
After you create the function, you can run it inside the database with SQL, or with one of the client libraries.
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="sql"
|
|
queryGroup="language"
|
|
>
|
|
<TabPanel id="sql" label="SQL">
|
|
|
|
```sql
|
|
select hello_world();
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="js" label="JavaScript">
|
|
|
|
```js
|
|
const { data, error } = await supabase.rpc('hello_world')
|
|
```
|
|
|
|
Reference: [`rpc()`](../../reference/javascript/rpc)
|
|
|
|
</TabPanel>
|
|
<$Show if="sdk:dart">
|
|
<TabPanel id="dart" label="Dart">
|
|
|
|
```dart
|
|
final data = await supabase
|
|
.rpc('hello_world');
|
|
```
|
|
|
|
Reference: [`rpc()`](../../reference/dart/rpc)
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:swift">
|
|
<TabPanel id="swift" label="Swift">
|
|
|
|
```swift
|
|
try await supabase.rpc("hello_world").execute()
|
|
```
|
|
|
|
Reference: [`rpc()`](../../reference/swift/rpc)
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:kotlin">
|
|
<TabPanel id="kotlin" label="Kotlin">
|
|
|
|
```kotlin
|
|
val data = supabase.postgrest.rpc("hello_world")
|
|
```
|
|
|
|
Reference: [`rpc()`](../../reference/kotlin/rpc)
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:python">
|
|
<TabPanel id="python" label="Python">
|
|
|
|
```python
|
|
data = supabase.rpc('hello_world').execute()
|
|
```
|
|
|
|
Reference: [`rpc()`](../../reference/python/rpc)
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:csharp">
|
|
<TabPanel id="csharp" label="C#">
|
|
|
|
```c#
|
|
await supabase.Rpc("hello_world", null);
|
|
```
|
|
|
|
Reference: [`Rpc()`](../../reference/csharp/rpc)
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
</Tabs>
|
|
|
|
## Returning data sets
|
|
|
|
A database function can also return a data set from a [table](../../guides/database/tables) or a view.
|
|
|
|
For example, take a database holding some Star Wars data:
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="data"
|
|
queryGroup="example-view"
|
|
>
|
|
<TabPanel id="data" label="Data">
|
|
|
|
### Planets
|
|
|
|
```
|
|
| id | name |
|
|
| --- | -------- |
|
|
| 1 | Tatooine |
|
|
| 2 | Alderaan |
|
|
| 3 | Kashyyyk |
|
|
```
|
|
|
|
### People
|
|
|
|
```
|
|
| id | name | planet_id |
|
|
| --- | ---------------- | --------- |
|
|
| 1 | Anakin Skywalker | 1 |
|
|
| 2 | Luke Skywalker | 1 |
|
|
| 3 | Princess Leia | 2 |
|
|
| 4 | Chewbacca | 3 |
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="sql" label="SQL">
|
|
|
|
```sql
|
|
create table planets (
|
|
id serial primary key,
|
|
name text
|
|
);
|
|
|
|
insert into planets
|
|
(id, name)
|
|
values
|
|
(1, 'Tattoine'),
|
|
(2, 'Alderaan'),
|
|
(3, 'Kashyyyk');
|
|
|
|
create table people (
|
|
id serial primary key,
|
|
name text,
|
|
planet_id bigint references planets
|
|
);
|
|
|
|
insert into people
|
|
(id, name, planet_id)
|
|
values
|
|
(1, 'Anakin Skywalker', 1),
|
|
(2, 'Luke Skywalker', 1),
|
|
(3, 'Princess Leia', 2),
|
|
(4, 'Chewbacca', 3);
|
|
```
|
|
|
|
</TabPanel>
|
|
</Tabs>
|
|
|
|
The following function returns all the planets:
|
|
|
|
```sql
|
|
create or replace function get_planets()
|
|
returns setof planets
|
|
language sql
|
|
as $$
|
|
select * from planets;
|
|
$$;
|
|
```
|
|
|
|
Because this function returns a table set, you can apply filters and selectors to it. To get the first planet only:
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="sql"
|
|
queryGroup="language"
|
|
>
|
|
<TabPanel id="sql" label="SQL">
|
|
|
|
```sql
|
|
select *
|
|
from get_planets()
|
|
where id = 1;
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="js" label="JavaScript">
|
|
|
|
```js
|
|
const { data, error } = supabase.rpc('get_planets').eq('id', 1)
|
|
```
|
|
|
|
</TabPanel>
|
|
<$Show if="sdk:dart">
|
|
<TabPanel id="dart" label="Dart">
|
|
|
|
```dart
|
|
final data = await supabase
|
|
.rpc('get_planets')
|
|
.eq('id', 1);
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:swift">
|
|
<TabPanel id="swift" label="Swift">
|
|
|
|
```swift
|
|
let response = try await supabase.rpc("get_planets").eq("id", value: 1).execute()
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:kotlin">
|
|
<TabPanel id="kotlin" label="Kotlin">
|
|
|
|
```kotlin
|
|
val data = supabase.postgrest.rpc("get_planets") {
|
|
filter {
|
|
eq("id", 1)
|
|
}
|
|
}
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:python">
|
|
<TabPanel id="python" label="Python">
|
|
|
|
```python
|
|
data = supabase.rpc('get_planets').eq('id', 1).execute()
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
</Tabs>
|
|
|
|
## 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.
|
|
|
|
```sql
|
|
create or replace function add_planet(name text)
|
|
returns bigint
|
|
language plpgsql
|
|
as $$
|
|
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:
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="sql"
|
|
queryGroup="language"
|
|
>
|
|
<TabPanel id="sql" label="SQL">
|
|
|
|
```sql
|
|
select * from add_planet('Jakku');
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="js" label="JavaScript">
|
|
|
|
```js
|
|
const { data, error } = await supabase.rpc('add_planet', { name: 'Jakku' })
|
|
```
|
|
|
|
</TabPanel>
|
|
<$Show if="sdk:dart">
|
|
<TabPanel id="dart" label="Dart">
|
|
|
|
```dart
|
|
final data = await supabase
|
|
.rpc('add_planet', params: { 'name': 'Jakku' });
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:swift">
|
|
<TabPanel id="swift" label="Swift">
|
|
|
|
Using `Encodable` type:
|
|
|
|
```swift
|
|
struct Planet: Encodable {
|
|
let name: String
|
|
}
|
|
|
|
try await supabase.rpc(
|
|
"add_planet",
|
|
params: Planet(name: "Jakku")
|
|
)
|
|
.execute()
|
|
```
|
|
|
|
Using `AnyJSON` convenience` type:
|
|
|
|
```swift
|
|
try await supabase.rpc(
|
|
"add_planet",
|
|
params: ["name": AnyJSON.string("Jakku")]
|
|
)
|
|
.execute()
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:kotlin">
|
|
<TabPanel id="kotlin" label="Kotlin">
|
|
|
|
```kotlin
|
|
val data = supabase.postgrest.rpc(
|
|
function = "add_planet",
|
|
parameters = buildJsonObject { //You can put here any serializable object including your own classes
|
|
put("name", "Jakku")
|
|
}
|
|
)
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:python">
|
|
<TabPanel id="python" label="Python">
|
|
|
|
```python
|
|
data = supabase.rpc('add_planet', params={'name': 'Jakku'}).execute()
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:csharp">
|
|
<TabPanel id="csharp" label="C#">
|
|
|
|
```c#
|
|
await supabase.Rpc("add_planet", new Dictionary<string, object> { { "name", "Jakku" } });
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
</Tabs>
|
|
|
|
## 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](../api).
|
|
|
|
For use cases that need low latency, use [Edge Functions](../../guides/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:
|
|
|
|
```sql
|
|
create function hello_world()
|
|
returns text
|
|
language plpgsql
|
|
security 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:
|
|
|
|
1. Revoke on a case-by-case basis. Revoke execute for the functions you want to protect, from both `public` and the role you're restricting:
|
|
|
|
```sql
|
|
revoke execute on function public.hello_world from public;
|
|
revoke execute on function public.hello_world from anon;
|
|
```
|
|
|
|
1. Restrict execution by default, then grant access to the roles that need each function.
|
|
|
|
To restrict every function that exists, revoke execute from both `public` and the role you want to restrict:
|
|
|
|
```sql
|
|
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 `public` and the role you want to restrict:
|
|
|
|
```sql
|
|
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:
|
|
|
|
```sql
|
|
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](/dashboard/project/_/logs/postgres-logs) in the Dashboard. Three severity levels appear by default:
|
|
|
|
- `log`
|
|
- `warning`
|
|
- `exception` (error level)
|
|
|
|
```sql
|
|
create function logging_example(
|
|
log_message text,
|
|
warning_message text,
|
|
error_message text
|
|
)
|
|
returns void
|
|
language plpgsql
|
|
as $$
|
|
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:
|
|
|
|
```sql
|
|
create or replace function error_if_null(some_val text)
|
|
returns text
|
|
language plpgsql
|
|
as $$
|
|
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:
|
|
|
|
```sql
|
|
-- throw error when condition is false
|
|
assert <some condition>, 'message';
|
|
```
|
|
|
|
For example:
|
|
|
|
```sql
|
|
create function assert_example(name text)
|
|
returns uuid
|
|
language plpgsql
|
|
as $$
|
|
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:
|
|
|
|
```sql
|
|
create function error_example()
|
|
returns void
|
|
language plpgsql
|
|
as $$
|
|
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
|
|
|
|
```sql
|
|
create or replace function advanced_example(num int default 10)
|
|
returns text
|
|
language plpgsql
|
|
as $$
|
|
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](../../reference/javascript/rpc) and [Flutter](../../reference/dart/rpc)
|
|
- Community client libraries: [github.com/supabase-community](https://github.com/supabase-community)
|
|
- Postgres Official Docs: [Chapter 9. Functions and Operators](https://www.postgresql.org/docs/current/functions.html)
|
|
- Postgres Reference: [CREATE FUNCTION](https://www.postgresql.org/docs/9.1/sql-createfunction.html)
|
|
|
|
## Deep dive
|
|
|
|
### Create Database Functions
|
|
|
|
<YouTube id="MJZCCpCYEqk" title="Creating a Postgres function in the dashboard" />
|
|
|
|
### Call Database Functions using JavaScript
|
|
|
|
<YouTube id="I6nnp9AINJk" title="Calling a Postgres function from JavaScript" />
|
|
|
|
### Using Database Functions to call an external API
|
|
|
|
<YouTube id="rARgrELRCwY" title="Calling an external API from a Postgres function" />
|