diff --git a/CN/modules/ROOT/nav.adoc b/CN/modules/ROOT/nav.adoc index fb9c4a4..a2dd994 100644 --- a/CN/modules/ROOT/nav.adoc +++ b/CN/modules/ROOT/nav.adoc @@ -27,6 +27,7 @@ ** xref:master/oracle_compatibility/compat_empty_string_to_null.adoc[18、空字符串转null] ** xref:master/oracle_compatibility/compat_call_into.adoc[19、CALL INTO] ** xref:master/oracle_compatibility/compat_read_only_view.adoc[20、视图只读] +** xref:master/oracle_compatibility/with_function_procedure.adoc[21、WITH FUNCTION/PROCEDURE] * 容器化与云服务 ** 容器化指南 *** xref:master/containerization/k8s_deployment.adoc[K8S部署] @@ -92,6 +93,7 @@ **** xref:master/compatibility_features_design/empty_string_to_null.adoc[空字符串转null] **** xref:master/compatibility_features_design/call_into.adoc[CALL INTO] **** xref:master/compatibility_features_design/read_only_view.adoc[视图只读] +**** xref:master/compatibility_features_design/with_function_procedure_impl.adoc[WITH FUNCTION/PROCEDURE] *** 内置函数 **** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] **** xref:master/oracle_builtin_functions/userenv.adoc[userenv] diff --git a/CN/modules/ROOT/pages/master/about_ivorysql.adoc b/CN/modules/ROOT/pages/master/about_ivorysql.adoc index 5688a4c..8d99c9c 100644 --- a/CN/modules/ROOT/pages/master/about_ivorysql.adoc +++ b/CN/modules/ROOT/pages/master/about_ivorysql.adoc @@ -87,4 +87,6 @@ IvorySQL是一个功能强大的开源对象关系数据库管理系统(ORDBMS) * 嵌套子函数 * sys_guid 函数 * 空字符串转null -* CALL INTO \ No newline at end of file +* CALL INTO +* 视图只读 +* WITH FUNCTION/PROCEDURE diff --git a/CN/modules/ROOT/pages/master/compatibility_features_design/with_function_procedure_impl.adoc b/CN/modules/ROOT/pages/master/compatibility_features_design/with_function_procedure_impl.adoc new file mode 100644 index 0000000..9ddf5df --- /dev/null +++ b/CN/modules/ROOT/pages/master/compatibility_features_design/with_function_procedure_impl.adoc @@ -0,0 +1,676 @@ +:sectnums: +:sectnumlevels: 5 + += WITH FUNCTION/PROCEDURE 实现说明 + +== 目的 + +本文档详细说明 IvorySQL 中 `WITH FUNCTION` 和 `WITH PROCEDURE` 功能的实现原理。该功能允许在 SQL 的 WITH 子句(公共表表达式,CTE)中直接定义 PL/SQL 函数和过程,实现 Oracle 的 Subquery Factoring with PL/SQL Declarations 特性。 + +== 实现说明 + +=== 系统分层架构 + +WITH 函数/过程的实现贯穿四个层次: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Layer 1: Oracle 解析器 (ora_gram.y + liboracle_parser.c) │ +│ ─ 扩展 with_clause 语法,允许 plsql_declarations │ +│ ─ 复用 OraBody_FUNC 词法机制将函数体捕获为 Sconst │ +│ ─ 输出: WithClause { plsql_defs: [InlineFunctionDef...], ctes: [...]}│ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ Layer 2: 语义分析 (parse_cte.c + parse_func.c) │ +│ ─ transformWithClause() 处理 InlineFunctionDef 节点 │ +│ ─ 解析函数签名,注册到 ParseState.p_with_func_list │ +│ ─ p_subprocfunc_hook 拦截对 WITH 函数的调用解析 │ +│ ─ FuncExpr.function_from = FUNC_FROM_WITH_CLAUSE ('w') │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ Layer 3: 规划器 (planner.c / createplan.c) │ +│ ─ 将 Query.withFuncDefs 复制到 PlannedStmt.withFuncDefs │ +│ ─ 无额外代价模型变更 │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ Layer 4: 执行器 (execExpr.c + pl_handler.c) │ +│ ─ ExecInitFunc() 识别 FUNC_FROM_WITH_CLAUSE │ +│ ─ 按需编译 WITH 函数体 │ +│ ─ 编译结果缓存在 EState.es_with_func_container │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +=== 语法与解析 + +==== 语法规则扩展 + +在 `ora_gram.y` 文件中扩展 `with_clause` 语法规则: + +[source,yacc] +---- +with_clause: + WITH plsql_declarations cte_list + { + WithClause *n = makeNode(WithClause); + n->plsql_defs = $2; + n->ctes = $3; + n->recursive = false; + n->location = @1; + $$ = n; + } + | WITH plsql_declarations + { + WithClause *n = makeNode(WithClause); + n->plsql_defs = $2; + n->ctes = NIL; + n->recursive = false; + n->location = @1; + $$ = n; + } + | WITH cte_list /* 现有语法保持不变 */ + | WITH_LA cte_list /* 现有 */ + | WITH RECURSIVE cte_list /* 现有 */ + ; + +plsql_declarations: + plsql_declaration + { $$ = list_make1($1); } + | plsql_declarations plsql_declaration + { $$ = lappend($1, $2); } + ; + +plsql_declaration: + FUNCTION ora_func_name opt_ora_func_args_with_defaults + RETURN func_return + ora_func_is_or_as Sconst ';' + { + InlineFunctionDef *n = makeNode(InlineFunctionDef); + n->funcname = strVal(llast($2)); + n->args = $3; + n->rettype = $5; + n->is_proc = false; + n->src = $7; + n->location = @2; + $$ = (Node *) n; + } + | PROCEDURE ora_func_name opt_procedure_args_with_defaults + ora_func_is_or_as Sconst ';' + { + InlineFunctionDef *n = makeNode(InlineFunctionDef); + n->funcname = strVal(llast($2)); + n->args = $3; + n->rettype = NULL; + n->is_proc = true; + n->src = $5; + n->location = @2; + $$ = (Node *) n; + } + ; +---- + +==== OraBody_FUNC 机制复用 + +Oracle 解析器已有 `set_oracle_plsql_body(OraBody_FUNC)` 机制,能将 IS/AS 后的 PL/SQL 体完整捕获为 Sconst 字符串。WITH FUNCTION 直接复用此机制,无需改动词法分析器。 + +[source,c] +---- +/* ora_gram.y */ +ora_func_is_or_as: + IS { set_oracle_plsql_body(yyscanner, OraBody_FUNC); } + | AS { set_oracle_plsql_body(yyscanner, OraBody_FUNC); } + ; +---- + +词法分析器(`liboracle_parser.c:439-652`)进入体捕获模式,跟踪 BEGIN/END/FUNCTION/PROCEDURE 嵌套深度,直到最外层 END 处停止,将全部文本作为单个 SCONST 返回。 + +==== 语法歧义处理 + +`FUNCTION` 和 `PROCEDURE` 都是 `unreserved_keyword`,可作 CTE 名。LALR(1) 状态表基于"第二个 token"自动消歧: + +| `WITH FUNCTION|PROCEDURE` 后第一个 token | LALR(1) 动作 | 走向 | +|--------------------------------|-------------|------| +| **IDENT** / 普通标识符 | shift(进入 `ora_func_name`) | plsql_declaration | +| **`AS`** | reduce | CTE 无列名列表 | +| **`(`** | reduce | CTE 有列名列表 | + +=== AST 节点设计 + +==== InlineFunctionDef 节点 + +新增 AST 节点表示 WITH 子句中的内嵌函数/过程定义: + +[source,c] +---- +/* parsenodes.h */ +typedef struct InlineFunctionDef +{ + NodeTag type; /* T_InlineFunctionDef */ + char *funcname; /* 函数/过程名(非限定名) */ + List *args; /* FunctionParameter 节点列表 */ + TypeName *rettype; /* 返回类型(过程为 NULL) */ + bool is_proc; /* true = 过程,false = 函数 */ + char *src; /* 函数体原始文本(IS/AS...END 全文) */ + ParseLoc location; /* 在原始 SQL 中的位置 */ +} InlineFunctionDef; +---- + +==== WithClause 扩展 + +[source,c] +---- +/* parsenodes.h */ +typedef struct WithClause +{ + NodeTag type; + List *plsql_defs; /* 新增:InlineFunctionDef 节点列表(ORA 模式) */ + List *ctes; /* 现有:CommonTableExpr 节点列表 */ + bool recursive; /* true = WITH RECURSIVE */ + ParseLoc location; +} WithClause; +---- + +==== FuncExpr 扩展 + +在 `primnodes.h` 中新增函数来源标识: + +[source,c] +---- +/* primnodes.h */ +#define FUNC_FROM_WITH_CLAUSE 'w' /* WITH 子句内嵌函数 */ + +/* 更新宏,将 'w' 纳入非 pg_proc 函数 */ +#define FUNC_EXPR_FROM_PG_PROC(function_from) \ + (function_from != FUNC_FROM_SUBPROCFUNC && \ + function_from != FUNC_FROM_PACKAGE && \ + function_from != FUNC_FROM_PACKGE_INITBODY && \ + function_from != FUNC_FROM_WITH_CLAUSE) +---- + +==== WithFuncEntry 结构体 + +ParseState 中新增轻量结构体用于存储函数签名: + +[source,c] +---- +/* parse_node.h */ +typedef struct WithFuncEntry +{ + char *funcname; + List *argtypes; /* Oid 列表 */ + Oid rettype; + bool is_proc; + int funcindex; /* 对应 FuncExpr.funcid */ + InlineFunctionDef *def; /* 指向原始定义节点 */ +} WithFuncEntry; +---- + +==== WithFuncContainer 运行时容器 + +[source,c] +---- +/* pl_handler.h */ +typedef struct WithFuncContainer +{ + int nfuncs; /* WITH 函数数量 */ + PLiSQL_subproc_function **funcs; /* 编译后的函数数组 */ + MemoryContext mcxt; /* 本容器的内存上下文 */ +} WithFuncContainer; +---- + +=== 语义分析 + +==== transformWithClause 扩展 + +在 `parse_cte.c` 中新增 `transformWithFuncDefs()` 函数: + +[source,c] +---- +/* parse_with_plsql.c */ +static void +transformWithFuncDefs(ParseState *pstate, List *plsql_defs) +{ + int funcindex = 0; + ListCell *lc; + + foreach(lc, plsql_defs) + { + InlineFunctionDef *ifd = (InlineFunctionDef *) lfirst(lc); + WithFuncEntry *entry = palloc(sizeof(WithFuncEntry)); + + /* 解析参数类型 */ + entry->argtypes = resolveWithFuncArgTypes(pstate, ifd->args); + + /* 解析返回类型 */ + entry->rettype = ifd->rettype ? + typenameTypeId(pstate, ifd->rettype) : InvalidOid; + + entry->funcname = ifd->funcname; + entry->is_proc = ifd->is_proc; + entry->funcindex = funcindex++; + entry->def = ifd; + + /* 检查重复定义 */ + checkDuplicateWithFunc(pstate->p_with_func_list, entry); + + pstate->p_with_func_list = lappend(pstate->p_with_func_list, entry); + } + + /* 安装函数查找钩子 */ + if (pstate->p_subprocfunc_hook == NULL) + pstate->p_subprocfunc_hook = withFuncLookupHook; +} +---- + +==== 函数调用解析钩子 + +`withFuncLookupHook` 拦截对 WITH 子句内嵌函数的调用: + +[source,c] +---- +/* parse_with_plsql.c */ +static FuncDetailCode +withFuncLookupHook(ParseState *pstate, List *funcname, + List **fargs, List *fargnames, int nargs, + Oid *argtypes, bool expand_variadic, bool expand_defaults, + bool proc_call, Oid *funcid, Oid *rettype, bool *retset, + int *nvargs, Oid *vatype, Oid **true_typeids, + List **argdefaults, void **pfunc) +{ + char *fname; + ListCell *lc; + + if (list_length(funcname) != 1) + return FUNCDETAIL_NOTFOUND; + + fname = strVal(linitial(funcname)); + + foreach(lc, pstate->p_with_func_list) + { + WithFuncEntry *entry = (WithFuncEntry *) lfirst(lc); + + if (strcmp(entry->funcname, fname) != 0) + continue; + + /* 检查参数数量和类型匹配 */ + if (!matchWithFuncArgs(entry, nargs, argtypes, fargnames, + true_typeids, argdefaults)) + continue; + + *funcid = (Oid) entry->funcindex; + *rettype = entry->rettype; + *retset = false; + *nvargs = 0; + *vatype = InvalidOid; + *pfunc = NULL; + + return entry->is_proc ? FUNCDETAIL_PROCEDURE : FUNCDETAIL_NORMAL; + } + + return FUNCDETAIL_NOTFOUND; +} +---- + +=== 执行器设计 + +==== ExecInitFunc 扩展 + +在 `execExpr.c` 中识别 `FUNC_FROM_WITH_CLAUSE`: + +[source,c] +---- +/* execExpr.c */ +if (funcexpr->function_from == FUNC_FROM_WITH_CLAUSE) +{ + scratch->d.func.finfo = palloc0(sizeof(FmgrInfo)); + scratch->d.func.fcinfo_data = palloc0(SizeForFunctionCallInfo(nargs)); + flinfo = scratch->d.func.finfo; + fcinfo = scratch->d.func.fcinfo_data; + + /* 使用专用调度函数 */ + flinfo->fn_addr = plisql_with_func_call_handler; + flinfo->fn_oid = funcid; + + /* fn_extra 存储 EState 指针 */ + flinfo->fn_extra = state->parent->state; + + fmgr_info_set_expr((Node *) node, flinfo); + InitFunctionCallInfoData(*fcinfo, flinfo, nargs, inputcollid, NULL, NULL); + scratch->d.func.fn_addr = flinfo->fn_addr; + scratch->d.func.nargs = nargs; + return; +} +---- + +==== 运行时调度函数 + +[source,c] +---- +/* pl_handler.c */ +Datum +plisql_with_func_call_handler(PG_FUNCTION_ARGS) +{ + EState *estate = (EState *) fcinfo->flinfo->fn_extra; + int funcindex = (int) fcinfo->flinfo->fn_oid; + WithFuncContainer *container; + + /* 懒加载:首次调用时编译所有 WITH 函数 */ + if (estate->es_with_func_container == NULL) + estate->es_with_func_container = buildWithFuncContainer(estate); + + container = estate->es_with_func_container; + + Assert(funcindex >= 0 && funcindex < container->nfuncs); + subprocfunc = container->funcs[funcindex]; + + return execWithFunction(subprocfunc, fcinfo); +} +---- + +==== WithFuncContainer 编译 + +[source,c] +---- +/* pl_handler.c */ +WithFuncContainer * +buildWithFuncContainer(EState *estate) +{ + PlannedStmt *pstmt = estate->es_plannedstmt; + MemoryContext oldcxt = MemoryContextSwitchTo(estate->es_query_cxt); + WithFuncContainer *container; + int nfuncs, i; + ListCell *lc; + + nfuncs = list_length(pstmt->withFuncDefs); + container = palloc0(sizeof(WithFuncContainer)); + container->nfuncs = nfuncs; + container->funcs = palloc0(nfuncs * sizeof(PLiSQL_subproc_function *)); + container->mcxt = CurrentMemoryContext; + + /* 编译阶段:建立共享编译上下文,以支持函数间互调 */ + plisql_push_subproc_func(); + plisql_start_subproc_func(); + + /* Pass 1:注册所有函数签名 */ + i = 0; + foreach(lc, pstmt->withFuncDefs) + { + InlineFunctionDef *ifd = (InlineFunctionDef *) lfirst(lc); + List *argitems = buildArgItemsFromFuncParams(ifd->args); + PLiSQL_type *rettype = ifd->rettype ? + buildPLiSQLType(ifd->rettype) : NULL; + + PLiSQL_subproc_function *subprocfunc = + plisql_build_subproc_function(ifd->funcname, argitems, + rettype, ifd->location); + subprocfunc->is_proc = ifd->is_proc; + subprocfunc->src = ifd->src; + + container->funcs[i++] = subprocfunc; + } + + /* Pass 2:编译每个函数体 */ + i = 0; + foreach(lc, pstmt->withFuncDefs) + { + InlineFunctionDef *ifd = (InlineFunctionDef *) lfirst(lc); + PLiSQL_subproc_function *subprocfunc = container->funcs[i++]; + + PLiSQL_stmt_block *action = + compileWithFuncBody(ifd->src, subprocfunc); + + plisql_set_subprocfunc_action(subprocfunc, action); + } + + plisql_pop_subproc_func(); + + MemoryContextSwitchTo(oldcxt); + return container; +} +---- + +=== 内存生命周期 + +[source] +---- +SQL 文本解析阶段 + ├── InlineFunctionDef 节点 + │ 生命周期:与解析树相同(ParseState.p_mem_cxt) + └── WithFuncEntry 列表 + 生命周期:与 ParseState 相同 + +查询分析阶段 + └── Query.withFuncDefs(InlineFunctionDef 列表,复制指针) + 生命周期:与 Query 相同 + +规划阶段 + └── PlannedStmt.withFuncDefs(同上) + 生命周期:与 PlannedStmt 相同(可被 plancache 持有) + +执行阶段 + ├── EState.es_with_func_container(WithFuncContainer) + │ 内存上下文:estate->es_query_cxt + │ 生命周期:绑定到 EState,ExecutorEnd() 时释放 + ├── PLiSQL_subproc_function 数组 + │ 生命周期:与 WithFuncContainer 相同 + └── PLiSQL_function(编译结果) + 生命周期:查询执行结束时自动释放 +---- + +=== 关键设计原则 + +1. **复用 OraBody_FUNC**:Oracle 解析器已有 `set_oracle_plsql_body(OraBody_FUNC)` 机制,WITH FUNCTION 直接复用,无需改动词法分析器。 + +2. **不写入系统目录**:WITH 函数的编译结果存储在 EState 本地,不写入 `pg_proc`,不产生 WAL,事务结束自动释放。 + +3. **延迟编译**:函数体在执行器初始化阶段(`ExecInitFunc`)才真正编译,解析和规划阶段只处理签名,避免了在 Plan 节点中存储指针的生命周期问题。 + +4. **与 PG_PARSER 隔离**:所有新逻辑受 `compatible_db == ORA_PARSER` 守卫,PostgreSQL 原有解析器路径不受影响。 + +5. **两阶段编译**:通过 two-pass 设计支持函数间互调(Pass 1 注册签名,Pass 2 编译函数体)。 + +=== 函数间互调 + +得益于 two-pass 编译设计,**WITH 子句内函数互调对声明顺序没有限制**:A 可以调用在 A 之后定义的 B,反之亦然。无需 Oracle PL/SQL 风格的显式前向声明。 + +[source,sql] +---- +-- 函数互调示例 +WITH + FUNCTION mul2(n NUMBER) RETURN NUMBER AS BEGIN RETURN n*2; END; + FUNCTION add1(n NUMBER) RETURN NUMBER AS BEGIN RETURN n+1; END; +SELECT mul2(add1(3)) FROM dual; -- add1 先定义但 mul2 可以调用它 +---- + +=== EXPLAIN 输出 + +==== 基本输出 + +[source,sql] +---- +EXPLAIN WITH + FUNCTION add_one(n NUMBER) RETURN NUMBER AS BEGIN RETURN n + 1; END; +SELECT add_one(5) FROM dual; +---- +输出包含:`WITH Function: add_one(number) RETURN number` + +==== VERBOSE 模式 + +[source,sql] +---- +EXPLAIN (VERBOSE ON) WITH + FUNCTION double(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n * 2; END; +SELECT double(3) FROM dual; +---- +输出包含:`Body: BEGIN RETURN n * 2; END` + +=== 错误处理 + +==== 重复定义检查 + +[source,sql] +---- +WITH + FUNCTION dup(n NUMBER) RETURN NUMBER AS BEGIN RETURN n; END; + FUNCTION dup(n NUMBER) RETURN NUMBER AS BEGIN RETURN n * 2; END; +SELECT dup(1) FROM dual; +-- ERROR: WITH clause function "dup" is defined more than once +---- + +==== PG_PARSER 模式拒绝 + +[source,sql] +---- +SET compatible_db = PG_PARSER; +WITH FUNCTION foo(n NUMBER) RETURN NUMBER AS BEGIN RETURN n; END; +SELECT foo(1); +-- ERROR: syntax error at or near "FUNCTION" +---- + +==== 函数体编译错误 + +[source,sql] +---- +WITH + FUNCTION broken(n NUMBER) RETURN NUMBER AS + BEGIN + RETRUN n; -- 拼写错误 + END; +SELECT broken(1) FROM dual; +-- ERROR: syntax error at or near "RETRUN" +---- +错误上下文:`while compiling WITH FUNCTION "broken_body"` + +==== 表函数用法拒绝 + +[source,sql] +---- +WITH + FUNCTION get_rows(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n; END; +SELECT * FROM get_rows(5); +-- ERROR: WITH clause function cannot be used as a table function +---- + +==== 限定名拒绝 + +[source,sql] +---- +WITH + FUNCTION public.qual_func(n NUMBER) RETURN NUMBER IS + BEGIN RETURN n; END; +SELECT qual_func(1) FROM dual; +-- ERROR: qualified name is not allowed in WITH FUNCTION declaration +---- + +=== 扩展的文件清单 + +==== 新增文件 + +| 文件 | 说明 | +|------|------| +| `src/backend/oracle_parser/ora_with_function.c` | WITH 函数运行时逻辑 | +| `src/backend/parser/parse_with_plsql.c` | `transformWithFuncDefs`、`withFuncLookupHook` | +| `src/include/oracle_parser/ora_with_function.h` | 头文件:`WithFuncEntry`、`WithFuncContainer` | +| `src/oracle_test/regress/sql/with_function.sql` | 回归测试(32 用例) | + +==== 修改现有文件 + +| 文件 | 修改内容 | +|------|----------| +| `src/include/nodes/parsenodes.h` | 添加 `InlineFunctionDef`;扩展 `WithClause.plsql_defs` 和 `Query.withFuncDefs` | +| `src/include/nodes/plannodes.h` | 扩展 `PlannedStmt.withFuncDefs` | +| `src/include/nodes/primnodes.h` | 添加 `FUNC_FROM_WITH_CLAUSE = 'w'` | +| `src/include/nodes/execnodes.h` | 扩展 `EState.es_with_func_container` | +| `src/include/parser/parse_node.h` | 扩展 `ParseState.p_with_func_list` | +| `src/backend/oracle_parser/ora_gram.y` | 扩展 `with_clause`、新增 `plsql_declarations`/`plsql_declaration` | +| `src/backend/parser/parse_cte.c` | `transformWithClause()` 调用 `transformWithFuncDefs` | +| `src/backend/parser/parse_func.c` | FuncExpr 标记逻辑 | +| `src/backend/parser/analyze.c` | 传递 `withFuncDefs` 到 Query 节点 | +| `src/backend/optimizer/plan/planner.c` | 传递 `withFuncDefs` 到 PlannedStmt | +| `src/backend/executor/execExpr.c` | `ExecInitFunc()` 处理 WITH 函数 | +| `src/pl/plisql/src/pl_handler.c` | `buildWithFuncContainer`、`plisql_with_func_call_handler` | +| `src/pl/plisql/src/pl_comp.c` | `plisql_parser_setup` 根据 flag gate `p_with_func_list` | +| `src/backend/commands/explain.c` | EXPLAIN 输出 `WITH Function:` / `WITH Procedure:` | +| `src/oracle_fe_utils/ora_psqlscan.l` | psql 客户端扫描器识别 `WITH FUNCTION/PROCEDURE` | + +=== 节点函数自动生成 + +PostgreSQL 16+ 引入了节点基础设施代码生成器(`src/backend/nodes/gen_node_support.pl`)。新增 `InlineFunctionDef` 节点后,构建系统自动重跑生成器,产出: + +| 自动生成的文件 | 包含内容 | +|--------------|---------| +| `copyfuncs.funcs.c` / `copyfuncs.switch.c` | `_copyInlineFunctionDef` | +| `equalfuncs.funcs.c` / `equalfuncs.switch.c` | `_equalInlineFunctionDef` | +| `outfuncs.funcs.c` / `outfuncs.switch.c` | `_outInlineFunctionDef` | +| `readfuncs.funcs.c` / `readfuncs.switch.c` | `_readInlineFunctionDef` | +| `nodetags.h` | `T_InlineFunctionDef = 498` | +| `queryjumblefuncs.funcs.c` | `_jumbleInlineFunctionDef` | + +== 使用示例 + +=== 最简单的内嵌函数 + +[source,sql] +---- +WITH + FUNCTION double_it(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n * 2; END; +SELECT double_it(5) FROM dual; +-- 输出:10 +---- + +=== 函数与 CTE 混合 + +[source,sql] +---- +WITH + FUNCTION tax(amt NUMBER) RETURN NUMBER AS + BEGIN RETURN amt * 0.1; END; + orders AS (SELECT 100 AS amount) +SELECT amount, tax(amount) FROM orders; +-- 输出:100 | 10 +---- + +=== 多个内嵌函数 + +[source,sql] +---- +WITH + FUNCTION add1(n NUMBER) RETURN NUMBER AS BEGIN RETURN n+1; END; + FUNCTION mul2(n NUMBER) RETURN NUMBER AS BEGIN RETURN n*2; END; +SELECT mul2(add1(3)) FROM dual; +-- 输出:8 +---- + +=== 递归函数 + +[source,sql] +---- +WITH + FUNCTION factorial(n NUMBER) RETURN NUMBER AS + BEGIN + IF n <= 1 THEN RETURN 1; END IF; + RETURN n * factorial(n-1); + END; +SELECT factorial(5) FROM dual; +-- 输出:120 +---- + +=== 与 DML 集成 + +[source,sql] +---- +-- INSERT 中使用内嵌函数 +WITH + FUNCTION get_bonus(sal NUMBER) RETURN NUMBER AS + BEGIN RETURN sal * 1.2; END; +INSERT INTO emp_bonus (empno, bonus) +SELECT empno, get_bonus(sal) FROM emp WHERE deptno = 10; + +---- + +NOTE: Oracle 不允许 WITH FUNCTION 位于 UPDATE、DELETE、MERGE 之前;IvorySQL 遵循相同限制,此类用法报 `ERRCODE_FEATURE_NOT_SUPPORTED` 错误。 diff --git a/CN/modules/ROOT/pages/master/oracle_compatibility/with_function_procedure.adoc b/CN/modules/ROOT/pages/master/oracle_compatibility/with_function_procedure.adoc new file mode 100644 index 0000000..1899904 --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_compatibility/with_function_procedure.adoc @@ -0,0 +1,313 @@ +:sectnums: +:sectnumlevels: 5 + += WITH FUNCTION/PROCEDURE + +== 目的 + +本文档解释 IvorySQL 中 `WITH FUNCTION` 和 `WITH PROCEDURE` 的用途,实现 Oracle 风格的 SQL 内嵌 PL/SQL 函数和过程功能。 + +`WITH FUNCTION/PROCEDURE` 是 Oracle 数据库的 Subquery Factoring with PL/SQL Declarations 特性,允许在 SQL 的 WITH 子句(公共表表达式,CTE)中直接定义 PL/SQL 函数和过程。 + +== 功能说明 + +=== 基本语法 + +在 Oracle 兼容模式(`compatible_db = ORA_PARSER`)下,WITH 子句支持以下扩展语法: + +[source,sql] +---- +WITH + FUNCTION func_name ( [ param_list ] ) RETURN return_type + { IS | AS } + [ declare_section ] + BEGIN + statements + END [ func_name ] ; + + PROCEDURE proc_name ( [ param_list ] ) + { IS | AS } + [ declare_section ] + BEGIN + statements + END [ proc_name ] ; + + cte_name AS ( SELECT ... ) +SELECT ... +---- + +=== 核心特性 + +- **作用域限制**:函数/过程在 WITH 子句中定义,作用域仅限于当前 SQL 语句 +- **混合排列**:函数/过程定义可与 CTE(AS (SELECT ...))混合使用 +- **完整 PL/SQL 语法**:函数体支持完整的 PL/SQL 语法(BEGIN...END) +- **不写入系统目录**:执行结束后自动销毁,不持久化到 `pg_proc` +- **仅 SELECT 上下文**:适用于 SELECT 语句及 INSERT...SELECT 语句;位于 UPDATE、DELETE、MERGE 之前时报错(与 Oracle 行为一致) + +=== 参数模式 + +内嵌过程支持标准 Oracle 参数模式: + +- `IN`(默认):输入参数 +- `OUT`:输出参数 +- `IN OUT`:双向参数 + +内嵌函数仅支持`IN`类型输入参数。 + +== 支持的语句类型 + +WITH 内嵌函数/过程仅允许出现在以下顶层语句中: + +- `SELECT` 语句(最常见) +- `INSERT ... SELECT` 语句(Oracle 兼容形式,WITH FUNCTION 位于 INSERT INTO 之后) + +以下语句**不支持**(Oracle 不允许,报 `ERRCODE_FEATURE_NOT_SUPPORTED`): + +- `WITH FUNCTION ... UPDATE ...`:WITH FUNCTION 不能位于 UPDATE 之前 +- `WITH FUNCTION ... DELETE ...`:WITH FUNCTION 不能位于 DELETE 之前 +- `WITH FUNCTION ... MERGE ...`:WITH FUNCTION 不能位于 MERGE 之前 + +如需在 DML 中使用可复用逻辑,应定义 schema 级别的函数(`CREATE FUNCTION`)。 + +== 语法示例 + +=== 最简单的内嵌函数 + +[source,sql] +---- +WITH + FUNCTION double_it(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n * 2; END; +SELECT double_it(5) FROM dual; +-- 期望输出:10 +---- + +=== 函数与 CTE 混合 + +[source,sql] +---- +WITH + FUNCTION tax(amt NUMBER) RETURN NUMBER AS + BEGIN RETURN amt * 0.1; END; + orders AS (SELECT 100 AS amount) +SELECT amount, tax(amount) FROM orders; +-- 期望输出:100 | 10 +---- + +=== 多个内嵌函数 + +[source,sql] +---- +WITH + FUNCTION add1(n NUMBER) RETURN NUMBER AS BEGIN RETURN n+1; END; + FUNCTION mul2(n NUMBER) RETURN NUMBER AS BEGIN RETURN n*2; END; +SELECT mul2(add1(3)) FROM dual; +-- 期望输出:8 +---- + +=== 递归函数 + +[source,sql] +---- +WITH + FUNCTION factorial(n NUMBER) RETURN NUMBER AS + BEGIN + IF n <= 1 THEN RETURN 1; END IF; + RETURN n * factorial(n-1); + END; +SELECT factorial(5) FROM dual; +-- 期望输出:120 +---- + +=== OUT 参数(仅限 PROCEDURE) + +WITH FUNCTION 不允许声明 `OUT` / `IN OUT` 参数(与 Oracle ORA-06572 行为一致); +仅 WITH PROCEDURE 允许 `OUT` / `IN OUT` 参数。 + +[source,sql] +---- +-- 正确:PROCEDURE 可声明 OUT 参数 +WITH + PROCEDURE swap(val IN NUMBER, result OUT NUMBER) AS + BEGIN + result := val * 10; + END; +SELECT 1 FROM dual; -- 过程由同一 WITH 块内的其他子程序调用,不直接出现在 SELECT 表达式中 + +-- 错误:FUNCTION 不允许 OUT 参数 +WITH + FUNCTION bad_func(val NUMBER, result OUT NUMBER) RETURN NUMBER AS + BEGIN RETURN val; END; +SELECT bad_func(5) FROM dual; +-- 期望输出:ERROR: WITH FUNCTION "bad_func" cannot declare OUT or IN OUT parameters +---- + +=== 默认参数值 + +[source,sql] +---- +WITH + FUNCTION calc(n NUMBER DEFAULT 10) RETURN NUMBER AS + BEGIN RETURN n * 2; END; +SELECT calc() FROM dual; +-- 期望输出:20 +---- + +=== 异常处理 + +[source,sql] +---- +WITH + FUNCTION safe_div(a NUMBER, b NUMBER) RETURN NUMBER AS + BEGIN + RETURN a / b; + EXCEPTION + WHEN OTHERS THEN RETURN NULL; + END; +SELECT safe_div(1, 0) FROM dual; +-- 期望输出:NULL +---- + +=== 与 DML 集成 + +[source,sql] +---- +-- 允许:INSERT INTO ... WITH FUNCTION ... SELECT ...(Oracle 兼容形式) +WITH + FUNCTION get_bonus(sal NUMBER) RETURN NUMBER AS + BEGIN RETURN sal * 1.2; END; +INSERT INTO emp_bonus (empno, bonus) +SELECT empno, get_bonus(sal) FROM emp WHERE deptno = 10; +---- + +NOTE: Oracle 不允许 WITH FUNCTION 位于 UPDATE、DELETE、MERGE 之前。 +IvorySQL 遵循相同限制,此类用法会报 `ERRCODE_FEATURE_NOT_SUPPORTED` 错误。 + +== 作用域与可见性 + +=== 作用域规则 + +- 内嵌函数/过程的作用域仅限当前 SQL 语句(及其子查询) +- 函数/过程可相互引用(前向声明后定义,支持互递归) +- 函数/过程不能与当前数据库中已有的同名同签名函数冲突(WITH 定义优先) +- 同一 WITH 子句中不允许定义同名、同参数类型的函数/过程 + +=== 子查询中可见 + +[source,sql] +---- +WITH + FUNCTION add_tax(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n * 1.1; END; +SELECT * FROM (SELECT add_tax(amount) AS total FROM orders); +---- + +== 与现有功能的关系 + +| 现有功能 | 关系 | +|---------|------| +| PL/iSQL 嵌套子程序 | 直接复用:利用现有编译/执行基础设施 | +| 标准 CTE(WITH...AS (SELECT ...)) | 共存:在同一 WITH 子句中混用 | +| `RECURSIVE` CTE | 共存:WITH RECURSIVE 与内嵌函数可同时使用 | +| Oracle Package | 类似:Package 的过程/函数也是 session 级临时注册 | +| PL/iSQL `CREATE FUNCTION` | 不同:WITH 内嵌函数不持久化,不写入系统目录 | + +== 错误处理 + +=== 重复定义 + +[source,sql] +---- +WITH + FUNCTION dup(n NUMBER) RETURN NUMBER AS BEGIN RETURN n; END; + FUNCTION dup(n NUMBER) RETURN NUMBER AS BEGIN RETURN n * 2; END; +SELECT dup(1) FROM dual; +-- 期望输出:ERROR: WITH clause function "dup" is defined more than once with the same argument types +---- + +=== 在 PG_PARSER 模式下使用 + +[source,sql] +---- +-- 在 PG_PARSER 模式下尝试使用 WITH FUNCTION 语法 +SET compatible_db = PG_PARSER; +WITH FUNCTION foo(n NUMBER) RETURN NUMBER AS BEGIN RETURN n; END; +SELECT foo(1); +-- 期望输出:ERROR: syntax error at or near "FUNCTION" +---- + +=== 函数体语法错误 + +[source,sql] +---- +WITH + FUNCTION broken(n NUMBER) RETURN NUMBER AS + BEGIN + RETRUN n; -- 拼写错误 + END; +SELECT broken(1) FROM dual; +-- 期望输出:ERROR: syntax error at or near "RETRUN" +---- + +=== 表函数用法拒绝 + +[source,sql] +---- +WITH + FUNCTION get_rows(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n; END; +SELECT * FROM get_rows(5); +-- 期望输出:ERROR: WITH clause function cannot be used as a table or set-returning function +---- + +=== 限定名拒绝 + +[source,sql] +---- +WITH + FUNCTION public.qual_func(n NUMBER) RETURN NUMBER IS + BEGIN RETURN n; END; +SELECT qual_func(1) FROM dual; +-- 期望输出:ERROR: qualified name is not allowed in WITH FUNCTION declaration +---- + +== EXPLAIN 输出 + +=== 基本输出 + +[source,sql] +---- +EXPLAIN WITH + FUNCTION add_one(n NUMBER) RETURN NUMBER AS BEGIN RETURN n + 1; END; +SELECT add_one(5) FROM dual; +-- 期望输出包含:WITH Function: add_one(number) RETURN number +---- + +=== VERBOSE 模式 + +[source,sql] +---- +EXPLAIN (VERBOSE ON) WITH + FUNCTION double(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n * 2; END; +SELECT double(3) FROM dual; +-- 期望输出包含:Body: BEGIN RETURN n * 2; END +---- + +== 限制与约束 + +1. **仅 Oracle 解析器模式**:该特性仅在 `compatible_db = ORA_PARSER` 时生效 +2. **不写入系统目录**:内嵌函数/过程在语句执行期间动态注册,执行结束后撤销 +3. **事务安全**:注册和撤销对事务完全透明,不产生 WAL 日志 +4. **并发安全**:多个并发会话各自拥有独立的内嵌函数/过程注册上下文 +5. **作用域限制**:内嵌函数不能在定义它的语句外部调用 +6. **不支持多态参数**:声明 `ANYELEMENT` 等多态参数会在调用时失败 +7. **不支持表函数用法**:`SELECT * FROM with_func(...)` 会被拒绝 + +== 清理 + +[source,sql] +---- +-- WITH FUNCTION/PROCEDURE 的作用域随语句结束自动清理 +-- 无需手动执行清理操作 +---- diff --git a/EN/modules/ROOT/nav.adoc b/EN/modules/ROOT/nav.adoc index 34a3ce3..c0af338 100644 --- a/EN/modules/ROOT/nav.adoc +++ b/EN/modules/ROOT/nav.adoc @@ -27,6 +27,7 @@ ** xref:master/oracle_compatibility/compat_empty_string_to_null.adoc[18、Empty String to NULL] ** xref:master/oracle_compatibility/compat_call_into.adoc[19、CALL INTO] ** xref:master/oracle_compatibility/compat_read_only_view.adoc[20、Read Only View] +** xref:master/oracle_compatibility/with_function_procedure_en.adoc[21、WITH FUNCTION/PROCEDURE] * Containerization and Cloud Service ** Containerization *** xref:master/containerization/k8s_deployment.adoc[K8S deployment] @@ -92,6 +93,7 @@ *** xref:master/compatibility_features_design/empty_string_to_null.adoc[Empty String to NULL] *** xref:master/compatibility_features_design/call_into.adoc[CALL INTO] *** xref:master/compatibility_features_design/read_only_view.adoc[Read Only View] +*** xref:master/compatibility_features_design/with_function_procedure_impl_en.adoc[WITH FUNCTION/PROCEDURE] ** Built-in Functions *** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] *** xref:master/oracle_builtin_functions/userenv.adoc[userenv] diff --git a/EN/modules/ROOT/pages/master/about_ivorysql.adoc b/EN/modules/ROOT/pages/master/about_ivorysql.adoc index bca8d38..101b4d8 100644 --- a/EN/modules/ROOT/pages/master/about_ivorysql.adoc +++ b/EN/modules/ROOT/pages/master/about_ivorysql.adoc @@ -81,4 +81,6 @@ IvorySQL is a powerful open source object-relational database management system * Nested Subfunctions * sys_guid Function * Empty String to NULL -* CALL INTO \ No newline at end of file +* CALL INTO +* View Read Only +* WITH FUNCTION/PROCEDURE diff --git a/EN/modules/ROOT/pages/master/compatibility_features_design/with_function_procedure_impl_en.adoc b/EN/modules/ROOT/pages/master/compatibility_features_design/with_function_procedure_impl_en.adoc new file mode 100644 index 0000000..67a363b --- /dev/null +++ b/EN/modules/ROOT/pages/master/compatibility_features_design/with_function_procedure_impl_en.adoc @@ -0,0 +1,784 @@ +:sectnums: +:sectnumlevels: 5 + += WITH FUNCTION/PROCEDURE Implementation Notes + +== Purpose + +This document describes the internal implementation of the `WITH FUNCTION` and +`WITH PROCEDURE` features in IvorySQL. The feature allows PL/SQL functions and +procedures to be defined directly inside the WITH clause (Common Table Expression, CTE) +of a SQL statement, implementing Oracle's _Subquery Factoring with PL/SQL Declarations_. + +== Implementation Overview + +=== Layered Architecture + +The implementation spans four layers: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Layer 1: Oracle Parser (ora_gram.y + liboracle_parser.c) │ +│ - Extend with_clause grammar to allow plsql_declarations │ +│ - Reuse OraBody_FUNC lexer mechanism to capture body as Sconst │ +│ - Output: WithClause { plsql_defs: [InlineFunctionDef...], ctes: [...]}│ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ Layer 2: Semantic Analysis (parse_cte.c + parse_func.c) │ +│ - transformWithClause() processes InlineFunctionDef nodes │ +│ - Resolve function signatures; register in ParseState.p_with_func_list│ +│ - p_subprocfunc_hook intercepts call resolution for WITH functions │ +│ - FuncExpr.function_from = FUNC_FROM_WITH_CLAUSE ('w') │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ Layer 3: Planner (planner.c / createplan.c) │ +│ - Copy Query.withFuncDefs into PlannedStmt.withFuncDefs │ +│ - No additional cost model changes │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ Layer 4: Executor (execExpr.c + pl_handler.c) │ +│ - ExecInitFunc() recognizes FUNC_FROM_WITH_CLAUSE │ +│ - Compiles WITH function body on demand (lazy compilation) │ +│ - Compilation result cached in EState.es_with_func_container │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +=== Grammar and Parsing + +==== Grammar Rule Extension + +The `with_clause` grammar rule in `ora_gram.y` is extended as follows: + +[source,yacc] +---- +with_clause: + WITH plsql_declarations cte_list + { + WithClause *n = makeNode(WithClause); + n->plsql_defs = $2; + n->ctes = $3; + n->recursive = false; + n->location = @1; + $$ = n; + } + | WITH plsql_declarations + { + WithClause *n = makeNode(WithClause); + n->plsql_defs = $2; + n->ctes = NIL; + n->recursive = false; + n->location = @1; + $$ = n; + } + | WITH cte_list /* existing syntax unchanged */ + | WITH_LA cte_list /* existing */ + | WITH RECURSIVE cte_list /* existing */ + ; + +plsql_declarations: + plsql_declaration + { $$ = list_make1($1); } + | plsql_declarations plsql_declaration + { $$ = lappend($1, $2); } + ; + +plsql_declaration: + FUNCTION ora_func_name opt_ora_func_args_with_defaults + RETURN func_return + ora_func_is_or_as Sconst ';' + { + InlineFunctionDef *n = makeNode(InlineFunctionDef); + n->funcname = strVal(llast($2)); + n->args = $3; + n->rettype = $5; + n->is_proc = false; + n->src = $7; + n->location = @2; + $$ = (Node *) n; + } + | PROCEDURE ora_func_name opt_procedure_args_with_defaults + ora_func_is_or_as Sconst ';' + { + InlineFunctionDef *n = makeNode(InlineFunctionDef); + n->funcname = strVal(llast($2)); + n->args = $3; + n->rettype = NULL; + n->is_proc = true; + n->src = $5; + n->location = @2; + $$ = (Node *) n; + } + ; +---- + +==== Reuse of the OraBody_FUNC Mechanism + +The Oracle parser already has a `set_oracle_plsql_body(OraBody_FUNC)` mechanism that +captures the entire PL/SQL body after IS/AS as a single Sconst string. WITH FUNCTION +reuses this mechanism directly; no changes to the lexer are required. + +[source,c] +---- +/* ora_gram.y */ +ora_func_is_or_as: + IS { set_oracle_plsql_body(yyscanner, OraBody_FUNC); } + | AS { set_oracle_plsql_body(yyscanner, OraBody_FUNC); } + ; +---- + +The lexer (`liboracle_parser.c:439-652`) enters body-capture mode, tracking the +BEGIN/END/FUNCTION/PROCEDURE nesting depth until the outermost END, and returns the +entire text as a single SCONST token. + +==== Handling Grammar Ambiguity + +Both `FUNCTION` and `PROCEDURE` are `unreserved_keyword` tokens and can therefore be +used as CTE names. The LALR(1) state table disambiguates automatically based on the +token that follows: + +|=== +| Token after `WITH FUNCTION\|PROCEDURE` | LALR(1) action | Path taken + +| **IDENT** / plain identifier +| shift (enters `ora_func_name`) +| plsql_declaration + +| **`AS`** +| reduce +| CTE without column-name list + +| **`(`** +| reduce +| CTE with column-name list +|=== + +=== AST Node Design + +==== InlineFunctionDef Node + +A new AST node represents an inline function/procedure definition inside the WITH clause: + +[source,c] +---- +/* parsenodes.h */ +typedef struct InlineFunctionDef +{ + NodeTag type; /* T_InlineFunctionDef */ + char *funcname; /* unqualified function/procedure name */ + List *args; /* list of FunctionParameter nodes */ + TypeName *rettype; /* return type (NULL for procedures) */ + bool is_proc; /* true = procedure, false = function */ + char *src; /* raw body text (full IS/AS...END text) */ + ParseLoc location; /* position in the original SQL text */ +} InlineFunctionDef; +---- + +==== WithClause Extension + +[source,c] +---- +/* parsenodes.h */ +typedef struct WithClause +{ + NodeTag type; + List *plsql_defs; /* NEW: InlineFunctionDef list (ORA mode) */ + List *ctes; /* existing: CommonTableExpr list */ + bool recursive; /* true = WITH RECURSIVE */ + ParseLoc location; +} WithClause; +---- + +==== FuncExpr Extension + +A new function-origin tag is added in `primnodes.h`: + +[source,c] +---- +/* primnodes.h */ +#define FUNC_FROM_WITH_CLAUSE 'w' /* inline function from WITH clause */ + +/* Update macro to exclude 'w' from the pg_proc path */ +#define FUNC_EXPR_FROM_PG_PROC(function_from) \ + (function_from != FUNC_FROM_SUBPROCFUNC && \ + function_from != FUNC_FROM_PACKAGE && \ + function_from != FUNC_FROM_PACKGE_INITBODY && \ + function_from != FUNC_FROM_WITH_CLAUSE) +---- + +==== WithFuncEntry Structure + +A lightweight structure added to ParseState stores function signatures: + +[source,c] +---- +/* parse_node.h */ +typedef struct WithFuncEntry +{ + char *funcname; + List *argtypes; /* list of Oids */ + Oid rettype; + bool is_proc; + int funcindex; /* corresponds to FuncExpr.funcid */ + InlineFunctionDef *def; /* pointer to the original definition node */ +} WithFuncEntry; +---- + +==== WithFuncContainer Runtime Container + +[source,c] +---- +/* pl_handler.h */ +typedef struct WithFuncContainer +{ + int nfuncs; /* number of WITH functions */ + PLiSQL_subproc_function **funcs; /* array of compiled functions */ + MemoryContext mcxt; /* memory context for this container */ +} WithFuncContainer; +---- + +=== Semantic Analysis + +==== transformWithClause Extension + +`transformWithFuncDefs()` is implemented in the new file `parse_with_plsql.c` and called +from `transformWithClause()` in `parse_cte.c`: + +[source,c] +---- +/* parse_with_plsql.c */ +static void +transformWithFuncDefs(ParseState *pstate, List *plsql_defs) +{ + int funcindex = 0; + ListCell *lc; + + foreach(lc, plsql_defs) + { + InlineFunctionDef *ifd = (InlineFunctionDef *) lfirst(lc); + WithFuncEntry *entry = palloc(sizeof(WithFuncEntry)); + + /* resolve parameter types */ + entry->argtypes = resolveWithFuncArgTypes(pstate, ifd->args); + + /* resolve return type */ + entry->rettype = ifd->rettype ? + typenameTypeId(pstate, ifd->rettype) : InvalidOid; + + entry->funcname = ifd->funcname; + entry->is_proc = ifd->is_proc; + entry->funcindex = funcindex++; + entry->def = ifd; + + /* check for duplicate definition */ + checkDuplicateWithFunc(pstate->p_with_func_list, entry); + + pstate->p_with_func_list = lappend(pstate->p_with_func_list, entry); + } + + /* install the function lookup hook */ + if (pstate->p_subprocfunc_hook == NULL) + pstate->p_subprocfunc_hook = withFuncLookupHook; +} +---- + +==== Function Call Resolution Hook + +`withFuncLookupHook` intercepts calls to inline functions defined in the WITH clause: + +[source,c] +---- +/* parse_with_plsql.c */ +static FuncDetailCode +withFuncLookupHook(ParseState *pstate, List *funcname, + List **fargs, List *fargnames, int nargs, + Oid *argtypes, bool expand_variadic, bool expand_defaults, + bool proc_call, Oid *funcid, Oid *rettype, bool *retset, + int *nvargs, Oid *vatype, Oid **true_typeids, + List **argdefaults, void **pfunc) +{ + char *fname; + ListCell *lc; + + if (list_length(funcname) != 1) + return FUNCDETAIL_NOTFOUND; + + fname = strVal(linitial(funcname)); + + foreach(lc, pstate->p_with_func_list) + { + WithFuncEntry *entry = (WithFuncEntry *) lfirst(lc); + + if (strcmp(entry->funcname, fname) != 0) + continue; + + /* check argument count and type compatibility */ + if (!matchWithFuncArgs(entry, nargs, argtypes, fargnames, + true_typeids, argdefaults)) + continue; + + *funcid = (Oid) entry->funcindex; + *rettype = entry->rettype; + *retset = false; + *nvargs = 0; + *vatype = InvalidOid; + *pfunc = NULL; + + return entry->is_proc ? FUNCDETAIL_PROCEDURE : FUNCDETAIL_NORMAL; + } + + return FUNCDETAIL_NOTFOUND; +} +---- + +=== Executor Design + +==== ExecInitFunc Extension + +`FUNC_FROM_WITH_CLAUSE` is recognized in `execExpr.c`: + +[source,c] +---- +/* execExpr.c */ +if (funcexpr->function_from == FUNC_FROM_WITH_CLAUSE) +{ + scratch->d.func.finfo = palloc0(sizeof(FmgrInfo)); + scratch->d.func.fcinfo_data = palloc0(SizeForFunctionCallInfo(nargs)); + flinfo = scratch->d.func.finfo; + fcinfo = scratch->d.func.fcinfo_data; + + /* use the dedicated dispatch function */ + flinfo->fn_addr = plisql_with_func_call_handler; + flinfo->fn_oid = funcid; + + /* fn_extra stores the EState pointer */ + flinfo->fn_extra = state->parent->state; + + fmgr_info_set_expr((Node *) node, flinfo); + InitFunctionCallInfoData(*fcinfo, flinfo, nargs, inputcollid, NULL, NULL); + scratch->d.func.fn_addr = flinfo->fn_addr; + scratch->d.func.nargs = nargs; + return; +} +---- + +==== Runtime Dispatch Function + +[source,c] +---- +/* pl_handler.c */ +Datum +plisql_with_func_call_handler(PG_FUNCTION_ARGS) +{ + EState *estate = (EState *) fcinfo->flinfo->fn_extra; + int funcindex = (int) fcinfo->flinfo->fn_oid; + WithFuncContainer *container; + + /* lazy load: compile all WITH functions on first call */ + if (estate->es_with_func_container == NULL) + estate->es_with_func_container = buildWithFuncContainer(estate); + + container = estate->es_with_func_container; + + Assert(funcindex >= 0 && funcindex < container->nfuncs); + subprocfunc = container->funcs[funcindex]; + + return execWithFunction(subprocfunc, fcinfo); +} +---- + +==== WithFuncContainer Compilation + +[source,c] +---- +/* pl_handler.c */ +WithFuncContainer * +buildWithFuncContainer(EState *estate) +{ + PlannedStmt *pstmt = estate->es_plannedstmt; + MemoryContext oldcxt = MemoryContextSwitchTo(estate->es_query_cxt); + WithFuncContainer *container; + int nfuncs, i; + ListCell *lc; + + nfuncs = list_length(pstmt->withFuncDefs); + container = palloc0(sizeof(WithFuncContainer)); + container->nfuncs = nfuncs; + container->funcs = palloc0(nfuncs * sizeof(PLiSQL_subproc_function *)); + container->mcxt = CurrentMemoryContext; + + /* establish a shared compilation context to support cross-function calls */ + plisql_push_subproc_func(); + plisql_start_subproc_func(); + + /* Pass 1: register all function signatures */ + i = 0; + foreach(lc, pstmt->withFuncDefs) + { + InlineFunctionDef *ifd = (InlineFunctionDef *) lfirst(lc); + List *argitems = buildArgItemsFromFuncParams(ifd->args); + PLiSQL_type *rettype = ifd->rettype ? + buildPLiSQLType(ifd->rettype) : NULL; + + PLiSQL_subproc_function *subprocfunc = + plisql_build_subproc_function(ifd->funcname, argitems, + rettype, ifd->location); + subprocfunc->is_proc = ifd->is_proc; + subprocfunc->src = ifd->src; + + container->funcs[i++] = subprocfunc; + } + + /* Pass 2: compile each function body */ + i = 0; + foreach(lc, pstmt->withFuncDefs) + { + InlineFunctionDef *ifd = (InlineFunctionDef *) lfirst(lc); + PLiSQL_subproc_function *subprocfunc = container->funcs[i++]; + + PLiSQL_stmt_block *action = + compileWithFuncBody(ifd->src, subprocfunc); + + plisql_set_subprocfunc_action(subprocfunc, action); + } + + plisql_pop_subproc_func(); + + MemoryContextSwitchTo(oldcxt); + return container; +} +---- + +=== Memory Lifecycle + +[source] +---- +SQL text parsing phase + ├── InlineFunctionDef nodes + │ Lifetime: same as the parse tree (ParseState.p_mem_cxt) + └── WithFuncEntry list + Lifetime: same as ParseState + +Query analysis phase + └── Query.withFuncDefs (InlineFunctionDef list, pointer copy) + Lifetime: same as Query + +Planning phase + └── PlannedStmt.withFuncDefs (same) + Lifetime: same as PlannedStmt (may be held by plancache) + +Execution phase + ├── EState.es_with_func_container (WithFuncContainer) + │ Memory context: estate->es_query_cxt + │ Lifetime: bound to EState, freed at ExecutorEnd() + ├── PLiSQL_subproc_function array + │ Lifetime: same as WithFuncContainer + └── PLiSQL_function (compilation result) + Lifetime: freed when query execution ends +---- + +=== Key Design Principles + +1. **Reuse OraBody_FUNC**: The Oracle parser's existing + `set_oracle_plsql_body(OraBody_FUNC)` mechanism is reused directly; no changes to + the lexer are needed. + +2. **No system catalog writes**: Compiled results are stored locally in EState, not + written to `pg_proc`, generate no WAL, and are freed automatically at transaction end. + +3. **Deferred compilation**: Function bodies are compiled during executor initialization + (`ExecInitFunc`), not during parsing or planning. This avoids lifecycle issues that + would arise from storing compiled pointers inside Plan nodes (which may be cached). + +4. **Isolated from PG_PARSER**: All new logic is guarded by + `compatible_db == ORA_PARSER`; the standard PostgreSQL parser path is unaffected. + +5. **Two-pass compilation**: A two-pass design supports mutual calls between functions + (Pass 1 registers all signatures; Pass 2 compiles each function body). + +=== Cross-Function Calls + +Thanks to the two-pass compilation design, **the declaration order of functions inside +the WITH clause does not matter**: A can call B even if B is defined after A, and vice +versa. No Oracle PL/SQL-style explicit forward declarations are required. + +[source,sql] +---- +-- Cross-function call example +WITH + FUNCTION mul2(n NUMBER) RETURN NUMBER AS BEGIN RETURN n*2; END; + FUNCTION add1(n NUMBER) RETURN NUMBER AS BEGIN RETURN n+1; END; +SELECT mul2(add1(3)) FROM dual; -- add1 is defined second, but mul2 can call it +---- + +=== EXPLAIN Output + +==== Basic Output + +[source,sql] +---- +EXPLAIN WITH + FUNCTION add_one(n NUMBER) RETURN NUMBER AS BEGIN RETURN n + 1; END; +SELECT add_one(5) FROM dual; +---- +Output includes: `WITH Function: add_one(number) RETURN number` + +==== VERBOSE Mode + +[source,sql] +---- +EXPLAIN (VERBOSE ON) WITH + FUNCTION double(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n * 2; END; +SELECT double(3) FROM dual; +---- +Output includes: `Body: BEGIN RETURN n * 2; END` + +=== Error Handling + +==== Duplicate Definition + +[source,sql] +---- +WITH + FUNCTION dup(n NUMBER) RETURN NUMBER AS BEGIN RETURN n; END; + FUNCTION dup(n NUMBER) RETURN NUMBER AS BEGIN RETURN n * 2; END; +SELECT dup(1) FROM dual; +-- ERROR: WITH clause function "dup" is defined more than once with the same argument types +---- + +==== Rejection in PG_PARSER Mode + +[source,sql] +---- +SET compatible_db = PG_PARSER; +WITH FUNCTION foo(n NUMBER) RETURN NUMBER AS BEGIN RETURN n; END; +SELECT foo(1); +-- ERROR: syntax error at or near "FUNCTION" +---- + +==== Function Body Compilation Error + +[source,sql] +---- +WITH + FUNCTION broken(n NUMBER) RETURN NUMBER AS + BEGIN + RETRUN n; -- typo + END; +SELECT broken(1) FROM dual; +-- ERROR: syntax error at or near "RETRUN" +---- +Error context: `while compiling WITH FUNCTION "broken_body"` + +==== Rejection of Table Function Usage + +[source,sql] +---- +WITH + FUNCTION get_rows(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n; END; +SELECT * FROM get_rows(5); +-- ERROR: WITH clause function cannot be used as a table or set-returning function +---- + +==== Rejection of Qualified Names + +[source,sql] +---- +WITH + FUNCTION public.qual_func(n NUMBER) RETURN NUMBER IS + BEGIN RETURN n; END; +SELECT qual_func(1) FROM dual; +-- ERROR: qualified name is not allowed in WITH FUNCTION declaration +---- + +=== File Inventory + +==== New Files + +|=== +| File | Description + +| `src/backend/oracle_parser/ora_with_function.c` +| WITH function runtime logic stub + +| `src/backend/parser/parse_with_plsql.c` +| `transformWithFuncDefs`, `withFuncLookupHook` + +| `src/include/oracle_parser/ora_with_function.h` +| Header: `WithFuncEntry`, `WithFuncContainer` + +| `src/oracle_test/regress/sql/with_function.sql` +| Regression tests (32 test cases) + +| `src/oracle_test/regress/expected/with_function.out` +| Expected output for regression tests +|=== + +==== Modified Existing Files + +|=== +| File | Changes + +| `src/include/nodes/parsenodes.h` +| Add `InlineFunctionDef`; extend `WithClause.plsql_defs` and `Query.withFuncDefs` + +| `src/include/nodes/plannodes.h` +| Extend `PlannedStmt.withFuncDefs` + +| `src/include/nodes/primnodes.h` +| Add `FUNC_FROM_WITH_CLAUSE = 'w'`; update `FUNC_EXPR_FROM_PG_PROC` macro + +| `src/include/nodes/execnodes.h` +| Extend `EState.es_with_func_container` + +| `src/include/parser/parse_node.h` +| Extend `ParseState.p_with_func_list` + +| `src/backend/oracle_parser/ora_gram.y` +| Extend `with_clause`; add `plsql_declarations` / `plsql_declaration` productions + +| `src/backend/parser/parse_cte.c` +| `transformWithClause()` calls `transformWithFuncDefs` + +| `src/backend/parser/parse_func.c` +| FuncExpr tagging logic; default-argument sync; `FUNC_MAX_ARGS` boundary check + +| `src/backend/parser/analyze.c` +| Propagate `withFuncDefs` into Query node + +| `src/backend/optimizer/plan/planner.c` +| Propagate `withFuncDefs` into PlannedStmt + +| `src/backend/optimizer/plan/setrefs.c` +| Skip reference resolution for WITH-clause functions + +| `src/backend/executor/execExpr.c` +| `ExecInitFunc()` handles WITH functions; three-level EState lookup chain + +| `src/backend/executor/execSRF.c` +| `init_sexpr` early-rejects WITH functions (cannot be used as table/SRF) + +| `src/backend/executor/execTuples.c` +| Explicitly exclude `FUNC_FROM_WITH_CLAUSE` from subproc return-type change path + +| `src/backend/utils/fmgr/funcapi.c` +| `get_internal_function_result_type` early-rejects WITH functions + +| `src/backend/commands/explain.c` +| EXPLAIN output: `WITH Function:` / `WITH Procedure:` signatures; VERBOSE Body output + +| `src/backend/commands/explain.c` (MERGE path) +| `IvytransformMergeStmt` sets `qry->withFuncDefs` (fixes T16 MERGE crash) + +| `src/pl/plisql/src/pl_handler.c` +| `buildWithFuncContainer`, `plisql_get_with_func`, `buildParamListForFunc` (typmod fix), + WITH-function compile error callback + +| `src/pl/plisql/src/pl_comp.c` +| `plisql_parser_setup` gates `p_with_func_list` exposure via `is_with_clause_func` flag + (prevents scope leakage) + +| `src/pl/plisql/src/plisql.h` +| Add `PLiSQL_function.is_with_clause_func` field + +| `src/oracle_fe_utils/ora_psqlscan.l` +| psql client scanner: recognize `WITH FUNCTION/PROCEDURE` and `EXPLAIN WITH ...`; + handle labeled END; handle nested BEGIN +|=== + +=== Automatically Generated Node Infrastructure Files + +PostgreSQL 16+ introduced a node-infrastructure code generator +(`src/backend/nodes/gen_node_support.pl`). After the `InlineFunctionDef` node is added +to the header, the build system automatically regenerates: + +|=== +| Auto-generated File | Contents + +| `copyfuncs.funcs.c` / `copyfuncs.switch.c` +| `_copyInlineFunctionDef` + +| `equalfuncs.funcs.c` / `equalfuncs.switch.c` +| `_equalInlineFunctionDef` + +| `outfuncs.funcs.c` / `outfuncs.switch.c` +| `_outInlineFunctionDef` + +| `readfuncs.funcs.c` / `readfuncs.switch.c` +| `_readInlineFunctionDef` + +| `nodetags.h` +| `T_InlineFunctionDef = 498` + +| `queryjumblefuncs.funcs.c` +| `_jumbleInlineFunctionDef` +|=== + +== Usage Examples + +=== Simplest Inline Function + +[source,sql] +---- +WITH + FUNCTION double_it(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n * 2; END; +SELECT double_it(5) FROM dual; +-- Output: 10 +---- + +=== Function Mixed with a CTE + +[source,sql] +---- +WITH + FUNCTION tax(amt NUMBER) RETURN NUMBER AS + BEGIN RETURN amt * 0.1; END; + orders AS (SELECT 100 AS amount) +SELECT amount, tax(amount) FROM orders; +-- Output: 100 | 10 +---- + +=== Multiple Inline Functions + +[source,sql] +---- +WITH + FUNCTION add1(n NUMBER) RETURN NUMBER AS BEGIN RETURN n+1; END; + FUNCTION mul2(n NUMBER) RETURN NUMBER AS BEGIN RETURN n*2; END; +SELECT mul2(add1(3)) FROM dual; +-- Output: 8 +---- + +=== Recursive Function + +[source,sql] +---- +WITH + FUNCTION factorial(n NUMBER) RETURN NUMBER AS + BEGIN + IF n <= 1 THEN RETURN 1; END IF; + RETURN n * factorial(n-1); + END; +SELECT factorial(5) FROM dual; +-- Output: 120 +---- + +=== Integration with DML + +[source,sql] +---- +-- Allowed: INSERT with inline function +WITH + FUNCTION get_bonus(sal NUMBER) RETURN NUMBER AS + BEGIN RETURN sal * 1.2; END; +INSERT INTO emp_bonus (empno, bonus) +SELECT empno, get_bonus(sal) FROM emp WHERE deptno = 10; +---- + +NOTE: Oracle does not allow WITH FUNCTION to precede UPDATE, DELETE, or MERGE. +IvorySQL enforces the same restriction; such usage raises `ERRCODE_FEATURE_NOT_SUPPORTED`. diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/with_function_procedure_en.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/with_function_procedure_en.adoc new file mode 100644 index 0000000..3e5ad53 --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_compatibility/with_function_procedure_en.adoc @@ -0,0 +1,345 @@ +:sectnums: +:sectnumlevels: 5 + += WITH FUNCTION/PROCEDURE + +== Purpose + +This document explains the `WITH FUNCTION` and `WITH PROCEDURE` features in IvorySQL, +which implement Oracle-style inline PL/SQL functions and procedures inside SQL statements. + +`WITH FUNCTION/PROCEDURE` is Oracle's _Subquery Factoring with PL/SQL Declarations_ feature. +It allows PL/SQL functions and procedures to be defined directly inside the WITH clause +(Common Table Expression, CTE) of a SQL statement. + +== Feature Description + +=== Basic Syntax + +In Oracle-compatible mode (`compatible_db = ORA_PARSER`), the WITH clause supports the +following extended syntax: + +[source,sql] +---- +WITH + FUNCTION func_name ( [ param_list ] ) RETURN return_type + { IS | AS } + [ declare_section ] + BEGIN + statements + END [ func_name ] ; + + PROCEDURE proc_name ( [ param_list ] ) + { IS | AS } + [ declare_section ] + BEGIN + statements + END [ proc_name ] ; + + cte_name AS ( SELECT ... ) +SELECT ... +---- + +=== Key Characteristics + +- **Scope-limited**: Functions/procedures defined in the WITH clause are visible only within + the current SQL statement. +- **Mixed usage**: Function/procedure definitions can be interleaved with standard CTEs + (`AS (SELECT ...)`). +- **Full PL/SQL syntax**: The function body supports the complete PL/SQL syntax (`BEGIN...END`). +- **No system catalog writes**: Definitions are automatically destroyed after execution; + they are not persisted to `pg_proc`. +- **SELECT context only**: Valid in SELECT statements and INSERT...SELECT statements. + Using WITH FUNCTION before UPDATE, DELETE, or MERGE raises an error (consistent with + Oracle behavior). + +=== Parameter Modes + +Inline procedures support the standard Oracle parameter modes: + +- `IN` (default): input parameter +- `OUT`: output parameter +- `IN OUT`: bidirectional parameter + +Inline functions only support `IN` parameters. + +== Supported Statement Types + +WITH inline functions/procedures are only allowed in the following top-level statements: + +- `SELECT` statements (most common) +- `INSERT ... SELECT` statements (Oracle-compatible form; WITH FUNCTION appears after + `INSERT INTO`) + +The following statements are **not supported** (Oracle does not allow them; +`ERRCODE_FEATURE_NOT_SUPPORTED` is raised): + +- `WITH FUNCTION ... UPDATE ...`: WITH FUNCTION cannot precede UPDATE +- `WITH FUNCTION ... DELETE ...`: WITH FUNCTION cannot precede DELETE +- `WITH FUNCTION ... MERGE ...`: WITH FUNCTION cannot precede MERGE + +To share reusable logic inside DML statements, define a schema-level function with +`CREATE FUNCTION` instead. + +== Syntax Examples + +=== Simplest Inline Function + +[source,sql] +---- +WITH + FUNCTION double_it(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n * 2; END; +SELECT double_it(5) FROM dual; +-- Expected output: 10 +---- + +=== Function Mixed with a CTE + +[source,sql] +---- +WITH + FUNCTION tax(amt NUMBER) RETURN NUMBER AS + BEGIN RETURN amt * 0.1; END; + orders AS (SELECT 100 AS amount) +SELECT amount, tax(amount) FROM orders; +-- Expected output: 100 | 10 +---- + +=== Multiple Inline Functions + +[source,sql] +---- +WITH + FUNCTION add1(n NUMBER) RETURN NUMBER AS BEGIN RETURN n+1; END; + FUNCTION mul2(n NUMBER) RETURN NUMBER AS BEGIN RETURN n*2; END; +SELECT mul2(add1(3)) FROM dual; +-- Expected output: 8 +---- + +=== Recursive Function + +[source,sql] +---- +WITH + FUNCTION factorial(n NUMBER) RETURN NUMBER AS + BEGIN + IF n <= 1 THEN RETURN 1; END IF; + RETURN n * factorial(n-1); + END; +SELECT factorial(5) FROM dual; +-- Expected output: 120 +---- + +=== OUT Parameters (PROCEDURE Only) + +WITH FUNCTION does not allow `OUT` or `IN OUT` parameters (consistent with Oracle +ORA-06572 behavior). Only WITH PROCEDURE may declare `OUT` / `IN OUT` parameters. + +[source,sql] +---- +-- Correct: PROCEDURE may declare OUT parameters +WITH + PROCEDURE swap(val IN NUMBER, result OUT NUMBER) AS + BEGIN + result := val * 10; + END; +SELECT 1 FROM dual; -- the procedure is called by another subprogram inside the same + -- WITH block, not directly from a SELECT expression + +-- Error: FUNCTION does not allow OUT parameters +WITH + FUNCTION bad_func(val NUMBER, result OUT NUMBER) RETURN NUMBER AS + BEGIN RETURN val; END; +SELECT bad_func(5) FROM dual; +-- Expected output: ERROR: WITH FUNCTION "bad_func" cannot declare OUT or IN OUT parameters +---- + +=== Default Parameter Values + +[source,sql] +---- +WITH + FUNCTION calc(n NUMBER DEFAULT 10) RETURN NUMBER AS + BEGIN RETURN n * 2; END; +SELECT calc() FROM dual; +-- Expected output: 20 +---- + +=== Exception Handling + +[source,sql] +---- +WITH + FUNCTION safe_div(a NUMBER, b NUMBER) RETURN NUMBER AS + BEGIN + RETURN a / b; + EXCEPTION + WHEN OTHERS THEN RETURN NULL; + END; +SELECT safe_div(1, 0) FROM dual; +-- Expected output: NULL +---- + +=== Integration with DML + +[source,sql] +---- +-- Allowed: INSERT INTO ... WITH FUNCTION ... SELECT ... (Oracle-compatible form) +WITH + FUNCTION get_bonus(sal NUMBER) RETURN NUMBER AS + BEGIN RETURN sal * 1.2; END; +INSERT INTO emp_bonus (empno, bonus) +SELECT empno, get_bonus(sal) FROM emp WHERE deptno = 10; +---- + +NOTE: Oracle does not allow WITH FUNCTION to precede UPDATE, DELETE, or MERGE. +IvorySQL enforces the same restriction; such usage raises `ERRCODE_FEATURE_NOT_SUPPORTED`. + +== Scope and Visibility + +=== Scope Rules + +- The scope of an inline function/procedure is limited to the SQL statement in which it + is defined (including subqueries within that statement). +- Functions/procedures may reference each other regardless of declaration order (mutual + recursion is supported). +- An inline definition shadows any existing database function with the same name and + signature (WITH definition takes precedence). +- Defining two functions/procedures with the same name and parameter types in a single + WITH clause is not allowed. + +=== Visibility Inside Subqueries + +[source,sql] +---- +WITH + FUNCTION add_tax(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n * 1.1; END; +SELECT * FROM (SELECT add_tax(amount) AS total FROM orders); +---- + +== Relationship with Existing Features + +|=== +| Existing Feature | Relationship + +| PL/iSQL nested subprograms +| Directly reused: leverages the existing compile/execute infrastructure. + +| Standard CTE (`WITH...AS (SELECT ...)`) +| Coexists: can be mixed in the same WITH clause. + +| `RECURSIVE` CTE +| Coexists: WITH RECURSIVE and inline functions can appear together. + +| Oracle Package +| Similar: package procedures/functions are also registered temporarily at session level. + +| PL/iSQL `CREATE FUNCTION` +| Different: WITH inline functions are not persisted and are not written to the system catalog. +|=== + +== Error Handling + +=== Duplicate Definition + +[source,sql] +---- +WITH + FUNCTION dup(n NUMBER) RETURN NUMBER AS BEGIN RETURN n; END; + FUNCTION dup(n NUMBER) RETURN NUMBER AS BEGIN RETURN n * 2; END; +SELECT dup(1) FROM dual; +-- Expected output: ERROR: WITH clause function "dup" is defined more than once with the same argument types +---- + +=== Usage in PG_PARSER Mode + +[source,sql] +---- +-- Attempting to use WITH FUNCTION syntax in PG_PARSER mode +SET compatible_db = PG_PARSER; +WITH FUNCTION foo(n NUMBER) RETURN NUMBER AS BEGIN RETURN n; END; +SELECT foo(1); +-- Expected output: ERROR: syntax error at or near "FUNCTION" +---- + +=== Function Body Syntax Error + +[source,sql] +---- +WITH + FUNCTION broken(n NUMBER) RETURN NUMBER AS + BEGIN + RETRUN n; -- typo + END; +SELECT broken(1) FROM dual; +-- Expected output: ERROR: syntax error at or near "RETRUN" +---- + +=== Rejection of Table Function Usage + +[source,sql] +---- +WITH + FUNCTION get_rows(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n; END; +SELECT * FROM get_rows(5); +-- Expected output: ERROR: WITH clause function cannot be used as a table or set-returning function +---- + +=== Rejection of Qualified Names + +[source,sql] +---- +WITH + FUNCTION public.qual_func(n NUMBER) RETURN NUMBER IS + BEGIN RETURN n; END; +SELECT qual_func(1) FROM dual; +-- Expected output: ERROR: qualified name is not allowed in WITH FUNCTION declaration +---- + +== EXPLAIN Output + +=== Basic Output + +[source,sql] +---- +EXPLAIN WITH + FUNCTION add_one(n NUMBER) RETURN NUMBER AS BEGIN RETURN n + 1; END; +SELECT add_one(5) FROM dual; +-- Expected output includes: WITH Function: add_one(number) RETURN number +---- + +=== VERBOSE Mode + +[source,sql] +---- +EXPLAIN (VERBOSE ON) WITH + FUNCTION double(n NUMBER) RETURN NUMBER AS + BEGIN RETURN n * 2; END; +SELECT double(3) FROM dual; +-- Expected output includes: Body: BEGIN RETURN n * 2; END +---- + +== Restrictions and Constraints + +1. **Oracle parser mode only**: This feature is active only when `compatible_db = ORA_PARSER`. +2. **No system catalog writes**: Inline functions/procedures are registered dynamically during + statement execution and deregistered immediately after; nothing is written to `pg_proc`. +3. **Transaction-safe**: Registration and deregistration are fully transparent to transactions; + no WAL is generated. +4. **Concurrency-safe**: Each concurrent session has its own independent inline + function/procedure registration context. +5. **Scope-limited**: An inline function cannot be called outside the statement that defines it. +6. **No polymorphic parameters**: Declaring parameters with types such as `ANYELEMENT` will + fail at call time. +7. **Not usable as a table function**: `SELECT * FROM with_func(...)` is rejected. + +== Cleanup + +[source,sql] +---- +-- The scope of WITH FUNCTION/PROCEDURE ends automatically with the statement. +-- No manual cleanup is required. +----