Skip to content

feat: add structured MATCH_RECOGNIZE support - #2634

Open
minleejae wants to merge 3 commits into
JSQLParser:masterfrom
minleejae:feat/match-recognize
Open

minleejae wants to merge 3 commits into
JSQLParser:masterfrom
minleejae:feat/match-recognize

Conversation

@minleejae

@minleejae minleejae commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

DuckDB also has ongoing work to add native MATCH_RECOGNIZE support in duckdb/duckdb#25255, which is currently open.

MATCH_RECOGNIZE queries currently fail at the FROM clause, including the BigQuery query reported in #2350. This adds an editable relation and row-pattern AST, with parsing, deparsing, visitor traversal and static validation for BigQuery, Oracle and Snowflake syntax.

Closes #2350.

Implementation

  • Add MatchRecognize as a FromItem wrapping its input, with independent input/result aliases and structured partitioning, ordering, measures, definitions, output modes, skip targets, subsets and options.
  • Represent pattern variables, groups, sequences, ordered alternatives, anchors, quantifiers, permutations and exclusions as typed nodes. Quantifier bounds remain expressions; PERMUTE is not expanded. RowPatternFunction keeps RUNNING/FINAL separate from the underlying function.
  • Support BigQuery empty alternatives, query-parameter bounds and longest-match options. A scoped lexer context handles adjacent $ anchors even during nested SELECT lookahead, without changing dollar literals elsewhere.
  • Support Oracle/Snowflake row output modes, targeted skips, permutations, exclusions and function modes, plus Oracle SUBSET. Explicit dialect selection controls BigQuery lexical rules and Snowflake's documented pattern precedence.
  • Preserve custom expression/source visitor callbacks, keep pattern qualifiers out of table discovery, and validate selected static dialect rules and feature capabilities.

Refactoring

Clause rendering is shared between the AST and deparser. The same change fixes STRUCT rendering that replayed original tokens instead of edited argument expressions, which otherwise breaks nested BigQuery measures.

Visitor traversal now follows parenthesized input joins/set operations, pivot children and scalar subqueries in expression validation. These paths are needed to inspect and rewrite the new clause without losing its nested expressions or source relations.

Rendering and validation are organized by clause responsibilities, and table discovery reuses existing null-safe visitor helpers. Codacy's T-SQL linter excludes only the new Oracle/BigQuery SQL fixtures, where its SQL Server requirements do not apply.

Validation

  • GitHub CI passed on commit 29798771: grammar ambiguity checks and Gradle/Maven on Linux, Windows and macOS. Codacy also passed with no new issues.
  • ./gradlew check --no-daemon: passed, 7,067 tests, 0 failures/errors, 25 skipped; grammar ambiguity, formatting, Checkstyle, PMD, SpotBugs and coverage checks passed.
  • mvn --batch-mode --no-transfer-progress verify: passed, 7,049 tests, 0 failures/errors, 25 skipped.
  • GoogleSQL reference evaluator 2026.9.1: all 15 original/deparsed fixture pairs executed with identical results, including [FEATURE] Add support for MATCH_RECOGNIZE clause (BigQuery) to JSQLParser #2350, longest-match behavior, reluctant/greedy matching, overlap, parameter bounds and nested STRUCT measures.
  • Oracle 26ai Free 23.26.2.0.0: all 17 original/deparsed fixture pairs executed with identical results, including SUBSET, PERMUTE, exclusions, RUNNING/FINAL and output/skip modes.
  • The 32 self-contained execution fixtures and reproduction details are included in test resources. An additional 67 syntax candidates from the three dialects round-trip successfully.
  • The added usage section builds with Sphinx warnings treated as errors.

Scope and remaining verification

GoogleSQL is the reference evaluator, not the BigQuery service. Snowflake syntax and AST behavior were checked against its official documentation and parser tests; no Snowflake account was available for execution. Its documentation gives alternation precedence over concatenation, so the Snowflake preset follows that rule and rendering inserts parentheses at mixed operator boundaries. Verifying that precedence against the service remains a useful review check.

This is a SQL parser/AST implementation. Function eligibility, aggregation/type rules, binding pattern-qualified columns to the input schema, and data-dependent matching or skip errors remain with the database. It does not implement a pattern execution engine or cross-dialect query translation.

Sources: BigQuery syntax, Oracle pattern matching, Snowflake syntax and precedence.

h4nsmuller added a commit to h4nsmuller/duckdb that referenced this pull request Sep 14, 2026
JSQLParser/JSqlParser#2634 adds MATCH_RECOGNIZE to JSqlParser with 32 fixtures,
17 of them executed against Oracle 23ai. Eleven parsed here; six did not, and
four of those six are SQL:2016 rather than Oracle extensions.

**AFTER MATCH SKIP TO <variable>.** The standard's skip target is
`TO [FIRST | LAST] <variable>`, where saying neither means the variable's last
row. Only the two explicit forms were accepted.

**The empty pattern.** `()` is a row pattern primary that matches where it stands
and takes no row, so `PATTERN (A ())` is one A and no more, and `PATTERN (A | ())`
falls through to an empty match. It compiles to a concatenation of nothing, which
the matcher already walks correctly.

**RUNNING and FINAL anywhere a call can appear.** They were parsed at the measure
level, so `RUNNING sum(x) + 1` worked and `1 + RUNNING sum(x)` did not, and
`RUNNING sum(x) + FINAL sum(x)` - the shape Oracle's fixture uses - could not be
written at all. They are now a prefix on a function call in the expression
grammar, which is where the standard puts them, and the ordinary binder reports
them outside MEASURES rather than leaving a missing function to explain it. A
column named `running` is still a column: the call after the keyword is what the
grammar needs to see.

**ALL ROWS PER MATCH SHOW EMPTY MATCHES / OMIT EMPTY MATCHES / WITH UNMATCHED
ROWS.** SHOW is what ALL ROWS PER MATCH already did. OMIT drops the rows of
empty matches. WITH UNMATCHED ROWS keeps the rows that are in no match at all,
reporting the row and nothing else - every measure is about a match, so a row
without one has none. That needed no executor change: unnesting drops a row whose
span list is empty, so the list becomes a single NULL span for those rows and the
measures above read NULL through it.

All 17 Oracle fixtures now run. Full suite green: 3,027,732 assertions in 6,840
test cases; MATCH_RECOGNIZE green under seven verification configs and storage
compatibility.

Claude-Session: https://claude.ai/code/session_01T8BCMvDgXtg7kL5RruuS2A
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.

[FEATURE] Add support for MATCH_RECOGNIZE clause (BigQuery) to JSQLParser

1 participant