Skip to main content

squonk_ast/ast/
match_recognize.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! Row pattern recognition — the `<source> MATCH_RECOGNIZE (…)` table factor
5//! (SQL:2016 `<row pattern recognition clause>`; Snowflake / Oracle).
6//!
7//! `MATCH_RECOGNIZE` runs a regular expression over the *rows* of a partition, in a
8//! defined order, and projects a row (or one row per match) from the matched runs.
9//! It is modelled as a table-factor suffix ([`TableFactor::MatchRecognize`]) — the
10//! `PIVOT`/`UNPIVOT` precedent — carrying one [`MatchRecognize`] operator core:
11//!
12//! ```text
13//! <source> MATCH_RECOGNIZE (
14//!     [ PARTITION BY <expr>, … ]
15//!     [ ORDER BY <sort>, … ]
16//!     [ MEASURES <expr> AS <name>, … ]
17//!     [ ONE ROW PER MATCH | ALL ROWS PER MATCH [ SHOW EMPTY MATCHES
18//!                                              | OMIT EMPTY MATCHES
19//!                                              | WITH UNMATCHED ROWS ] ]
20//!     [ AFTER MATCH SKIP { PAST LAST ROW | TO NEXT ROW
21//!                        | TO FIRST <sym> | TO LAST <sym> } ]
22//!     PATTERN ( <row pattern> )
23//!     [ SUBSET <name> = ( <sym>, … ), … ]
24//!     [ DEFINE <sym> AS <condition>, … ]
25//! )
26//! ```
27//!
28//! # Parity with, and deviations from, sqlparser-rs
29//!
30//! The shape follows sqlparser-rs's `MatchRecognize` family (`Measure`,
31//! `RowsPerMatch`, `EmptyMatchesMode`, `AfterMatchSkip`, `SymbolDefinition`,
32//! `MatchRecognizePattern`, `RepetitionQuantifier`), reshaped per ADR-0011 where
33//! their model is lossy:
34//!
35//! - **Anchors are inlined into [`MatchRecognizePattern`].** sqlparser-rs nests them
36//!   in a separate `MatchRecognizeSymbol { Named, Start, End }` reached through
37//!   `Pattern::Symbol`/`Exclude`/`Permute`; we hoist [`Start`](MatchRecognizePattern::Start)
38//!   and [`End`](MatchRecognizePattern::End) to sibling pattern variants so the tree
39//!   is one recursive enum, not two.
40//! - **[`Exclude`](MatchRecognizePattern::Exclude) and
41//!   [`Permute`](MatchRecognizePattern::Permute) carry full sub-patterns**, not the
42//!   bare symbols sqlparser-rs restricts them to — the SQL:2016 grammar allows a
43//!   pattern inside `{- … -}` and each `PERMUTE(…)` argument, so the symbol-only
44//!   shape is lossy.
45//! - **[`Concat`](MatchRecognizePattern::Concat) is added** as an explicit
46//!   sequence node (sqlparser-rs has it too), and a single-element sequence/branch is
47//!   flattened to its inner pattern so `PATTERN (A)` is a bare
48//!   [`Symbol`](MatchRecognizePattern::Symbol), never a one-element `Concat`.
49//! - **`SUBSET` is modelled** ([`SubsetDefinition`]) — the Oracle union-variable
50//!   clause sqlparser-rs omits entirely.
51//! - We keep sqlparser-rs's [`RepetitionQuantifier`] arms verbatim (no *reluctant*
52//!   `?` suffix): the reluctant marker needs a `?` token that the eager
53//!   context-free tokenizer (ADR-0005) cannot produce in pattern position (it is the
54//!   anonymous placeholder / a stray byte), so there is nothing to round-trip.
55//!
56//! Both operators are gated on
57//! [`TableFactorSyntax::match_recognize`](crate::dialect::TableFactorSyntax) (Snowflake
58//! and Lenient). See the parser's `match_recognize` module for the lexer-reachability
59//! notes on the `$` anchor and `?` quantifier.
60
61use super::{Expr, Extension, Ident, NoExt, OrderByExpr, TableFactor};
62use crate::vocab::Meta;
63use thin_vec::ThinVec;
64
65/// The `MATCH_RECOGNIZE (…)` operator core — the row-pattern-recognition clause
66/// applied to a [`source`](Self::source) relation, hosted by
67/// [`TableFactor::MatchRecognize`](crate::ast::TableFactor) (which owns the trailing
68/// `AS <alias>`).
69///
70/// `source` is boxed to break the `TableFactor` → `MatchRecognize` → `TableFactor`
71/// type cycle (the [`Pivot`](crate::ast::Pivot) precedent), and the whole core is
72/// boxed again inside the table-factor variant to keep that hot enum lean (ADR-0007).
73/// Every clause but [`pattern`](Self::pattern) is optional; the field order matches
74/// the fixed clause order the grammar requires.
75#[derive(Clone, Debug, PartialEq, Eq, Hash)]
76#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
77#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
78pub struct MatchRecognize<X: Extension = NoExt> {
79    /// Input source for this syntax.
80    pub source: Box<TableFactor<X>>,
81    /// `PARTITION BY <expr>, …` — the partitions matched independently; empty when
82    /// absent (the whole input is one partition).
83    pub partition_by: ThinVec<Expr<X>>,
84    /// `ORDER BY <sort>, …` — the row order the pattern matches against; empty when
85    /// absent.
86    pub order_by: ThinVec<OrderByExpr<X>>,
87    /// `MEASURES <expr> AS <name>, …` — the columns computed from a match; empty when
88    /// absent.
89    pub measures: ThinVec<Measure<X>>,
90    /// `ONE ROW PER MATCH` / `ALL ROWS PER MATCH [ … ]`; `None` leaves the engine
91    /// default (`ONE ROW PER MATCH`) unwritten.
92    pub rows_per_match: Option<RowsPerMatch>,
93    /// `AFTER MATCH SKIP …` — where matching resumes after a match; `None` leaves the
94    /// default (`AFTER MATCH SKIP PAST LAST ROW`) unwritten.
95    pub after_match_skip: Option<AfterMatchSkip>,
96    /// `PATTERN ( <row pattern> )` — the row-pattern regular expression. Mandatory.
97    pub pattern: MatchRecognizePattern,
98    /// `SUBSET <name> = ( <sym>, … ), …` — union pattern variables (Oracle); empty
99    /// when absent.
100    pub subsets: ThinVec<SubsetDefinition>,
101    /// `DEFINE <sym> AS <condition>, …` — the boolean conditions defining each
102    /// pattern variable; empty when absent (an undefined variable matches every row).
103    pub define: ThinVec<SymbolDefinition<X>>,
104    /// Source location and node identity.
105    pub meta: Meta,
106}
107
108/// One `MEASURES` item: `<expr> AS <name>` — an expression computed over a match,
109/// projected under an output name.
110#[derive(Clone, Debug, PartialEq, Eq, Hash)]
111#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
112#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
113pub struct Measure<X: Extension = NoExt> {
114    /// Expression evaluated by this syntax.
115    pub expr: Expr<X>,
116    /// Alias assigned by this syntax.
117    pub alias: Ident,
118    /// Source location and node identity.
119    pub meta: Meta,
120}
121
122/// The `ONE ROW PER MATCH` / `ALL ROWS PER MATCH` output cardinality.
123///
124/// A tag enum (no spanned children) — [`AllRows`](Self::AllRows) carries the optional
125/// empty-match mode that only the `ALL ROWS` form admits.
126#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
127#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
128#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
129pub enum RowsPerMatch {
130    /// `ONE ROW PER MATCH` — one summary row per match.
131    OneRow,
132    /// `ALL ROWS PER MATCH [ SHOW EMPTY MATCHES | OMIT EMPTY MATCHES
133    /// | WITH UNMATCHED ROWS ]` — one row per matched (and optionally unmatched) input
134    /// row.
135    AllRows(Option<EmptyMatchesMode>),
136}
137
138/// The `ALL ROWS PER MATCH` empty/unmatched-row treatment.
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
140#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
141#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
142pub enum EmptyMatchesMode {
143    /// `SHOW EMPTY MATCHES`.
144    Show,
145    /// `OMIT EMPTY MATCHES`.
146    Omit,
147    /// `WITH UNMATCHED ROWS`.
148    WithUnmatched,
149}
150
151/// The `AFTER MATCH SKIP …` clause — where the matcher resumes after a match.
152///
153/// A spanned node (every variant carries `meta`) because the
154/// [`ToFirst`](Self::ToFirst)/[`ToLast`](Self::ToLast) forms name a pattern variable.
155#[derive(Clone, Debug, PartialEq, Eq, Hash)]
156#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
157#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
158pub enum AfterMatchSkip {
159    /// `AFTER MATCH SKIP PAST LAST ROW` (the engine default).
160    PastLastRow {
161        /// Source location and node identity.
162        meta: Meta,
163    },
164    /// `AFTER MATCH SKIP TO NEXT ROW`.
165    ToNextRow {
166        /// Source location and node identity.
167        meta: Meta,
168    },
169    /// `AFTER MATCH SKIP TO FIRST <sym>`.
170    ToFirst {
171        /// Pattern symbol referenced by this syntax.
172        symbol: Ident,
173        /// Source location and node identity.
174        meta: Meta,
175    },
176    /// `AFTER MATCH SKIP TO LAST <sym>`.
177    ToLast {
178        /// Pattern symbol referenced by this syntax.
179        symbol: Ident,
180        /// Source location and node identity.
181        meta: Meta,
182    },
183}
184
185/// One `SUBSET` union-variable definition: `<name> = ( <member>, … )` — a pattern
186/// variable standing for the union of the listed variables' rows (Oracle).
187#[derive(Clone, Debug, PartialEq, Eq, Hash)]
188#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
189#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
190pub struct SubsetDefinition {
191    /// Name referenced by this syntax.
192    pub name: Ident,
193    /// members in source order.
194    pub members: ThinVec<Ident>,
195    /// Source location and node identity.
196    pub meta: Meta,
197}
198
199/// One `DEFINE` clause: `<sym> AS <condition>` — the boolean condition a row must
200/// satisfy to be classified as the pattern variable `symbol`.
201#[derive(Clone, Debug, PartialEq, Eq, Hash)]
202#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
203#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
204pub struct SymbolDefinition<X: Extension = NoExt> {
205    /// Interned source spelling.
206    pub symbol: Ident,
207    /// The boolean condition a row must satisfy to be classified as this variable.
208    pub definition: Expr<X>,
209    /// Source location and node identity.
210    pub meta: Meta,
211}
212
213/// The row-pattern regular expression inside `PATTERN ( … )` — a recursive grammar of
214/// pattern variables, anchors, grouping, alternation, concatenation, exclusion,
215/// permutation, and quantifiers.
216///
217/// Non-generic in the extension parameter: a pattern references only variable names
218/// ([`Ident`]) and structural operators, never an [`Expr`]. Every variant carries
219/// `meta`, so the whole tree is addressable (ADR-0002); the recursion is bounded by
220/// the parser's shared depth guard.
221#[derive(Clone, Debug, PartialEq, Eq, Hash)]
222#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
223#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
224pub enum MatchRecognizePattern {
225    /// A pattern variable reference (`A`, `STRT`).
226    Symbol {
227        /// Pattern symbol referenced by this syntax.
228        symbol: Ident,
229        /// Source location and node identity.
230        meta: Meta,
231    },
232    /// The partition-start anchor `^`.
233    Start {
234        /// Source location and node identity.
235        meta: Meta,
236    },
237    /// The partition-end anchor `$`.
238    End {
239        /// Source location and node identity.
240        meta: Meta,
241    },
242    /// A concatenation (sequence) `a b c` — matched in order.
243    Concat {
244        /// patterns in source order.
245        patterns: ThinVec<MatchRecognizePattern>,
246        /// Source location and node identity.
247        meta: Meta,
248    },
249    /// An alternation `a | b | c` — matched as the first branch that succeeds.
250    Alternation {
251        /// patterns in source order.
252        patterns: ThinVec<MatchRecognizePattern>,
253        /// Source location and node identity.
254        meta: Meta,
255    },
256    /// A parenthesized group `( … )`.
257    Group {
258        /// The grouped sub-pattern.
259        pattern: Box<MatchRecognizePattern>,
260        /// Source location and node identity.
261        meta: Meta,
262    },
263    /// An exclusion `{- … -}` — matched but omitted from `ALL ROWS PER MATCH` output.
264    Exclude {
265        /// The excluded sub-pattern.
266        pattern: Box<MatchRecognizePattern>,
267        /// Source location and node identity.
268        meta: Meta,
269    },
270    /// `PERMUTE ( p1, p2, … )` — matches the arguments in any order.
271    Permute {
272        /// patterns in source order.
273        patterns: ThinVec<MatchRecognizePattern>,
274        /// Source location and node identity.
275        meta: Meta,
276    },
277    /// A quantified pattern `p*`, `p+`, `p{2,3}` — repeat `pattern` per `quantifier`.
278    Repetition {
279        /// The repeated sub-pattern.
280        pattern: Box<MatchRecognizePattern>,
281        /// The repetition count or range (`*`/`+`/`?`/`{m,n}`); see [`RepetitionQuantifier`].
282        quantifier: RepetitionQuantifier,
283        /// Source location and node identity.
284        meta: Meta,
285    },
286}
287
288/// A row-pattern quantifier — the repetition count applied to a pattern factor.
289///
290/// A tag enum (its bounds are plain `u32`s, no spanned children). Mirrors
291/// sqlparser-rs verbatim. The `AtMostOne` (`?`) form has no lexer-reachable parser
292/// path — see the module docs — but is retained for parity and lossless rendering.
293#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
294#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
295#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
296pub enum RepetitionQuantifier {
297    /// `*` — zero or more.
298    ZeroOrMore,
299    /// `+` — one or more.
300    OneOrMore,
301    /// `?` — zero or one.
302    AtMostOne,
303    /// `{n}` — exactly `n`.
304    Exactly(u32),
305    /// `{n,}` — at least `n`.
306    AtLeast(u32),
307    /// `{,m}` — at most `m`.
308    AtMost(u32),
309    /// `{n,m}` — between `n` and `m` (inclusive).
310    Range(u32, u32),
311}