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
16 changes: 15 additions & 1 deletion src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub struct Query {
pub body: Box<SetExpr>,
/// ORDER BY
pub order_by: Option<OrderBy>,
/// `LIMIT ... OFFSET ... | LIMIT <offset>, <limit>`
/// `LIMIT ... OFFSET ... | LIMIT <offset>, <limit> | LIMIT <limit>% [OFFSET ...]`
pub limit_clause: Option<LimitClause>,
/// `FETCH { FIRST | NEXT } <N> [ PERCENT ] { ROW | ROWS } | { ONLY | WITH TIES }`
pub fetch: Option<Fetch>,
Expand Down Expand Up @@ -3124,6 +3124,13 @@ pub enum LimitClause {
/// The limit expression.
limit: Expr,
},
/// Percentage limit syntax: `LIMIT <limit>% [OFFSET <offset>]`.
Percent {
/// Percentage quantity.
limit: Expr,
/// Optional `OFFSET` expression.
offset: Option<Offset>,
},
}

impl fmt::Display for LimitClause {
Expand All @@ -3149,6 +3156,13 @@ impl fmt::Display for LimitClause {
LimitClause::OffsetCommaLimit { offset, limit } => {
write!(f, " LIMIT {offset}, {limit}")
}
LimitClause::Percent { limit, offset } => {
write!(f, " LIMIT {limit}%")?;
if let Some(offset) = offset {
write!(f, " {offset}")?;
}
Ok(())
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ impl Spanned for LimitClause {
.chain(limit_by.iter().map(|i| i.span())),
),
LimitClause::OffsetCommaLimit { offset, limit } => offset.span().union(&limit.span()),
LimitClause::Percent { limit, offset } => {
union_spans(core::iter::once(limit.span()).chain(offset.as_ref().map(|i| i.span())))
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/duckdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,8 @@ impl Dialect for DuckDbDialect {
fn supports_numeric_literal_underscores(&self) -> bool {
true
}

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

/// Supports `LIMIT <expression>%`.
fn supports_limit_percent(&self) -> bool {
false
}

/// Returns true if the dialect supports concatenating of string literal
/// Example: `SELECT 'Hello ' "world" => SELECT 'Hello world'`
fn supports_string_literal_concatenation(&self) -> bool {
Expand Down
85 changes: 83 additions & 2 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,11 @@ pub struct Parser<'a> {
failed_derived_table_factor_positions: BTreeSet<usize>,
}

enum ParsedLimit {
Rows(Option<Expr>),
Percent(Expr),
}

/// Copy marker for a [`ParserError`] cached by the `parse_prefix` failure
/// memoization, so the caches hold no strings.
#[derive(Debug, Clone, Copy)]
Expand Down Expand Up @@ -1390,6 +1395,10 @@ impl<'a> Parser<'a> {
self.parse_subexpr(self.dialect.prec_unknown())
}

fn parse_expr_until(&mut self, terminator: fn(&Self) -> bool) -> Result<Expr, ParserError> {
self.parse_subexpr_inner(self.dialect.prec_unknown(), terminator)
}

/// Parse expression with optional alias and order by.
pub fn parse_expr_with_alias_and_order_by(
&mut self,
Expand All @@ -1411,8 +1420,16 @@ impl<'a> Parser<'a> {
}

/// Parse tokens until the precedence changes.
#[cfg_attr(feature = "recursive-protection", recursive::recursive)]
pub fn parse_subexpr(&mut self, precedence: u8) -> Result<Expr, ParserError> {
self.parse_subexpr_inner(precedence, |_| false)
}

#[cfg_attr(feature = "recursive-protection", recursive::recursive)]
fn parse_subexpr_inner(
&mut self,
precedence: u8,
terminator: fn(&Self) -> bool,
) -> Result<Expr, ParserError> {
let _guard = self.recursion_counter.try_decrease()?;
debug!("parsing expr");
let mut expr = self.parse_prefix()?;
Expand All @@ -1431,6 +1448,11 @@ impl<'a> Parser<'a> {

debug!("prefix: {expr:?}");
loop {
// Recursive operands and nested expressions keep their own boundaries.
if terminator(self) {
break;
}

let next_precedence = self.get_next_precedence()?;
debug!("next precedence: {next_precedence:?}");

Expand Down Expand Up @@ -13573,7 +13595,15 @@ impl<'a> Parser<'a> {
};

let (limit, limit_by) = if self.parse_keyword(Keyword::LIMIT) {
let expr = self.parse_limit()?;
let expr = match self.parse_limit_quantity()? {
ParsedLimit::Rows(expr) => expr,
ParsedLimit::Percent(limit) => {
if offset.is_none() && self.parse_keyword(Keyword::OFFSET) {
offset = Some(self.parse_offset()?);
}
return Ok(Some(LimitClause::Percent { limit, offset }));
}
};

if self.dialect.supports_limit_comma()
&& offset.is_none()
Expand Down Expand Up @@ -13617,6 +13647,57 @@ impl<'a> Parser<'a> {
}
}

fn parse_limit_quantity(&mut self) -> Result<ParsedLimit, ParserError> {
if !self.dialect.supports_limit_percent() {
return self.parse_limit().map(ParsedLimit::Rows);
}

if self.parse_keyword(Keyword::ALL) {
return Ok(ParsedLimit::Rows(None));
}

let limit = self.parse_expr_until(Parser::at_limit_percent_suffix)?;
if !self.consume_token(&Token::Mod) {
return Ok(ParsedLimit::Rows(Some(limit)));
}

if matches!(
&limit,
Expr::Value(value) if matches!(&value.value, Value::Number(_, true))
) {
return self.expected_ref("an expression", self.peek_token_ref());
}

Ok(ParsedLimit::Percent(limit))
}

fn at_limit_percent_suffix(&self) -> bool {
if self.peek_token_ref().token != Token::Mod {
return false;
}

match &self.peek_nth_token_ref(1).token {
Token::EOF | Token::SemiColon | Token::RParen => true,
Token::Word(word) => match word.keyword {
Keyword::OFFSET | Keyword::RETURNING => true,
Keyword::ON => matches!(
&self.peek_nth_token_ref(2).token,
Token::Word(next) if next.keyword == Keyword::CONFLICT
),
Keyword::WITH => match &self.peek_nth_token_ref(2).token {
Token::Word(next) if next.keyword == Keyword::DATA => true,
Token::Word(next) if next.keyword == Keyword::NO => matches!(
&self.peek_nth_token_ref(3).token,
Token::Word(data) if data.keyword == Keyword::DATA
),
_ => false,
},
_ => false,
},
_ => false,
}
}

/// Parse a table object for insertion
/// e.g. `some_database.some_table` or `FUNCTION some_table_func(...)`
pub fn parse_table_object(&mut self) -> Result<TableObject, ParserError> {
Expand Down
Loading