Skip to content

feat(trino): parse WITH FUNCTION ... RETURNS ... RETURN inline UDFs [CLAUDE]#7934

Open
rusackas wants to merge 3 commits into
tobymao:mainfrom
rusackas:trino-udf-1-core
Open

feat(trino): parse WITH FUNCTION ... RETURNS ... RETURN inline UDFs [CLAUDE]#7934
rusackas wants to merge 3 commits into
tobymao:mainfrom
rusackas:trino-udf-1-core

Conversation

@rusackas

@rusackas rusackas commented Jul 23, 2026

Copy link
Copy Markdown

This re-splits #7926 (closed as too large to review in one pass) into small, incremental PRs per @geooo109's request. This is PR 1 of ~7. I know it's just as many files touched, but it's +154 -15 rather than +493 -16 at least ;)

Trino supports declaring SQL UDFs inline in a WITH clause preceding the query (https://trino.io/docs/current/udf/sql.html):

WITH FUNCTION meaning_of_life()
  RETURNS tinyint
  RETURN 42
SELECT meaning_of_life()

These currently fail to parse (Expecting (). Happens to resolve #5178 (closed), it seems.

This PR is the minimal slice: TrinoParser._parse_cte recognizes a FUNCTION <name>(...) entry in a WITH clause (as opposed to a CTE literally named function, e.g. WITH function AS (SELECT 1)) and parses it into a new exp.FunctionSpecification node, with a mandatory RETURNS clause and a RETURN <expr> body — enough to round-trip the literal example from #5178. Inline functions live in their own WITH clause before the query's own WITH clause per the grammar, so TrinoGenerator.with_sql emits WITH <functions> WITH <ctes> when both are present. walk_in_scope treats FunctionSpecification as opaque so qualify doesn't try to resolve UDF parameters as outer-scope columns.

Next steps (planned follow-up PRs, each gated on the previous)

  1. This PR — core WITH FUNCTION ... RETURNS ... RETURN ...
  2. Routine characteristics (LANGUAGE, DETERMINISTIC/NOT DETERMINISTIC, CALLED/RETURNS NULL ON NULL INPUT, SECURITY, COMMENT)
  3. BEGIN...END block bodies + DECLARE/SET, including the semicolon/chunk-continuation handling so routine bodies don't get split as separate statements
  4. IF/ELSEIF/ELSE — reusing/extending exp.IfBlock per review feedback on feat(trino): support inline SQL UDFs (WITH FUNCTION ... SELECT) [CLAUDE] #7926, instead of a new expression type
  5. CASE...WHEN...END CASE
  6. WHILE...DO...END WHILE — reusing/extending exp.WhileBlock with a label arg, per review feedback
  7. LOOP/REPEAT/ITERATE/LEAVE

PRs 4-7 depend on #3 (they all wrap bodies in BEGIN...END); PR 2 and PR 3 are independent of each other, both depending only on this one. Let me know if this subdivision works better. I can "stack" the PRs here if that helps lay out the roadmap, I'll just have to deal with reabases. 😅

Disclosure: implemented with Claude Code, reviewed, tested (make unit, make style) and understood by me (again, I really do look at this stuff. See, it's me, a huuuuuuumannnnn!).

Trino supports declaring SQL UDFs inline in a WITH clause preceding the
query (https://trino.io/docs/current/udf/sql.html). These currently fail
to parse ("Expecting (") because a WITH-clause entry starting with
FUNCTION isn't recognized. Closes tobymao#5178.

This is the minimal slice: TrinoParser._parse_cte recognizes a
`FUNCTION <name>(...)` entry (as opposed to a CTE literally named
"function") and parses it into a new exp.FunctionSpecification node, with
a mandatory RETURNS clause and a RETURN <expr> body. Inline functions live
in their own WITH clause before the query's own WITH clause per the
grammar, so TrinoGenerator.with_sql emits `WITH <functions> WITH <ctes>`
when both are present. walk_in_scope treats FunctionSpecification as
opaque so qualify does not try to resolve UDF parameters as outer-scope
columns.

Not covered here, tracked as follow-ups: routine characteristics
(LANGUAGE, DETERMINISTIC, SECURITY, ...), BEGIN...END block bodies with
DECLARE/SET, and control statements (IF, CASE, LOOP/WHILE/REPEAT). This
supersedes tobymao#7926, which bundled all of the above and was closed as too
large to review in one pass.

Disclosure: implemented with Claude Code, reviewed, tested (make unit,
make style) and understood by me.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@rusackas

Copy link
Copy Markdown
Author

As mentioned in the PR description, I'm happy to see if we can get through this first, then tackle next steps, or "stack" some PRs (in draft) to show how this will all come together.

@geooo109 geooo109 self-assigned this Jul 23, 2026

@geooo109 geooo109 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice work, thanks for taking into account my comments and breaking the first PR into smaller ones.

Comment thread sqlglot/parsers/trino.py Outdated
Comment thread sqlglot/generators/trino.py Outdated
Comment thread sqlglot/optimizer/scope.py Outdated
Comment thread sqlglot/generator.py Outdated
Comment thread tests/dialects/test_trino.py
Comment thread tests/dialects/test_trino.py Outdated
Comment thread tests/dialects/test_trino.py Outdated
@geooo109

geooo109 commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

One last comment, we also need to cover this:

  WITH FUNCTION f(x INTEGER) RETURNS INTEGER RETURN x
  WITH RECURSIVE t AS (SELECT 1 AS n)
  SELECT F(n) FROM t

@rusackas

Copy link
Copy Markdown
Author

Thanks for the feedback... digging in!

rusackas and others added 2 commits July 24, 2026 14:32
- Parse routine characteristics via the existing _parse_properties()
  instead of a bespoke _parse_routine_characteristics wrapper.
- Simplify TrinoGenerator.with_sql to delegate to the base
  implementation for the CTE portion instead of reimplementing it.
- Use properties(..., prefix=" ") instead of a separate blank-check line.
- Revert the walk_in_scope/FunctionSpecification opacity change; that's
  deferred to a follow-up once merge_subqueries and friends are audited.
- Trim test_inline_udf: drop explanatory comments, the find_all-based
  scope-traversal assertions, and the invalid-input cases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…TION

WITH FUNCTION f(...) ... WITH RECURSIVE t AS (...) SELECT ... failed to
parse: the base _parse_with() only checks for RECURSIVE once, right after
the very first WITH token, so it choked trying to parse a RECURSIVE that
shows up on the query's own (second) WITH clause. TrinoParser._parse_with
now also checks for RECURSIVE after a later bare WITH.

Also guards add_recursive_cte_column_names (inherited from Presto) against
non-CTE entries in a recursive With's expressions -- it assumed every
entry had an "alias" arg, which a FunctionSpecification doesn't, and
crashed with a KeyError building this AST directly rather than through
the parser.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@rusackas

rusackas commented Jul 24, 2026

Copy link
Copy Markdown
Author

Indeed, found and fixed a gap while poking at WITH FUNCTION f(...) ... WITH RECURSIVE t AS (...) SELECT ... failed to parse, since RECURSIVE was only ever checked right after the first WITH token. Fixed in the latest commit, with a test.

@rusackas

Copy link
Copy Markdown
Author

I think/hope we're cleaned up now, but you're a fantastic reviewer, so I'll just cross my fingers for now.

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.

Trino: CTE User-Defined Function Parse Error

2 participants