use std::sync::Arc;
use crate::ast::{Extension, NoExt, SourceStore, Statement};
use crate::error::{ParseError, ParseResult};
use super::engine::Parser;
use super::{Dialect, ParseConfig, Parsed};
#[derive(Debug)]
pub struct Recovered<S: SourceStore = Arc<str>, X: Extension = NoExt> {
parsed: Parsed<S, X>,
errors: Vec<ParseError>,
}
impl<S: SourceStore, X: Extension> Recovered<S, X> {
pub fn parsed(&self) -> &Parsed<S, X> {
&self.parsed
}
pub fn statements(&self) -> &[Statement<X>] {
self.parsed.statements()
}
pub fn errors(&self) -> &[ParseError] {
&self.errors
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn into_parts(self) -> (Parsed<S, X>, Vec<ParseError>) {
(self.parsed, self.errors)
}
}
pub fn parse_recovering(src: &str) -> ParseResult<Recovered> {
parse_recovering_with(src, ParseConfig::default())
}
pub fn parse_recovering_with<D: Dialect>(
src: &str,
config: ParseConfig<D>,
) -> ParseResult<Recovered<Arc<str>, D::Ext>> {
collect_recovered::<D>(
src,
config.dialect,
config.recursion_limit,
config.capture_trivia,
config.parse_float_as_decimal,
)
}
fn collect_recovered<D>(
src: &str,
dialect: D,
recursion_limit: usize,
capture_trivia: bool,
parse_float_as_decimal: bool,
) -> ParseResult<Recovered<Arc<str>, D::Ext>>
where
D: Dialect,
{
let string_literals = dialect.features().string_literals;
let parser = if capture_trivia {
Parser::streaming_with_trivia(src, dialect)?
} else {
Parser::streaming(src, dialect)?
};
let mut parser = parser
.recursion_limit(recursion_limit)
.parse_float_as_decimal(parse_float_as_decimal);
let mut statements = Vec::new();
let mut errors = Vec::new();
loop {
match parser.parse_next_statement() {
Ok(Some(statement)) => statements.push(statement),
Ok(None) => break,
Err(error) => {
errors.push(error);
match parser.recover_to_statement_boundary() {
Ok(true) => {}
Ok(false) => break,
Err(resync_error) => {
if errors.last() != Some(&resync_error) {
errors.push(resync_error);
}
break;
}
}
}
}
}
let trivia = parser.take_trivia();
let resolver = parser.finish();
Ok(Recovered {
parsed: Parsed::new(Arc::from(src), resolver, statements, string_literals)
.with_trivia(trivia),
errors,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{Expr, ObjectName, Resolver as _, SelectItem, SetExpr, Symbol};
use crate::parser::{TestDialect, parse_with};
fn projection_column_symbol(statement: &Statement) -> Symbol {
let Statement::Query { query, .. } = statement else {
panic!("expected a query statement");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
match &select.projection[0] {
SelectItem::Expr {
expr:
Expr::Column {
name: ObjectName(parts),
..
},
..
} => parts[0].sym,
other => panic!("expected a single column projection, got {other:?}"),
}
}
#[test]
fn collects_every_error_and_keeps_the_well_formed_statements() {
let src = "SELECT alpha; FROM x; SELECT beta; ); SELECT gamma";
let recovered = parse_recovering_with(src, crate::ParseConfig::new(TestDialect))
.expect("streaming setup succeeds");
assert_eq!(recovered.statements().len(), 3);
let resolver = recovered.parsed().resolver();
let columns: Vec<&str> = recovered
.statements()
.iter()
.map(|stmt| resolver.resolve(projection_column_symbol(stmt)))
.collect();
assert_eq!(columns, ["alpha", "beta", "gamma"]);
assert!(recovered.has_errors());
assert_eq!(recovered.errors().len(), 2);
for error in recovered.errors() {
assert!(
!error.span.is_synthetic(),
"each error carries a source span: {error}",
);
}
assert!(recovered.errors()[0].span.start() < recovered.errors()[1].span.start());
assert_eq!(recovered.parsed().source(), src);
}
#[test]
fn fail_fast_default_reports_only_the_first_error() {
let src = "SELECT alpha; FROM x; SELECT beta; ); SELECT gamma";
let recovered =
parse_recovering_with(src, crate::ParseConfig::new(TestDialect)).expect("setup");
assert_eq!(recovered.errors().len(), 2, "recovery sees both");
let error = parse_with(src, crate::ParseConfig::new(TestDialect))
.expect_err("default path is fail-fast");
assert_eq!(error.span, recovered.errors()[0].span);
}
#[test]
fn well_formed_script_recovers_with_no_errors() {
let recovered = parse_recovering_with(
"SELECT 1; SELECT 2; SELECT 3",
crate::ParseConfig::new(TestDialect),
)
.expect("setup");
assert_eq!(recovered.statements().len(), 3);
assert!(recovered.errors().is_empty());
assert!(!recovered.has_errors());
}
#[test]
fn recovers_to_end_of_input_when_the_last_statement_is_broken() {
let recovered = parse_recovering_with(
"SELECT 1; SELECT FROM",
crate::ParseConfig::new(TestDialect),
)
.expect("setup");
assert_eq!(recovered.statements().len(), 1);
assert_eq!(recovered.errors().len(), 1);
assert!(!recovered.errors()[0].span.is_synthetic());
}
#[test]
fn a_new_lexical_fault_while_resyncing_is_reported_then_recovery_stops() {
let recovered = parse_recovering_with(
"SELECT alpha; ) 'unterminated",
crate::ParseConfig::new(TestDialect),
)
.expect("setup");
assert_eq!(recovered.statements().len(), 1);
assert_eq!(recovered.errors().len(), 2);
assert_ne!(recovered.errors()[0].span, recovered.errors()[1].span);
}
#[test]
fn a_repeated_lexical_fault_while_resyncing_is_not_double_reported() {
let recovered = parse_recovering_with(
"SELECT alpha; SELECT 'unterminated",
crate::ParseConfig::new(TestDialect),
)
.expect("setup");
assert_eq!(recovered.statements().len(), 1);
assert_eq!(recovered.errors().len(), 1);
}
#[test]
fn into_parts_yields_the_root_and_errors() {
let src = "SELECT kept; )";
let (parsed, errors) = parse_recovering_with(src, crate::ParseConfig::new(TestDialect))
.expect("setup")
.into_parts();
assert_eq!(parsed.statements().len(), 1);
assert_eq!(errors.len(), 1);
assert_eq!(parsed.to_string(), "SELECT kept");
}
#[test]
fn with_options_defaults_to_no_trivia() {
let src = "SELECT alpha; /* oops */ FROM x -- trailing";
assert!(
parse_recovering_with(src, crate::ParseConfig::new(TestDialect))
.expect("setup")
.parsed()
.trivia()
.is_empty()
);
let recovered =
parse_recovering_with(src, ParseConfig::default().dialect(TestDialect)).expect("setup");
assert!(recovered.parsed().trivia().is_empty());
}
#[test]
fn with_options_honours_trivia_capture_across_a_recovered_statement() {
use crate::tokenizer::TriviaKind::{BlockComment, LineComment};
let src = "SELECT alpha; /* oops */ FROM x; -- trailing\nSELECT beta";
let options = ParseConfig::default().capture_trivia(true);
let recovered = parse_recovering_with(src, options.dialect(TestDialect)).expect("setup");
assert_eq!(
recovered.statements().len(),
2,
"both well-formed statements survive"
);
assert_eq!(
recovered.errors().len(),
1,
"the FROM statement is recorded as broken"
);
let kinds: Vec<_> = recovered
.parsed()
.trivia()
.iter()
.map(|range| range.kind())
.collect();
assert!(
kinds.contains(&BlockComment),
"the comment leading the broken statement: {kinds:?}",
);
assert!(
kinds.contains(&LineComment),
"the comment past the resync, leading the next good statement: {kinds:?}",
);
}
}