Skip to content

support function/procedure in WITH clause#1320

Closed
bigplaice wants to merge 2 commits into
IvorySQL:masterfrom
bigplaice:with_function_procedure
Closed

support function/procedure in WITH clause#1320
bigplaice wants to merge 2 commits into
IvorySQL:masterfrom
bigplaice:with_function_procedure

Conversation

@bigplaice
Copy link
Copy Markdown
Collaborator

@bigplaice bigplaice commented May 6, 2026

support function/procedure in WITH clause

fix #1067

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for Oracle-style WITH FUNCTION and WITH PROCEDURE inline subprograms, enabling inline function/procedure definitions within WITH clauses for use in subsequent DML statements (SELECT, INSERT, UPDATE, DELETE, MERGE).
    • Added EXPLAIN support to display inline function and procedure definitions in query plans.
  • Tests

    • Added comprehensive test suite covering functionality, parameters, control flow, recursion, and edge cases for WITH FUNCTION/PROCEDURE features.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

This PR adds Oracle PL/SQL WITH FUNCTION/WITH PROCEDURE support, enabling inline function and procedure definitions in SQL statements. Changes span the parser (Oracle grammar and analysis), executor (function initialization), optimizer (plan propagation), and PL/iSQL runtime (function compilation and invocation), with comprehensive test coverage.

Changes

Oracle WITH FUNCTION/WITH PROCEDURE Support

Layer / File(s) Summary
Data Structure Definitions
src/include/nodes/parsenodes.h, src/include/nodes/primnodes.h, src/include/nodes/execnodes.h, src/include/nodes/plannodes.h
New InlineFunctionDef struct to represent inline function/procedure definitions. WithClause gains plsql_defs field, Query gains withFuncDefs field, PlannedStmt gains withFuncDefs field, EState gains es_with_func_container pointer. New macro FUNC_FROM_WITH_CLAUSE added to primnodes.h.
Parser Header and API
src/include/oracle_parser/ora_with_function.h
Introduces public types WithFuncContainer and WithFuncEntry to manage inline functions at parse and runtime. Exports transformWithFuncDefs, withFuncLookupHook, and buildWithFuncContainer functions for phase-wise processing.
Parser Grammar Extension
src/backend/oracle_parser/ora_gram.y
Extends WITH clause to accept PL/SQL declarations (functions/procedures) before CTEs. Introduces plsql_declaration and plsql_declarations grammar rules with precedence markers to resolve shift/reduce conflicts.
Parser State and Utilities
src/include/parser/parse_node.h, src/backend/parser/Makefile, src/backend/parser/meson.build
Adds p_with_func_list field to ParseState to track inline functions during parsing. Build files include new parse_with_plsql.c source file.
Core Parse Analysis
src/backend/parser/parse_with_plsql.c, src/backend/parser/parse_cte.c
New file parse_with_plsql.c implements two-phase handling: parse-analysis to resolve argument types and register functions, and withFuncLookupHook for runtime resolution. parse_cte.c integrates transformWithFuncDefs call when processing WITH clauses in ORA_PARSER mode.
Function Resolution
src/backend/parser/parse_func.c, src/backend/parser/parse_clause.c, src/backend/parser/analyze.c
parse_func.c detects WITH-clause functions and retries lookups up the ParseState chain. parse_clause.c skips OUT parameter validation for WITH-clause functions. analyze.c propagates plsql_defs to Query.withFuncDefs in DELETE, INSERT, SELECT, VALUES, SETOP, and UPDATE transforms.
Optimizer and Plan Propagation
src/backend/optimizer/plan/planner.c
standard_planner copies parse->withFuncDefs to the final PlannedStmt.withFuncDefs.
Executor Function Initialization
src/backend/executor/execExpr.c, src/backend/executor/execSRF.c, src/backend/executor/execTuples.c
execExpr.c extends ExecInitFunc to detect and dispatch WITH-clause functions via dedicated EState, bypassing privilege checks. execSRF.c and execTuples.c add guards to skip return-type adjustments for WITH-clause functions.
PL/iSQL Runtime Integration
src/pl/plisql/src/pl_comp.c, src/pl/plisql/src/pl_handler.c, src/pl/plisql/src/plisql.h
plisql_compile_inline signature extended with initial_rettype and initial_is_proc parameters. pl_handler.c adds FUNC_FROM_WITH_CLAUSE dispatch branch with state save/restore for recursive calls. New global plisql_active_with_func_entries tracks active WITH-clause entries.
EXPLAIN Output and Utilities
src/backend/commands/explain.c, contrib/ivorysql_ora/src/merge/ora_merge.c
explain.c introduces ExplainPrintWithFunctions to render inline function/procedure definitions in EXPLAIN output. ora_merge.c propagates stmt->withClause->plsql_defs to qry->withFuncDefs in MERGE transformation.
Scanner and Lexer
src/oracle_fe_utils/ora_psqlscan.l
Enhances semicolon-termination logic to correctly handle WITH FUNCTION/PROCEDURE and EXPLAIN WITH blocks by detecting these patterns and deferring statement termination when appropriate. Refines identifier-counting logic to avoid false positives in block detection.
Test Suite
src/oracle_test/regress/sql/with_function.sql, src/oracle_test/regress/expected/with_function.out, src/oracle_test/regress/parallel_schedule
Comprehensive regression tests covering 19 functional test blocks (T01–T19), 5 error scenarios (E01–E05), and 2 EXPLAIN plan blocks (X01–X02). Tests DML contexts (INSERT, UPDATE, DELETE, MERGE), control flow, recursion, scope, parameter modes, and error handling.

Sequence Diagram

sequenceDiagram
    participant Parser as Parser (ora_gram.y)
    participant Analyze as Parse Analyze (parse_cte.c)
    participant WithFunc as With Func (parse_with_plsql.c)
    participant ParseFunc as Parse Func (parse_func.c)
    participant Planner as Optimizer (planner.c)
    participant Exec as Executor (execExpr.c)
    participant PlISql as PL/iSQL (pl_handler.c)

    Parser->>Parser: Parse WITH FUNCTION/PROCEDURE
    Parser->>Analyze: WithClause.plsql_defs
    Analyze->>WithFunc: transformWithFuncDefs()
    WithFunc->>WithFunc: Resolve arg types, register
    WithFunc->>ParseFunc: Install lookup hook
    ParseFunc->>ParseFunc: Function call resolution
    ParseFunc->>ParseFunc: Set FUNC_FROM_WITH_CLAUSE
    Analyze->>Planner: Query.withFuncDefs
    Planner->>Planner: Copy to PlannedStmt
    Exec->>Exec: ExecInitFunc detects WITH
    Exec->>PlISql: Dispatch via EState
    PlISql->>PlISql: Compile/execute inline
    PlISql-->>Exec: Return result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • IvorySQL/IvorySQL#944: Modifies function-call and subprocedure handling in execExpr, execTuples, execSRF, parse_clause with overlapping return-type and OUT-parameter logic.
  • IvorySQL/IvorySQL#986: Changes PL/iSQL runtime internals in pl_handler.c and modifies plisql.h function signatures (plisql_compile_inline).
  • IvorySQL/IvorySQL#935: Modifies ora_psqlscan.l parser state and PL/iSQL inline compiler signature through plisql_compile_inline.

Suggested reviewers

  • OreoYang
  • jiaoshuntian
  • NotHimmel

🐰 With Functions now bloom in Oracle's way,
Inline spells cast within a single day,
Parser strings them, Executor runs with glee,
Recursion hops through PL/iSQL decree,
A rabbit's dream—procedures here to stay!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the main objective of the changeset: adding support for functions and procedures in the WITH clause, which aligns with all major changes across parser, executor, and test files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/backend/executor/execSRF.c (1)

739-776: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

init_sexpr routes WITH clause functions to fmgr_subproc_info_cxt after the macro change

Lines 739–747 classify any non-pg_proc function as is_subproc_func:

if (!FUNC_EXPR_FROM_PG_PROC(funcexpr->function_from))
    is_subproc_func = true;   // ← true for FUNC_FROM_WITH_CLAUSE after macro change

Lines 773–776 then call fmgr_subproc_info_cxt for all such functions:

if (is_subproc_func)
    fmgr_subproc_info_cxt(foid, &(sexpr->func), sexprCxt);  // ← wrong for WITH clause

If a WITH clause function is used as a set-returning function in the SELECT list (ExecInitFunctionResultSet → init_sexpr) or in a FROM clause (ExecInitTableFunctionResult → init_sexpr), this path will be reached. fmgr_subproc_info_cxt expects a PLiSQL subproc structure (with a valid parent_func / nsubprocfuncs) that WITH clause functions don't provide, which will crash or corrupt state.

Apply the same pattern already used in execExpr.c (context snippet 1):

🛡️ Proposed fix for init_sexpr
-	bool		is_subproc_func = false;
+	bool		is_subproc_func = false;
+	bool		is_with_func = false;

 	if (node != NULL && nodeTag(node) == T_FuncExpr)
 	{
 		FuncExpr   *funcexpr = (FuncExpr *) node;

-		if (!FUNC_EXPR_FROM_PG_PROC(funcexpr->function_from))
+		if (funcexpr->function_from == FUNC_FROM_WITH_CLAUSE)
+			is_with_func = true;
+		else if (!FUNC_EXPR_FROM_PG_PROC(funcexpr->function_from))
 			is_subproc_func = true;
 	}

-	if (!is_subproc_func)
+	if (!is_subproc_func && !is_with_func)
 	{
 		/* Verify EXECUTE privilege before invoking the function. */

And in the fmgr lookup block, add appropriate handling for is_with_func (either a dedicated lookup or a fallback to fmgr_info_cxt, depending on how WITH clause function entries are registered).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/executor/execSRF.c` around lines 739 - 776, init_sexpr currently
treats any FuncExpr that is not from pg_proc as a "subproc" (using
FUNC_EXPR_FROM_PG_PROC), which after the macro change will mark WITH-clause
functions as subprocs and cause fmgr_subproc_info_cxt to be called incorrectly;
update init_sexpr to detect FUNC_FROM_WITH_CLAUSE (e.g. add an is_with_func
boolean alongside is_subproc_func) and follow the same pattern used in
execExpr.c: if is_with_func call the regular fmgr lookup (fmgr_info_cxt) or a
dedicated WITH-clause fmgr lookup instead of fmgr_subproc_info_cxt, and only
call fmgr_subproc_info_cxt when the FuncExpr truly represents a PL/pgSQL subproc
(is_subproc_func).
src/oracle_fe_utils/ora_psqlscan.l (2)

1138-1142: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

END label; in a WITH FUNCTION body permanently suppresses semicolon termination

Line 1139 resets ora_plsql_expect_end_symbol to END_SYMBOL_INVALID for any identifier token. When END foo_func; (labeled end) is used inside a WITH FUNCTION body:

  1. END sets END_SYMBOL_SEMICOLON and decrements begin_depth to 0.
  2. foo_func (identifier) hits line 1139 and resets the state to END_SYMBOL_INVALID.
  3. The ; handler never enters the END_SYMBOL_SEMICOLON branch, so skip_this_semi stays false and cancel_semicolon_terminator is never cleared.
  4. The subsequent SELECT …; is suppressed by cancel_semicolon_terminator=true, and the statement never terminates via ;. The scanner waits for more input indefinitely.

For CREATE FUNCTION/CREATE PROCEDURE this is benign (the / slash terminates the statement), but for WITH FUNCTION — which relies on the ; path — this is a correctness gap that breaks any function body using END func_name;, which is valid Oracle PL/SQL syntax.

A minimal fix is to detect the labeled-end pattern (identifier immediately after END when END_SYMBOL_SEMICOLON is already set) and avoid resetting the symbol:

🐛 Proposed fix
 					/* Not final End state .This state needs to be cleared */
-					if (cur_state->ora_plsql_expect_end_symbol)
+					if (cur_state->ora_plsql_expect_end_symbol &&
+						/* Preserve SEMICOLON state for "END label;" — the label
+						 * must not clear the symbol set by the preceding END. */
+						!(cur_state->ora_plsql_expect_end_symbol == END_SYMBOL_SEMICOLON &&
+						  (cur_state->identifiers[0] == 'w' ||
+						   cur_state->identifiers[0] == 'e')))
 						cur_state->ora_plsql_expect_end_symbol = END_SYMBOL_INVALID;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oracle_fe_utils/ora_psqlscan.l` around lines 1138 - 1142, The code
unconditionally clears cur_state->ora_plsql_expect_end_symbol to
END_SYMBOL_INVALID on any identifier which breaks labeled END (e.g., "END foo;")
for WITH FUNCTION; change the logic so that when
cur_state->ora_plsql_expect_end_symbol == END_SYMBOL_SEMICOLON and the
identifier immediately follows an END token (i.e., the labeled-end pattern), you
do NOT reset ora_plsql_expect_end_symbol (leave END_SYMBOL_SEMICOLON so the
subsequent ';' clears cancel_semicolon_terminator and terminates the statement);
otherwise continue to clear to END_SYMBOL_INVALID as before—use the existing
parser state (previous token marker / begin_depth / token-type checks) to detect
the "identifier immediately after END" case and keep END_SYMBOL_SEMICOLON
intact.

798-866: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

skip_this_semi fires on inner END; inside nested BEGIN…END blocks, permanently breaking scanner state

The skip_this_semi=true branch (line 839) is entered whenever ora_plsql_expect_end_symbol == END_SYMBOL_SEMICOLON and identifiers[0] is 'w'/'e', with no check against begin_depth. Because every END keyword (including those closing inner BEGIN…END blocks) unconditionally sets END_SYMBOL_SEMICOLON (line 1191) and decrements begin_depth, the first inner END; inside a WITH FUNCTION body wrongly triggers the state-reset path.

Consider:

WITH FUNCTION foo RETURN NUMBER IS
BEGIN           -- begin_depth: 0→1
  <<nested>>
  BEGIN         -- begin_depth: 1→2
    RETURN 1;
  END;          -- ← fires skip_this_semi prematurely!
  RETURN 2;
END;            -- scanner never sees this as function-end
SELECT foo FROM DUAL;

After the inner END;:

  1. begin_depth decrements 2 → 1.
  2. skip_this_semi=true fires: cancel_semicolon_terminatorfalse, identifiers[1..3]0, identifier_count1.
  3. With identifiers[1]=0, the big condition in the identifier rule is now false, so the outer END never decrements begin_depth (it stays at 1) and never sets END_SYMBOL_SEMICOLON.
  4. The final ; sees begin_depth=1 ≠ 0 → the statement never terminates; the scanner blocks waiting for more input.

The analogous CREATE FUNCTION path is immune because it terminates via /, not ;.

The fix requires a three-way split: distinguish the outermost END; (depth==0 → skip_this_semi), inner END; in a WITH FUNCTION body (depth>0 → clear symbol only), and all other contexts (→ END_SYMBOL_SLASH):

🐛 Proposed fix
-					if (cur_state->identifiers[0] == 'w' ||
-						cur_state->identifiers[0] == 'e')
-					{
+					if ((cur_state->identifiers[0] == 'w' ||
+						 cur_state->identifiers[0] == 'e') &&
+						cur_state->begin_depth == 0)
+					{
 						/*
 						 * WITH FUNCTION/PROCEDURE (or EXPLAIN WITH FUNCTION/PROCEDURE):
-						 * this END; closes one function/procedure body.  The statement
+						 * this END; closes the outermost function/procedure body.  The
+						 * statement continues — either with another FUNCTION/PROCEDURE
+						 * definition or with the actual SELECT/INSERT/etc.
 						 */
 						cur_state->cancel_semicolon_terminator = false;
 						cur_state->ora_plsql_expect_end_symbol = END_SYMBOL_INVALID;
 						if (cur_state->identifiers[0] == 'e')
 						{
 							cur_state->identifiers[2] = 0;
 							cur_state->identifiers[3] = 0;
 							cur_state->identifier_count = 2;
 						}
 						else
 						{
 							cur_state->identifiers[1] = 0;
 							cur_state->identifiers[2] = 0;
 							cur_state->identifiers[3] = 0;
 							cur_state->identifier_count = 1;
 						}
 						skip_this_semi = true;
 					}
+					else if (cur_state->identifiers[0] == 'w' ||
+							 cur_state->identifiers[0] == 'e')
+					{
+						/*
+						 * Nested END; within a WITH FUNCTION/PROCEDURE body.
+						 * Cancel the symbol; keep cancel_semicolon_terminator and
+						 * begin_depth intact so the outer END; is handled correctly.
+						 */
+						cur_state->ora_plsql_expect_end_symbol = END_SYMBOL_INVALID;
+					}
 					else
 						cur_state->ora_plsql_expect_end_symbol = END_SYMBOL_SLASH;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oracle_fe_utils/ora_psqlscan.l` around lines 798 - 866, The current ';'
handler resets identifiers and sets skip_this_semi whenever
ora_plsql_expect_end_symbol == END_SYMBOL_SEMICOLON and identifiers[0] ==
'w'/'e', which incorrectly fires for inner END; inside nested BEGIN…END; blocks;
change the branch to three-way based on cur_state->begin_depth: if begin_depth
== 0 keep the existing skip_this_semi behavior (reset
cancel_semicolon_terminator, clear ora_plsql_expect_end_symbol and reset
identifiers/identifier_count), if begin_depth > 0 DO NOT reset identifiers or
identifier_count—only clear ora_plsql_expect_end_symbol and
cancel_semicolon_terminator so inner END; doesn't dismantle the WITH/FUNCTION
tracking, and otherwise (the existing else) set ora_plsql_expect_end_symbol =
END_SYMBOL_SLASH; update the code around the ';' case that references
cur_state->ora_plsql_expect_end_symbol, cur_state->identifiers,
cur_state->identifier_count, cur_state->cancel_semicolon_terminator, and
cur_state->begin_depth accordingly.
🧹 Nitpick comments (9)
src/include/nodes/primnodes.h (1)

818-818: 💤 Low value

Stale comment on function_from field — WITH clause not mentioned

The inline comment still reads /* proc func, subproc func, package func */. It should be updated to include WITH clause inline functions.

📝 Proposed fix
-	char		function_from;	/* proc func, subproc func, package func */
+	char		function_from;	/* proc func, subproc func, package func, WITH clause inline func */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/include/nodes/primnodes.h` at line 818, Update the stale inline comment
for the struct field function_from in primnodes.h to mention WITH clause inline
functions as well as proc/subproc/package funcs; locate the field named
function_from in the struct (char function_from) and change its comment text to
something like "/* proc func, subproc func, package func, WITH-clause inline
func */" so the comment accurately documents all possible sources.
src/backend/parser/parse_with_plsql.c (2)

287-310: 💤 Low value

Default-expression append loop relies on trailing-defaults invariant.

The Assert(defexpr != NULL) at line 306 will trip if analyzeddefaults ever contains a NULL slot beyond position nargs (i.e., non-trailing defaults). Grammar likely enforces trailing defaults today, but a defensive check that produces a real error would be more robust than an assert in a production build (where the assert is compiled out and a NULL would be inserted into *fargs).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/parser/parse_with_plsql.c` around lines 287 - 310, The loop that
appends default expressions uses Assert(defexpr != NULL) which can be compiled
out, so replace the assert with a runtime check that raises an error when a NULL
default is encountered beyond nargs: inside the foreach over
entry->analyzeddefaults, if defexpr == NULL and skip > nargs then call the
PostgreSQL error reporting (ereport/errmsg) to raise a clear error (e.g.,
invalid function header/defaults) rather than allowing a NULL to be appended to
*fargs; keep the rest of the logic intact so provided args are still skipped and
non-NULL defaults are copied.

92-114: ⚡ Quick win

Pass pstate to parser_errposition so duplicate-definition errors point at the source location.

parser_errposition(NULL, …) returns 0 (no position) regardless of newentry->def->location, so the error message loses caret positioning. The caller already has a ParseState; threading it through checkDuplicateWithFunc keeps the diagnostic informative.

♻️ Proposed change
 static void
-checkDuplicateWithFunc(List *func_list, WithFuncEntry * newentry)
+checkDuplicateWithFunc(ParseState *pstate, List *func_list, WithFuncEntry * newentry)
 {
@@
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_FUNCTION),
 					 errmsg("WITH clause function \"%s\" is defined more than once with the same argument types",
 							newentry->funcname),
-					 parser_errposition(NULL, newentry->def->location)));
+					 parser_errposition(pstate, newentry->def->location)));

And update the call site at line 189 accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/parser/parse_with_plsql.c` around lines 92 - 114, The function
checkDuplicateWithFunc currently calls parser_errposition(NULL,
newentry->def->location) which yields no caret; modify checkDuplicateWithFunc to
accept a ParseState *pstate parameter, and pass pstate into
parser_errposition(pstate, newentry->def->location) when reporting the
duplicate-function error; update the caller that builds func_list (the site that
invokes checkDuplicateWithFunc) to forward its ParseState pointer to the new
function signature so diagnostics point at the correct source location; ensure
function prototype and any declarations use the new ParseState *pstate
parameter.
src/backend/parser/parse_func.c (1)

319-406: ⚖️ Poor tradeoff

Three near-duplicate WITH-hook call sites — consider consolidating.

The hook is invoked with the same large argument block in three separate spots: as the registered p_subprocfunc_hook (line 321), in the parent-chain retry loop (line 373), and in the recursive-PL/iSQL fallback (line 397). The argument list is identical aside from pstate/ancestor. Extracting a small static helper (try_with_func_lookup(pstate, …)) would reduce duplication, make future signature changes safer, and remove the symbol-equality coupling on withFuncLookupHook. No behavioral change required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/parser/parse_func.c` around lines 319 - 406, There are three
near-duplicate calls to withFuncLookupHook (via p_subprocfunc_hook, a
parent-chain loop, and a PL/iSQL fallback) that pass the same large argument
list; create a small static helper like try_with_func_lookup(ParseState *state,
List *funcname, List **fargs, List *argnames, int nargs, Oid *actual_arg_types,
bool func_variadic, bool proc_call, Oid *funcid, Oid *rettype, bool *retset, int
*nvargs, Oid *vatype, List **declared_arg_types, List **argdefaults, void
**pfunc) which encapsulates calling withFuncLookupHook(state, ...) and returns
the FUNCDETAIL_* result (and when it finds a match sets function_from to
FUNC_FROM_WITH_CLAUSE). Replace the three direct invocations (the
p_subprocfunc_hook branch that compares to withFuncLookupHook, the ancestor loop
call, and the p_with_func_list fallback) with calls to this helper using the
appropriate ParseState (pstate or ancestor); remove the symbol-equality check
against withFuncLookupHook and use the helper’s return value to decide setting
function_from, preserving all semantics and argument order.
src/backend/commands/explain.c (2)

770-881: ⚡ Quick win

Heavy duplication between TEXT and non-TEXT branches.

The two branches contain near-identical logic for building the name(mode argname type, ...) RETURN rettype signature (lines 788‑817 vs 842‑873), including the parameter mode switch and rettype handling. Extracting a small helper such as format_inline_function_signature(StringInfo, InlineFunctionDef *) would remove ~40 lines of copy/paste and prevent the two branches from drifting (e.g., one already drifted on body output — see the related comment).

♻️ Sketch of helper extraction
static void
format_inline_function_signature(StringInfo buf, InlineFunctionDef *ifd)
{
    ListCell   *alc;
    bool        first_arg = true;

    appendStringInfo(buf, "%s(", ifd->funcname);
    foreach(alc, ifd->args)
    {
        FunctionParameter *fp = (FunctionParameter *) lfirst(alc);

        if (!first_arg)
            appendStringInfoString(buf, ", ");
        first_arg = false;

        switch (fp->mode)
        {
            case FUNC_PARAM_OUT:   appendStringInfoString(buf, "OUT ");    break;
            case FUNC_PARAM_INOUT: appendStringInfoString(buf, "IN OUT "); break;
            default: break;
        }
        if (fp->name)
            appendStringInfo(buf, "%s ", fp->name);
        appendStringInfoString(buf, TypeNameToString(fp->argType));
    }
    appendStringInfoChar(buf, ')');
    if (!ifd->is_proc && ifd->rettype != NULL)
        appendStringInfo(buf, " RETURN %s", TypeNameToString(ifd->rettype));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/commands/explain.c` around lines 770 - 881, The code duplicates
signature-building logic in the TEXT and non-TEXT branches when formatting
InlineFunctionDef signatures; extract that repeated logic into a helper (e.g.,
format_inline_function_signature(StringInfo buf, InlineFunctionDef *ifd)) and
replace the blocks in both branches with calls to that helper; ensure the helper
handles argument modes, names, types and rettype exactly as in the original (use
InlineFunctionDef, FunctionParameter, fp->mode, fp->name, fp->argType,
ifd->is_proc, ifd->rettype and TypeNameToString), keep existing surrounding
behavior (in the non-TEXT branch call ExplainPropertyText("Signature",
buf->data, es); in the TEXT branch write buf->data into es->str after
ExplainIndentText) and free any temporary buffer as before.

776-776: 💤 Low value

Use lfirst_node for consistent node extraction.

InlineFunctionDef is a node-tagged struct (with NodeTag type field). Extract it using lfirst_node(InlineFunctionDef, lc) at lines 776 and 835 to match the established convention used elsewhere in this file for similar node types (e.g., lfirst_node(Query, ...) at line 229, lfirst_node(RangeTblEntry, ...) at line 913). This adds IsA assertions in assert-enabled builds for better runtime safety.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/commands/explain.c` at line 776, Replace the raw cast usage of
lfirst(lc) to extract InlineFunctionDef nodes with the node-safe helper: use
lfirst_node(InlineFunctionDef, lc) wherever InlineFunctionDef is being extracted
(the two occurrences around the InlineFunctionDef handling, previously using
"InlineFunctionDef *ifd = (InlineFunctionDef *) lfirst(lc)"). This follows the
established pattern (e.g., lfirst_node(Query, ...) usage) and ensures an IsA
check in assert builds for runtime safety.
src/backend/oracle_parser/ora_gram.y (1)

997-999: ⚡ Quick win

Add regression tests for the DML-vs-CTE disambiguation path.

This precedence-based resolution is subtle; please lock it down with parser tests for cases like WITH FUNCTION ... INSERT/UPDATE/DELETE/MERGE ... and WITH FUNCTION ... <cte_name> AS (...) ....

Also applies to: 14393-14404

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/oracle_parser/ora_gram.y` around lines 997 - 999, Add regression
tests to lock down the precedence-based DML vs CTE disambiguation implemented
around the tokens DELETE_P, INSERT, UPDATE, MERGE and the PLSQL_DEFS_END
handling in ora_gram.y: create parser tests that cover "WITH FUNCTION ...
INSERT/UPDATE/DELETE/MERGE ..." (ensuring those are parsed as DML following a
WITH FUNCTION) and also tests for "WITH FUNCTION ... <cte_name> AS (...) ..."
(ensuring the name is parsed as a CTE), include one test per DML token plus
positive and negative CTE cases, run them against the parser so the ambiguity is
exercised and fails if precedence regresses.
src/include/nodes/parsenodes.h (1)

1662-1663: ⚡ Quick win

Update the WithClause note to reflect withFuncDefs propagation.

The note says WithClause does not propagate into Query, but inline defs now do via Query.withFuncDefs. Please update the comment to avoid future confusion.

Proposed comment update
- * Note: WithClause does not propagate into the Query representation;
- * but CommonTableExpr does.
+ * Note: WithClause itself does not propagate into Query representation.
+ * CommonTableExprs propagate via Query.cteList, and ORA_PARSER inline
+ * function/procedure definitions propagate via Query.withFuncDefs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/include/nodes/parsenodes.h` around lines 1662 - 1663, The comment should
be updated to state that while WithClause historically did not propagate into
Query, function definitions contained in WithClause now do propagate via
Query.withFuncDefs; change the note referencing WithClause and CommonTableExpr
in parsenodes.h so it mentions that WithClause itself still isn't copied into
Query but its inline function definitions are carried over through the
Query.withFuncDefs field (referencing the WithClause struct and the
Query.withFuncDefs member and keeping CommonTableExpr behavior noted).
src/oracle_test/regress/sql/with_function.sql (1)

8-13: ⚡ Quick win

T02 does not actually validate WITH PROCEDURE execution.

The SELECT 'procedure ok' result is independent of do_nothing, so this only proves parsing, not runtime behavior. Please make the assertion depend on the procedure path.

Proposed test adjustment
 -- T02: WITH PROCEDURE (void — verified via side-effect SELECT)
 WITH PROCEDURE do_nothing IS
 BEGIN
   NULL;
 END;
-SELECT 'procedure ok' AS result FROM dual;
+WITH
+  PROCEDURE do_nothing IS
+  BEGIN
+    NULL;
+  END;
+  FUNCTION call_proc RETURN VARCHAR2 IS
+  BEGIN
+    do_nothing;
+    RETURN 'procedure ok';
+  END;
+SELECT call_proc() AS result FROM dual;

As per coding guidelines **/sql/*.sql: Test SQL files. Ensure comprehensive coverage of features.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oracle_test/regress/sql/with_function.sql` around lines 8 - 13, The test
currently only parses WITH PROCEDURE do_nothing but the SELECT 'procedure ok'
result is independent; update the test so the SELECT outcome depends on a
side-effect of the WITH PROCEDURE named do_nothing (e.g., have do_nothing
perform a visible side-effect such as inserting/updating a value or setting a
local variable reachable by the SELECT, then assert the SELECT reads that
value), so execution of do_nothing is required to produce the expected result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/commands/explain.c`:
- Around line 815-880: The TEXT branch that iterates pstmt->withFuncDefs should
mirror the non-TEXT verbose behavior: before the foreach loop emit a header line
like "WITH Functions:" (use appendStringInfo(es->str, "WITH Functions:\n"); and
ExplainIndentText/es->str for indentation), and inside the loop after printing
the signature (where sig is appended and printed) add a check if (es->verbose &&
ifd->src) to emit the body (e.g. call ExplainIndentText(es);
appendStringInfo(es->str, "Body: %s\n", ifd->src);) so the inline function body
(ifd->src) is shown in TEXT VERBOSE; use the existing symbols ifd->src,
es->verbose, ExplainIndentText and appendStringInfo(es->str, ...) so the output
matches the non-TEXT branch and provides context for each function.

In `@src/backend/parser/parse_clause.c`:
- Around line 4066-4070: Replace the silent "continue" for
FUNC_FROM_WITH_CLAUSE: instead of skipping validation, explicitly handle
WITH-clause routines by either (A) rejecting them if they contain OUT or INOUT
parameters or (B) resolving their argument modes and letting the existing
validation path run; implement (A) by checking the function's arg-mode array
(e.g. proargmodes / argmodes) when func->function_from == FUNC_FROM_WITH_CLAUSE
and raising an error if any mode is OUT or INOUT, or implement (B) by removing
the continue and falling through so the existing argument-mode
resolution/validation code executes for FUNC_FROM_WITH_CLAUSE as it does for
catalog PL/pgsql functions and subprocs. Ensure you reference
FUNC_FROM_WITH_CLAUSE and func->function_from in your change and preserve
existing validation logic for arg modes.

In `@src/backend/parser/parse_cte.c`:
- Around line 126-134: The error hint currently references internal symbols
compatible_db and ORA_PARSER; update the errmsg/errhint in the WITH
FUNCTION/PROCEDURE check (the block that tests withClause->plsql_defs and calls
transformWithFuncDefs(pstate, withClause->plsql_defs)) so the user-facing hint
instructs how to set the GUC: e.g., tell users to set ivorysql.compatible_mode
to "oracle" (or use the equivalent ALTER SYSTEM/SET command) instead of
mentioning compatible_db or ORA_PARSER.

In `@src/backend/parser/parse_func.c`:
- Around line 906-923: The code that syncs actual_arg_types from fargs can write
past the fixed-size actual_arg_types[FUNC_MAX_ARGS] if withFuncLookupHook
appended defaults; add a defensive bound check when computing new_nargs (use
FUNC_MAX_ARGS) and reject or truncate with the same error path used elsewhere
(e.g., increment nargs handling that checks FUNC_MAX_ARGS and reports an error
via ereport/elog). Specifically, in the block that sets new_nargs and loops over
fargs (the code that updates actual_arg_types and variables
nargs/nargsplusdefs), ensure new_nargs <= FUNC_MAX_ARGS before writing into
actual_arg_types (and raise the same error when exceeded), mirroring the guard
used in the surrounding default-handling block that references FUNC_MAX_ARGS.

In `@src/backend/parser/parse_with_plsql.c`:
- Line 145: The bug is that funcindex is initialized to 0 in
transformWithFuncDefs so the first inline WITH function gets FuncExpr.funcid ==
0 (InvalidOid); change the initialization in transformWithFuncDefs so funcindex
starts at 1 (or alternatively add and use a dedicated field for the
inline-function index instead of casting a zero-based index to Oid) so that
entry->funcindex and subsequently FuncExpr.funcid are never 0; update any code
that assigns or casts entry->funcindex to Oid (e.g. where *funcid is set) to
rely on the new 1-based scheme.

In `@src/include/nodes/primnodes.h`:
- Around line 780-783: The change to FUNC_EXPR_FROM_PG_PROC causes
!FUNC_EXPR_FROM_PG_PROC to be true for FUNC_FROM_WITH_CLAUSE, so update
init_sexpr to first check for FUNC_FROM_WITH_CLAUSE before treating a function
as a subprocedure: in init_sexpr, replace the unguarded "if
(!FUNC_EXPR_FROM_PG_PROC(funcexpr->function_from)) is_subproc_func = true" with
a two-way check that sets is_with_func when funcexpr->function_from ==
FUNC_FROM_WITH_CLAUSE else sets is_subproc_func when
!FUNC_EXPR_FROM_PG_PROC(...); ensure callers that invoke
fmgr_subproc_info_cxt(foid, &(sexpr->func), sexprCxt) only run when
is_subproc_func is true, and audit other unguarded uses of
!FUNC_EXPR_FROM_PG_PROC (e.g., dependency.c) to apply the same guard pattern.

In `@src/oracle_test/regress/sql/with_function.sql`:
- Around line 131-133: The test currently calls _wf_scope_test without ever
defining it, producing a false-positive; update the test to first declare the
WITH FUNCTION _wf_scope_test(...) and use it inside the same statement (a
positive in-statement call to _wf_scope_test) and then, in a separate statement
afterwards, call _wf_scope_test(1) from outside to assert the out-of-scope
failure; ensure the in-statement call and the external call are separate
statements so the second triggers the expected scope error.

In `@src/pl/plisql/src/pl_comp.c`:
- Around line 1627-1634: Only assign pstate->p_with_func_list when the current
ParseState belongs to the same WITH-function execution context: check that
plisql_active_with_func_entries is non-NIL AND that the current pstate's
execution context matches the saved plisql_active_with_func_estate (e.g. compare
pstate->p_estate or equivalent field against plisql_active_with_func_estate)
before setting p_with_func_list; this ensures only parses running in the owning
WITH-call can see those WITH-function entries (references: p_with_func_list,
plisql_active_with_func_entries, plisql_active_with_func_estate, pstate).

In `@src/pl/plisql/src/pl_handler.c`:
- Around line 317-322: The WITH-function path only copies the parameter type OID
into ParamExternData (paramLI->params[i].ptype) but not the parsed typmod, so
declarations like varchar2(10) lose their modifier; update the code that fills
ParamListInfo in pl_handler.c to also copy the parsed typmod into each
ParamExternData (set paramLI->params[i].ptypmod = fp->argtypmod or the
corresponding parsed typmod field on the formal parameter), since
plisql_compile_inline() expects both param->ptype and param->ptypmod when
rebuilding parameter variables.
- Around line 473-489: The error-context callback (errcallback) is pushed onto
error_context_stack before calling buildParamListForFunc, typenameTypeId, and
plisql_compile_inline, but if any of those functions throws the final restore
(error_context_stack = errcallback.previous) is skipped; wrap the compilation
block that sets paramLI, rettype, calls plisql_compile_inline and
fixupCompiledReturnType in a PG_TRY / PG_FINALLY pair and in the PG_FINALLY
ensure error_context_stack is restored (pop errcallback) so the
with_func_compile_error_callback / errctx frame is always removed even on
errors.

---

Outside diff comments:
In `@src/backend/executor/execSRF.c`:
- Around line 739-776: init_sexpr currently treats any FuncExpr that is not from
pg_proc as a "subproc" (using FUNC_EXPR_FROM_PG_PROC), which after the macro
change will mark WITH-clause functions as subprocs and cause
fmgr_subproc_info_cxt to be called incorrectly; update init_sexpr to detect
FUNC_FROM_WITH_CLAUSE (e.g. add an is_with_func boolean alongside
is_subproc_func) and follow the same pattern used in execExpr.c: if is_with_func
call the regular fmgr lookup (fmgr_info_cxt) or a dedicated WITH-clause fmgr
lookup instead of fmgr_subproc_info_cxt, and only call fmgr_subproc_info_cxt
when the FuncExpr truly represents a PL/pgSQL subproc (is_subproc_func).

In `@src/oracle_fe_utils/ora_psqlscan.l`:
- Around line 1138-1142: The code unconditionally clears
cur_state->ora_plsql_expect_end_symbol to END_SYMBOL_INVALID on any identifier
which breaks labeled END (e.g., "END foo;") for WITH FUNCTION; change the logic
so that when cur_state->ora_plsql_expect_end_symbol == END_SYMBOL_SEMICOLON and
the identifier immediately follows an END token (i.e., the labeled-end pattern),
you do NOT reset ora_plsql_expect_end_symbol (leave END_SYMBOL_SEMICOLON so the
subsequent ';' clears cancel_semicolon_terminator and terminates the statement);
otherwise continue to clear to END_SYMBOL_INVALID as before—use the existing
parser state (previous token marker / begin_depth / token-type checks) to detect
the "identifier immediately after END" case and keep END_SYMBOL_SEMICOLON
intact.
- Around line 798-866: The current ';' handler resets identifiers and sets
skip_this_semi whenever ora_plsql_expect_end_symbol == END_SYMBOL_SEMICOLON and
identifiers[0] == 'w'/'e', which incorrectly fires for inner END; inside nested
BEGIN…END; blocks; change the branch to three-way based on
cur_state->begin_depth: if begin_depth == 0 keep the existing skip_this_semi
behavior (reset cancel_semicolon_terminator, clear ora_plsql_expect_end_symbol
and reset identifiers/identifier_count), if begin_depth > 0 DO NOT reset
identifiers or identifier_count—only clear ora_plsql_expect_end_symbol and
cancel_semicolon_terminator so inner END; doesn't dismantle the WITH/FUNCTION
tracking, and otherwise (the existing else) set ora_plsql_expect_end_symbol =
END_SYMBOL_SLASH; update the code around the ';' case that references
cur_state->ora_plsql_expect_end_symbol, cur_state->identifiers,
cur_state->identifier_count, cur_state->cancel_semicolon_terminator, and
cur_state->begin_depth accordingly.

---

Nitpick comments:
In `@src/backend/commands/explain.c`:
- Around line 770-881: The code duplicates signature-building logic in the TEXT
and non-TEXT branches when formatting InlineFunctionDef signatures; extract that
repeated logic into a helper (e.g., format_inline_function_signature(StringInfo
buf, InlineFunctionDef *ifd)) and replace the blocks in both branches with calls
to that helper; ensure the helper handles argument modes, names, types and
rettype exactly as in the original (use InlineFunctionDef, FunctionParameter,
fp->mode, fp->name, fp->argType, ifd->is_proc, ifd->rettype and
TypeNameToString), keep existing surrounding behavior (in the non-TEXT branch
call ExplainPropertyText("Signature", buf->data, es); in the TEXT branch write
buf->data into es->str after ExplainIndentText) and free any temporary buffer as
before.
- Line 776: Replace the raw cast usage of lfirst(lc) to extract
InlineFunctionDef nodes with the node-safe helper: use
lfirst_node(InlineFunctionDef, lc) wherever InlineFunctionDef is being extracted
(the two occurrences around the InlineFunctionDef handling, previously using
"InlineFunctionDef *ifd = (InlineFunctionDef *) lfirst(lc)"). This follows the
established pattern (e.g., lfirst_node(Query, ...) usage) and ensures an IsA
check in assert builds for runtime safety.

In `@src/backend/oracle_parser/ora_gram.y`:
- Around line 997-999: Add regression tests to lock down the precedence-based
DML vs CTE disambiguation implemented around the tokens DELETE_P, INSERT,
UPDATE, MERGE and the PLSQL_DEFS_END handling in ora_gram.y: create parser tests
that cover "WITH FUNCTION ... INSERT/UPDATE/DELETE/MERGE ..." (ensuring those
are parsed as DML following a WITH FUNCTION) and also tests for "WITH FUNCTION
... <cte_name> AS (...) ..." (ensuring the name is parsed as a CTE), include one
test per DML token plus positive and negative CTE cases, run them against the
parser so the ambiguity is exercised and fails if precedence regresses.

In `@src/backend/parser/parse_func.c`:
- Around line 319-406: There are three near-duplicate calls to
withFuncLookupHook (via p_subprocfunc_hook, a parent-chain loop, and a PL/iSQL
fallback) that pass the same large argument list; create a small static helper
like try_with_func_lookup(ParseState *state, List *funcname, List **fargs, List
*argnames, int nargs, Oid *actual_arg_types, bool func_variadic, bool proc_call,
Oid *funcid, Oid *rettype, bool *retset, int *nvargs, Oid *vatype, List
**declared_arg_types, List **argdefaults, void **pfunc) which encapsulates
calling withFuncLookupHook(state, ...) and returns the FUNCDETAIL_* result (and
when it finds a match sets function_from to FUNC_FROM_WITH_CLAUSE). Replace the
three direct invocations (the p_subprocfunc_hook branch that compares to
withFuncLookupHook, the ancestor loop call, and the p_with_func_list fallback)
with calls to this helper using the appropriate ParseState (pstate or ancestor);
remove the symbol-equality check against withFuncLookupHook and use the helper’s
return value to decide setting function_from, preserving all semantics and
argument order.

In `@src/backend/parser/parse_with_plsql.c`:
- Around line 287-310: The loop that appends default expressions uses
Assert(defexpr != NULL) which can be compiled out, so replace the assert with a
runtime check that raises an error when a NULL default is encountered beyond
nargs: inside the foreach over entry->analyzeddefaults, if defexpr == NULL and
skip > nargs then call the PostgreSQL error reporting (ereport/errmsg) to raise
a clear error (e.g., invalid function header/defaults) rather than allowing a
NULL to be appended to *fargs; keep the rest of the logic intact so provided
args are still skipped and non-NULL defaults are copied.
- Around line 92-114: The function checkDuplicateWithFunc currently calls
parser_errposition(NULL, newentry->def->location) which yields no caret; modify
checkDuplicateWithFunc to accept a ParseState *pstate parameter, and pass pstate
into parser_errposition(pstate, newentry->def->location) when reporting the
duplicate-function error; update the caller that builds func_list (the site that
invokes checkDuplicateWithFunc) to forward its ParseState pointer to the new
function signature so diagnostics point at the correct source location; ensure
function prototype and any declarations use the new ParseState *pstate
parameter.

In `@src/include/nodes/parsenodes.h`:
- Around line 1662-1663: The comment should be updated to state that while
WithClause historically did not propagate into Query, function definitions
contained in WithClause now do propagate via Query.withFuncDefs; change the note
referencing WithClause and CommonTableExpr in parsenodes.h so it mentions that
WithClause itself still isn't copied into Query but its inline function
definitions are carried over through the Query.withFuncDefs field (referencing
the WithClause struct and the Query.withFuncDefs member and keeping
CommonTableExpr behavior noted).

In `@src/include/nodes/primnodes.h`:
- Line 818: Update the stale inline comment for the struct field function_from
in primnodes.h to mention WITH clause inline functions as well as
proc/subproc/package funcs; locate the field named function_from in the struct
(char function_from) and change its comment text to something like "/* proc
func, subproc func, package func, WITH-clause inline func */" so the comment
accurately documents all possible sources.

In `@src/oracle_test/regress/sql/with_function.sql`:
- Around line 8-13: The test currently only parses WITH PROCEDURE do_nothing but
the SELECT 'procedure ok' result is independent; update the test so the SELECT
outcome depends on a side-effect of the WITH PROCEDURE named do_nothing (e.g.,
have do_nothing perform a visible side-effect such as inserting/updating a value
or setting a local variable reachable by the SELECT, then assert the SELECT
reads that value), so execution of do_nothing is required to produce the
expected result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 68afd7be-f244-4751-a057-fa2c4e2943b6

📥 Commits

Reviewing files that changed from the base of the PR and between 35e086c and c30aab0.

📒 Files selected for processing (27)
  • contrib/ivorysql_ora/src/merge/ora_merge.c
  • src/backend/commands/explain.c
  • src/backend/executor/execExpr.c
  • src/backend/executor/execSRF.c
  • src/backend/executor/execTuples.c
  • src/backend/optimizer/plan/planner.c
  • src/backend/oracle_parser/ora_gram.y
  • src/backend/parser/Makefile
  • src/backend/parser/analyze.c
  • src/backend/parser/meson.build
  • src/backend/parser/parse_clause.c
  • src/backend/parser/parse_cte.c
  • src/backend/parser/parse_func.c
  • src/backend/parser/parse_with_plsql.c
  • src/include/nodes/execnodes.h
  • src/include/nodes/parsenodes.h
  • src/include/nodes/plannodes.h
  • src/include/nodes/primnodes.h
  • src/include/oracle_parser/ora_with_function.h
  • src/include/parser/parse_node.h
  • src/oracle_fe_utils/ora_psqlscan.l
  • src/oracle_test/regress/expected/with_function.out
  • src/oracle_test/regress/parallel_schedule
  • src/oracle_test/regress/sql/with_function.sql
  • src/pl/plisql/src/pl_comp.c
  • src/pl/plisql/src/pl_handler.c
  • src/pl/plisql/src/plisql.h

Comment on lines +815 to +880
if (!ifd->is_proc && ifd->rettype != NULL)
appendStringInfo(&sig, " RETURN %s",
TypeNameToString(ifd->rettype));

ExplainPropertyText("Signature", sig.data, es);

if (es->verbose && ifd->src)
ExplainPropertyText("Body", ifd->src, es);

pfree(sig.data);

ExplainCloseGroup("WITH Function", NULL, true, es);
}

ExplainCloseGroup("WITH Functions", "WITH Functions", false, es);
}
else
{
foreach(lc, pstmt->withFuncDefs)
{
InlineFunctionDef *ifd = (InlineFunctionDef *) lfirst(lc);
StringInfoData sig;
ListCell *alc;
bool first_arg = true;

initStringInfo(&sig);

appendStringInfo(&sig, "WITH %s: %s(",
ifd->is_proc ? "Procedure" : "Function",
ifd->funcname);

foreach(alc, ifd->args)
{
FunctionParameter *fp = (FunctionParameter *) lfirst(alc);

if (!first_arg)
appendStringInfoString(&sig, ", ");
first_arg = false;

switch (fp->mode)
{
case FUNC_PARAM_OUT:
appendStringInfoString(&sig, "OUT ");
break;
case FUNC_PARAM_INOUT:
appendStringInfoString(&sig, "IN OUT ");
break;
default:
break;
}

if (fp->name)
appendStringInfo(&sig, "%s ", fp->name);
appendStringInfoString(&sig, TypeNameToString(fp->argType));
}
appendStringInfoChar(&sig, ')');
if (!ifd->is_proc && ifd->rettype != NULL)
appendStringInfo(&sig, " RETURN %s",
TypeNameToString(ifd->rettype));

ExplainIndentText(es);
appendStringInfo(es->str, "%s\n", sig.data);

pfree(sig.data);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Body output inconsistent between TEXT and non-TEXT formats.

In the non-TEXT branch the function body is emitted under EXPLAIN VERBOSE (line 821‑822), but the TEXT branch (lines 832‑880) never emits ifd->src, even with VERBOSE. Users running EXPLAIN (VERBOSE) in the default text format will silently lose the inline subprogram body, which is the most useful piece of information for these inline definitions. Also, unlike ExplainPrintJIT / ExplainPrintSettings, the TEXT branch does not emit a section header (e.g. WITH Functions:) to delimit the block, so the per-function lines appear without context after the plan tree.

Consider mirroring the verbose-body behavior (and an optional header) in the TEXT branch.

📝 Suggested addition for the TEXT branch
 	else
 	{
+		if (pstmt->withFuncDefs != NIL)
+		{
+			ExplainIndentText(es);
+			appendStringInfoString(es->str, "WITH Functions:\n");
+			es->indent++;
+		}
 		foreach(lc, pstmt->withFuncDefs)
 		{
@@
 			ExplainIndentText(es);
 			appendStringInfo(es->str, "%s\n", sig.data);
 
+			if (es->verbose && ifd->src)
+			{
+				ExplainIndentText(es);
+				appendStringInfo(es->str, "Body: %s\n", ifd->src);
+			}
+
 			pfree(sig.data);
 		}
+		if (pstmt->withFuncDefs != NIL)
+			es->indent--;
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/commands/explain.c` around lines 815 - 880, The TEXT branch that
iterates pstmt->withFuncDefs should mirror the non-TEXT verbose behavior: before
the foreach loop emit a header line like "WITH Functions:" (use
appendStringInfo(es->str, "WITH Functions:\n"); and ExplainIndentText/es->str
for indentation), and inside the loop after printing the signature (where sig is
appended and printed) add a check if (es->verbose && ifd->src) to emit the body
(e.g. call ExplainIndentText(es); appendStringInfo(es->str, "Body: %s\n",
ifd->src);) so the inline function body (ifd->src) is shown in TEXT VERBOSE; use
the existing symbols ifd->src, es->verbose, ExplainIndentText and
appendStringInfo(es->str, ...) so the output matches the non-TEXT branch and
provides context for each function.

Comment on lines +14423 to +14435
FUNCTION ora_func_name opt_ora_func_args_with_defaults
RETURN func_return
ora_func_is_or_as Sconst ';'
{
InlineFunctionDef *n = makeNode(InlineFunctionDef);
/* ora_func_name returns a list; take the last element */
n->funcname = strVal(llast($2));
n->args = $3;
n->rettype = $5;
n->is_proc = false;
n->src = $7;
n->location = @2;
$$ = (Node *) n;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject qualified names in inline declarations instead of silently truncating them.

Line 14429 and Line 14441 take strVal(llast($2)), which drops leading qualifiers from ora_func_name. That can parse a qualified name but bind/store the wrong identifier.

💡 Proposed fix
 		FUNCTION ora_func_name opt_ora_func_args_with_defaults
 		RETURN func_return
 		ora_func_is_or_as Sconst ';'
 			{
 				InlineFunctionDef *n = makeNode(InlineFunctionDef);
-				/* ora_func_name returns a list; take the last element */
-				n->funcname = strVal(llast($2));
+				if (list_length($2) != 1)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("qualified name is not allowed in WITH FUNCTION declaration"),
+							 parser_errposition(`@2`)));
+				n->funcname = strVal(linitial($2));
 				n->args = $3;
 				n->rettype = $5;
 				n->is_proc = false;
 				n->src = $7;
 				n->location = `@2`;
@@
 		| PROCEDURE ora_func_name opt_procedure_args_with_defaults
 		ora_func_is_or_as Sconst ';'
 			{
 				InlineFunctionDef *n = makeNode(InlineFunctionDef);
-				n->funcname = strVal(llast($2));
+				if (list_length($2) != 1)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("qualified name is not allowed in WITH PROCEDURE declaration"),
+							 parser_errposition(`@2`)));
+				n->funcname = strVal(linitial($2));
 				n->args = $3;
 				n->rettype = NULL;
 				n->is_proc = true;
 				n->src = $5;
 				n->location = `@2`;

Also applies to: 14437-14447

Comment on lines +4066 to +4070
else if (func->function_from == FUNC_FROM_WITH_CLAUSE)
{
/* WITH-clause functions have no OUT params */
continue;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't silently skip OUT/IN OUT validation for WITH-clause routines.

This continue makes FUNC_FROM_WITH_CLAUSE calls bypass the same argument-mode checks applied to catalog PL/iSQL functions and subprocs. If WITH-clause routines are supposed to forbid OUT/IN OUT params here, reject them explicitly; otherwise resolve their arg modes and run the existing validation path instead of skipping it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/parser/parse_clause.c` around lines 4066 - 4070, Replace the
silent "continue" for FUNC_FROM_WITH_CLAUSE: instead of skipping validation,
explicitly handle WITH-clause routines by either (A) rejecting them if they
contain OUT or INOUT parameters or (B) resolving their argument modes and
letting the existing validation path run; implement (A) by checking the
function's arg-mode array (e.g. proargmodes / argmodes) when func->function_from
== FUNC_FROM_WITH_CLAUSE and raising an error if any mode is OUT or INOUT, or
implement (B) by removing the continue and falling through so the existing
argument-mode resolution/validation code executes for FUNC_FROM_WITH_CLAUSE as
it does for catalog PL/pgsql functions and subprocs. Ensure you reference
FUNC_FROM_WITH_CLAUSE and func->function_from in your change and preserve
existing validation logic for arg modes.

Comment on lines +126 to +134
if (withClause->plsql_defs != NIL)
{
if (compatible_db != ORA_PARSER)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("WITH FUNCTION/PROCEDURE is only supported in Oracle compatibility mode"),
errhint("Set compatible_db = ORA_PARSER to enable this feature.")));
transformWithFuncDefs(pstate, withClause->plsql_defs);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the user-facing GUC name and accepted value(s).
rg -nP -C2 'DefineCustom\w+Variable\([^)]*"ivorysql\.compatible_mode"' --type=c
rg -nP -C2 'compatible_mode' -g '*.sgml' -g '*.md'

Repository: IvorySQL/IvorySQL

Length of output: 43


🏁 Script executed:

# Search for compatible_db variable definition and usage
rg -n 'compatible_db' --type=c | head -20

Repository: IvorySQL/IvorySQL

Length of output: 2416


🏁 Script executed:

# Search for any GUC variable related to compatibility or oracle
rg -n 'compatible' --type=c | grep -i 'define\|variable\|guc' | head -20

Repository: IvorySQL/IvorySQL

Length of output: 1899


🏁 Script executed:

# Look for configuration-related files that might define GUC
fd -e 'conf.d' -o -e 'postgresql.conf' -o 'guc.c' -o 'variables.c' | head -20

Repository: IvorySQL/IvorySQL

Length of output: 178


🏁 Script executed:

# Search for how this setting is documented in code or comments
rg -n 'compatible.*mode\|oracle.*compat' --type=c --type=h | head -30

Repository: IvorySQL/IvorySQL

Length of output: 43


🏁 Script executed:

# Look at the full GUC definition for ivorysql.compatible_mode
sed -n '401,430p' src/backend/utils/misc/ivy_guc.c

Repository: IvorySQL/IvorySQL

Length of output: 932


🏁 Script executed:

# Find the enum definition for compatible_db values
rg -n 'enum.*compatible|ORA_PARSER|PG_PARSER' --type=c --type=h | grep -E '(enum|ORA_PARSER|PG_PARSER)' | head -20

Repository: IvorySQL/IvorySQL

Length of output: 1678


🏁 Script executed:

# Look at the check_compatible_mode function
sed -n '457,480p' src/backend/utils/misc/ivy_guc.c

Repository: IvorySQL/IvorySQL

Length of output: 919


🏁 Script executed:

# Find the db_parser_options definition
rg -n 'db_parser_options' --type=c -A 5

Repository: IvorySQL/IvorySQL

Length of output: 813


Update errhint to reference the user-facing GUC instead of internal C symbols.

The current hint "Set compatible_db = ORA_PARSER to enable this feature." references compatible_db (a C variable) and ORA_PARSER (an enum constant), neither of which users can set directly. Users interact with the GUC ivorysql.compatible_mode, which accepts the value "oracle".

 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("WITH FUNCTION/PROCEDURE is only supported in Oracle compatibility mode"),
-					 errhint("Set compatible_db = ORA_PARSER to enable this feature.")));
+					 errhint("Set ivorysql.compatible_mode = oracle to enable this feature.")));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/parser/parse_cte.c` around lines 126 - 134, The error hint
currently references internal symbols compatible_db and ORA_PARSER; update the
errmsg/errhint in the WITH FUNCTION/PROCEDURE check (the block that tests
withClause->plsql_defs and calls transformWithFuncDefs(pstate,
withClause->plsql_defs)) so the user-facing hint instructs how to set the GUC:
e.g., tell users to set ivorysql.compatible_mode to "oracle" (or use the
equivalent ALTER SYSTEM/SET command) instead of mentioning compatible_db or
ORA_PARSER.

Comment on lines +906 to +923
{
int new_nargs = list_length(fargs);

if (new_nargs > nargs)
{
ListCell *lc;
int j = 0;

foreach(lc, fargs)
{
if (j >= nargs)
actual_arg_types[j] = exprType((Node *) lfirst(lc));
j++;
}
nargs = new_nargs;
nargsplusdefs = nargs;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a FUNC_MAX_ARGS bound check when syncing argument types.

actual_arg_types[] is a fixed-size FUNC_MAX_ARGS array (line 120). The earlier check at line 159 only validates the original fargs length; after withFuncLookupHook appends pre-analyzed default expressions to *fargs (parse_with_plsql.c line 308), new_nargs could in theory exceed FUNC_MAX_ARGS, leading to an out-of-bounds write at line 917. Mirroring the guard used in the surrounding default-handling block would close the gap.

🛡️ Proposed defensive bound check
 	{
 		int			new_nargs = list_length(fargs);
 
 		if (new_nargs > nargs)
 		{
 			ListCell   *lc;
 			int			j = 0;
 
+			if (new_nargs > FUNC_MAX_ARGS)
+				ereport(ERROR,
+						(errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+						 errmsg_plural("cannot pass more than %d argument to a function",
+									   "cannot pass more than %d arguments to a function",
+									   FUNC_MAX_ARGS,
+									   FUNC_MAX_ARGS),
+						 parser_errposition(pstate, location)));
 			foreach(lc, fargs)
 			{
 				if (j >= nargs)
 					actual_arg_types[j] = exprType((Node *) lfirst(lc));
 				j++;
 			}
 			nargs = new_nargs;
 			nargsplusdefs = nargs;
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/parser/parse_func.c` around lines 906 - 923, The code that syncs
actual_arg_types from fargs can write past the fixed-size
actual_arg_types[FUNC_MAX_ARGS] if withFuncLookupHook appended defaults; add a
defensive bound check when computing new_nargs (use FUNC_MAX_ARGS) and reject or
truncate with the same error path used elsewhere (e.g., increment nargs handling
that checks FUNC_MAX_ARGS and reports an error via ereport/elog). Specifically,
in the block that sets new_nargs and loops over fargs (the code that updates
actual_arg_types and variables nargs/nargsplusdefs), ensure new_nargs <=
FUNC_MAX_ARGS before writing into actual_arg_types (and raise the same error
when exceeded), mirroring the guard used in the surrounding default-handling
block that references FUNC_MAX_ARGS.

Comment on lines 780 to +783
#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_PACKGE_INITBODY \
&& function_from != FUNC_FROM_WITH_CLAUSE)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all files using FUNC_EXPR_FROM_PG_PROC so they can be audited
# for missing FUNC_FROM_WITH_CLAUSE guards.
rg -n "FUNC_EXPR_FROM_PG_PROC" --type=c -A2 -B2

Repository: IvorySQL/IvorySQL

Length of output: 16197


🏁 Script executed:

# Get context around the problematic line in execSRF.c (lines 740-780)
sed -n '740,780p' src/backend/executor/execSRF.c | cat -n

Repository: IvorySQL/IvorySQL

Length of output: 1715


🏁 Script executed:

# Check the full init_sexpr function to understand scope
grep -n "init_sexpr" src/backend/executor/execSRF.c | head -5

Repository: IvorySQL/IvorySQL

Length of output: 408


🏁 Script executed:

# Verify the fmgr_subproc_info_cxt function signature and where it's called
rg -n "fmgr_subproc_info_cxt" --type=c | head -20

Repository: IvorySQL/IvorySQL

Length of output: 664


🏁 Script executed:

# Find all negated uses of FUNC_EXPR_FROM_PG_PROC (the risky pattern)
rg -n "!FUNC_EXPR_FROM_PG_PROC" --type=c -A3 -B1

Repository: IvorySQL/IvorySQL

Length of output: 5695


🏁 Script executed:

# Get the fmgr_subproc_info_cxt implementation
sed -n '141,200p' src/backend/utils/fmgr/fmgr.c | cat -n

Repository: IvorySQL/IvorySQL

Length of output: 2323


🏁 Script executed:

# Check if WITH clause functions could have parent_func == NULL
rg -n "FUNC_FROM_WITH_CLAUSE" --type=c -B2 -A2 | head -40

Repository: IvorySQL/IvorySQL

Length of output: 2553


🏁 Script executed:

# Check what happens in the actual call path - examine subproc_should_change_return_type
rg -n "subproc_should_change_return_type" --type=c | head -10

Repository: IvorySQL/IvorySQL

Length of output: 987


🏁 Script executed:

# Get context on execSRF.c around the problem area (lines 730-780)
sed -n '730,780p' src/backend/executor/execSRF.c | cat -n

Repository: IvorySQL/IvorySQL

Length of output: 2067


🏁 Script executed:

# Check the actual implementation of plisql_subproc_should_change_return_type
sed -n '3594,3650p' src/pl/plisql/src/pl_package.c | cat -n

Repository: IvorySQL/IvorySQL

Length of output: 2088


🏁 Script executed:

# Verify the macro definition one more time to be absolutely clear
cat src/include/nodes/primnodes.h | grep -A5 "define FUNC_EXPR_FROM_PG_PROC"

Repository: IvorySQL/IvorySQL

Length of output: 332


🏁 Script executed:

# Check if fmgr_subproc_info_cxt is the only concern or if there are callers that validate parent_func
rg -n "parent_func" --type=c src/pl/plisql/src/pl_package.c | head -20

Repository: IvorySQL/IvorySQL

Length of output: 559


🏁 Script executed:

# Check funcapi.c context around line 455
sed -n '450,465p' src/backend/utils/fmgr/funcapi.c | cat -n

Repository: IvorySQL/IvorySQL

Length of output: 700


🏁 Script executed:

# Check functioncmds.c at lines 3013 and 3065
sed -n '3010,3020p' src/backend/commands/functioncmds.c | cat -n

Repository: IvorySQL/IvorySQL

Length of output: 344


🏁 Script executed:

# Check what calls init_sexpr to understand the WITH clause function flow
rg -n "init_sexpr" src/backend/executor/execSRF.c -B5 -A5

Repository: IvorySQL/IvorySQL

Length of output: 1919


🏁 Script executed:

# Verify the macro one more time: when WITH clause, FUNC_EXPR_FROM_PG_PROC should be false
python3 << 'EOF'
# Test the macro logic
FUNC_FROM_SUBPROCFUNC = 's'
FUNC_FROM_PACKAGE = 'p'
FUNC_FROM_PACKGE_INITBODY = 'b'
FUNC_FROM_WITH_CLAUSE = 'w'
FUNC_FROM_PG_PROC = 'n'  # normal

def FUNC_EXPR_FROM_PG_PROC(function_from):
    return (function_from != FUNC_FROM_SUBPROCFUNC and 
            function_from != FUNC_FROM_PACKAGE and
            function_from != FUNC_FROM_PACKGE_INITBODY and
            function_from != FUNC_FROM_WITH_CLAUSE)

# Test cases
test_cases = [
    (FUNC_FROM_WITH_CLAUSE, "WITH clause"),
    (FUNC_FROM_SUBPROCFUNC, "SUBPROCFUNC"),
    (FUNC_FROM_PACKAGE, "PACKAGE"),
    (FUNC_FROM_PG_PROC, "PG_PROC"),
]

for func_from, name in test_cases:
    result = FUNC_EXPR_FROM_PG_PROC(func_from)
    negated = not result
    print(f"{name:20} -> FUNC_EXPR_FROM_PG_PROC={result}, !FUNC_EXPR_FROM_PG_PROC={negated}")
EOF

Repository: IvorySQL/IvorySQL

Length of output: 393


🏁 Script executed:

# Check dependency.c more carefully - verify if it uses positive form (safe)
sed -n '2230,2240p' src/backend/catalog/dependency.c | cat -n

Repository: IvorySQL/IvorySQL

Length of output: 404


🏁 Script executed:

# Check if WITH clause functions set parent_func properly
rg -n "parent_func" --type=c src/backend/parser/ | head -20

Repository: IvorySQL/IvorySQL

Length of output: 131


🏁 Script executed:

# Verify the crash scenario: check if subproc_should_change_return_type is actually called in the init_sexpr path
sed -n '730,800p' src/backend/executor/execSRF.c | cat -n

Repository: IvorySQL/IvorySQL

Length of output: 3000


🏁 Script executed:

# Check if parent_func is set for WITH clause functions specifically
rg -n "parent_func" --type=c src/backend/parser/parse_func.c -B3 -A3

Repository: IvorySQL/IvorySQL

Length of output: 368


🏁 Script executed:

# Verify: does fmgr_subproc_info_cxt itself fail or just set up incorrectly?
# And check if WITH clause functions would have correct parent_func
sed -n '330,390p' src/backend/parser/parse_func.c | cat -n

Repository: IvorySQL/IvorySQL

Length of output: 2704


🏁 Script executed:

# Look for where WITH clause functions would be created/handled during parse
rg -n "plisql_get_with_func\|FUNC_FROM_WITH_CLAUSE" --type=c src/pl/plisql/src/pl_handler.c -B2 -A2

Repository: IvorySQL/IvorySQL

Length of output: 43


🏁 Script executed:

# Find where is_subproc_func is actually used or if there are downstream effects
rg -n "is_subproc_func" src/backend/executor/execSRF.c

Repository: IvorySQL/IvorySQL

Length of output: 181


🏁 Script executed:

# Check if the WITH clause lookup actually sets parent_func to NULL
sed -n '500,580p' src/pl/plisql/src/pl_handler.c | cat -n

Repository: IvorySQL/IvorySQL

Length of output: 2704


🏁 Script executed:

# Verify the exact behavior: execSRF.c lines 140-180 to see complete context
sed -n '140,180p' src/backend/executor/execSRF.c | cat -n

Repository: IvorySQL/IvorySQL

Length of output: 1620


!FUNC_EXPR_FROM_PG_PROC now returns true for FUNC_FROM_WITH_CLAUSE; unguarded call sites risk treating WITH functions as subproc functions

After this macro update, code that gates subproc/package-specific logic with !FUNC_EXPR_FROM_PG_PROC without a prior check for FUNC_FROM_WITH_CLAUSE will incorrectly route WITH clause functions through subproc code paths.

Key risk in init_sexpr (execSRF.c, lines 745–746):

// Unguarded — treats WITH clause functions as subproc:
if (!FUNC_EXPR_FROM_PG_PROC(funcexpr->function_from))
    is_subproc_func = true;

// Later (line 774), calls subproc-only handler:
if (is_subproc_func)
    fmgr_subproc_info_cxt(foid, &(sexpr->func), sexprCxt);

fmgr_subproc_info_cxt is designed for PLiSQL subprocedures and expects different function metadata than WITH clause functions provide. This path is triggered whenever a WITH clause function is used as a set-returning function or in a FROM clause context.

Correct pattern already in place (execExpr.c, lines 2784–2787):

if (funcexpr->function_from == FUNC_FROM_WITH_CLAUSE)
    is_with_func = true;
else if (!FUNC_EXPR_FROM_PG_PROC(funcexpr->function_from))
    is_subproc_func = true;

Apply this same guard to init_sexpr. Also audit dependency.c (line 2233) and other unguarded !FUNC_EXPR_FROM_PG_PROC call sites for similar issues.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/include/nodes/primnodes.h` around lines 780 - 783, The change to
FUNC_EXPR_FROM_PG_PROC causes !FUNC_EXPR_FROM_PG_PROC to be true for
FUNC_FROM_WITH_CLAUSE, so update init_sexpr to first check for
FUNC_FROM_WITH_CLAUSE before treating a function as a subprocedure: in
init_sexpr, replace the unguarded "if
(!FUNC_EXPR_FROM_PG_PROC(funcexpr->function_from)) is_subproc_func = true" with
a two-way check that sets is_with_func when funcexpr->function_from ==
FUNC_FROM_WITH_CLAUSE else sets is_subproc_func when
!FUNC_EXPR_FROM_PG_PROC(...); ensure callers that invoke
fmgr_subproc_info_cxt(foid, &(sexpr->func), sexprCxt) only run when
is_subproc_func is true, and audit other unguarded uses of
!FUNC_EXPR_FROM_PG_PROC (e.g., dependency.c) to apply the same guard pattern.

Comment on lines +131 to +133
-- T17: WITH FUNCTION scope — function is not visible outside its statement
SELECT _wf_scope_test(1) FROM dual;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

T17 is currently a false-positive scope test.

This case never defines _wf_scope_test beforehand, so the error is expected even if scope handling is broken. Add a positive in-statement use first, then the out-of-scope failure check.

Proposed test fix
 -- T17: WITH FUNCTION scope — function is not visible outside its statement
+WITH FUNCTION _wf_scope_test(n NUMBER) RETURN NUMBER IS
+BEGIN RETURN n + 1; END;
+SELECT _wf_scope_test(1) FROM dual;
 SELECT _wf_scope_test(1) FROM dual;

As per coding guidelines **/sql/*.sql: Test SQL files. Ensure comprehensive coverage of features.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- T17: WITH FUNCTION scope — function is not visible outside its statement
SELECT _wf_scope_test(1) FROM dual;
-- T17: WITH FUNCTION scope — function is not visible outside its statement
WITH FUNCTION _wf_scope_test(n NUMBER) RETURN NUMBER IS
BEGIN RETURN n + 1; END;
SELECT _wf_scope_test(1) FROM dual;
SELECT _wf_scope_test(1) FROM dual;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oracle_test/regress/sql/with_function.sql` around lines 131 - 133, The
test currently calls _wf_scope_test without ever defining it, producing a
false-positive; update the test to first declare the WITH FUNCTION
_wf_scope_test(...) and use it inside the same statement (a positive
in-statement call to _wf_scope_test) and then, in a separate statement
afterwards, call _wf_scope_test(1) from outside to assert the out-of-scope
failure; ensure the in-statement call and the external call are separate
statements so the second triggers the expected scope error.

Comment on lines +1627 to +1634
/*
* If we are currently executing a WITH-clause function, expose the WITH
* function entries on this ParseState so that parse_func.c can resolve
* recursive calls (and calls to sibling WITH functions) even though the
* hook slot is occupied by plisql_subprocfunc_ref.
*/
if (plisql_active_with_func_entries != NIL)
pstate->p_with_func_list = plisql_active_with_func_entries;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate p_with_func_list by the owning WITH-call context.

This publishes the outer query's WITH-function entries to any PL/iSQL parse done while a WITH function is on the stack. If a WITH function calls a normal stored PL/iSQL function, SQL parsed in that callee can also see the caller's WITH entries and mis-resolve an ordinary function name against the outer WITH clause. The handler now saves plisql_active_with_func_estate, but this path never consults it. Please restrict the exposure to the matching WITH-function execution context instead of checking only plisql_active_with_func_entries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pl/plisql/src/pl_comp.c` around lines 1627 - 1634, Only assign
pstate->p_with_func_list when the current ParseState belongs to the same
WITH-function execution context: check that plisql_active_with_func_entries is
non-NIL AND that the current pstate's execution context matches the saved
plisql_active_with_func_estate (e.g. compare pstate->p_estate or equivalent
field against plisql_active_with_func_estate) before setting p_with_func_list;
this ensures only parses running in the owning WITH-call can see those
WITH-function entries (references: p_with_func_list,
plisql_active_with_func_entries, plisql_active_with_func_estate, pstate).

Comment on lines +317 to +322
paramLI->paramnames[i] = fp->name;
paramLI->params[i].value = (Datum) 0;
paramLI->params[i].isnull = true;
paramLI->params[i].pflags = 0;
paramLI->params[i].ptype = typenameTypeId(NULL, fp->argType);
paramLI->params[i].pmode = (char) fp->mode;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve declared typmods in WITH-function parameters.

Only the type OID is copied into ParamListInfo here. plisql_compile_inline() later builds each parameter variable from param->ptype and param->ptypmod, so declarations like varchar2(10) or numeric(8,2) lose their modifier when compiled through the WITH-function path. Please carry the parsed typmod into each ParamExternData.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pl/plisql/src/pl_handler.c` around lines 317 - 322, The WITH-function
path only copies the parameter type OID into ParamExternData
(paramLI->params[i].ptype) but not the parsed typmod, so declarations like
varchar2(10) lose their modifier; update the code that fills ParamListInfo in
pl_handler.c to also copy the parsed typmod into each ParamExternData (set
paramLI->params[i].ptypmod = fp->argtypmod or the corresponding parsed typmod
field on the formal parameter), since plisql_compile_inline() expects both
param->ptype and param->ptypmod when rebuilding parameter variables.

Comment on lines +473 to +489
/* Set up error context so compilation errors name the function */
errctx.funcname = ifd->funcname;
errctx.is_proc = ifd->is_proc;
errcallback.callback = with_func_compile_error_callback;
errcallback.arg = &errctx;
errcallback.previous = error_context_stack;
error_context_stack = &errcallback;

paramLI = buildParamListForFunc(ifd->args);
rettype = (ifd->rettype != NULL)
? typenameTypeId(NULL, ifd->rettype)
: InvalidOid;
compiled = plisql_compile_inline(ifd->src, paramLI, false,
rettype, ifd->is_proc);
fixupCompiledReturnType(compiled, rettype, ifd->is_proc);

error_context_stack = errcallback.previous;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Always pop the error-context callback on compilation failure.

If buildParamListForFunc(), typenameTypeId(), or plisql_compile_inline() throws, the restore at Line 489 is skipped and error_context_stack keeps a pointer to this stack-local errcallback. The next error can then walk a dead frame. Wrap the compile block in PG_TRY/PG_FINALLY so the callback is removed unconditionally.

🩹 Suggested structure
 		errctx.funcname = ifd->funcname;
 		errctx.is_proc  = ifd->is_proc;
 		errcallback.callback = with_func_compile_error_callback;
 		errcallback.arg		 = &errctx;
 		errcallback.previous = error_context_stack;
 		error_context_stack  = &errcallback;
-
-		paramLI  = buildParamListForFunc(ifd->args);
-		rettype = (ifd->rettype != NULL)
-				  ? typenameTypeId(NULL, ifd->rettype)
-				  : InvalidOid;
-		compiled = plisql_compile_inline(ifd->src, paramLI, false,
-										 rettype, ifd->is_proc);
-		fixupCompiledReturnType(compiled, rettype, ifd->is_proc);
-
-		error_context_stack = errcallback.previous;
+		PG_TRY();
+		{
+			paramLI  = buildParamListForFunc(ifd->args);
+			rettype = (ifd->rettype != NULL)
+					  ? typenameTypeId(NULL, ifd->rettype)
+					  : InvalidOid;
+			compiled = plisql_compile_inline(ifd->src, paramLI, false,
+											 rettype, ifd->is_proc);
+			fixupCompiledReturnType(compiled, rettype, ifd->is_proc);
+		}
+		PG_FINALLY();
+		{
+			error_context_stack = errcallback.previous;
+		}
+		PG_END_TRY();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/* Set up error context so compilation errors name the function */
errctx.funcname = ifd->funcname;
errctx.is_proc = ifd->is_proc;
errcallback.callback = with_func_compile_error_callback;
errcallback.arg = &errctx;
errcallback.previous = error_context_stack;
error_context_stack = &errcallback;
paramLI = buildParamListForFunc(ifd->args);
rettype = (ifd->rettype != NULL)
? typenameTypeId(NULL, ifd->rettype)
: InvalidOid;
compiled = plisql_compile_inline(ifd->src, paramLI, false,
rettype, ifd->is_proc);
fixupCompiledReturnType(compiled, rettype, ifd->is_proc);
error_context_stack = errcallback.previous;
/* Set up error context so compilation errors name the function */
errctx.funcname = ifd->funcname;
errctx.is_proc = ifd->is_proc;
errcallback.callback = with_func_compile_error_callback;
errcallback.arg = &errctx;
errcallback.previous = error_context_stack;
error_context_stack = &errcallback;
PG_TRY();
{
paramLI = buildParamListForFunc(ifd->args);
rettype = (ifd->rettype != NULL)
? typenameTypeId(NULL, ifd->rettype)
: InvalidOid;
compiled = plisql_compile_inline(ifd->src, paramLI, false,
rettype, ifd->is_proc);
fixupCompiledReturnType(compiled, rettype, ifd->is_proc);
}
PG_FINALLY();
{
error_context_stack = errcallback.previous;
}
PG_END_TRY();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pl/plisql/src/pl_handler.c` around lines 473 - 489, The error-context
callback (errcallback) is pushed onto error_context_stack before calling
buildParamListForFunc, typenameTypeId, and plisql_compile_inline, but if any of
those functions throws the final restore (error_context_stack =
errcallback.previous) is skipped; wrap the compilation block that sets paramLI,
rettype, calls plisql_compile_inline and fixupCompiledReturnType in a PG_TRY /
PG_FINALLY pair and in the PG_FINALLY ensure error_context_stack is restored
(pop errcallback) so the with_func_compile_error_callback / errctx frame is
always removed even on errors.

@bigplaice bigplaice closed this May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

oracle compatibility feature: WITH clause supports procedures and functions

1 participant