squonk 2.0.0

Extensible, fast, multi-dialect SQL tokenizer and parser for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Moderately AI Inc.

//! Resilient, multi-error parsing over the error-sink seam.
//!
//! The default parse path is strictly fail-fast: the first error short-circuits and
//! no partial tree is returned. Tooling that wants *every* diagnostic in a
//! script — a compiler-style "all errors in the file" — calls [`parse_recovering`]
//! instead. On a statement's parse error it records the diagnostic and resynchronizes
//! at the next `;` (panic-mode recovery, the statement separator as the single sync
//! point), then resumes, so one run yields BOTH the well-formed statements (as
//! ordinary AST) AND the collected errors for the broken ones. It is an additional
//! entry point, not a mode of the default one: fail-fast [`parse_with`](super::parse_with)
//! stays the untouched lean path, since the two return fundamentally different shapes
//! (a single `Result<Parsed>` vs. a partial tree plus a list of errors).
//!
//! This is statement-level recovery only; clause/expression-level resync is
//! deliberately deferred until a consumer needs it. No error *nodes* enter the AST:
//! there is no lossless CST, so a broken statement contributes an entry to
//! [`Recovered::errors`], never a node to the tree. The partial AST and the
//! diagnostics travel out of band, side by side.
//!
//! [`Recovered`] is a differently-shaped result from [`Parsed`] — partial AST plus a
//! diagnostic list, not a single tree — so it keeps its own verb pair rather than
//! folding into [`ParseConfig`]. [`parse_recovering_with`] still composes every
//! configuration field that a `Parsed`-carrying root can honour, including
//! [`ParseConfig::capture_trivia`]: the returned [`Recovered`] embeds a real
//! [`Parsed`], so its [`parsed()`](Recovered::parsed) exposes the same trivia queries.

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};

/// The result of a recovering parse: the partial AST plus every collected error.
///
/// Composes the owned [`Parsed`] root — the sole holder of the source and
/// resolver that give the recovered statements meaning — with the out-of-band
/// diagnostics for the statements that failed to parse. A fully well-formed script
/// yields the same statements as [`parse_with`](super::parse_with) and an empty
/// [`errors`](Self::errors); a broken statement is absent from the tree and present
/// in `errors` (no error nodes).
#[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> {
    /// The owned [`Parsed`] root of the well-formed statements, with the source and
    /// resolver they were parsed against. Use it exactly like a [`parse_with`]
    /// result — render, resolve symbols, query line/column.
    ///
    /// [`parse_with`]: super::parse_with
    pub fn parsed(&self) -> &Parsed<S, X> {
        &self.parsed
    }

    /// The recovered statements, in source order — shorthand for
    /// [`parsed().statements()`](Parsed::statements).
    pub fn statements(&self) -> &[Statement<X>] {
        self.parsed.statements()
    }

    /// Every collected parse error, in source order — one per statement that failed.
    ///
    /// Each carries its byte [`Span`](crate::ast::Span), so a diagnostic can resolve
    /// line/column through the [`parsed`](Self::parsed) root's
    /// [`span_line_col`](Parsed::span_line_col). Empty when the whole script parsed.
    pub fn errors(&self) -> &[ParseError] {
        &self.errors
    }

    /// Whether any statement failed to parse.
    pub fn has_errors(&self) -> bool {
        !self.errors.is_empty()
    }

    /// Split into the owned [`Parsed`] root and the collected errors.
    pub fn into_parts(self) -> (Parsed<S, X>, Vec<ParseError>) {
        (self.parsed, self.errors)
    }
}

/// Parse `src` under `dialect`, recovering past errors to collect every diagnostic.
///
/// The resilient counterpart to [`parse_with`](super::parse_with): rather than
/// returning the first error, it records each broken statement's error, skips to the
/// next `;` boundary, and resumes, so the returned [`Recovered`] carries both the
/// well-formed statements and every collected error. The
/// default fail-fast [`parse_with`] is untouched and remains the lean path; reach for
/// this only when a tool wants all diagnostics at once.
///
/// # Errors
///
/// Returns a [`ParseError`] only if streaming setup fails (the tokenizer guards that
/// `src` fits in `u32` bytes), exactly as [`parse_with`](super::parse_with) does.
/// Per-statement errors do *not* surface here — they are collected into
/// [`Recovered::errors`].
///
/// [`parse_with`]: super::parse_with
pub fn parse_recovering(src: &str) -> ParseResult<Recovered> {
    parse_recovering_with(src, ParseConfig::default())
}

/// [`parse_recovering`] honouring `config`, including recursion depth and trivia capture.
///
/// # Errors
///
/// As [`parse_recovering`]. A per-statement recursion-limit error is collected into
/// [`Recovered::errors`] like any other, then recovery resumes at the next `;`.
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,
    )
}

/// Shared recovering body over an `Arc<str>` root (the shared-ownership tier)
/// — the sole recovering tier, since there is no `parse_recovering_rc` (unlike
/// [`collect_parsed`](super::collect_parsed) /
/// [`collect_parsed_with_trivia`](super::collect_parsed_with_trivia), which are also
/// instantiated for the `Rc<str>` tier). `capture_trivia` picks between the same two cursor constructors those use, so a recovering parse can carry the same out-of-band trivia index a collecting one can.
///
/// Drives the per-statement step directly so a failed statement does not fuse the
/// run: on `Err` it records the diagnostic and resynchronizes at the next `;` via
/// [`recover_to_statement_boundary`](Parser::recover_to_statement_boundary), then
/// continues, until end of input. A lexical fault while resynchronizing cannot be
/// stepped over, so recovery stops there (the input is un-tokenizable past that
/// point); its diagnostic is recorded unless it merely repeats the one the statement
/// already failed on.
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,
{
    // Capture the parse's string-literal syntax before the dialect is moved into the
    // parser, so the root can materialise string values dialect-correctly (ADR-0006).
    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() {
                    // Resynced at a `;` — a known statement start; more may follow.
                    Ok(true) => {}
                    // Consumed to end of input without a boundary: recovery is
                    // complete. Stop here rather than re-entering `parse_next_statement`
                    // — an unterminated construct at EOF pins the cursor (its scan runs
                    // to EOF then errors, so the failing peek reports the fault while a
                    // re-advance reports EOF, making no token progress), so re-parsing
                    // would re-fail at the same position forever.
                    Ok(false) => break,
                    // The skip itself hit a lexical fault: no further progress is
                    // possible, so stop. Record it unless it is the very fault the
                    // statement already failed on (a bad token blocks both the parse
                    // and the skip at the same span).
                    Err(resync_error) => {
                        if errors.last() != Some(&resync_error) {
                            errors.push(resync_error);
                        }
                        break;
                    }
                }
            }
        }
    }

    // `take_trivia` is always safe to call: it drains an empty index when the parser
    // above was built without trivia capture, so `with_trivia` here is a no-op in
    // that case rather than a branch.
    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};

    /// The interned symbol of the sole projection column of a single `SELECT col`.
    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() {
        // Two broken statements (`FROM x` cannot begin a statement; `)` cannot either)
        // among three well-formed ones. Recovery resynchronizes at each `;` so all
        // three good statements parse and both errors are reported.
        let src = "SELECT alpha; FROM x; SELECT beta; ); SELECT gamma";
        let recovered = parse_recovering_with(src, crate::ParseConfig::new(TestDialect))
            .expect("streaming setup succeeds");

        // The three well-formed statements survive, as ordinary, usable AST.
        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"]);

        // Both broken statements are reported, each with a real (non-synthetic) span.
        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}",
            );
        }
        // Errors are in source order: the `FROM` error precedes the `)` error.
        assert!(recovered.errors()[0].span.start() < recovered.errors()[1].span.start());

        // The root still owns the original source (ADR-0001).
        assert_eq!(recovered.parsed().source(), src);
    }

    #[test]
    fn fail_fast_default_reports_only_the_first_error() {
        // The default path stays strictly fail-fast: it returns the FIRST error and
        // never reaches the second broken statement — the contrast that proves
        // recovery is opt-in and the lean default is untouched.
        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");
        // The single fail-fast error is exactly the first one recovery collected.
        assert_eq!(error.span, recovered.errors()[0].span);
    }

    #[test]
    fn well_formed_script_recovers_with_no_errors() {
        // Recovery does not change the happy path: every statement parses, 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() {
        // A broken final statement with no trailing `;` resynchronizes to EOF: the
        // earlier good statement survives and the one error is reported.
        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() {
        // A statement that cannot begin (`)` is not a statement start) is a grammar
        // error the parser hits at the leading token without scanning ahead; resyncing
        // forward then reaches an unterminated string — a *different*, freshly-scanned
        // lexical fault, recorded before recovery stops (the input cannot be tokenized
        // past it). Contrast `SELECT FROM '…`, where the parser itself reaches the
        // string, so the fault is the *parse* error, not a separate resync one.
        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);
        // The grammar error and the lexical resync fault are at distinct positions.
        assert_ne!(recovered.errors()[0].span, recovered.errors()[1].span);
    }

    #[test]
    fn a_repeated_lexical_fault_while_resyncing_is_not_double_reported() {
        // Here the statement fails *on* the unterminated string, and the resync hits
        // the same fault at the same span — it must not be reported twice.
        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);
        // The detached root still renders the well-formed statement canonically.
        assert_eq!(parsed.to_string(), "SELECT kept");
    }

    #[test]
    fn with_options_defaults_to_no_trivia() {
        // Recovery's zero-cost-off contract mirrors `parse_with`'s (ADR-0005):
        // neither the argument-free `parse_recovering` nor
        // `parse_recovering_with` with default options captures 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() {
        // Trivia capture composes with recovery: comment runs both before and after
        // a broken statement still land on the recovered root, proving the
        // panic-mode resync does not disturb the cursor's trivia sink.
        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:?}",
        );
    }
}