Skip to main content

squonk/
lib.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4// docs.rs feature-gate banners (ticket docs-rs-feature-gate-banners): turn on rustdoc's `doc_cfg` for the nightly docs.rs build only — gated by the `docsrs` cfg docs.rs sets — so feature-gated items render an "Available on crate feature X" banner; auto_cfg is on by default at crate level once `doc_cfg` is enabled (the old `doc_auto_cfg` gate was merged into `doc_cfg` and removed in Rust 1.92), and the whole thing is inert on the pinned stable toolchain (the cfg is never set there).
5#![cfg_attr(docsrs, feature(doc_cfg))]
6// Crate-level (not the shared `[workspace.lints]` table): `missing_docs` is opted into
7// per crate. The two published crates — this one and `squonk-ast` — each deny it, but the
8// bindings and dev tooling (`squonk-python`, `squonk-wasm`, `squonk-sourcegen`) carry
9// undocumented public surface, so a workspace-wide deny would break them.
10#![deny(missing_docs)]
11//! `squonk` — an extensible, fast, multi-dialect SQL tokenizer and parser.
12//!
13//! The AST lives in the `squonk-ast` crate and is re-exported here as [`ast`],
14//! so most users only need to depend on this crate. This crate adds the zero-copy
15//! [`tokenizer`], the identifier [`interner`], structured [`error`]s, the
16//! monomorphized [`Parser`] engine (recursive descent + a Pratt expression core),
17//! the [`Dialect`] system, and the owned [`Parsed`] root. Several dialects ship
18//! (`BuiltinDialect::ALL` is the selectable list) — `Ansi`, the always-compiled
19//! SQL-standard baseline `parse` defaults to, plus the feature-gated presets
20//! `Postgres`, `MySql`, `Sqlite`, `DuckDb`, `QuiltDb`, `BigQuery`, `Hive`, `ClickHouse`,
21//! `Databricks`, `Mssql`, `Snowflake`, `Redshift`, and the permissive `Lenient`,
22//! each carrying a release-contract support tier (`docs/support-tiers.md`);
23//! `full` turns them all on — over a query surface spanning the SELECT
24//! family (CTEs, set operations, joins, window functions, and expression forms such
25//! as `IS [NOT] DISTINCT FROM`) plus DDL, DML, DCL, TCL, and utility statements
26//! (including `TRUNCATE` and `COMMENT ON`). Parse via [`parse`], [`parse_with`], or
27//! [`Parser`].
28//!
29//! # Design records
30//!
31//! The architectural decisions behind these choices — owned tree, interned
32//! identifiers, dialect-as-data, render modes, and the rest — are recorded as ADRs in
33//! the repository's [`docs/adr`](https://github.com/moderately-ai/squonk/tree/main/docs/adr)
34//! directory.
35//!
36//! # Entry points
37//!
38//! Caller-tunable knobs that leave the return type unchanged are fields on
39//! [`ParseConfig`] and are passed to the corresponding `_with` entry point:
40//! [`parse_with`], [`parse_rc_with`], [`parse_recovering_with`], or
41//! [`statements_with`]. A genuinely different result shape earns its own verb:
42//! [`Recovered`] carries a partial AST plus diagnostics, while [`Statements`] is a
43//! lazy iterator. A new knob belongs on [`ParseConfig`] unless it changes what the
44//! function returns.
45//!
46//! # Examples
47//!
48//! ## Parse
49//!
50//! [`parse`] defaults to the [`Ansi`](dialect::Ansi) dialect;
51//! [`parse_with`] selects another, such as `Postgres`. Both
52//! return an owned, `'static` [`Parsed`] tree: the source is moved into
53//! the root, so the tree never borrows the input string.
54//!
55//! ```
56//! use squonk::dialect::Ansi;
57//! use squonk::{ParseConfig, parse, parse_with};
58//!
59//! // ANSI by default.
60//! let parsed = parse("SELECT 1").expect("a well-formed query parses");
61//! assert_eq!(parsed.statements().len(), 1);
62//!
63//! // ...or name a dialect explicitly. Non-default dialects (e.g. `Postgres`)
64//! // are available under their cargo feature.
65//! let parsed = parse_with("SELECT 1", ParseConfig::new(Ansi)).expect("parses under ANSI");
66//! assert_eq!(parsed.statements().len(), 1);
67//! ```
68//!
69//! The default [`parse_with`] root is `Parsed<Arc<str>>` — `Send + Sync`, so it can
70//! cross threads. The tree's ownership tiers let a caller trade that reach for a
71//! cheaper one: [`parse_rc_with`] roots the tree in a non-atomic `Rc<str>` for
72//! single-thread use, and [`Parsed::into_statements`] drops the source and resolver
73//! entirely for callers that only inspect statement *shape*.
74//!
75//! ```
76//! use squonk::dialect::Ansi;
77//! use squonk::{ParseConfig, parse_rc_with, parse_with};
78//!
79//! // Default tier: an `Arc<str>` root, `Send + Sync` (can cross threads).
80//! let arc = parse_with("SELECT 1", ParseConfig::new(Ansi)).expect("Arc<str> root");
81//! assert_eq!(arc.statements().len(), 1);
82//!
83//! // Single-thread tier: a non-atomic `Rc<str>` root, the cheapest refcount.
84//! let rc = parse_rc_with("SELECT 1", ParseConfig::new(Ansi)).expect("Rc<str> root");
85//! assert_eq!(rc.statements().len(), 1);
86//!
87//! // Structure-only tier: drop the source and resolver, keep the statements.
88//! let statements = arc.into_statements();
89//! assert_eq!(statements.len(), 1);
90//! ```
91//!
92//! ## Inspect
93//!
94//! [`Parsed::statements`] yields the statements in source order. Match a node to
95//! walk into it; pair any [`Symbol`](ast::Symbol) it holds with the tree's
96//! [`resolver`](Parsed::resolver) to recover the interned identifier text.
97//!
98//! ```
99//! use squonk::ast::Resolver as _;
100//! use squonk::ast::{Expr, SelectItem, SetExpr, Statement};
101//! use squonk::parse;
102//!
103//! let parsed = parse("SELECT name FROM users").expect("parses");
104//! let Statement::Query { query, .. } = &parsed.statements()[0] else {
105//!     panic!("expected a query statement");
106//! };
107//! let SetExpr::Select { select, .. } = &query.body else {
108//!     panic!("expected a SELECT body");
109//! };
110//! let SelectItem::Expr { expr: Expr::Column { name, .. }, .. } = &select.projection[0] else {
111//!     panic!("expected a column projection");
112//! };
113//! // A node stores interned `Symbol`s; the resolver gives them their text back.
114//! assert_eq!(parsed.resolver().resolve(name.0[0].sym), "name");
115//! ```
116//!
117//! ## Render canonical SQL
118//!
119//! The simplest path is the [`Display`](std::fmt::Display) impl on the [`Parsed`]
120//! root, which the root can offer because it owns the source and resolver a render
121//! needs. To render a *detached* node — one not behind a `Parsed` — pair
122//! it with a [`RenderCtx`](ast::render::RenderCtx) via
123//! [`displayed`](ast::render::RenderExt::displayed).
124//!
125//! ```
126//! use squonk::ast::render::{RenderConfig, RenderCtx, RenderExt};
127//! use squonk::parse;
128//!
129//! // Canonical rendering normalizes keyword case and spacing, round-tripping SQL.
130//! let parsed = parse("select 1 +  2").expect("parses");
131//! assert_eq!(parsed.to_string(), "SELECT 1 + 2");
132//!
133//! // The same text for one statement, threaded explicitly through a RenderCtx.
134//! let config = RenderConfig::default();
135//! let ctx = RenderCtx::new(parsed.resolver(), parsed.source(), &config);
136//! assert_eq!(parsed.statements()[0].displayed(&ctx).to_string(), "SELECT 1 + 2");
137//! ```
138//!
139//! ## Pre-sized render
140//!
141//! [`Parsed`]'s `Display` is the convenient path, but `.to_string()` starts from an
142//! empty buffer and pays the reallocation-doubling chain up to the output size
143//! (a render-perf audit measured 7 reallocations for a 270-byte statement).
144//! On a render-dominant path — transpile-many, or rewrite-then-render — prefer
145//! [`Parsed::to_sql`], which reserves `source().len()` up front and renders in one
146//! allocation, or [`Parsed::render_into`] with a buffer reused across trees. All
147//! three produce byte-identical canonical SQL.
148//!
149//! ```
150//! use squonk::parse;
151//!
152//! let parsed = parse("select 1, 2, 3").expect("parses");
153//!
154//! // Pre-sized: reserves `source().len()` and renders in one allocation.
155//! assert_eq!(parsed.to_sql(), "SELECT 1, 2, 3");
156//!
157//! // Or render into a caller-owned buffer, reusable across many trees.
158//! let mut buf = String::new();
159//! parsed.render_into(&mut buf).expect("rendering into a String is infallible");
160//! assert_eq!(buf, "SELECT 1, 2, 3");
161//! ```
162//!
163//! ## Rewriting the AST (`Visit` / `VisitMut`)
164//!
165//! The retained, rewritable AST is the crate's differentiator. The
166//! generated [`Visit`](ast::generated::visit::Visit) /
167//! [`VisitMut`](ast::generated::visit::VisitMut) traits expose a `visit_*` hook per
168//! node type over the whole tree; override the few you care about and call the
169//! matching `walk_*` to recurse. The [`Parsed`] root is shared, so clone its
170//! statements to rewrite them ([`Statement`](ast::Statement) is `Clone`) while the
171//! root keeps the source and resolver a render needs. A rewrite may only reuse
172//! [`Symbol`](ast::Symbol)s already interned in the tree — the root's resolver is
173//! frozen, so brand-new identifier text has no symbol to point at (the graft-safety
174//! rule). Fuller walkthroughs live in `examples/`: `rewrite_qualify`,
175//! `rewrite_redact`, and `analyze_tables` (run e.g. `cargo run --example
176//! rewrite_qualify`).
177//!
178//! ```
179//! use squonk::ast::generated::visit::{VisitMut, walk_expr_mut};
180//! use squonk::ast::render::{RenderConfig, RenderCtx, RenderExt as _};
181//! use squonk::ast::Expr;
182//! use squonk::parse;
183//!
184//! // Strip the qualifier from every column (`t.id` -> `id`): a mutable walk reusing
185//! // the tree's own interned symbols (the frozen resolver mints no new ones).
186//! struct Unqualify;
187//! impl VisitMut for Unqualify {
188//!     fn visit_expr_mut(&mut self, node: &mut Expr) {
189//!         if let Expr::Column { name, .. } = node {
190//!             while name.0.len() > 1 {
191//!                 name.0.remove(0);
192//!             }
193//!         }
194//!         walk_expr_mut(self, node);
195//!     }
196//! }
197//!
198//! let parsed = parse("SELECT t.id, t.name FROM t").expect("parses");
199//! // Clone the statements to rewrite them while the root keeps source + resolver.
200//! let mut statements = parsed.statements().to_vec();
201//! for statement in &mut statements {
202//!     Unqualify.visit_statement_mut(statement);
203//! }
204//! let config = RenderConfig::default();
205//! let ctx = RenderCtx::new(parsed.resolver(), parsed.source(), &config);
206//! assert_eq!(statements[0].displayed(&ctx).to_string(), "SELECT id, name FROM t");
207//! ```
208//!
209//! ## Debug a detached node
210//!
211//! Prefer the canonical path above whenever you can: [`Parsed`]'s `Display` and
212//! [`displayed`](ast::render::RenderExt::displayed) render exact SQL because they
213//! travel with the matched source and resolver, so they cannot mismatch. Reach for
214//! [`debug_sql`](ast::render::RenderExt::debug_sql) only to debug a *detached* or
215//! *synthesized* node — one lifted out of its [`Parsed`] root, whose symbols may not
216//! belong to the resolver you have on hand.
217//!
218//! It takes the resolver as an **explicit argument** — never a hidden thread-local
219//! or global — so it cannot *silently* render with the wrong resolver; an unknown
220//! symbol becomes a visible `<unresolved>` placeholder instead of panicking; and
221//! literals are spelled by kind (debug never slices source), so it cannot emit
222//! misleading bytes from a mismatched context.
223//!
224//! ```
225//! use squonk::ast::render::RenderExt;
226//! use squonk::interner::Interner;
227//! use squonk::parse;
228//!
229//! let parsed = parse("SELECT amount FROM ledger").expect("parses");
230//! let stmt = &parsed.statements()[0];
231//!
232//! // With the node's own resolver, identifiers resolve and the shape is exact.
233//! assert_eq!(stmt.debug_sql(parsed.resolver()).to_string(), "SELECT amount FROM ledger");
234//!
235//! // A foreign resolver (here an empty one) knows none of these symbols, so debug
236//! // rendering marks each with a placeholder rather than panicking — where the
237//! // canonical path would instead panic on the mismatched resolver.
238//! let foreign = Interner::new().freeze();
239//! assert_eq!(
240//!     stmt.debug_sql(&foreign).to_string(),
241//!     "SELECT <unresolved> FROM <unresolved>",
242//! );
243//! ```
244//!
245//! ## Redacted render
246//!
247//! [`RenderMode::Redacted`](ast::render::RenderMode::Redacted) masks identifier and
248//! literal *content* — `id` for every identifier, `?` for every literal — while
249//! keeping query *shape*, yielding a stable, PII-free fingerprint.
250//! Set it on a [`RenderConfig`](ast::render::RenderConfig).
251//!
252//! ```
253//! use squonk::ast::render::{RenderConfig, RenderCtx, RenderExt, RenderMode};
254//! use squonk::parse;
255//!
256//! let parsed = parse("SELECT name, 42 FROM users WHERE id = 7").expect("parses");
257//! let config = RenderConfig { mode: RenderMode::Redacted, ..RenderConfig::default() };
258//! let ctx = RenderCtx::new(parsed.resolver(), parsed.source(), &config);
259//! assert_eq!(
260//!     parsed.statements()[0].displayed(&ctx).to_string(),
261//!     "SELECT id, ? FROM id WHERE id = ?",
262//! );
263//! ```
264//!
265//! ## Target-dialect render
266//!
267//! Tier-1 rendering above is infallible for the neutral SQL surface. The Tier-2
268//! [`Renderer`](render::Renderer) renders *for a specific dialect target*: it
269//! validates that the target can express each statement before spelling it, prefers
270//! the target's type spellings, and *rejects* — rather than mis-renders — a
271//! construct the target lacks.
272//!
273//! ```
274//! use squonk::dialect::Ansi;
275//! use squonk::parse;
276//! use squonk::render::Renderer;
277//!
278//! // The ANSI target spells the standard type name for a CAST.
279//! let parsed = parse("SELECT CAST(a AS VARCHAR(5))").expect("parses");
280//! assert_eq!(
281//!     Renderer::new(Ansi).render_parsed(&parsed).expect("ANSI can spell this"),
282//!     "SELECT CAST(a AS CHARACTER VARYING(5))",
283//! );
284//!
285//! // A PostgreSQL `$1` placeholder has no ANSI spelling, so the ANSI target rejects
286//! // it with a span-carrying diagnostic instead of emitting invalid SQL. (Parsing
287//! // PostgreSQL requires the `postgres` feature.)
288//! # #[cfg(feature = "postgres")] {
289//! # use squonk::dialect::Postgres;
290//! # use squonk::{ParseConfig, parse_with};
291//! # use squonk::render::RenderErrorKind;
292//! let pg = parse_with("SELECT $1", ParseConfig::new(Postgres)).expect("parses under PostgreSQL");
293//! let error = Renderer::new(Ansi).render_parsed(&pg).expect_err("ANSI has no $n");
294//! assert_eq!(error.kind(), RenderErrorKind::Unsupported);
295//! assert!(error.span().is_some());
296//! # }
297//! ```
298//!
299//! ## Transpile
300//!
301//! [`transpile`] packages the two-step parse-then-target-render into one call:
302//! parse `sql` under a source dialect, render it for a target. Like the
303//! [`Renderer`](render::Renderer) it wraps, it is *syntactic* transpilation with
304//! rejection — a construct the target cannot spell fails with a
305//! span-carrying [`TranspileError`], never invalid SQL — not sqlglot-style
306//! semantic rewriting. It takes no options by design; a caller wanting a recursion
307//! limit, trivia, a redacted mode, or a [`Renderer`](render::Renderer) reused
308//! across inputs composes [`parse_with`] and [`Renderer`](render::Renderer)
309//! directly.
310//!
311//! ```
312//! # #[cfg(feature = "postgres")] {
313//! use squonk::dialect::{Ansi, Postgres};
314//! use squonk::render::RenderErrorKind;
315//! use squonk::{transpile, TranspileError};
316//!
317//! // A PostgreSQL cast transpiles to ANSI, which prefers the standard type name.
318//! let ansi = transpile("SELECT CAST(a AS VARCHAR(5))", Postgres, Ansi)
319//!     .expect("ANSI can spell this cast");
320//! assert_eq!(ansi, "SELECT CAST(a AS CHARACTER VARYING(5))");
321//!
322//! // A PostgreSQL `$1` placeholder has no ANSI spelling, so transpilation rejects
323//! // it with a span-carrying diagnostic rather than emitting invalid SQL.
324//! let error = transpile("SELECT $1", Postgres, Ansi).expect_err("ANSI has no $n");
325//! let TranspileError::Render(rejection) = error else {
326//!     panic!("expected a render rejection, not a parse error");
327//! };
328//! assert_eq!(rejection.kind(), RenderErrorKind::Unsupported);
329//! assert!(rejection.span().is_some());
330//! # }
331//! ```
332//!
333//! ## Custom dialects
334//!
335//! A [`Dialect`] is *data*: its [`features`](Dialect::features) returns a
336//! const [`FeatureSet`](ast::dialect::FeatureSet) the parser reads field by field.
337//! Build one from a preset plus a [`FeatureDelta`](ast::dialect::FeatureDelta):
338//! [`FeatureSet::with`](ast::dialect::FeatureSet::with) applies the delta unchecked
339//! (the fast path the presets use), while
340//! [`FeatureSet::try_with`](ast::dialect::FeatureSet::try_with) returns a
341//! [`LexicalConflict`](ast::dialect::LexicalConflict) if the delta makes two features
342//! fight over the same tokenizer trigger (e.g. `$1` as both a money literal and a
343//! positional parameter). [`is_lexically_consistent`](ast::dialect::FeatureSet::is_lexically_consistent)
344//! is the same check as a bool, usable in a `const` assertion.
345//!
346//! ```
347//! use squonk::ast::NoExt;
348//! use squonk::ast::dialect::{FeatureDelta, FeatureSet, ParameterSyntax};
349//! use squonk::{Dialect, ParseConfig, parse_with};
350//!
351//! // ANSI, plus anonymous `?` parameter placeholders.
352//! const ANSI_WITH_PARAMS: FeatureSet = FeatureSet::ANSI.with(
353//!     FeatureDelta::EMPTY.parameters(ParameterSyntax {
354//!         anonymous_question: true,
355//!         ..ParameterSyntax::ANSI
356//!     }),
357//! );
358//! // The delta claims no trigger another feature already owns, so the set is
359//! // consistent — checkable at compile time.
360//! const _: () = assert!(ANSI_WITH_PARAMS.is_lexically_consistent());
361//!
362//! #[derive(Clone, Copy)]
363//! struct AnsiWithParams;
364//! impl Dialect for AnsiWithParams {
365//!     type Ext = NoExt;
366//!     fn features(&self) -> &FeatureSet {
367//!         &ANSI_WITH_PARAMS
368//!     }
369//! }
370//!
371//! // `?` now parses where stock ANSI would reject it.
372//! let parsed = parse_with("SELECT ?", ParseConfig::new(AnsiWithParams)).expect("the custom dialect parses `?`");
373//! assert_eq!(parsed.statements().len(), 1);
374//! ```
375//!
376//! ## Diagnostics
377//!
378//! A failed parse returns a [`ParseError`](error::ParseError): a byte
379//! [`Span`](ast::Span) plus what the parser *expected* and *found*; an
380//! input that ends mid-construct reports [`Found::EndOfInput`](error::Found::EndOfInput).
381//! Spans are byte offsets by design; recover line/column from the source
382//! through an [`ast::LineIndex`], or via [`Parsed::span_line_col`] when a tree is in
383//! hand.
384//!
385//! ```
386//! use squonk::ast::LineIndex;
387//! use squonk::parse;
388//!
389//! // `FROM` cannot begin a statement, so the parse fails on the second line.
390//! let src = "SELECT 1;\nFROM t";
391//! let error = parse(src).expect_err("FROM is not a statement");
392//!
393//! assert_eq!(error.span.start(), 10);
394//! assert!(error.to_string().contains("found FROM"), "{error}");
395//!
396//! // Map the error's byte span to a zero-based (line, column) for display.
397//! let (line, column) = LineIndex::from_str(src).lookup(error.span.start());
398//! assert_eq!((line, column), (1, 0)); // second line, first column
399//! ```
400//!
401//! ## Recovering parse
402//!
403//! The default parse is fail-fast — the first error short-circuits. To
404//! collect *every* diagnostic in a multi-statement script (compiler-style "all errors
405//! in the file"), use [`parse_recovering`]: it records each broken statement's error,
406//! resynchronizes at the next `;`, and resumes. The returned [`Recovered`] carries
407//! both the well-formed statements — as ordinary AST — and the
408//! [`errors`](Recovered::errors) for the broken ones (no error nodes enter
409//! the tree).
410//!
411//! ```
412//! use squonk::dialect::Ansi;
413//! use squonk::{ParseConfig, parse_recovering_with};
414//!
415//! // The middle statement is malformed; the outer two are well-formed.
416//! let recovered =
417//!     parse_recovering_with("SELECT 1; SELECT FROM t; SELECT 2", ParseConfig::new(Ansi)).expect("recovers");
418//! assert!(recovered.has_errors());
419//! assert_eq!(recovered.errors().len(), 1);
420//! // The two good statements are still parsed and usable.
421//! assert_eq!(recovered.statements().len(), 2);
422//! ```
423//!
424//! ## Trivia
425//!
426//! Comments and whitespace are skipped at zero cost by default. To recover them — for
427//! a formatter, linter, or doc-comment extractor — set
428//! [`ParseConfig::capture_trivia`] and pass it to [`parse_with`]. The
429//! captured runs hang off the [`Parsed`] root, queryable by offset via
430//! [`Parsed::trivia`], [`Parsed::trivia_in`], and [`Parsed::trivia_before`]; the
431//! statements themselves stay trivia-free.
432//!
433//! ```
434//! use squonk::dialect::Ansi;
435//! use squonk::{ParseConfig, parse_with};
436//!
437//! let parsed = parse_with("SELECT /* note */ 1", ParseConfig::new(Ansi).capture_trivia(true)).expect("parses with trivia");
438//! // Every skipped run is recoverable from the root and slices back out of source.
439//! let comment = parsed.trivia().iter().find_map(|run| {
440//!     let span = run.span();
441//!     let text = &parsed.source()[span.start() as usize..span.end() as usize];
442//!     text.starts_with("/*").then_some(text)
443//! });
444//! assert_eq!(comment, Some("/* note */"));
445//! // `trivia_before` recovers a token's leading trivia by its start offset.
446//! assert!(!parsed.trivia_before(18).is_empty()); // the `1` token starts at byte 18
447//! ```
448//!
449//! ## Serialization (`serde` feature)
450//!
451//! With the `serde` feature on, a [`Parsed`] root round-trips through any serde
452//! format. The document is self-contained — the source, the resolver's dynamic
453//! string table, and the statement tree with its numeric symbols — so a reloaded tree
454//! resolves and renders identically, including across processes. Deserialization runs
455//! behind a format-agnostic depth cap
456//! (`DEFAULT_DESERIALIZE_DEPTH`, in the `serde` feature's `ast::serde_depth` module) so
457//! untrusted bytes cannot rebuild a hostile-deep tree that overflows the stack on its
458//! first drop/render/visit (the deserialize counterpart of the parser's recursion
459//! guard). Beyond depth, deserialization also *validates the loaded content*
460//! in one walk before returning: every non-synthetic span is checked in bounds for the
461//! `source` (`start <= end <= source.len()`), every numeric symbol is checked to resolve
462//! in the rebuilt table, and a table carrying duplicate entries — which would silently
463//! misresolve every later symbol, since re-interning dedupes — is rejected. A violation
464//! is a clean deserialize error naming the first offending span/symbol, so a hand-crafted
465//! document cannot smuggle in an out-of-table symbol that panics on the first
466//! canonical render (`Resolver::resolve`) or an out-of-bounds span that slices the wrong
467//! text — the untrusted-input hardening the depth cap began.
468//!
469//! ```
470//! # #[cfg(feature = "serde")] {
471//! use squonk::{Parsed, parse};
472//!
473//! let parsed = parse("SELECT a, b FROM t WHERE a > 1").expect("parses");
474//! let json = serde_json::to_string(&parsed).expect("serializes");
475//! let restored: Parsed = serde_json::from_str(&json).expect("round-trips");
476//! // The reloaded tree renders byte-identically to the original.
477//! assert_eq!(restored.to_sql(), parsed.to_sql());
478//! # }
479//! ```
480//!
481//! # Performance
482//!
483//! For high-throughput or allocation-heavy parsing, the cheapest win is the global allocator the *final binary* links: set a fast general-purpose allocator (e.g. `mimalloc` or `jemalloc`) for **~15-19% of parse time on alloc-heavy SQL** (measured by `bench/benches/alloc_probe.rs`, `--profile profiling`). A library cannot set this itself, and `squonk` deliberately takes no allocator dependency, so it is the consumer's choice — workload-dependent, and negligible on tiny statements or any workload that is not allocation-bound.
484//!
485//! ```ignore
486//! #[global_allocator]
487//! static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
488//! ```
489//!
490//! ## Parallel parsing
491//!
492//! Each [`parse_with`] result is an owned, `Send + Sync` `Parsed<Arc<str>>`,
493//! so parsing many *independent* SQL strings parallelizes with no feature, dependency,
494//! or code from this crate — just the caller's thread pool (e.g.
495//! `inputs.par_iter().map(|s| parse_with(s, ParseConfig::new(Ansi)))` under rayon). Aggregate throughput
496//! scales near-linearly to the performance-core count: a measured 1.16M parses/sec on
497//! one thread rises to ~7.3M/sec on 14 threads (Apple M4 Max, 10 performance + 4
498//! efficiency cores), ~85-97% efficiency through 4 threads and hardware-bound
499//! (memory-bandwidth/DVFS) beyond — and *not* allocator-bound (parse scales identically
500//! to an allocation-light tokenize control at every thread count). Size the pool to the
501//! **performance**-core count — efficiency cores add sub-proportional throughput — and
502//! pair with a fast global allocator (above) for allocation-heavy inputs. Numbers:
503//! `docs/performance.md` §3.
504//!
505//! BATCH parsing — one multi-statement input into a single [`Parsed`] — is deliberately
506//! *not* parallelized: the deterministic interner merge is Amdahl-capped at ~1.1×, so it
507//! earns no owned `parallel` feature. Parallelize across independent *inputs*, not within
508//! one. The scoped-thread form below needs no dependency; a rayon `par_iter` is the same
509//! idea with a managed pool.
510//!
511//! ```
512//! use std::thread;
513//!
514//! use squonk::dialect::Ansi;
515//! use squonk::{ParseConfig, Parsed, parse_with};
516//!
517//! let inputs = ["SELECT 1", "INSERT INTO t VALUES (1)", "UPDATE t SET a = 1"];
518//!
519//! // Fan the independent parses out across scoped threads; each `Parsed` is
520//! // `Send + Sync`, so it crosses the thread boundary back to the caller.
521//! let parsed: Vec<Parsed> = thread::scope(|scope| {
522//!     let handles: Vec<_> = inputs
523//!         .iter()
524//!         .map(|&sql| scope.spawn(move || parse_with(sql, ParseConfig::new(Ansi)).expect("each input parses")))
525//!         .collect();
526//!     handles
527//!         .into_iter()
528//!         .map(|handle| handle.join().expect("no worker panics"))
529//!         .collect()
530//! });
531//!
532//! assert_eq!(parsed.len(), inputs.len());
533//! assert!(parsed.iter().all(|tree| tree.statements().len() == 1));
534//! ```
535//!
536//! # Other language bindings
537//!
538//! Two `publish = false` bindings crates in this workspace wrap the parser for other
539//! runtimes: `squonk-wasm` exposes a tiny JS-value
540//! `parse`/`parse_recovering`/`version` surface to the browser/edge, and
541//! `squonk-python` packages the serde JSON surface as a maturin extension
542//! module (`json.loads` in a thin Python wrapper). See each crate's README for
543//! build, size, and untrusted-input notes.
544
545/// The SQL abstract syntax tree, re-exported from `squonk-ast`.
546pub use squonk_ast as ast;
547
548#[cfg(feature = "serde-serialize")]
549pub mod bindings;
550pub mod dialect;
551pub mod error;
552/// The pretty-printing formatter (layout-IR renderer + comment attachment). Gated
553/// behind the non-default `document-render` feature so the serialize-only default and
554/// wasm builds stay lean — the formatter compiles only when a consumer opts in.
555#[cfg(feature = "document-render")]
556pub mod format;
557pub mod interner;
558pub mod parser;
559pub mod render;
560pub mod tokenizer;
561
562/// [`parse`] is the crate's default-dialect (`Ansi`) convenience, re-exported here so the crate's own leading example (`use squonk::parse;`) works from the root; [`BuiltinDialect`]/[`parse_builtin`] add runtime built-in dialect selection, the compile-time-free sibling of [`parse_with`].
563pub use dialect::{
564    BuiltinDialect, ParseBuiltinDialectError, parse, parse_builtin, parse_builtin_with,
565    parse_recovering_builtin, parse_recovering_builtin_with, tokenize_with_builtin,
566    tokenize_with_builtin_trivia,
567};
568/// The parser engine, dialect trait, owned root, and entry points.
569pub use parser::{
570    ClauseKw, ClauseMark, ClauseMarkIndex, DEFAULT_RECURSION_LIMIT, Dialect, ParseConfig, Parsed,
571    Parser, Statements, StockParsed, parse_rc, parse_rc_with, parse_with, statements,
572    statements_with,
573};
574/// Resilient multi-error parsing: collect every diagnostic and the partial AST in
575/// one run, instead of stopping at the first error like the default [`parse_with`].
576pub use parser::{Recovered, parse_recovering, parse_recovering_with};
577/// The one-call [`transpile`] convenience — parse under a source dialect, render for a
578/// target — and its [`TranspileError`], re-exported from [`render`] beside the
579/// lower-level [`Renderer`](render::Renderer) building block it composes.
580pub use render::{TranspileError, transpile};