feat(stream): add SQL MATCH_RECOGNIZE as a pure matcher over WatermarkSort - #26584
Open
dahankzter wants to merge 10 commits into
Open
feat(stream): add SQL MATCH_RECOGNIZE as a pure matcher over WatermarkSort#26584dahankzter wants to merge 10 commits into
dahankzter wants to merge 10 commits into
Conversation
The operator is not EOWC-specific: it buffers out-of-order rows, emits the rows before each watermark in order, and forwards the watermark -- a general watermark-driven sort that EOWC conversion, gap fill and EOWC over-window happen to use. Naming it for what it does prepares it for its next consumer: MATCH_RECOGNIZE as an ordered-input matcher, where the sort satisfies the ordered-input requirement in front of the matcher rather than being an emit-mode detail. Mechanical: type, module file, macro entry and plan-display name; planner test outputs regenerated. No behavior change. Signed-off-by: Henrik Ma Johansson <dahankzter@gmail.com>
The SQL:2016 row-pattern-recognition table factor: PARTITION BY /
ORDER BY, MEASURES, ONE ROW PER MATCH, AFTER MATCH SKIP {PAST LAST ROW
| TO NEXT ROW | TO FIRST/LAST var}, PATTERN with concatenation,
alternation, grouping, quantifiers (greedy and reluctant) and PERMUTE,
SUBSET, DEFINE, and WITHIN. AST mirrors upstream sqlparser-rs shapes.
The binder rejects the clause explicitly for now; binding lands next in
the series. Downstream exhaustive matches (catalog rename recursion,
sqlsmith reducer) are extended alongside.
Carried verbatim from the review branch of risingwavelabs#25899.
Signed-off-by: Henrik Ma Johansson <dahankzter@gmail.com>
The binder lowers MEASURES and DEFINE to expressions over synthetic navigation-slot rows, registers pattern variables and SUBSET unions as alias blocks, and enforces the validation battery: plain-column PARTITION BY / ORDER BY, ascending-only ordering, quantifier bounds and the whole-pattern NFA state budget, PREV restricted to reads inside the match span (an exact minimum-start-distance walk over the pattern), physical NEXT rejected in DEFINE, WITHIN bounds must be positive constants, aggregate/navigation modifier rejection in both MEASURES and DEFINE, and skip-target validation against pattern-only variables. LogicalMatchRecognize carries the standard trait set (predicate-pushdown barrier, column pruning into the clause's expressions, rewrite-for- stream with an identity output mapping). to_stream performs the append-only and watermark checks and then defers: the stream plan -- an ordered-input matcher over a WatermarkSort -- lands next in the series, together with the planner test data. Carried from the review branch of risingwavelabs#25899 (rounds 2-4 of review hardening included). Signed-off-by: Henrik Ma Johansson <dahankzter@gmail.com>
The Thompson-construction NFA and its walkers, carried with all review hardening: the preference-order finder (first accepting path in transition order gives greedy/reluctant quantifiers and ordered alternation their semantics), the pull-based match scan, terminal- finality probing that follows the same preference order (may_extend), per-start (state, position) failure memoization sound for path- independent DEFINE predicates and recorded only at consumption boundaries, the per-visit evaluation budget that stops walks without fabricating verdicts, and the static accept-reachability precompute. Unit-tested standalone (33 tests, including the catastrophic- backtracking collapse and the budget's conservative outcomes); the executor that drives it over ordered input lands next in the series. Carried from the review branch of risingwavelabs#25899. Signed-off-by: Henrik Ma Johansson <dahankzter@gmail.com>
The sort executor emits rows in (sort column, buffer-table key) order, so the emission order is shaped entirely by the inferred table key. with_secondary_order appends caller-specified columns to that key right after the sort column -- before the distribution and stream keys -- making the emission order the caller's full ORDER BY with a deterministic tiebreak. No executor or proto change; existing callers keep plain watermark sorting. First consumer is MATCH_RECOGNIZE as an ordered-input matcher, whose multi-column ORDER BY must survive the sort. Also audited the sort buffer's watermark comparison while here: it already emits strictly below the watermark (a row AT the watermark is retained), matching the tree-wide convention. Signed-off-by: Henrik Ma Johansson <dahankzter@gmail.com>
… WatermarkSort The matcher no longer owns ordering. The stream plan hash-shards the append-only input by the PARTITION BY key, inserts a WatermarkSort over the full ORDER BY (leading watermark column plus secondary order columns), and places the matcher directly above it in the same fragment -- an exchange between them would destroy the ordering the sort just established. Rows therefore reach the matcher already in ORDER BY order, strictly below each forwarded watermark, and the matcher owns only NFA state and match finalization. The plain query form is the supported form: with ordering hoisted out, emission is on match completion rather than an emit-mode property, so the previous EMIT ON WINDOW CLOSE requirement is gone. The proto node carries an explicit input mode (EVENT_TIME now; PROCESSING_TIME reserved for arrival-order matching) and a single state table -- the retained rows referenced by live partial matches -- in place of the previous buffer plus two wakeup-frontier tables: the sort owns out-of-order buffering and time-triggering, so the frontier machinery has no reason to exist. The executor builder is a loud stub; the ordered-input executor lands next in the series. Signed-off-by: Henrik Ma Johansson <dahankzter@gmail.com>
The matcher core the ordered-input executor drives: feed rows in ORDER BY order and re-derive the provisional match set over the unfrozen suffix (consumed history is never rescanned), freeze matches whose scan region is dead at the boundary, and finalize consumed prefixes with position rebasing (finalize_before_seq's no-straddle contract, with the straddling-finalize fast path proven by a 200-seed randomized operation-sequence oracle against the batch scan). The catastrophic-backtracking defenses meter every walk INCLUDING the finder: advance/rescan drive the budgeted, memoized pull scan (Nfa::next_match) and thread the per-visit budget and memoizability flag through the freeze-gate liveness checks. A spent budget is never converted into a verdict -- the scan stops with an incomplete tail and the freeze loop holds (exhaustion treated as "alive"), so the degradation is latency, never a wrong or lost match. The out-of-order invalidation machinery (truncate_from_seq) and the provisional-changelog helpers are compiled test-only: unreachable under a WatermarkSort, they stay proven by the differential oracle for the input modes that would need them. Signed-off-by: Henrik Ma Johansson <dahankzter@gmail.com>
Consumes rows already in full ORDER BY order from the upstream WatermarkSort (same fragment), so the executor owns only NFA state and match finalization. Each partition keeps an incremental matcher fed on arrival plus the retained rows its live matches still reference; there is no out-of-order buffer, and consumed history is never rescanned. Emission is on decidability, gated by match_is_final: the first provisional match is emitted once every gap position before it is provably dead at the boundary (an alive gap could still yield an earlier, leftmost-preferred match) and no more-preferred path from its own start can be completed by future rows (may_extend probed at the buffer boundary -- "a later row exists" is NOT finality: a preferred branch can be blocked past the follower), or once its WITHIN deadline has strictly passed the watermark, which decides both questions at once. Emitting consumes everything up to the match's skip-resume position, including earlier still-live partials -- the same abandonment the batch scan performs, and what keeps the hidden _match_id (the start row's seq) unique forever. The matcher rebases in place where its finalize contract allows and is rebuilt from the survivors otherwise. The watermark drives only WITHIN finality and dead-prefix pruning; the output carries no watermark columns, so none is forwarded. Every NFA walk shares a per-visit scan budget (one budget per row on the data path, per partition visit on the watermark path, so one pathological partition cannot starve the others) with DEFINE-slot memoization where sound; exhaustion degrades conservatively -- nothing further emitted or evicted, everything undecided retained, reported once per pass. State is one table of retained rows keyed (partition..., order..., seq), where seq is a plain per-actor counter seeded above every retained row on rebuild: it is the PK tiebreaker for equal ORDER BY keys, so it must be monotonic in arrival order for recovery to re-feed ties exactly as the live matcher saw them. Recovery and rescale rebuild each partition's matcher by re-feeding its rows in key order, one batched advance per partition -- no emission during rebuild, since an emittable match is always consumed in the epoch it emits. The executor builder replaces the loud stub and rejects input modes other than EVENT_TIME; the pattern decoder re-validates the PERMUTE and quantifier size caps so a corrupt or skewed plan fails with an error instead of allocating factorially. Slot resolution and DefineMatcher navigation are carried verbatim from the review branch with their unit tests. Signed-off-by: Henrik Ma Johansson <dahankzter@gmail.com>
…plan The full family carried from the review branch, adapted to the split architecture: the EMIT ON WINDOW CLOSE clause is gone everywhere (plain form is the form), and the one test that asserted on the operator's internals -- idle-partition WITHIN eviction -- now distinguishes the two state tables the split produces: the WatermarkSort's buffer holds rows not yet released by the watermark, the matcher's table holds only rows a live partial still references. The timed-out partial must vanish from the matcher's table with no further input in its partition; the unreleased sentinel sits in the sort's buffer where it now belongs. The EOWC-requirement test is deleted with the requirement. Three additions pin the emission gate and the rebuild order end to end: preference supersession (a provisional match held while a preferred branch is blocked past its follower, and while a gap position is still alive -- each with both the superseding and the killing ending, plus an idle-partition starvation control), PERMUTE preference (CLASSIFIER distinguishes the listing-order branch from an arbitrary ordering choice, which a count-only assertion cannot), and a recovery scenario with a multi-row partial over TIED order keys (re-feed order must equal arrival order, pinning the seq monotonicity the rebuild relies on). 29 files plus the recovery serial (exercising the rebuild-by-refeed recovery path end to end) and the rescale test, all green. Signed-off-by: Henrik Ma Johansson <dahankzter@gmail.com>
dahankzter
force-pushed
the
feat/match-recognize-v2
branch
from
August 4, 2026 20:26
5e4ef85 to
1524fb0
Compare
Between the state-table storage metrics and the expression-error surface, the operator itself was a black box: nothing said how many matches a view produced, how fast its buffer was draining, or whether the scan budget was degrading it. Three counters, registered and labelled exactly like the neighbouring over-window set (table_id/actor_id/fragment_id): - stream_match_recognize_matches_emitted_count - stream_match_recognize_evicted_rows_count (rows leaving the buffer, whether consumed by an emitted match or pruned as a dead prefix) - stream_match_recognize_scan_budget_exhausted_count The last one is the alerting hook for catastrophic-backtracking degradation: the log line is deduplicated to once per message pass, while the counter counts every affected visit. docs/metrics/inventory.tsv is regenerated (the extractor is a CI gate); the regeneration also picks up rows for metrics that arrived on main without one. Signed-off-by: Henrik Ma Johansson <dahankzter@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I hereby agree to the terms of the RisingWave Labs, Inc. Contributor License Agreement.
What's changed and what's your intention?
Supersedes #25899, implementing the architecture @chenzl25 proposed in review: split ordering from matching.
MATCH_RECOGNIZEbecomes a pure ordered-input matcher, and all ordering concerns (buffering, out-of-order arrival, watermark release) live in the sort operator that already owns them.Architecture
StreamEowcSort→StreamWatermarkSort: renamed (it is watermark-driven, not EOWC-specific) and extended with secondary order columns — a state-table PK extension after the sort column — so it realizes the fullORDER BY, not just the leading time column.WITHINdeadline has strictly passed the watermark, which decides both at once. Results appear when they are decidable, not at window close; the EOWC-only restriction from feat(stream): add SQL MATCH_RECOGNIZE (row pattern recognition) #25899 is gone.seq, the PK tiebreaker for equal ORDER BY keys, is a per-actor monotonic counter seeded above every retained row on rebuild, so ties are re-fed in arrival order and replay is deterministic.WITHINfinality and dead-prefix pruning; it does not forward watermarks.MatchRecognizeInputModeproto enum:EVENT_TIMEimplemented here;PROCESSING_TIMEreserved for a follow-up, and the executor rejects any other mode rather than running against input whose ordering guarantee does not hold.SQL scope (unchanged from #25899 v1)
Streaming, append-only input,
ONE ROW PER MATCH;PARTITION BY+ watermarkedORDER BY;PATTERNwith concat, alternation, grouping, quantifiers* + ? {n,m}(greedy + reluctant),PERMUTE;DEFINEwith running navigation (PREV/NEXT/FIRST/LAST, cross-variable refs);MEASURESincl.CLASSIFIER(),SUBSET, aggregates;AFTER MATCH SKIPvariants;WITHIN.Known costs and planned follow-ups
WITHINdeadline precompute, label interning.WITHIN; a partial withoutWITHINis retained until its partition decides it — noted atCREATE); wiring this into memory accounting is a follow-up.PROCESSING_TIMEinput mode: enum reserved, planned follow-up.PATTERN (a*)yields no summary row on non-matching input (SQL:2016 would emit one; Flink rejects such patterns).ALL ROWS PER MATCH,MATCH_NUMBER(), pattern anchors^/$, and batch execution are not implemented (same as feat(stream): add SQL MATCH_RECOGNIZE (row pattern recognition) #25899).docs/dev/src/design/match-recognize.md) rewrite for this architecture and operator metrics land as follow-ups on this PR.Tests
PARTITION BYdistribution pinning), parser tests, 86 stream unit tests including a 200-seed randomized operation-sequence oracle checking the incremental matcher against the batch scan and unit tests for the emission-finality gate.e2e_test/streaming/match_recognize*.slt) covering skip modes, navigation,WITHINboundaries, alternation preference (held vs superseded vs killed),PERMUTElisting-order preference viaCLASSIFIER(), and state-table introspection; plus a recovery serial (kill/recover, including a multi-row partial over tied order keys) and rescale (ALTER ... SET PARALLELISM) — all green on a local cluster.fmt/clippyclean.Relevant CI coverage a maintainer may want to enable (I can't add labels as an external contributor): e2e + recovery deterministic simulation, SQLSmith, and
user-facing-changes(new SQL syntax).Checklist
Documentation
Release note
RisingWave now supports the SQL
MATCH_RECOGNIZEclause (row pattern recognition) on streaming, append-only inputs with a watermarkedORDER BYcolumn. Patterns are matched per partition in event-time order, and each match is emitted as soon as it is decidable. Supported:ONE ROW PER MATCH, quantified patterns withPERMUTE,DEFINEwith navigation functions,MEASURESwithCLASSIFIER()and aggregates,AFTER MATCH SKIPvariants, andWITHINtime bounds.