Supabase 数据库函数编写指南:从安全总则到生产级 PostgreSQL 函数模板
本指南基于开源仓库 Supabase 官方整理的 数据库函数编写规则(AI 编码助手 Cursor Rules 之一)展开。该文档沉淀了一套"写安全、可优化、易维护的 PostgreSQL 函数"的工程规范,适用于在 Supabase 平台上编写业务函数、RPC(Remote Procedure Call)与触发器函数。读完本文,你将掌握 SECURITY INVOKER/DEFINER 的取舍、search_path 加固、IMMUTABLE/STABLE/VOLATILE 声明等核心原则,并能直接复用仓库内置的五组 SQL 模板,在真实迁移文件中对照落地。
一、规则文档的定位:一套写给"函数作者"的权威清单
在仓库中,examples/prompts/database-functions.md 是 examples/prompts 目录下的一组"代码生成规则"之一,同目录还包含 code-format-sql.md、database-create-migration.md、database-rls-policies.md 等姊妹规则。这些文件带有统一的 YAML frontmatter:
description: Guidelines for writing Supabase database functions
alwaysApply: false
其中 description 用于让编码助手理解该规则的主题;alwaysApply: false 表示这是一条按需加载的规则(仅在用户请求"编写数据库函数"这类任务时才注入上下文)。它既可以作为 Cursor 等 AI 助手的系统规则,也可以当作人类开发者复查自己 SQL 时的自查清单——这也是本文把它当作权威方法论展开的原因。
Supabase 的核心是 Postgres 本身:业务逻辑既可以用客户端 SDK 拼装,也可以下沉为数据库函数,通过 PostgREST 以 RPC 形式暴露给前端。规则文档的全部内容,正是在回答一个关键问题:如何在 Supabase 上写出既安全又高性能的数据库函数。
二、总则一:默认 SECURITY INVOKER,只在必要时用 SECURITY DEFINER
规则的第一条总则,是要求函数默认以调用者的权限执行:
SECURITY INVOKER(PostgreSQL 默认值):函数以调用该函数的用户的权限执行。表所有者是否能访问、RLS 是否放行,都取决于调用者身份,天然更贴合前端直连数据库的访问控制模型。SECURITY DEFINER:函数以**函数创建者(owner)**的权限执行,可绕过调用者对底层对象的权限限制。它便于封装"受限入口",但同时扩大了提权面,一旦函数内有注入点或逻辑漏洞,后果更严重。
规则明确要求:默认使用 SECURITY INVOKER;只有存在明确需求时才使用 SECURITY DEFINER,并且必须向使用者解释清楚理由。这一点在本仓库的真实迁移中得到了印证——supabase/migrations/20260728035858_create_interfaces_feedback.sql 中,向 interfaces_feedback 表插入反馈的唯一通道就是 security definer 函数:
-- 表本身不开放 INSERT 授权,也不存在 INSERT 策略
create function public.submit_interfaces_feedback(
feedback text,
user_agent text default null,
user_id text default null,
project_ref text default null,
metadata jsonb default null
)
returns uuid
security definer
set search_path = ''
language plpgsql
as $$
...
$$;
该迁移的注释解释了"为什么必须用 SECURITY DEFINER":提交必须经由该函数,以便服务端生成 delete_token 并只返回一次给提交者(见该文件第 55-57 行注释,其中明确写了 "There is deliberately no insert grant or policy on the table itself")。这是规则中"明确需求 + 说明理由"的教科书式案例。
再比如 supabase/migrations/20240626184716_misc_database_fixes.sql 中查询系统表的 ipv6_active_status 函数,也以 security definer 声明——因为函数需要代表应用去读取被保护的平台内部表。可见:决定因素是"调用者自己是否有权访问所需对象",而不是"图省事"。
三、总则二:把 search_path 置空,并用全限定名引用一切对象
规则第二条总则可能是本仓库所有函数迁移中出现频率最高、也最容易被忽略的安全加固:
set search_path = ''
为什么必须这么做? search_path 决定 PostgreSQL 在解析未限定对象名(如直接写 order_items)时依次查找哪些 schema。如果它包含不受信任的 schema,攻击者可以在其中创建同名对象(如伪装的 public.users 或恶意函数),诱使你的函数在不知情的情况下引用恶意对象——这就是所谓的 schema 提权 / search_path 劫持风险。把 search_path 固定为空字符串后,任何对象引用都不会被隐式解析,所有访问目标必须在代码里写死:
from public.order_items -- schema.table 全限定
代价是需要额外自律:函数体内每一张表、每一个函数调用都必须显式写出 schema。规则甚至要求对操作符这类看似与 schema 无关的语法也做显式处理。本仓库的向量检索函数就是绝佳范例——supabase/migrations/20240626184716_misc_database_fixes.sql 中的 match_page_sections_v2 同时用到了 set search_path = '' 与全限定的 pgvector 负内积操作符:
create or replace function match_page_sections_v2(
embedding vector(1536),
match_threshold float,
min_content_length int
)
returns setof page_section
language plpgsql
set search_path = ''
as $$
#variable_conflict use_variable
begin
return query
select *
from public.page_section
where length(page_section.content) >= min_content_length
and (page_section.embedding operator(public.<#>) embedding) * -1 > match_threshold
order by page_section.embedding operator(public.<#>) embedding;
end;
$$;
operator(public.<#>) 这种写法正是为了在 search_path 为空时仍能精确解析自定义操作符,同时规避名称冲突。此外注意该文件的 1-6 行——函数写完后还能用 alter function ... set search_path = ''; 对历史遗留函数做补救式加固,说明这一安全约定在本仓库中是被系统性执行的。
另一个高频配套关键词 #variable_conflict use_variable 会在同一文件与本仓库的 content.update_error_code 函数中出现,它解决的是函数参数名与表列名同名时 PL/pgSQL 的变量解析歧义,属于"显式、无歧义"编码风格的一部分。
四、总则三:保证 SQL 合法且适配 Supabase 上下文
规则要求函数体内所有查询都是合法的 PostgreSQL SQL,并且与运行上下文(Supabase)兼容。具体到本仓库的实际约束包括:
- 函数最终可能经 PostgREST 暴露为 RPC,因此返回结构(标量、
setof复合类型、table(...)输出参数)必须与 API 消费方式匹配——match_page_sections_v2 特意返回setof page_section的注释说明就是为了能利用 PostgREST 的资源嵌入(resource embedding)与其它表 JOIN; - 函数可能被直接授予
anon/authenticated角色调用,也可能只作为触发器在服务端内部执行,权限模型不同(详见下文"第八节"); - 若用到 pgvector 等扩展类型(
vector(1536)),需确认扩展已在目标数据库启用。
一句话总结:函数不是"跑通就行"的孤立脚本,而是数据库 API 与安全边界的一部分。
五、最佳实践一:最小化副作用
规则建议:优先编写"返回结果"的函数,而非"修改数据"的函数;除非确有特定目的(例如触发器),否则避免让函数产生副作用。
好处显而易见:
- 可预测、易测试:同样的入参得到同样的结果,便于在 SQL 编辑器中单独
select验证; - 便于优化器处理(配合后文的
IMMUTABLE/STABLE声明,函数调用可被缓存或下推); - 减少隐式数据变更带来的审计与并发难题。
仓库里 utils.update_timestamp()(见下文)这类函数是"刻意保留副作用"的合法例外——它服务于触发器机制,属于被明确批准的特殊用途。
六、最佳实践二:使用显式类型
函数参数与返回值都应写明具体类型,避免 anyelement、长度缺省的 text/varchar 混用等"宽松类型"带来的歧义与安全隐患。
本仓库的 content.update_error_code 是很好的示范:四个参数 code text、service text、http_status_code smallint default null、message text default null 全部显式声明,并为可选参数提供了 default null,使函数既能全量更新也能做部分字段维护;返回值固定为 boolean,明确表达"本次是否存在真实变更"的语义(第 43-45 行通过 returning true into result 与 coalesce(result, false) 精确区分了插入/更新与无变化)。显式类型还直接服务于 PostgREST——它能据参数类型生成可用的 OpenAPI 风格的 RPC 端点。
七、最佳实践三:优先声明 IMMUTABLE 或 STABLE
PostgreSQL 用三种稳定性标签告知优化器"函数可被优化到什么程度":
| 标签 | 含义 | 能否被优化器缓存/重排 | 适用场景 |
|---|---|---|---|
IMMUTABLE |
相同入参永远返回相同结果,且不读表、不改库 | 完全可被常量折叠、索引(如表达式索引)与缓存 | 纯计算,如字符串拼接、哈希、数学运算 |
STABLE |
单条 SQL 语句内结果稳定,不修改数据库 | 语句级可缓存 | 读取数据库的查询函数 |
VOLATILE |
每次调用结果都可能变化,或会修改数据 | 不做跨调用假设 | 写操作、取 now()/随机数等 |
规则建议:能声明 IMMUTABLE 或 STABLE 就绝不默认 VOLATILE;只有当函数确实修改数据或有副作用时才用 VOLATILE。标签声明错误会有真实代价:把读库函数误标为 IMMUTABLE,可能让优化器缓存过期数据;把纯函数误标为 VOLATILE,则白白丢失建立表达式索引与内联优化的机会。
规则的第五个示例模板正是把这类纯计算函数写成 language sql + IMMUTABLE(见下文章节九),这是所有标签中最容易被优化器利用的组合。
八、最佳实践四:触发器函数必须附带完整的 CREATE TRIGGER
如果函数被用作触发器,规则要求一并给出把函数挂到目标表与事件的 CREATE TRIGGER 语句(如 BEFORE UPDATE),并注明 FOR EACH ROW 等触发粒度,保证代码开箱即用。
本仓库的 supabase/migrations/20250521181337_error_code_table.sql 提供了一个可直接对照的触发器函数实例:
create or replace function utils.update_timestamp()
returns trigger
set search_path = ''
language plpgsql
as $$
begin
new.updated_at = now();
return new;
end;
$$;
grant execute on function utils.update_timestamp() to anon;
grant execute on function utils.update_timestamp() to authenticated;
随后它被同一个迁移文件挂到两张内容表上(第 41-44 行、第 81-84 行):
create or replace trigger sync_updated_at_content_service
before update on content.service
for each row
execute function utils.update_timestamp();
注意该实例的工程细节与规则完全呼应:
new.updated_at := now(); ... return new;修改的是 NEW 行而非直接UPDATE表,避免了在行级触发器里递归触发自身的经典陷阱;- 函数定义在独立的
utilsschema,表在contentschema——正是对全限定名与 schema 职责划分的践行; - 该迁移文件第 1-10 行先用
alter default privileges撤销了对anon/authenticated的默认函数执行权,再对需要暴露的函数逐个grant execute。这补充了规则未展开的一个 Supabase 细节:PostgreSQL 默认向PUBLIC授予函数 EXECUTE,而生产环境中应遵循最小权限,把执行权精确授予anon与authenticated(或仅保留服务端内部使用)。
九、五组开箱即用的函数模板(原文档完整继承)
规则文档给出了五组模板,覆盖了函数编写的绝大多数形态。以下全部保留原文,并补充逐行要点注释,可直接复制到 SQL 编辑器或迁移文件中使用。
模板一:SECURITY INVOKER 的最简函数
create or replace function my_schema.hello_world()
returns text
language plpgsql
security invoker
set search_path = ''
as $$
begin
return 'hello world';
end;
$$;
要点:security invoker 与 set search_path = '' 成对出现;returns text 显式声明返回类型;函数体不含任何未限定对象引用,因此不存在 search_path 风险。
模板二:带参数与全限定对象名的函数
create or replace function public.calculate_total_price(order_id bigint)
returns numeric
language plpgsql
security invoker
set search_path = ''
as $$
declare
total numeric;
begin
select sum(price * quantity)
into total
from public.order_items
where order_id = calculate_total_price.order_id;
return total;
end;
$$;
要点:查询语句引用了 public.order_items 全限定表名;where order_id = calculate_total_price.order_id 用函数名限定参数,避免参数名与潜在列名的歧义。此写法正是触发 #variable_conflict 场景的另一种规避手段。该函数只读不写且依赖表内容,实操中应声明为 stable 以帮助优化器(模板示例未写,属于读者可自行按第七节原则补强的点)。
模板三:行级触发器函数 + 配套触发器
create or replace function my_schema.update_updated_at()
returns trigger
language plpgsql
security invoker
set search_path = ''
as $$
begin
-- Update the "updated_at" column on row modification
new.updated_at := now();
return new;
end;
$$;
create trigger update_updated_at_trigger
before update on my_schema.my_table
for each row
execute function my_schema.update_updated_at();
要点:returns trigger 是触发器函数的硬性要求;触发器的 execute function 语法(PostgreSQL 11+)替代了旧的 execute procedure;规则模板与本仓库 utils.update_timestamp 结构一致,可互相印证。
模板四:带错误处理的函数
create or replace function my_schema.safe_divide(numerator numeric, denominator numeric)
returns numeric
language plpgsql
security invoker
set search_path = ''
as $$
begin
if denominator = 0 then
raise exception 'Division by zero is not allowed';
end if;
return numerator / denominator;
end;
$$;
要点:用 raise exception 主动抛出语义明确的错误,而不是让数据库报出晦涩的除零错误。结合 Supabase 上下文:该异常会经 PostgREST 转换为 API 错误返回,因此错误文案应当面向调用方可理解。若希望该函数参与表达式索引或常量折叠,同样可进一步声明 immutable——它不读表、无副作用,完全符合条件。
模板五:IMMUTABLE + SQL 语言的纯函数
create or replace function my_schema.full_name(first_name text, last_name text)
returns text
language sql
security invoker
set search_path = ''
immutable
as $$
select first_name || ' ' || last_name;
$$;
要点:这是"最可优化"的函数形态——language sql 允许优化器将函数体内联展开,immutable 允许常量折叠与表达式索引。full_name 这类确定性字符串拼接,正是建立表达式索引(如 create index on users (my_schema.full_name(first_name, last_name)))的理想候选。注意 as $$ ... $$ 内直接是 SQL 表达式(select)而非 PL/pgSQL 的 begin/end 块。
十、模板与仓库实战的映射:每一条规则都有真实落点
为避免"纸上谈兵",下表把规则条款与本仓库可核查的迁移文件一一对应:
| 规则 / 模板 | 仓库中的真实实现(相对路径) |
|---|---|
触发器函数 + CREATE TRIGGER、set search_path = '' |
error_code_table.sql 的 utils.update_timestamp() 及其挂载到 content.service / content.error 的两个触发器 |
显式参数类型 + default null 可选参数 + 返回明确语义 |
error_code_update_functions.sql 的 content.update_error_code(code, service, ...) |
set search_path = '' + 全限定对象/操作符 + #variable_conflict |
misc_database_fixes.sql 的 match_page_sections_v2、docs_search_embeddings、docs_search_fts 等检索函数 |
SECURITY DEFINER 用于封装受限写入口(并附理由) |
create_interfaces_feedback.sql 的 public.submit_interfaces_feedback() |
最小执行权限:撤销默认 EXECUTE、按角色精确 grant |
error_code_table.sql 中对 utils schema 默认权限的 revoke/grant 组合 |
此外,这些函数全部按 Supabase CLI 的迁移规范存放在 supabase/migrations 目录,文件以 20260728035858_short_description.sql(YYYYMMDDHHmmss_ 前缀)命名。这与同目录姊妹规则 database-create-migration.md 约定的命名规范完全一致——函数与表结构一样,都应作为版本化迁移被审阅、被回滚、被追踪,而不是随手执行的一次性脚本。本仓库的 docs 站点数据库还包含大量配套的 RLS 策略与函数协作案例(可参考 database-rls-policies.md 了解 auth.uid() 等授权函数),印证了"函数 + 策略"共同构成访问控制闭环的架构。
十一、在 Supabase 项目里如何部署与验证
结合规则文档与本仓库的工程实践,落地一份数据库函数的推荐路径如下:
- 写迁移文件:把函数放进以
YYYYMMDDHHmmss_描述.sql命名的文件(如20240906123045_create_profiles.sql),放入supabase/migrations/目录。本仓库自身的函数变更(hybrid_search.sql 等)全部遵循该模式。 - 推送数据库:通过 Supabase CLI 将迁移应用到本地或远端数据库(例如
supabase db push/ 本地supabase start场景下的迁移重放)。日常调试也可以直接把create function ...粘贴进 Dashboard 的 SQL 编辑器执行。 - 验证权限:按第七节模板为函数授予执行权,确认
anon/authenticated是否能按预期调用 RPC,服务端内部的触发器函数则不需要对客户端授权。 - 验证安全声明:用只拥有基础权限的角色实际调用一次函数,确认
SECURITY INVOKER函数遵循 RLS 与列级授权、SECURITY DEFINER函数只暴露了最小必要能力。
整个项目由 pnpm-workspace.yaml 组织为 monorepo,而数据库侧代码统一收敛在 supabase/config.toml 与 supabase/migrations 之下——这意味着"数据库函数"在 Supabase 工程里是一等公民:它被版本管理、被同行评审、被 CI 保护,而不仅仅是应用层的一段字符串。
十二、把规则文档接入 AI 编码助手:Cursor Rules 的使用方式
如果你正在用 Cursor 等 AI 编程助手开发 Supabase 项目,可以把 examples/prompts/database-functions.md 作为 .cursor/rules 中的一条规则文件。它 frontmatter 中的 description 字段会在用户请求"创建/修改数据库函数"时被检索命中,而 alwaysApply: false 保证规则只在相关任务中被注入,避免无关请求时消耗上下文。其余 database-*.md 规则(迁移、RLS 策略)可按同样的方式配套启用,使 AI 在生成 DDL、函数与策略时遵循一致的安全基线。
结语:把"安全"写进函数的第一行
回顾整份规则,它真正的核心不是语法,而是三条安全心智模型:权限上默认最小化(INVOKER)、解析上默认无歧义(search_path = '' + 全限定名)、能力声明上默认最诚实(IMMUTABLE/STABLE,副作用尽量为零)。对照本仓库数百个真实迁移可以看到,这些不是纸面建议,而是 Supabase 自身文档站点数据库持续遵循的工程纪律。下次新建函数时,不妨以上述五组模板为起点,再问自己一句:这段函数如果被一个只拥有 anon 权限的攻击者精心构造入参调用,会发生什么?
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00