Write custom PostgreSQL functions in SQL and PL/pgSQL
PostgreSQL database functions allow you to encapsulate business logic directly in your database. Functions can be written in SQL, PL/pgSQL, or other procedural languages, and can be called from your application code via the Supabase client libraries.
create or replace function get_shipping_cost( total_amount numeric, country_code text)returns numericlanguage plpgsqlas $$declare shipping_cost numeric;begin -- Free shipping for orders over $100 if total_amount >= 100 then return 0; end if; -- Different rates by country case country_code when 'US' then shipping_cost := 9.99; when 'CA' then shipping_cost := 12.99; when 'GB' then shipping_cost := 14.99; else shipping_cost := 19.99; end case; return shipping_cost;end;$$;
create or replace function get_product_by_id(product_id bigint)returns table ( id bigint, name text, price numeric, in_stock boolean)language sqlas $$ select id, name, price, (quantity > 0) as in_stock from products where id = product_id;$$;
create or replace function get_top_rated_movies(min_rating numeric)returns table ( id bigint, name text, rating numeric, release_year integer)language sqlas $$ select id, name, rating, release_year from movies where rating >= min_rating order by rating desc limit 10;$$;
create or replace function get_active_users()returns setof users -- Returns rows matching the users table structurelanguage sqlas $$ select * from users where deleted_at is null and last_login > now() - interval '30 days';$$;
create or replace function update_inventory( product_id bigint, quantity_change integer)returns booleanlanguage plpgsqlas $$declare current_qty integer;begin -- Get current quantity select quantity into current_qty from products where id = product_id; -- Check if we have enough stock if current_qty + quantity_change < 0 then raise exception 'Insufficient inventory'; end if; -- Update quantity update products set quantity = quantity + quantity_change, updated_at = now() where id = product_id; return true;end;$$;
-- Runs with creator's permissions (security definer)create function admin_function()returns voidlanguage plpgsqlsecurity definerset search_path = publicas $$begin -- Can access tables the creator has access to delete from sensitive_table where expired = true;end;$$;-- Runs with caller's permissions (security invoker - default)create function user_function()returns voidlanguage plpgsqlsecurity invokeras $$begin -- Can only access what the caller has access to select * from public.products;end;$$;
When using security definer, always set search_path to prevent security vulnerabilities.
-- Revoke public executionrevoke execute on function sensitive_function() from public;revoke execute on function sensitive_function() from anon;-- Grant to specific rolegrant execute on function sensitive_function() to authenticated;
Restrict all new functions by default:
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;
create or replace function withdraw_funds( account_id bigint, amount numeric)returns voidlanguage plpgsqlas $$declare current_balance numeric;begin -- Get current balance select balance into current_balance from accounts where id = account_id; -- Validate amount if amount <= 0 then raise exception 'Amount must be positive'; end if; -- Check sufficient funds if current_balance < amount then raise exception 'Insufficient funds. Balance: %, Requested: %', current_balance, amount; end if; -- Perform withdrawal update accounts set balance = balance - amount where id = account_id;end;$$;
create or replace function process_order(order_id bigint)returns voidlanguage plpgsqlas $$declare order_total numeric;begin select total into order_total from orders where id = order_id; -- Throw error if order not found assert order_total is not null, 'Order not found'; -- Throw error if invalid total assert order_total > 0, 'Invalid order total'; -- Process the order...end;$$;
create or replace function safe_insert_product(product_name text)returns textlanguage plpgsqlas $$begin insert into products (name) values (product_name); return 'Success';exception when unique_violation then return 'Product already exists'; when others then return 'Error: ' || sqlerrm;end;$$;
create or replace function debug_function(input_val text)returns textlanguage plpgsqlas $$declare result text;begin raise log 'Function started with input: %', input_val; -- Do some work result := upper(input_val); raise log 'Result calculated: %', result; return result;end;$$;
-- IMMUTABLE: always returns same result for same inputscreate function add_numbers(a int, b int)returns intlanguage sqlimmutableas $$ select a + b;$$;-- STABLE: same result within a single querycreate function get_current_date()returns datelanguage sqlstableas $$ select current_date;$$;-- VOLATILE: result may change (default)create function generate_id()returns uuidlanguage sqlvolatileas $$ select gen_random_uuid();$$;