49 releases
Uses new Rust 2024
| new 0.3.1 | Apr 11, 2026 |
|---|---|
| 0.3.0 | Apr 8, 2026 |
| 0.2.17 | Apr 7, 2026 |
| 0.2.15 | Mar 18, 2026 |
| 0.0.36 | Mar 16, 2026 |
#190 in Database interfaces
Used in syntaqlite-cli
2MB
42K
SLoC
syntaqlite
Parse, format, and validate SQLite SQL from Rust using SQLite's own grammar and tokenizer. No approximations: if SQLite accepts it, syntaqlite parses it.
Docs · Playground · GitHub
[dependencies]
syntaqlite = "0.2.5"
The default feature set includes parsing, formatting, and validation for SQLite. See Features for fine-grained control.
Formatting
use syntaqlite::Formatter;
let mut fmt = Formatter::new();
let output = fmt.format("select id, name from users where active = 1").unwrap();
assert_eq!(output, "SELECT id, name\nFROM users\nWHERE active = 1;\n");
Configure line width, indentation, keyword casing, and semicolons:
use syntaqlite::{FormatConfig, Formatter, KeywordCase};
let config = FormatConfig::default()
.with_line_width(120)
.with_indent_width(4)
.with_keyword_case(KeywordCase::Lower);
let mut fmt = Formatter::with_config(&config);
let output = fmt.format("SELECT 1").unwrap();
The Formatter is reusable across calls and recycles internal buffers.
Parsing
use syntaqlite::{Parser, ParseOutcome};
let parser = Parser::new();
let mut session = parser.parse("SELECT 1 + 2; SELECT 3");
loop {
match session.next() {
ParseOutcome::Ok(stmt) => {
// stmt.root() returns the typed AST node
}
ParseOutcome::Err(err) => {
eprintln!("parse error at offset {}: {}", err.offset(), err.message());
}
ParseOutcome::Done => break,
}
}
The parser is incremental: it yields one statement at a time, so you can process multi-statement inputs without buffering everything upfront.
Validation
Check SQL against a schema without touching a database. Catches unknown tables, columns, functions, CTE column mismatches, and more.
use syntaqlite::{
SemanticAnalyzer, Catalog, CatalogLayer, ValidationConfig,
sqlite_dialect,
};
let mut analyzer = SemanticAnalyzer::new();
let mut catalog = Catalog::new(sqlite_dialect());
catalog.layer_mut(CatalogLayer::Database)
.insert_table("users", Some(vec!["id".into(), "name".into()]), false);
let model = analyzer.analyze(
"SELECT id, email FROM users",
&catalog,
&ValidationConfig::default(),
);
for diag in model.diagnostics() {
// severity: Error or Warning
// message: structured enum (UnknownColumn, UnknownTable, etc.)
// help: optional "did you mean?" suggestion
println!("[{:?}] {}", diag.severity(), diag.message());
}
Output:
[Warning] unknown column 'email'
Catalog layers
The Catalog has five layers resolved in order: Query, Document, Connection, Database, Dialect. For most use cases, insert your schema into the Database layer:
let layer = catalog.layer_mut(CatalogLayer::Database);
// Known columns: validates column references
layer.insert_table("orders", Some(vec!["id".into(), "total".into()]), false);
// Unknown columns: table exists but accepts any column reference
layer.insert_table("legacy_data", None, false);
// Views
layer.insert_view("active_users", Some(vec!["id".into(), "name".into()]));
// Custom functions
use syntaqlite::{FunctionCategory, AritySpec};
layer.insert_function_overload("my_func", FunctionCategory::Scalar, AritySpec::Exact(2));
Rendering diagnostics
Use DiagnosticRenderer for rustc-style error output:
use syntaqlite::DiagnosticRenderer;
let renderer = DiagnosticRenderer::new(source, "query.sql");
for diag in model.diagnostics() {
renderer.render_diagnostic(diag).unwrap();
}
error: unknown column 'email'
--> query.sql:1:12
|
1 | SELECT id, email FROM users
| ^~~~~
= help: did you mean 'name'?
Column lineage
For SELECT statements, validation results include column lineage tracing each output column back to its source:
if let Some(lineage) = model.lineage() {
for col in lineage.columns() {
println!("{} <- {}", col.name(), col.origin());
}
}
Version and compile-flag pinning
Pin the parser to a specific SQLite version or set of compile-time flags to match your target environment:
use syntaqlite::{SqliteVersion, SqliteFlags, SqliteFlag, sqlite_dialect};
// Version pinning
let dialect = sqlite_dialect()
.with_version(SqliteVersion::V3_35);
// Compile-time flags
let flags = SqliteFlags::default()
.with(SqliteFlag::EnableMathFunctions)
.with(SqliteFlag::EnableFts5);
let dialect = sqlite_dialect()
.with_cflags(flags);
Alternatively, use the pin-version and pin-cflags Cargo features to bake these in at compile time via environment variables, eliminating runtime branching:
SYNTAQLITE_SQLITE_VERSION=3035000 cargo build --features pin-version
Features
| Feature | Default | Description |
|---|---|---|
sqlite |
Yes | SQLite dialect (grammar, tokens, built-in functions) |
fmt |
Yes | SQL formatter |
validation |
Yes | Semantic validation (schema checks, suggestions) |
serde |
No | Serialize/Deserialize for diagnostics and AST nodes |
serde-json |
No | JSON convenience helpers |
lsp |
No | Language server protocol implementation |
pin-version |
No | Pin SQLite version at compile time |
pin-cflags |
No | Pin compile-time flags at compile time |
experimental-embedded |
No | SQL extraction from Python/TypeScript strings |
To use only the parser without formatting or validation:
[dependencies]
syntaqlite = { version = "0.2.5", default-features = false, features = ["sqlite"] }
License
Apache 2.0. SQLite components are public domain under the SQLite blessing.