Skip to content

Multi-Stage: Add SQL:2016 MATCH_RECOGNIZE (row pattern recognition) - #19311

Open
xiangfu0 wants to merge 3 commits into
apache:masterfrom
xiangfu0:claude/match-recognize-oss
Open

Multi-Stage: Add SQL:2016 MATCH_RECOGNIZE (row pattern recognition)#19311
xiangfu0 wants to merge 3 commits into
apache:masterfrom
xiangfu0:claude/match-recognize-oss

Conversation

@xiangfu0

@xiangfu0 xiangfu0 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Adds SQL:2016 row pattern recognition to Pinot's multi-stage query engine: validation, logical/wire plans, exchange planning, and an intermediate-stage matcher.

Pinot's parser already accepts the MATCH_RECOGNIZE grammar through Calcite's Babel parser. This PR adds the missing supported-subset validation and execution path.

Design and delivery boundary

The parser validation, MatchNode wire representation, exchange rule, runtime operator, and expression semantics form one end-to-end query operator and must land atomically. Splitting those layers would either expose syntax that cannot execute or dispatch plans that servers cannot interpret. Existing non-MATCH_RECOGNIZE queries keep their existing plan and execution behavior; unsupported MATCH_RECOGNIZE modes fail closed with actionable validation errors. The PR remains labeled design-review for maintainer sign-off before landing.

Rollback is likewise atomic: revert the feature commits and stop issuing MATCH_RECOGNIZE queries. There is no persisted-data or table-schema migration.

Front end and planner

  • Registers PREV/NEXT/FIRST/LAST/CLASSIFIER/MATCH_NUMBER/RUNNING/FINAL in the strict PinotOperatorTable allow-list.
  • Adds MatchRecognizeValidator, including SQL:2016's omitted AFTER MATCH default (SKIP PAST LAST ROW) and actionable rejection of deferred constructs such as ALL ROWS PER MATCH, SUBSET, PERMUTE, exclusions, WITHIN, aggregates in DEFINE, explicit null ordering, expression partition/order keys, and multi-value partition or aggregate inputs.
  • Adds MatchNode and a self-contained recursive RowPattern; PatternFieldRef preserves pattern-variable identity instead of degrading to a plain input reference.
  • Hash-distributes by PARTITION BY, then sorts by the exchange's actual partition-key order plus the requested order keys. Queries without PARTITION BY require the explicit allowMatchRecognizeWithoutPartitionBy query option and execute as one global partition.
  • The v2 physical optimizer and lite mode are not supported yet. The planner now checks the effective broker defaults plus query overrides and rejects these combinations explicitly; SET usePhysicalOptimizer = false selects the supported path.
  • Sender-side sorting for sort exchanges is tracked separately in [MSE] Support sender-side sorting for sort exchanges #19395.

Runtime and resource safety

  • Compiles prioritized NFAs for source-order alternation, greedy/reluctant quantifiers, bounded quantifiers, and anchors.
  • Evaluates DEFINE predicates and MEASURES over a classifier tape, with bounded navigation and exact decimal/integer aggregation behavior.
  • Processes one monotonically ordered partition at a time without retaining an unbounded closed-partition set or allocating a key tuple per row.
  • Emits at most 1,024 rows per transfer block and resumes matching state across calls; upstream error blocks supersede buffered output.
  • Checks cancellation/deadline state every 1,024 matcher transitions.
  • Stores backtracking state in primitive choice/counter logs and enforces a non-configurable 64 MiB cap on their combined array payload before growth.
  • Throws instead of truncating when a row, step, or retained-state limit is reached.

Resource-limit precedence is node hint, query option, server config, then default:

Limit Query option Hint key Server config
Rows buffered per partition maxRowsInMatchPartition max_rows_in_match_partition pinot.query.match.max.rows.per.partition
NFA transitions per start position maxStepsPerMatchAttempt max_steps_per_match_attempt pinot.query.match.max.steps.per.attempt

Scope

v1 covers PARTITION BY, mandatory ORDER BY, MEASURES, ONE ROW PER MATCH, all four supported AFTER MATCH SKIP modes, pattern alternation/concatenation/quantifiers/anchors, and single-variable aggregates in measures.

Rolling-upgrade boundary

MatchNode is plan oneof field 19. Older servers preserve that protobuf field as unknown but see the node oneof as unset (NODE_NOT_SET), so they cannot execute a dispatched MATCH_RECOGNIZE stage. Upgrade all servers before issuing these queries; there is no mixed-version fallback. Existing query node kinds remain wire-compatible and are unaffected.

Testing

  • The 342-test focused planner/common/runtime suite passed, covering validation, rel conversion, exchange planning, plan serialization, legacy-descriptor behavior, option precedence, navigation offsets, numeric precision, NFA behavior, cancellation, retained-state bounds, output chunking, and operator errors. After the final retained-state cap changes, the 32 affected matcher/operator tests were rerun and passed.
  • MatchRecognizeIntegrationTest: 13/13 passed against a real two-server cluster, including partition order, cross-segment isolation, global execution without PARTITION BY, omitted/explicit skip semantics, and no-null/all-null/mixed-null aggregate behavior with null handling enabled and disabled.
  • Full 63-module dependency reactor test-compile passed on JDK 25.
  • Spotless, checkstyle, license formatting/checking, and git diff --check passed for all affected modules.

@codecov-commenter

codecov-commenter commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.21088% with 402 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.66%. Comparing base (bbbed25) to head (b612fab).

Files with missing lines Patch % Lines
.../query/runtime/operator/match/MatchExpression.java 58.33% 50 Missing and 30 partials ⚠️
.../pinot/query/runtime/operator/match/MatchTerm.java 66.66% 19 Missing and 25 partials ⚠️
...he/pinot/query/runtime/operator/MatchOperator.java 82.38% 20 Missing and 11 partials ⚠️
.../query/planner/logical/RelToPlanNodeConverter.java 83.33% 15 Missing and 15 partials ⚠️
.../query/planner/logical/PlanNodeToRelConverter.java 61.64% 12 Missing and 16 partials ⚠️
...he/pinot/query/planner/explain/PlanNodeMerger.java 0.00% 23 Missing ⚠️
...pache/pinot/query/planner/plannode/RowPattern.java 77.88% 12 Missing and 11 partials ⚠️
.../pinot/query/validate/MatchRecognizeValidator.java 87.11% 4 Missing and 17 partials ⚠️
...apache/pinot/query/planner/plannode/MatchNode.java 80.00% 6 Missing and 10 partials ⚠️
.../query/planner/logical/EquivalentStagesFinder.java 0.00% 12 Missing ⚠️
... and 24 more
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19311      +/-   ##
============================================
+ Coverage     57.71%   67.66%   +9.95%     
- Complexity        7     1430    +1423     
============================================
  Files          2686     3499     +813     
  Lines        163987   225929   +61942     
  Branches      26627    35762    +9135     
============================================
+ Hits          94640   152884   +58244     
+ Misses        61352    60838     -514     
- Partials       7995    12207    +4212     
Flag Coverage Δ
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 67.66% <77.21%> (+9.95%) ⬆️
lane-a 100.00% <ø> (ø)
lane-b 0.00% <ø> (ø)
temurin 67.66% <77.21%> (+9.95%) ⬆️
unittests 67.66% <77.21%> (+9.95%) ⬆️
unittests1 57.91% <77.21%> (+0.20%) ⬆️
unittests2 39.02% <0.68%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@xiangfu0 xiangfu0 added feature New functionality multi-stage Related to the multi-stage query engine query Related to query processing sql-compliance Related to SQL standard compliance configuration Config changes (addition/deletion/change in behavior) release-notes Referenced by PRs that need attention when compiling the next release notes labels Aug 19, 2026
@xiangfu0
xiangfu0 force-pushed the claude/match-recognize-oss branch from ededa2b to 3435400 Compare August 20, 2026 09:03
Comment thread pinot-query-runtime/src/test/resources/queries/MatchRecognize.json
@xiangfu0
xiangfu0 force-pushed the claude/match-recognize-oss branch 3 times, most recently from 7ebcfdc to 5f27d70 Compare August 26, 2026 09:21

@xiangfu0 xiangfu0 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review of current head 5f27d70c.

The correctness, rolling-upgrade, and resource-behavior issues below should be addressed before merge. I did not duplicate the two existing unresolved threads about v2/lite rejection and _closedPartitionKeys cardinality.

Process follow-ups:

  • This is a 56-file, 7.5K-line change spanning protobuf, planning, execution, configuration, and integration. Please either link the reviewed design/maintainer agreement for keeping it as one vertical slice, or split it into reviewable stacked PRs.
  • Please link the sender-side-sorting TODO to a tracking issue.
  • Both commits contain AI Co-Authored-By trailers; repository guidance says to omit those, so please rewrite the commit messages before merge.

Comment thread pinot-common/src/main/proto/plan.proto
@xiangfu0
xiangfu0 force-pushed the claude/match-recognize-oss branch 4 times, most recently from 9623e5c to ccb8dea Compare August 29, 2026 09:07
@xiangfu0 xiangfu0 added design-review Requires design review before implementation backward-incompat Introduces a backward-incompatible API or behavior change upgrade-incompat PR may introduce incompatibility during upgrade of an installation labels Aug 29, 2026
Adds row pattern recognition to the multi-stage query engine, following the
same shape as the UNNEST support added in apache#17168: a new plan node, an
exchange-insertion rule, and an intermediate-stage operator.

Pinot's parser already accepted the full MATCH_RECOGNIZE grammar (Parser.jj
carries Calcite's Babel production). What was missing was operator-table
registration, validation, a plan node, and all of execution.

Front end
- Register PREV/NEXT/FIRST/LAST/CLASSIFIER/MATCH_NUMBER/RUNNING/FINAL in
  PinotOperatorTable, which is a strict allow-list.
- New MatchRecognizeValidator runs on the SqlNode tree before conversion. It
  rewrites an OMITTED AFTER MATCH clause to SKIP PAST LAST ROW: Calcite
  substitutes SKIP TO NEXT ROW, but SQL:2016, Trino, Snowflake and Oracle all
  default to SKIP PAST LAST ROW. The two differ in whether matches overlap, so
  a query ported from another engine would otherwise silently return different
  rows. Conversion erases the omitted-vs-explicit distinction, so the rewrite
  has to happen here.
- Deferred constructs are rejected at planning time with actionable messages
  rather than silently mis-executing: ALL ROWS PER MATCH, SUBSET, PERMUTE,
  pattern exclusions, WITHIN, aggregates in DEFINE, NULLS FIRST/LAST, and
  ORDER BY / PARTITION BY on expressions (the last of which otherwise fail
  inside SqlToRelConverter with AssertionError or ClassCastException).

Plan and wire format
- MatchNode at plan.proto tag 19, encoding the pattern as a self-contained
  recursive RowPattern with a symbol table rather than a RexCall tree, with
  field numbers reserved for SUBSET and WITHIN.
- PatternFieldRef in expressions.proto, so RexPatternFieldRef can no longer
  degrade to a plain InputRef and produce wrong-but-type-correct results.
- PinotMatchExchangeNodeInsertRule hash-distributes on the PARTITION BY keys
  and prepends them to the sort collation, so rows arrive clustered and the
  operator can match and flush one partition at a time. A missing PARTITION BY
  is rejected by default, since it collapses the table onto one worker.

Runtime
- PatternToNfaCompiler builds an NFA with prioritized transitions (alternation
  in source order; greedy takes the loop edge first, reluctant the exit edge;
  {n,m} via counter registers rather than state unrolling), so depth-first
  traversal yields the SQL:2016 preferred match first.
- MatchOperator evaluates DEFINE predicates over a classifier tape supporting
  PREV/NEXT/FIRST/LAST/CLASSIFIER/MATCH_NUMBER, emits MEASURES, and advances
  per the AFTER MATCH SKIP mode.
- Guardrails throw rather than truncate, since a truncated pattern result is a
  silently wrong one: maxRowsInMatch, maxStepsPerMatchAttempt, and an
  empty-cycle guard.

v1 covers PARTITION BY, mandatory ORDER BY, MEASURES, ONE ROW PER MATCH, all
four AFTER MATCH SKIP modes, the full pattern algebra including reluctant
quantifiers and anchors, and single-variable aggregates in MEASURES.

MATCH_RECOGNIZE is not yet supported under the v2 physical optimizer or lite
mode; queries there are covered by ignore flags rather than silently wrong
results.
Upstream now enforces `///` markdown doc comments (JEP 467) over `/** */`
Javadoc via a checkstyle RegexpCheck, and this feature branch predates that
rule. Converts all 168 Javadoc blocks across the 23 MATCH_RECOGNIZE files, and
removes an `org.apache.calcite.sql.SqlLiteral` import that the feature commit
added without ever using (UnusedImports flags it independently).

Formatting only: `{@link X}` becomes `[X]`, `{@link X label}` becomes
`[label][X]`, `{@code x}` becomes a backtick span, `<p>` becomes an empty ///
line, and the remaining HTML becomes its markdown equivalent. No documentation
text was reworded or dropped, and the removed import is the only non-comment
line changed.

Verified: checkstyle reports 0 violations in each of pinot-spi, pinot-common,
pinot-query-planner, pinot-query-runtime and pinot-integration-tests (run
per-module, since a combined reactor stops at the first failure and hides the
rest); `javadoc -Xdoclint:reference,syntax` is clean over the changed sources;
pinot-query-planner 1598 tests and pinot-query-runtime 4611 tests still pass.
Harden planner validation, configuration precedence, wire compatibility, partition ordering, matcher resource bounds, cancellation, and output buffering. Add focused and two-server integration coverage for null semantics, global execution, legacy plans, numeric precision, and retained matcher state.
@xiangfu0
xiangfu0 force-pushed the claude/match-recognize-oss branch from ccb8dea to b612fab Compare August 29, 2026 10:05
@xiangfu0

Copy link
Copy Markdown
Contributor Author

Addressed the process follow-ups on head b612fab43e:

  • Expanded the PR description with the end-to-end design/atomicity rationale, rollback boundary, exact supported/unsupported modes, resource-limit precedence, and the required all-servers-first rolling-upgrade sequence. The design-review label remains so maintainer sign-off is still explicit before landing.
  • Linked the sender-side sort TODO to [MSE] Support sender-side sorting for sort exchanges #19395.
  • Rewrote the feature commits to remove both AI Co-Authored-By trailers.
  • Replied to and resolved all 13 inline threads with the pushed fix and regression evidence.

Local validation passed: the 342-test focused suite, a 32-test matcher/operator rerun after the final retained-state changes, 13/13 two-server integration cases, the full 63-module test-compile reactor, Spotless, checkstyle, license checks, and diff hygiene. Fresh CI for this rewritten head is running.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backward-incompat Introduces a backward-incompatible API or behavior change configuration Config changes (addition/deletion/change in behavior) design-review Requires design review before implementation feature New functionality multi-stage Related to the multi-stage query engine query Related to query processing release-notes Referenced by PRs that need attention when compiling the next release notes sql-compliance Related to SQL standard compliance upgrade-incompat PR may introduce incompatibility during upgrade of an installation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants