use crate::ast::{
CaseStatement, CloseCursorStatement, CompoundStatement, ConditionValue, ConditionalBranch,
Declaration, FetchCursorStatement, HandlerAction, HandlerCondition, Ident, IfStatement,
IterateStatement, LeaveStatement, LoopStatement, Meta, OpenCursorStatement, RepeatStatement,
ReturnStatement, Span, Statement, WhileStatement,
};
use crate::error::ParseResult;
use crate::tokenizer::{Punctuation, TokenKind};
use thin_vec::{ThinVec, thin_vec};
use super::Dialect;
use super::engine::Parser;
#[derive(Clone, Copy, PartialEq, Eq)]
enum LabelKind {
Loop,
Block,
}
#[derive(Clone, Copy)]
struct LabelFrame {
span: Span,
kind: LabelKind,
}
impl<'a, D: Dialect> Parser<'a, D> {
pub(super) fn parse_body_statement(&mut self) -> ParseResult<Statement<D::Ext>> {
let mut labels = ThinVec::new();
self.parse_body_statement_scoped(&mut labels)
}
fn parse_body_statement_scoped(
&mut self,
labels: &mut ThinVec<LabelFrame>,
) -> ParseResult<Statement<D::Ext>> {
if !self.features().statement_ddl_gates.compound_statements {
return Err(self.unexpected(
"a statement (compound-statement bodies are a stored-program feature)",
));
}
if self.peek_starts_body_construct()? {
let span = self.current_span()?;
let mut guard = self.enter_recursion(span)?;
guard.parser().parse_body_construct(labels)
} else {
self.parse_statement()
}
}
pub(super) fn peek_starts_body_construct(&mut self) -> ParseResult<bool> {
if self.peek_nth_is_punct(1, Punctuation::Colon)? {
return Ok(true);
}
Ok(self.peek_is_contextual_keyword("BEGIN")?
|| self.peek_is_contextual_keyword("IF")?
|| self.peek_is_contextual_keyword("CASE")?
|| self.peek_is_contextual_keyword("LOOP")?
|| self.peek_is_contextual_keyword("WHILE")?
|| self.peek_is_contextual_keyword("REPEAT")?
|| self.peek_is_contextual_keyword("LEAVE")?
|| self.peek_is_contextual_keyword("ITERATE")?
|| self.peek_is_contextual_keyword("RETURN")?
|| self.peek_is_contextual_keyword("OPEN")?
|| self.peek_is_contextual_keyword("FETCH")?
|| self.peek_is_contextual_keyword("CLOSE")?)
}
#[inline(never)]
fn parse_body_construct(
&mut self,
labels: &mut ThinVec<LabelFrame>,
) -> ParseResult<Statement<D::Ext>> {
let start = self.current_span()?;
let open_label = if self.peek_nth_is_punct(1, Punctuation::Colon)? {
let label = self.parse_ident()?;
self.expect_punct(Punctuation::Colon, "`:` after a block or loop label")?;
Some(label)
} else {
None
};
if let Some(label) = &open_label {
if self.label_in_scope(labels, label.meta.span, false) {
let found = self.span_text(label.meta.span).to_owned();
return Err(self.error_at(
label.meta.span,
"a block or loop label not already active in an enclosing scope",
found,
));
}
let kind = if self.peek_is_contextual_keyword("BEGIN")? {
LabelKind::Block
} else {
LabelKind::Loop
};
labels.push(LabelFrame {
span: label.meta.span,
kind,
});
}
let result = self.dispatch_body_construct(labels, start, open_label.clone());
if open_label.is_some() {
labels.pop();
}
result
}
fn dispatch_body_construct(
&mut self,
labels: &mut ThinVec<LabelFrame>,
start: Span,
label: Option<Ident>,
) -> ParseResult<Statement<D::Ext>> {
if self.peek_is_contextual_keyword("BEGIN")? {
self.parse_compound_statement(labels, start, label)
} else if self.peek_is_contextual_keyword("LOOP")? {
self.parse_loop_statement(labels, start, label)
} else if self.peek_is_contextual_keyword("WHILE")? {
self.parse_while_statement(labels, start, label)
} else if self.peek_is_contextual_keyword("REPEAT")? {
self.parse_repeat_statement(labels, start, label)
} else if label.is_some() {
Err(self.unexpected("`BEGIN`, `LOOP`, `WHILE`, or `REPEAT` after a label"))
} else if self.peek_is_contextual_keyword("IF")? {
self.parse_if_statement(labels, start)
} else if self.peek_is_contextual_keyword("CASE")? {
self.parse_case_statement(labels, start)
} else if self.peek_is_contextual_keyword("LEAVE")? {
self.parse_leave_statement(labels, start)
} else if self.peek_is_contextual_keyword("ITERATE")? {
self.parse_iterate_statement(labels, start)
} else if self.peek_is_contextual_keyword("RETURN")? {
self.parse_return_statement(start)
} else if self.peek_is_contextual_keyword("OPEN")? {
self.parse_open_cursor_statement(start)
} else if self.peek_is_contextual_keyword("FETCH")? {
self.parse_fetch_cursor_statement(start)
} else if self.peek_is_contextual_keyword("CLOSE")? {
self.parse_close_cursor_statement(start)
} else {
Err(self.unexpected("a compound-body statement"))
}
}
fn parse_compound_statement(
&mut self,
labels: &mut ThinVec<LabelFrame>,
start: Span,
label: Option<Ident>,
) -> ParseResult<Statement<D::Ext>> {
self.expect_contextual_keyword("BEGIN")?;
let declarations = self.parse_declaration_prefix(labels)?;
let body = self.parse_block_body(labels, &["END"])?;
self.expect_contextual_keyword("END")?;
let end_label = self.parse_optional_end_label()?;
self.check_label_match(&label, &end_label)?;
let (inner_meta, meta) = self.body_meta_pair(start);
Ok(Statement::Compound {
compound: Box::new(CompoundStatement {
label,
declarations,
body,
end_label,
meta: inner_meta,
}),
meta,
})
}
fn parse_declaration_prefix(
&mut self,
labels: &mut ThinVec<LabelFrame>,
) -> ParseResult<ThinVec<Declaration<D::Ext>>> {
let mut declarations = ThinVec::new();
let mut max_phase: u8 = 0;
while self.peek_is_contextual_keyword("DECLARE")? {
let span = self.current_span()?;
let declaration = self.parse_declaration(labels)?;
let phase = match &declaration {
Declaration::Variable { .. } | Declaration::Condition { .. } => 0,
Declaration::Cursor { .. } => 1,
Declaration::Handler { .. } => 2,
};
if phase < max_phase {
let message = if phase == 0 {
"a variable or condition declaration to precede every cursor and handler \
declaration"
} else {
"a cursor declaration to precede every handler declaration"
};
let found = self.span_text(span).to_owned();
return Err(self.error_at(span, message, found));
}
max_phase = max_phase.max(phase);
declarations.push(declaration);
self.expect_punct(Punctuation::Semicolon, "`;` after a declaration")?;
}
Ok(declarations)
}
fn parse_declaration(
&mut self,
labels: &mut ThinVec<LabelFrame>,
) -> ParseResult<Declaration<D::Ext>> {
let start = self.current_span()?;
self.expect_contextual_keyword("DECLARE")?;
if self.peek_is_contextual_keyword("CONTINUE")?
|| self.peek_is_contextual_keyword("EXIT")?
|| self.peek_is_contextual_keyword("UNDO")?
{
return self.parse_handler_declaration(labels, start);
}
let first = self.parse_ident()?;
if self.eat_contextual_keyword("CONDITION")? {
self.expect_contextual_keyword("FOR")?;
let value = self.parse_condition_value()?;
let meta = self.make_meta(start.union(self.preceding_span()));
return Ok(Declaration::Condition {
name: first,
value,
meta,
});
}
if self.eat_contextual_keyword("CURSOR")? {
self.expect_contextual_keyword("FOR")?;
let query = self.parse_query()?;
let meta = self.make_meta(start.union(self.preceding_span()));
return Ok(Declaration::Cursor {
name: first,
query: Box::new(query),
meta,
});
}
let mut names = thin_vec![first];
while self.eat_punct(Punctuation::Comma)? {
names.push(self.parse_ident()?);
}
let data_type = self.parse_data_type()?;
let default = if self.eat_contextual_keyword("DEFAULT")? {
Some(self.parse_expr()?)
} else {
None
};
let meta = self.make_meta(start.union(self.preceding_span()));
Ok(Declaration::Variable {
names,
data_type,
default,
meta,
})
}
fn parse_handler_declaration(
&mut self,
labels: &mut ThinVec<LabelFrame>,
start: Span,
) -> ParseResult<Declaration<D::Ext>> {
let action = if self.eat_contextual_keyword("CONTINUE")? {
HandlerAction::Continue
} else if self.eat_contextual_keyword("EXIT")? {
HandlerAction::Exit
} else {
self.expect_contextual_keyword("UNDO")?;
HandlerAction::Undo
};
self.expect_contextual_keyword("HANDLER")?;
self.expect_contextual_keyword("FOR")?;
let conditions = self.parse_comma_separated(Self::parse_handler_condition)?;
let body = self.parse_body_statement_scoped(labels)?;
let meta = self.make_meta(start.union(self.preceding_span()));
Ok(Declaration::Handler {
action,
conditions,
body: Box::new(body),
meta,
})
}
fn parse_condition_value(&mut self) -> ParseResult<ConditionValue> {
let start = self.current_span()?;
if self.eat_contextual_keyword("SQLSTATE")? {
let value_keyword = self.eat_contextual_keyword("VALUE")?;
let sqlstate = self.expect_string_literal("a SQLSTATE string literal")?;
let meta = self.make_meta(start.union(self.preceding_span()));
Ok(ConditionValue::SqlState {
value_keyword,
sqlstate,
meta,
})
} else {
let code = self.expect_unsigned_integer_literal("a MySQL error code")?;
let meta = self.make_meta(start.union(self.preceding_span()));
Ok(ConditionValue::ErrorCode { code, meta })
}
}
fn parse_handler_condition(&mut self) -> ParseResult<HandlerCondition> {
let start = self.current_span()?;
if self.eat_contextual_keyword("SQLSTATE")? {
let value_keyword = self.eat_contextual_keyword("VALUE")?;
let sqlstate = self.expect_string_literal("a SQLSTATE string literal")?;
let meta = self.make_meta(start.union(self.preceding_span()));
Ok(HandlerCondition::SqlState {
value_keyword,
sqlstate,
meta,
})
} else if self.eat_contextual_keyword("SQLWARNING")? {
Ok(HandlerCondition::SqlWarning {
meta: self.make_meta(start.union(self.preceding_span())),
})
} else if self.eat_contextual_keyword("SQLEXCEPTION")? {
Ok(HandlerCondition::SqlException {
meta: self.make_meta(start.union(self.preceding_span())),
})
} else if self.eat_contextual_keyword("NOT")? {
self.expect_contextual_keyword("FOUND")?;
Ok(HandlerCondition::NotFound {
meta: self.make_meta(start.union(self.preceding_span())),
})
} else if matches!(
self.peek()?.map(|token| token.kind),
Some(TokenKind::Number)
) {
let code = self.expect_unsigned_integer_literal("a MySQL error code")?;
Ok(HandlerCondition::ErrorCode {
code,
meta: self.make_meta(start.union(self.preceding_span())),
})
} else {
let name = self.parse_ident()?;
Ok(HandlerCondition::ConditionName {
name,
meta: self.make_meta(start.union(self.preceding_span())),
})
}
}
fn parse_if_statement(
&mut self,
labels: &mut ThinVec<LabelFrame>,
start: Span,
) -> ParseResult<Statement<D::Ext>> {
self.expect_contextual_keyword("IF")?;
let mut branches =
thin_vec![self.parse_conditional_branch(labels, &["ELSEIF", "ELSE", "END"])?];
while self.eat_contextual_keyword("ELSEIF")? {
branches.push(self.parse_conditional_branch(labels, &["ELSEIF", "ELSE", "END"])?);
}
let else_body = if self.eat_contextual_keyword("ELSE")? {
Some(self.parse_block_body(labels, &["END"])?)
} else {
None
};
self.expect_contextual_keyword("END")?;
self.expect_contextual_keyword("IF")?;
let (inner_meta, meta) = self.body_meta_pair(start);
Ok(Statement::If {
if_statement: Box::new(IfStatement {
branches,
else_body,
meta: inner_meta,
}),
meta,
})
}
fn parse_case_statement(
&mut self,
labels: &mut ThinVec<LabelFrame>,
start: Span,
) -> ParseResult<Statement<D::Ext>> {
self.expect_contextual_keyword("CASE")?;
let operand = if self.peek_is_contextual_keyword("WHEN")? {
None
} else {
Some(Box::new(self.parse_expr()?))
};
self.expect_contextual_keyword("WHEN")?;
let mut when_branches =
thin_vec![self.parse_conditional_branch(labels, &["WHEN", "ELSE", "END"])?];
while self.eat_contextual_keyword("WHEN")? {
when_branches.push(self.parse_conditional_branch(labels, &["WHEN", "ELSE", "END"])?);
}
let else_body = if self.eat_contextual_keyword("ELSE")? {
Some(self.parse_block_body(labels, &["END"])?)
} else {
None
};
self.expect_contextual_keyword("END")?;
self.expect_contextual_keyword("CASE")?;
let (inner_meta, meta) = self.body_meta_pair(start);
Ok(Statement::Case {
case_statement: Box::new(CaseStatement {
operand,
when_branches,
else_body,
meta: inner_meta,
}),
meta,
})
}
fn parse_conditional_branch(
&mut self,
labels: &mut ThinVec<LabelFrame>,
terminators: &[&'static str],
) -> ParseResult<ConditionalBranch<D::Ext>> {
let start = self.current_span()?;
let guard = self.parse_expr()?;
self.expect_contextual_keyword("THEN")?;
let body = self.parse_block_body(labels, terminators)?;
let meta = self.make_meta(start.union(self.preceding_span()));
Ok(ConditionalBranch { guard, body, meta })
}
fn parse_loop_statement(
&mut self,
labels: &mut ThinVec<LabelFrame>,
start: Span,
label: Option<Ident>,
) -> ParseResult<Statement<D::Ext>> {
self.expect_contextual_keyword("LOOP")?;
let body = self.parse_block_body(labels, &["END"])?;
self.expect_contextual_keyword("END")?;
self.expect_contextual_keyword("LOOP")?;
let end_label = self.parse_optional_end_label()?;
self.check_label_match(&label, &end_label)?;
let (inner_meta, meta) = self.body_meta_pair(start);
Ok(Statement::Loop {
loop_statement: Box::new(LoopStatement {
label,
body,
end_label,
meta: inner_meta,
}),
meta,
})
}
fn parse_while_statement(
&mut self,
labels: &mut ThinVec<LabelFrame>,
start: Span,
label: Option<Ident>,
) -> ParseResult<Statement<D::Ext>> {
self.expect_contextual_keyword("WHILE")?;
let condition = self.parse_expr()?;
self.expect_contextual_keyword("DO")?;
let body = self.parse_block_body(labels, &["END"])?;
self.expect_contextual_keyword("END")?;
self.expect_contextual_keyword("WHILE")?;
let end_label = self.parse_optional_end_label()?;
self.check_label_match(&label, &end_label)?;
let (inner_meta, meta) = self.body_meta_pair(start);
Ok(Statement::While {
while_statement: Box::new(WhileStatement {
label,
condition: Box::new(condition),
body,
end_label,
meta: inner_meta,
}),
meta,
})
}
fn parse_repeat_statement(
&mut self,
labels: &mut ThinVec<LabelFrame>,
start: Span,
label: Option<Ident>,
) -> ParseResult<Statement<D::Ext>> {
self.expect_contextual_keyword("REPEAT")?;
let body = self.parse_block_body(labels, &["UNTIL"])?;
self.expect_contextual_keyword("UNTIL")?;
let condition = self.parse_expr()?;
self.expect_contextual_keyword("END")?;
self.expect_contextual_keyword("REPEAT")?;
let end_label = self.parse_optional_end_label()?;
self.check_label_match(&label, &end_label)?;
let (inner_meta, meta) = self.body_meta_pair(start);
Ok(Statement::Repeat {
repeat: Box::new(RepeatStatement {
label,
body,
condition: Box::new(condition),
end_label,
meta: inner_meta,
}),
meta,
})
}
fn parse_leave_statement(
&mut self,
labels: &ThinVec<LabelFrame>,
start: Span,
) -> ParseResult<Statement<D::Ext>> {
self.expect_contextual_keyword("LEAVE")?;
let label = self.parse_ident()?;
if !self.label_in_scope(labels, label.meta.span, false) {
let found = self.span_text(label.meta.span).to_owned();
return Err(self.error_at(
label.meta.span,
"a LEAVE target naming a block or loop label active in an enclosing scope",
found,
));
}
let (inner_meta, meta) = self.body_meta_pair(start);
Ok(Statement::Leave {
leave: Box::new(LeaveStatement {
label,
meta: inner_meta,
}),
meta,
})
}
fn parse_iterate_statement(
&mut self,
labels: &ThinVec<LabelFrame>,
start: Span,
) -> ParseResult<Statement<D::Ext>> {
self.expect_contextual_keyword("ITERATE")?;
let label = self.parse_ident()?;
if !self.label_in_scope(labels, label.meta.span, true) {
let found = self.span_text(label.meta.span).to_owned();
return Err(self.error_at(
label.meta.span,
"an ITERATE target naming a loop label active in an enclosing scope",
found,
));
}
let (inner_meta, meta) = self.body_meta_pair(start);
Ok(Statement::Iterate {
iterate: Box::new(IterateStatement {
label,
meta: inner_meta,
}),
meta,
})
}
fn label_in_scope(&self, labels: &ThinVec<LabelFrame>, name: Span, require_loop: bool) -> bool {
let target = self.span_text(name);
labels.iter().any(|frame| {
(!require_loop || frame.kind == LabelKind::Loop)
&& self.span_text(frame.span).eq_ignore_ascii_case(target)
})
}
fn parse_return_statement(&mut self, start: Span) -> ParseResult<Statement<D::Ext>> {
let return_span = self.current_span()?;
self.expect_contextual_keyword("RETURN")?;
if !self.body_return_allowed {
let found = self.span_text(return_span).to_owned();
return Err(self.error_at(
return_span,
"a stored-function body (RETURN is not allowed outside a function)",
found,
));
}
let value = self.parse_expr()?;
let (inner_meta, meta) = self.body_meta_pair(start);
Ok(Statement::Return {
return_statement: Box::new(ReturnStatement {
value,
meta: inner_meta,
}),
meta,
})
}
fn parse_open_cursor_statement(&mut self, start: Span) -> ParseResult<Statement<D::Ext>> {
self.expect_contextual_keyword("OPEN")?;
let cursor = self.parse_ident()?;
let (inner_meta, meta) = self.body_meta_pair(start);
Ok(Statement::OpenCursor {
open: Box::new(OpenCursorStatement {
cursor,
meta: inner_meta,
}),
meta,
})
}
fn parse_close_cursor_statement(&mut self, start: Span) -> ParseResult<Statement<D::Ext>> {
self.expect_contextual_keyword("CLOSE")?;
let cursor = self.parse_ident()?;
let (inner_meta, meta) = self.body_meta_pair(start);
Ok(Statement::CloseCursor {
close: Box::new(CloseCursorStatement {
cursor,
meta: inner_meta,
}),
meta,
})
}
fn parse_fetch_cursor_statement(&mut self, start: Span) -> ParseResult<Statement<D::Ext>> {
self.expect_contextual_keyword("FETCH")?;
let next_keyword = self.eat_contextual_keyword("NEXT")?;
let from_keyword = self.eat_contextual_keyword("FROM")?;
if next_keyword && !from_keyword {
return Err(self.unexpected("`FROM` after `NEXT`"));
}
let cursor = self.parse_ident()?;
self.expect_contextual_keyword("INTO")?;
let targets = self.parse_comma_separated(Self::parse_ident)?;
let (inner_meta, meta) = self.body_meta_pair(start);
Ok(Statement::FetchCursor {
fetch: Box::new(FetchCursorStatement {
next_keyword,
from_keyword,
cursor,
targets,
meta: inner_meta,
}),
meta,
})
}
fn parse_block_body(
&mut self,
labels: &mut ThinVec<LabelFrame>,
terminators: &[&'static str],
) -> ParseResult<ThinVec<Statement<D::Ext>>> {
let mut body = ThinVec::new();
while self.peek()?.is_some() && !self.peek_is_any_contextual_keyword(terminators)? {
let statement = self.parse_body_statement_scoped(labels)?;
self.expect_punct(
Punctuation::Semicolon,
"`;` to terminate a compound-body statement",
)?;
body.push(statement);
}
Ok(body)
}
fn parse_optional_end_label(&mut self) -> ParseResult<Option<Ident>> {
let is_label = matches!(
self.peek()?.map(|token| token.kind),
Some(TokenKind::Word | TokenKind::QuotedIdent | TokenKind::Keyword(_))
);
if is_label {
Ok(Some(self.parse_ident()?))
} else {
Ok(None)
}
}
fn check_label_match(
&mut self,
open: &Option<Ident>,
close: &Option<Ident>,
) -> ParseResult<()> {
if let (Some(open), Some(close)) = (open, close) {
let matches = self
.span_text(open.meta.span)
.eq_ignore_ascii_case(self.span_text(close.meta.span));
if !matches {
let found = self.span_text(close.meta.span).to_owned();
return Err(self.error_at(
close.meta.span,
"an end label matching the opening block label",
found,
));
}
}
Ok(())
}
fn peek_is_any_contextual_keyword(&mut self, words: &[&'static str]) -> ParseResult<bool> {
for &word in words {
if self.peek_is_contextual_keyword(word)? {
return Ok(true);
}
}
Ok(false)
}
fn body_meta_pair(&mut self, start: Span) -> (Meta, Meta) {
let span = start.union(self.preceding_span());
(self.make_meta(span), self.make_meta(span))
}
}
#[cfg(test)]
mod tests {
use crate::ast::dialect::FeatureSet;
use crate::ast::{
CaseStatement, CompoundStatement, ConditionValue, Declaration, HandlerAction,
HandlerCondition, IfStatement, Statement,
};
use crate::error::ParseErrorKind;
use crate::parser::{FeatureDialect, Parser};
use crate::render::Renderer;
use crate::tokenizer::tokenize;
const MYSQL: FeatureDialect = FeatureDialect {
features: &FeatureSet::MYSQL,
};
const ANSI: FeatureDialect = FeatureDialect {
features: &FeatureSet::ANSI,
};
fn parse_body(src: &str) -> Statement {
let tokens = tokenize(src).expect("the fragment lexes");
let mut parser = Parser::new(src, &tokens, MYSQL);
parser
.parse_body_statement()
.unwrap_or_else(|err| panic!("{src}: {err:?}"))
}
fn reject_body(src: &str, dialect: FeatureDialect) -> ParseErrorKind {
let tokens = tokenize(src).expect("the fragment lexes");
let mut parser = Parser::new(src, &tokens, dialect);
parser
.parse_body_statement()
.expect_err(&format!("{src} must reject"))
.kind
}
fn assert_round_trips(src: &str) {
let tokens = tokenize(src).expect("the fragment lexes");
let mut parser = Parser::new(src, &tokens, MYSQL);
let first = parser
.parse_body_statement()
.unwrap_or_else(|err| panic!("{src}: {err:?}"));
let resolver = parser.finish();
let rendered = Renderer::new(MYSQL)
.render_statement(&first, &resolver, src)
.unwrap_or_else(|err| panic!("{src}: render {err}"));
let second = parse_body(&rendered);
assert_eq!(
first, second,
"round-trip changed the AST\n in: {src}\n out: {rendered}"
);
}
#[test]
fn empty_compound_block_parses() {
let Statement::Compound { compound, .. } = parse_body("BEGIN END") else {
panic!("expected a compound block");
};
assert!(compound.declarations.is_empty());
assert!(compound.body.is_empty());
assert!(compound.label.is_none() && compound.end_label.is_none());
}
#[test]
fn compound_block_with_declarations_and_body_parses() {
let src = "BEGIN DECLARE x INT DEFAULT 0; DECLARE y, z INT; SELECT x; SELECT y; END";
let Statement::Compound { compound, .. } = parse_body(src) else {
panic!("expected a compound block");
};
let CompoundStatement {
declarations, body, ..
} = *compound;
assert_eq!(declarations.len(), 2, "two DECLARE items");
assert_eq!(body.len(), 2, "two body statements");
let Declaration::Variable { names, default, .. } = &declarations[0] else {
panic!("first declaration is a variable");
};
assert_eq!(names.len(), 1);
assert!(default.is_some(), "DEFAULT parsed");
let Declaration::Variable { names, default, .. } = &declarations[1] else {
panic!("second declaration is a variable");
};
assert_eq!(names.len(), 2, "two names share a type");
assert!(default.is_none());
}
#[test]
fn labelled_compound_block_parses() {
let Statement::Compound { compound, .. } = parse_body("blk: BEGIN SELECT 1; END blk")
else {
panic!("expected a compound block");
};
assert!(compound.label.is_some());
assert!(compound.end_label.is_some());
}
#[test]
fn all_declaration_forms_parse_in_order() {
let src = "BEGIN \
DECLARE v INT; \
DECLARE c CONDITION FOR SQLSTATE '42S02'; \
DECLARE cur CURSOR FOR SELECT 1; \
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; \
SELECT 1; END";
let Statement::Compound { compound, .. } = parse_body(src) else {
panic!("expected a compound block");
};
assert_eq!(compound.declarations.len(), 4);
assert!(matches!(
compound.declarations[0],
Declaration::Variable { .. }
));
assert!(matches!(
compound.declarations[1],
Declaration::Condition { .. }
));
assert!(matches!(
compound.declarations[2],
Declaration::Cursor { .. }
));
let Declaration::Handler {
action, conditions, ..
} = &compound.declarations[3]
else {
panic!("fourth declaration is a handler");
};
assert_eq!(*action, HandlerAction::Continue);
assert!(matches!(conditions[0], HandlerCondition::NotFound { .. }));
}
#[test]
fn condition_declaration_accepts_error_code_and_sqlstate() {
let Statement::Compound { compound, .. } = parse_body(
"BEGIN DECLARE a CONDITION FOR 1051; DECLARE b CONDITION FOR SQLSTATE VALUE '42S02'; SELECT 1; END",
) else {
panic!("expected a compound block");
};
assert!(matches!(
compound.declarations[0],
Declaration::Condition {
value: ConditionValue::ErrorCode { .. },
..
}
));
assert!(matches!(
compound.declarations[1],
Declaration::Condition {
value: ConditionValue::SqlState {
value_keyword: true,
..
},
..
}
));
}
#[test]
fn cursor_ops_parse() {
let src = "BEGIN OPEN cur; FETCH cur INTO x; FETCH NEXT FROM cur INTO x, y; CLOSE cur; END";
let Statement::Compound { compound, .. } = parse_body(src) else {
panic!("expected a compound block");
};
assert!(matches!(compound.body[0], Statement::OpenCursor { .. }));
assert!(matches!(compound.body[1], Statement::FetchCursor { .. }));
assert!(matches!(compound.body[3], Statement::CloseCursor { .. }));
let Statement::FetchCursor { fetch, .. } = &compound.body[2] else {
panic!("third body statement is a FETCH");
};
assert!(fetch.next_keyword && fetch.from_keyword);
assert_eq!(fetch.targets.len(), 2);
}
#[test]
fn if_elseif_else_parses() {
let Statement::If { if_statement, .. } =
parse_body("IF x > 0 THEN SELECT 1; ELSEIF x < 0 THEN SELECT 2; ELSE SELECT 3; END IF")
else {
panic!("expected an IF statement");
};
let IfStatement {
branches,
else_body,
..
} = *if_statement;
assert_eq!(branches.len(), 2, "IF + one ELSEIF");
assert!(else_body.is_some());
}
#[test]
fn simple_and_searched_case_parse() {
let Statement::Case { case_statement, .. } = parse_body(
"CASE x WHEN 1 THEN SELECT 1; WHEN 2 THEN SELECT 2; ELSE SELECT 3; END CASE",
) else {
panic!("expected a CASE statement");
};
let CaseStatement {
operand,
when_branches,
else_body,
..
} = *case_statement;
assert!(operand.is_some(), "simple CASE carries an operand");
assert_eq!(when_branches.len(), 2);
assert!(else_body.is_some());
let Statement::Case { case_statement, .. } =
parse_body("CASE WHEN x > 0 THEN SELECT 1; END CASE")
else {
panic!("expected a CASE statement");
};
assert!(
case_statement.operand.is_none(),
"searched CASE has no operand"
);
}
#[test]
fn loops_and_labels_parse() {
assert!(matches!(
parse_body("lp: LOOP LEAVE lp; END LOOP lp"),
Statement::Loop { .. }
));
assert!(matches!(
parse_body("WHILE x > 0 DO SELECT 1; END WHILE"),
Statement::While { .. }
));
assert!(matches!(
parse_body("REPEAT SELECT 1; UNTIL x > 0 END REPEAT"),
Statement::Repeat { .. }
));
assert!(matches!(
parse_body("wl: WHILE x > 0 DO ITERATE wl; END WHILE wl"),
Statement::While { .. }
));
}
#[test]
fn nested_compound_blocks_parse() {
let Statement::Compound { compound, .. } =
parse_body("BEGIN BEGIN SELECT 1; END; SELECT 2; END")
else {
panic!("expected a compound block");
};
assert_eq!(compound.body.len(), 2);
assert!(matches!(compound.body[0], Statement::Compound { .. }));
}
#[test]
fn top_level_compound_is_not_a_statement() {
use crate::parser::parse_with;
let err = parse_with("BEGIN SELECT 1; END", MYSQL)
.expect_err("a top-level compound block must reject");
assert_eq!(err.kind, ParseErrorKind::Syntax);
}
#[test]
fn compound_body_is_gated_off_under_ansi() {
assert_eq!(
reject_body("BEGIN SELECT 1; END", ANSI),
ParseErrorKind::Syntax
);
}
#[test]
fn declare_ordering_is_enforced() {
assert_eq!(
reject_body(
"BEGIN DECLARE cur CURSOR FOR SELECT 1; DECLARE x INT; SELECT 1; END",
MYSQL
),
ParseErrorKind::Syntax,
"a variable after a cursor rejects (1337 class)"
);
assert_eq!(
reject_body(
"BEGIN DECLARE EXIT HANDLER FOR SQLWARNING SET x = 1; DECLARE cur CURSOR FOR SELECT 1; SELECT 1; END",
MYSQL
),
ParseErrorKind::Syntax,
"a cursor after a handler rejects (1338 class)"
);
assert!(matches!(
parse_body("BEGIN DECLARE c CONDITION FOR 1051; DECLARE v INT; SELECT 1; END"),
Statement::Compound { .. }
));
}
#[test]
fn duplicate_label_in_scope_rejects() {
assert_eq!(
reject_body(
"lbl: LOOP lbl: LOOP LEAVE lbl; END LOOP lbl; END LOOP lbl",
MYSQL
),
ParseErrorKind::Syntax,
);
}
#[test]
fn leave_and_iterate_resolve_labels_across_nesting() {
assert!(matches!(
parse_body("lp: LOOP LEAVE lp; END LOOP lp"),
Statement::Loop { .. }
));
assert!(matches!(
parse_body("lp: LOOP ITERATE lp; END LOOP lp"),
Statement::Loop { .. }
));
assert!(matches!(
parse_body("blk: BEGIN LEAVE blk; END blk"),
Statement::Compound { .. }
));
assert!(matches!(
parse_body("blk: BEGIN lp: LOOP LEAVE blk; END LOOP lp; END blk"),
Statement::Compound { .. }
));
assert!(matches!(
parse_body("wl: WHILE x > 0 DO ITERATE wl; END WHILE wl"),
Statement::While { .. }
));
assert!(matches!(
parse_body("rp: REPEAT ITERATE rp; UNTIL x > 0 END REPEAT rp"),
Statement::Repeat { .. }
));
assert!(matches!(
parse_body("lp: LOOP LEAVE LP; END LOOP lp"),
Statement::Loop { .. }
));
assert!(matches!(
parse_body("BEGIN l: LOOP LEAVE l; END LOOP l; l: LOOP LEAVE l; END LOOP l; END"),
Statement::Compound { .. }
));
}
#[test]
fn undeclared_leave_iterate_label_rejects() {
assert_eq!(
reject_body("BEGIN LEAVE nope; END", MYSQL),
ParseErrorKind::Syntax,
"LEAVE of an unknown label rejects",
);
assert_eq!(
reject_body("BEGIN ITERATE nope; END", MYSQL),
ParseErrorKind::Syntax,
"ITERATE of an unknown label rejects",
);
assert_eq!(
reject_body("BEGIN LEAVE lp; lp: LOOP SELECT 1; END LOOP lp; END", MYSQL),
ParseErrorKind::Syntax,
);
assert_eq!(
reject_body("BEGIN lp: LOOP SELECT 1; END LOOP lp; LEAVE lp; END", MYSQL),
ParseErrorKind::Syntax,
);
}
#[test]
fn iterate_rejects_a_block_label() {
assert_eq!(
reject_body("blk: BEGIN ITERATE blk; END blk", MYSQL),
ParseErrorKind::Syntax,
);
assert_eq!(
reject_body(
"blk: BEGIN lp: LOOP ITERATE blk; END LOOP lp; END blk",
MYSQL
),
ParseErrorKind::Syntax,
);
}
#[test]
fn duplicate_label_check_is_case_insensitive() {
assert_eq!(
reject_body(
"lbl: LOOP LBL: LOOP LEAVE LBL; END LOOP LBL; END LOOP lbl",
MYSQL
),
ParseErrorKind::Syntax,
);
}
#[test]
fn mismatched_end_label_rejects() {
assert_eq!(
reject_body("blk: BEGIN SELECT 1; END other", MYSQL),
ParseErrorKind::Syntax,
);
}
#[test]
fn body_statements_round_trip() {
for src in [
"BEGIN END",
"BEGIN SELECT 1; SELECT 2; END",
"BEGIN DECLARE x INTEGER DEFAULT 0; SELECT x; END",
"BEGIN DECLARE a, b INTEGER; SELECT 1; END",
"BEGIN DECLARE c CONDITION FOR SQLSTATE '42S02'; SELECT 1; END",
"BEGIN DECLARE cur CURSOR FOR SELECT 1; OPEN cur; CLOSE cur; END",
"BEGIN DECLARE CONTINUE HANDLER FOR NOT FOUND SELECT 1; SELECT 1; END",
"IF x > 0 THEN SELECT 1; ELSEIF x < 0 THEN SELECT 2; ELSE SELECT 3; END IF",
"CASE x WHEN 1 THEN SELECT 1; ELSE SELECT 2; END CASE",
"CASE WHEN x > 0 THEN SELECT 1; END CASE",
"lp: LOOP LEAVE lp; END LOOP lp",
"wl: WHILE x > 0 DO ITERATE wl; END WHILE wl",
"REPEAT SELECT 1; UNTIL x > 0 END REPEAT",
"BEGIN FETCH NEXT FROM cur INTO x, y; END",
"RETURN x + 1",
] {
assert_round_trips(src);
}
}
}