Skip to content

feat(trino): support inline SQL UDFs (WITH FUNCTION ... SELECT) [CLAUDE]#7926

Open
rusackas wants to merge 1 commit into
tobymao:mainfrom
rusackas:trino-inline-udf
Open

feat(trino): support inline SQL UDFs (WITH FUNCTION ... SELECT) [CLAUDE]#7926
rusackas wants to merge 1 commit into
tobymao:mainfrom
rusackas:trino-inline-udf

Conversation

@rusackas

@rusackas rusackas commented Jul 21, 2026

Copy link
Copy Markdown

Howdy! I was fixing an Issue on Superset, and it was suggested I file the fix "upstream" so here we go!

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

WITH FUNCTION meaning_of_life()
  RETURNS tinyint
  BEGIN
    DECLARE a tinyint DEFAULT CAST(6 AS tinyint);
    DECLARE b tinyint DEFAULT CAST(7 AS tinyint);
    RETURN a * b;
  END
SELECT meaning_of_life()

These fail to parse (Expecting (), and the semicolons inside BEGIN ... END bodies additionally split the statement in sqlglot.parse. #5178 was closed as low priority with an invitation for a well-tested PR, so here's an attempt. The same gap bit us over in Apache Superset (apache/superset#26162), and I'd rather fix it here than carry a custom dialect downstream.

Parsing follows the functionSpecification / controlStatement grammar from Trino's SqlBase.g4. TrinoParser._parse_cte recognizes FUNCTION <name> entries and parses them into a new exp.FunctionSpecification node (a CTE literally named function still parses as a CTE). Routine bodies support the full control statement grammar (RETURN, BEGIN, DECLARE, SET, CASE, IF/ELSEIF, LOOP, WHILE, REPEAT/UNTIL, ITERATE, LEAVE, loop labels), reusing Declare/Set/Return/Block where they fit and adding statement-level expressions for the rest. Since routine statements are semicolon-separated, the body parser continues across the chunks produced in Parser._parse, the same way _parse_block already does for procedures, so multi-statement scripts still split correctly around the UDF query.

Routine characteristics reuse the existing property machinery, with Trino generator fixes so they round-trip: DETERMINISTIC instead of IMMUTABLE, SECURITY INVOKER instead of SQL SECURITY INVOKER, and DECLARE ... DEFAULT instead of =. Per the grammar, inline functions can't be comma-mixed with named queries, 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.

Not covered: AS $$...$$ bodies for LANGUAGE PYTHON UDFs (would need $$ tokenizer support in Trino), and CREATE FUNCTION with a BEGIN body, which still splits on inner semicolons today. Happy to follow up on either if there's interest.

Disclosure: implemented with Claude Code, but reviewed, tested (make unit, make unitc, make style) and understood by me. [Edit: it's true, I really look at stuff]

Trino queries can declare SQL UDFs inline in a WITH clause preceding the
query (https://trino.io/docs/current/udf/sql.html). These failed to parse
(Expecting () and the semicolons inside BEGIN ... END routine bodies split
the statement. Closes tobymao#5178.

- TrinoParser parses FUNCTION entries in a WITH clause into a new
  exp.FunctionSpecification node, following the functionSpecification /
  controlStatement grammar from Trino's SqlBase.g4. A CTE literally named
  "function" still parses as a CTE.
- Routine bodies support the full control statement grammar: RETURN,
  BEGIN ... END, DECLARE, SET, CASE ... END CASE, IF/ELSEIF/ELSE ... END IF,
  LOOP, WHILE ... DO, REPEAT ... UNTIL, ITERATE, LEAVE, and loop labels.
  New statement-level expressions: CaseStatement, IfStatement,
  LoopStatement, WhileStatement, RepeatStatement, Iterate, Leave; DECLARE,
  SET, RETURN and BEGIN reuse Declare, Set, Return and Block (with a new
  begin flag).
- The body parser continues across the semicolon-separated chunks produced
  in Parser._parse, the same way _parse_block does for procedures, so
  sqlglot.parse no longer splits routine bodies into separate statements.
- Routine characteristics reuse the existing property machinery, with
  Trino generator fixes so they round-trip: DETERMINISTIC instead of
  IMMUTABLE, SECURITY INVOKER instead of SQL SECURITY INVOKER, and
  DECLARE ... DEFAULT instead of =.
- Inline functions live in their own WITH clause before the query's own
  WITH clause per Trino's 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 and variables as outer-scope columns.

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

Copy link
Copy Markdown
Author

CC @betodealmeida :D Thanks for suggesting I do this

This comment was marked as low quality.

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.

2 participants