From 8bef0fc58a0d75e058cd8e55476b0cb4521f9c78 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Tue, 15 Sep 2026 10:59:49 +0300 Subject: [PATCH] Snowflake: parse MATCH_RECOGNIZE window semantics --- src/ast/mod.rs | 10 +++++----- src/ast/query.rs | 28 ++++++++++++++++++++++++++++ src/ast/spans.rs | 6 +++++- src/keywords.rs | 1 + src/parser/mod.rs | 13 ++++++++++++- tests/sqlparser_common.rs | 7 +++++-- 6 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d7f83fbba..46af91962 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -97,11 +97,11 @@ pub use self::query::{ IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn, LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, - MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, - OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, - PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, - RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, - SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SelectModifiers, + MatchRecognizeSymbol, MatchRecognizeWindowSemantic, Measure, NamedWindowDefinition, + NamedWindowExpr, NonBlock, Offset, OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, + OrderByKind, OrderByOptions, PipeOperator, PivotValueSource, ProjectionSelect, Query, + RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, + Select, SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SelectModifiers, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, TableIndexHintForClause, TableIndexHintType, TableIndexHints, TableIndexType, TableSample, TableSampleBucket, diff --git a/src/ast/query.rs b/src/ast/query.rs index 75e91eb7b..9925d7743 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1987,6 +1987,9 @@ impl fmt::Display for PivotValueSource { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] /// An item in the `MEASURES` clause of `MATCH_RECOGNIZE`. pub struct Measure { + /// Explicit window-frame semantic for the measure. + #[cfg_attr(feature = "serde", serde(default))] + pub window_semantic: Option, /// Expression producing the measure value. pub expr: Expr, /// Alias for the measure column. @@ -1995,10 +1998,35 @@ pub struct Measure { impl fmt::Display for Measure { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(window_semantic) = self.window_semantic { + write!(f, "{window_semantic} ")?; + } write!(f, "{} AS {}", self.expr, self.alias) } } +/// Window-frame semantic for a `MATCH_RECOGNIZE` measure. +/// +/// See . +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MatchRecognizeWindowSemantic { + /// The frame ends at the current row. + Running, + /// The frame ends at the last row of the match. + Final, +} + +impl fmt::Display for MatchRecognizeWindowSemantic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Running => write!(f, "RUNNING"), + Self::Final => write!(f, "FINAL"), + } + } +} + /// The rows per match option in a `MATCH_RECOGNIZE` operation. /// /// See . diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 58ad2b41a..afc289c10 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2116,7 +2116,11 @@ impl Spanned for SymbolDefinition { impl Spanned for Measure { fn span(&self) -> Span { - let Measure { expr, alias } = self; + let Measure { + window_semantic: _, + expr, + alias, + } = self; expr.span().union(&alias.span) } diff --git a/src/keywords.rs b/src/keywords.rs index 5c141bf4b..e008fc035 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -911,6 +911,7 @@ define_keywords!( ROW_NUMBER, RULE, RUN, + RUNNING, SAFE, SAFE_CAST, SAMPLE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ddea2ee7c..f72a91e3f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -16709,10 +16709,21 @@ impl<'a> Parser<'a> { let measures = if self.parse_keyword(Keyword::MEASURES) { self.parse_comma_separated(|p| { + let window_semantic = if p.parse_keyword(Keyword::RUNNING) { + Some(MatchRecognizeWindowSemantic::Running) + } else if p.parse_keyword(Keyword::FINAL) { + Some(MatchRecognizeWindowSemantic::Final) + } else { + None + }; let expr = p.parse_expr()?; let _ = p.parse_keyword(Keyword::AS); let alias = p.parse_identifier()?; - Ok(Measure { expr, alias }) + Ok(Measure { + window_semantic, + expr, + alias, + }) })? } else { vec![] diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e28884bb2..b2632a5da 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -13246,8 +13246,8 @@ fn test_match_recognize() { "ORDER BY price_date ", "MEASURES ", "MATCH_NUMBER() AS match_number, ", - "FIRST(price_date) AS start_date, ", - "LAST(price_date) AS end_date ", + "FINAL FIRST(price_date) AS start_date, ", + "RUNNING LAST(price_date) AS end_date ", "ONE ROW PER MATCH ", "AFTER MATCH SKIP TO LAST row_with_price_increase ", "PATTERN (row_before_decrease row_with_price_decrease+ row_with_price_increase+) ", @@ -13268,14 +13268,17 @@ fn test_match_recognize() { }], measures: vec![ Measure { + window_semantic: None, expr: call("MATCH_NUMBER", []), alias: Ident::new("match_number"), }, Measure { + window_semantic: Some(MatchRecognizeWindowSemantic::Final), expr: call("FIRST", [Expr::Identifier(Ident::new("price_date"))]), alias: Ident::new("start_date"), }, Measure { + window_semantic: Some(MatchRecognizeWindowSemantic::Running), expr: call("LAST", [Expr::Identifier(Ident::new("price_date"))]), alias: Ident::new("end_date"), },