Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 48 additions & 10 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2347,15 +2347,7 @@ impl fmt::Display for WindowSpec {
if !is_first {
SpaceOrNewline.fmt(f)?;
}
if let Some(end_bound) = &window_frame.end_bound {
write!(
f,
"{} BETWEEN {} AND {}",
window_frame.units, window_frame.start_bound, end_bound
)?;
} else {
write!(f, "{} {}", window_frame.units, window_frame.start_bound)?;
}
window_frame.fmt(f)?;
}
Ok(())
}
Expand All @@ -2378,7 +2370,26 @@ pub struct WindowFrame {
/// indicates the shorthand form (e.g. `ROWS 1 PRECEDING`), which must
/// behave the same as `end_bound = WindowFrameBound::CurrentRow`.
pub end_bound: Option<WindowFrameBound>,
// TBD: EXCLUDE
/// Rows excluded from the window frame.
pub exclusion: Option<WindowFrameExclusion>,
}

impl fmt::Display for WindowFrame {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(end_bound) = &self.end_bound {
write!(
f,
"{} BETWEEN {} AND {}",
self.units, self.start_bound, end_bound
)?;
} else {
write!(f, "{} {}", self.units, self.start_bound)?;
}
if let Some(exclusion) = &self.exclusion {
write!(f, " EXCLUDE {exclusion}")?;
}
Ok(())
}
}

impl Default for WindowFrame {
Expand All @@ -2390,6 +2401,7 @@ impl Default for WindowFrame {
units: WindowFrameUnits::Range,
start_bound: WindowFrameBound::Preceding(None),
end_bound: None,
exclusion: None,
}
}
}
Expand Down Expand Up @@ -2417,6 +2429,32 @@ impl fmt::Display for WindowFrameUnits {
}
}

/// Rows excluded from a window frame.
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum WindowFrameExclusion {
/// `CURRENT ROW`.
CurrentRow,
/// `GROUP`.
Group,
/// `TIES`.
Ties,
/// `NO OTHERS`.
NoOthers,
}

impl fmt::Display for WindowFrameExclusion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match self {
WindowFrameExclusion::CurrentRow => "CURRENT ROW",
WindowFrameExclusion::Group => "GROUP",
WindowFrameExclusion::Ties => "TIES",
WindowFrameExclusion::NoOthers => "NO OTHERS",
})
}
}

/// Specifies Ignore / Respect NULL within window functions.
/// For example
/// `FIRST_VALUE(column2) IGNORE NULLS OVER (PARTITION BY column1)`
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/duckdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ impl Dialect for DuckDbDialect {
true
}

fn supports_window_frame_exclusion(&self) -> bool {
true
}

fn supports_group_by_expr(&self) -> bool {
true
}
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ impl Dialect for GenericDialect {
true
}

fn supports_window_frame_exclusion(&self) -> bool {
true
}

fn supports_partition_by_after_order_by(&self) -> bool {
true
}
Expand Down
5 changes: 5 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ pub trait Dialect: Debug + Any {
false
}

/// Returns true if the dialect supports `EXCLUDE` in window frames.
fn supports_window_frame_exclusion(&self) -> bool {
false
}

/// Returns true if the dialect supports `ARRAY_AGG() [WITHIN GROUP (ORDER BY)]` expressions.
/// Otherwise, the dialect should expect an `ORDER BY` without the `WITHIN GROUP` clause, e.g. [`ANSI`]
///
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/postgresql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ impl Dialect for PostgreSqlDialect {
true
}

fn supports_window_frame_exclusion(&self) -> bool {
true
}

fn is_reserved_for_identifier(&self, kw: Keyword) -> bool {
if matches!(kw, Keyword::INTERVAL) {
false
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ impl Dialect for SQLiteDialect {
true
}

fn supports_window_frame_exclusion(&self) -> bool {
true
}

fn supports_start_transaction_modifier(&self) -> bool {
true
}
Expand Down
43 changes: 43 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2693,13 +2693,56 @@ impl<'a> Parser<'a> {
} else {
(self.parse_window_frame_bound()?, None)
};
let exclusion = if self.dialect.supports_window_frame_exclusion()
&& self.parse_keyword(Keyword::EXCLUDE)
{
Some(self.parse_window_frame_exclusion()?)
} else {
None
};
Ok(WindowFrame {
units,
start_bound,
end_bound,
exclusion,
})
}

/// Parse the exclusion that follows `EXCLUDE` in a window frame.
pub fn parse_window_frame_exclusion(&mut self) -> Result<WindowFrameExclusion, ParserError> {
match self.parse_one_of_keywords(&[
Keyword::CURRENT,
Keyword::GROUP,
Keyword::TIES,
Keyword::NO,
]) {
Some(Keyword::CURRENT) => {
self.expect_keyword_is(Keyword::ROW)?;
Ok(WindowFrameExclusion::CurrentRow)
}
Some(Keyword::GROUP) => Ok(WindowFrameExclusion::Group),
Some(Keyword::TIES) => Ok(WindowFrameExclusion::Ties),
Some(Keyword::NO) => {
let is_others = matches!(
&self.peek_token_ref().token,
Token::Word(word)
if word.quote_style.is_none()
&& word.value.eq_ignore_ascii_case("OTHERS")
);
if is_others {
self.advance_token();
Ok(WindowFrameExclusion::NoOthers)
} else {
self.expected_ref("OTHERS", self.peek_token_ref())
}
}
_ => self.expected_ref(
"CURRENT ROW, GROUP, TIES, or NO OTHERS",
self.peek_token_ref(),
),
}
}

/// Parse a window frame bound: `CURRENT ROW` or `<n> PRECEDING|FOLLOWING`.
pub fn parse_window_frame_bound(&mut self) -> Result<WindowFrameBound, ParserError> {
if self.parse_keywords(&[Keyword::CURRENT, Keyword::ROW]) {
Expand Down
Loading