Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## September 2026: parser refresh

- Updated the benchmarked parsers to their latest versions: sqlparser-rs 0.62 to 0.63 (git 9296011a), polyglot-sql 0.5.1 to 0.11.0 (git 47342ba6), pg_query.rs 6.1.1 to 6.2.0 (git 0e382742), qusql-parse 0.8 to 0.11, sqlglot-rust 0.10.1 to 0.10.29, sqlite3-parser 0.16 to 0.17, turso_parser 0.6.1 to 0.7.2. databend-common-ast and orql were already at their newest releases.
- The time machine gains twelve new release points (sqlparser-rs 0.63, polyglot-sql 0.6.3 through 0.11.0, qusql-parse 0.9 through 0.11, sqlite3-parser 0.17, turso_parser 0.7.2) and the sqlglot-rust 0.10 milestone moves to 0.10.29, so the trends end at the current code.
- sqlite3-parser 0.17 parses into a bumpalo arena owned by the caller, so the benchmark and the time-machine adapter now pass a fresh arena per parse.
- The abstract states the September 2026 snapshot: permissiveness and false-positive figures per parser, the new speed spread with polyglot-sql back inside the pack, and the 95-version time machine.
- Repository metadata (stars, forks, commits, contributors, downloads, last releases, fuzzing, audit, deny, sanitizer and unsafe facts) refreshed from live sources.
- orql was removed entirely. Its repository has had no movement, its workspace does not build on stable rustc, and the reported macros defect has gone unaddressed. Oracle remains a benchmarked dialect, covered by the multi-dialect parsers.

## June 2026: parser refresh and a failed-to-parse badge

- Updated the benchmarked parsers to their latest versions: polyglot-sql 0.4.4 to 0.5.1, sqlglot-rust 0.10.0 to 0.10.1, and the git-tracked sqlparser-rs and pg_query.rs to their current commits (sqlparser-rs now at b3760221). turso_parser stays on 0.6.1 since the only newer version is a prerelease.
Expand Down
17 changes: 11 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
[workspace]
members = [".", "viz", "web", "badgegen", "membench", "oracle", "timemachine", "featurescan"]
default-members = ["."]
resolver = "2"
# Resolver 3 resolves under `rust-version`, so a dependency raising its MSRV
# in a patch release is not selected and needs no hand-written upper bound.
resolver = "3"

[workspace.package]
rust-version = "1.88"

[package]
name = "sql_ast_benchmark"
Expand All @@ -14,6 +19,7 @@ authors = ["Luca Cappelletti"]
keywords = ["sql", "parser", "benchmark", "postgresql"]
categories = ["development-tools::profiling"]
publish = false
rust-version.workspace = true

[lints.rust]
unsafe_code = "forbid"
Expand All @@ -30,13 +36,12 @@ cast_sign_loss = { level = "allow", priority = 1 }
sqlparser = { git = "https://github.com/sqlparser-rs/sqlparser-rs" }
# libpg_query, the PostgreSQL server's own parser, the reference for PostgreSQL.
pg_query = { git = "https://github.com/pganalyze/pg_query.rs" }
qusql-parse = "0.8.0"
qusql-parse = "0.11.0"
polyglot-sql = { git = "https://github.com/tobilg/polyglot" }
databend-common-ast = "0.2.5"
orql = { git = "https://codeberg.org/xitep/orql" }
sqlglot-rust = "0.10.1"
sqlite3-parser = "0.16.0"
turso_parser = "0.6.1"
sqlglot-rust = "0.10.29"
sqlite3-parser = "0.17.0"
turso_parser = "0.7.2"
fallible-iterator = "0.3.0"
viz = { path = "viz" }
serde = { version = "1", features = ["derive"] }
Expand Down
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,24 @@ Benchmarking Rust SQL parsers on a real-world corpus of 340,938 statements acros

Choosing a SQL parser for a Rust project means weighing dialect coverage, correctness, and speed, yet those trade-offs are seldom measured on realistic input. We benchmarked the actively maintained Rust SQL parsers on a large, multi-dialect corpus of real-world statements so the choice can rest on evidence rather than on each library's own claims.

We evaluated nine parser libraries: [sqlparser-rs](https://github.com/sqlparser-rs/sqlparser-rs) (Apache DataFusion), [pg_query.rs](https://github.com/pganalyze/pg_query.rs) and its faster summary mode (Rust bindings to [libpg_query](https://github.com/pganalyze/libpg_query), PostgreSQL's own parser), [databend-common-ast](https://crates.io/crates/databend-common-ast), [polyglot-sql](https://github.com/tobilg/polyglot), [sqlglot-rust](https://crates.io/crates/sqlglot-rust), [qusql-parse](https://crates.io/crates/qusql-parse), [sqlite3-parser](https://crates.io/crates/sqlite3-parser) (lemon-rs), and [turso_parser](https://crates.io/crates/turso_parser) (the SQLite parser from Turso), plus [orql](https://codeberg.org/xitep/orql) on Oracle. We ran them against a corpus of 340,938 statements spanning 13 dialects, drawn from each engine's own regression suites and official samples and committed compressed so every run is reproducible.
We evaluated eight parser libraries: [sqlparser-rs](https://github.com/sqlparser-rs/sqlparser-rs) (Apache DataFusion), [pg_query.rs](https://github.com/pganalyze/pg_query.rs) and its faster summary mode (Rust bindings to [libpg_query](https://github.com/pganalyze/libpg_query), PostgreSQL's own parser), [databend-common-ast](https://crates.io/crates/databend-common-ast), [polyglot-sql](https://github.com/tobilg/polyglot), [sqlglot-rust](https://crates.io/crates/sqlglot-rust), [qusql-parse](https://crates.io/crates/qusql-parse), [sqlite3-parser](https://crates.io/crates/sqlite3-parser) (lemon-rs), and [turso_parser](https://crates.io/crates/turso_parser) (the SQLite parser from Turso). We ran them against a corpus of 340,938 statements spanning 13 dialects, drawn from each engine's own regression suites and official samples and committed compressed so every run is reproducible.

We exercised each parser in the dialect that matches the corpus under test. Where a dialect has a runnable engine, we labelled each statement valid or invalid with the real database engine itself, run in Docker via [testcontainers](https://github.com/testcontainers/testcontainers-rs): a statement counts as valid unless the engine reports a syntax error, so a missing table or column still counts as parsed. Against that ground truth we scored the parsers on recall (valid statements accepted), false positives (invalid statements wrongly accepted), and display round-trip stability. The other dialects have no runnable engine, so their statements count as provenance-valid and the metric is simply the acceptance rate. Across all dialects, we captured speed as a per-statement parse-time distribution over every accepted statement, and memory as the peak and retained bytes per statement under a counting allocator. A batch axis additionally parses each parser's whole accepted set as a single script, showing what bulk parsing amortizes, and a time machine benchmarks the historical releases of every pure-Rust parser (59 versions in total, including every sqlparser-rs minor since January 2023), so each parser page also charts how coverage, speed, and memory evolved across releases.
We exercised each parser in the dialect that matches the corpus under test. Where a dialect has a runnable engine, we labelled each statement valid or invalid with the real database engine itself, run in Docker via [testcontainers](https://github.com/testcontainers/testcontainers-rs): a statement counts as valid unless the engine reports a syntax error, so a missing table or column still counts as parsed. Against that ground truth we scored the parsers on recall (valid statements accepted), false positives (invalid statements wrongly accepted), and display round-trip stability. The other dialects have no runnable engine, so their statements count as provenance-valid and the metric is simply the acceptance rate. Across all dialects, we captured speed as a per-statement parse-time distribution over every accepted statement, and memory as the peak and retained bytes per statement under a counting allocator. A batch axis additionally parses each parser's whole accepted set as a single script, showing what bulk parsing amortizes, and a time machine benchmarks the historical releases of every pure-Rust parser (95 versions across 7 families, including every sqlparser-rs minor since July 2020), so each parser page also charts how coverage, speed, and memory evolved across releases.

On their home dialect the reference bindings are exact by construction, so the more telling comparison is among the pure-Rust parsers. There, [sqlparser-rs](https://github.com/sqlparser-rs/sqlparser-rs) is the most broadly capable, the permissive parsers such as [polyglot-sql](https://github.com/tobilg/polyglot) accept the most statements but pay for it with a high false-positive rate, and the stricter parsers reject more in exchange for precision. Speed spans more than an order of magnitude, from well under a microsecond per statement for the fastest parsers to the low single-digit microseconds for most, with [polyglot-sql](https://github.com/tobilg/polyglot) a clear outlier at roughly fifteen. No parser leads on every axis, so the right choice comes down to what a given project values most: broad coverage, few false positives, or raw speed.
On their home dialect the reference bindings are exact by construction, so the more telling comparison is among the pure-Rust parsers. There, [sqlparser-rs](https://github.com/sqlparser-rs/sqlparser-rs) is the most broadly capable. Permissiveness turns out to be expensive. [sqlglot-rust](https://crates.io/crates/sqlglot-rust) recalls 98 percent of valid statements yet wrongly accepts 85 percent of invalid ones, [polyglot-sql](https://github.com/tobilg/polyglot) reaches 92 percent recall at 50 percent false positives, and the two SQLite lemon parsers, [sqlite3-parser](https://crates.io/crates/sqlite3-parser) and [turso_parser](https://crates.io/crates/turso_parser), accept nearly every valid statement while still wrongly accepting six in ten. The strictest, [databend-common-ast](https://crates.io/crates/databend-common-ast) and [qusql-parse](https://crates.io/crates/qusql-parse), hold false positives to 6 and 12 percent but recall only 53 and 74 percent. Speed spans more than an order of magnitude, from half a microsecond to just under one for the fastest, [qusql-parse](https://crates.io/crates/qusql-parse) and [turso_parser](https://crates.io/crates/turso_parser), to the six to nine microsecond band occupied by [polyglot-sql](https://github.com/tobilg/polyglot), [sqlparser-rs](https://github.com/sqlparser-rs/sqlparser-rs) and [databend-common-ast](https://crates.io/crates/databend-common-ast). No parser leads on every axis, so the right choice comes down to what a given project values most: broad coverage, few false positives, or raw speed.

## Parsers Under Test

| Parser | Version | Source | Implementation | Dialects |
| --- | --- | --- | --- | --- |
| **[sqlparser-rs](https://github.com/sqlparser-rs/sqlparser-rs)** | 0.62.0 | git [`b376022`](https://github.com/sqlparser-rs/sqlparser-rs/commit/b3760221) | Pure Rust, handwritten recursive descent | 14 dedicated dialects |
| **[sqlglot-rust](https://crates.io/crates/sqlglot-rust)** | 0.10.1 | crates.io | Pure Rust, standalone port of Python sqlglot | 30 (parser currently dialect-agnostic) |
| **[polyglot-sql](https://github.com/tobilg/polyglot)** | 0.5.1 | git [`e3a8913`](https://github.com/tobilg/polyglot/commit/e3a8913a) | Pure Rust, transpiler | 32 |
| **[pg_query.rs](https://github.com/pganalyze/pg_query.rs)** | 6.1.1 | git [`7e189a9`](https://github.com/pganalyze/pg_query.rs/commit/7e189a9dd1d4e441a2d44e6655c793f101bba3fa) | Rust FFI to C (libpg_query) | PostgreSQL |
| **[qusql-parse](https://crates.io/crates/qusql-parse)** | 0.8.0 | crates.io | Pure Rust, zero-copy | PostgreSQL, MariaDB/MySQL, SQLite |
| **[sqlparser-rs](https://github.com/sqlparser-rs/sqlparser-rs)** | 0.63.0 | git [`9296011a`](https://github.com/sqlparser-rs/sqlparser-rs/commit/9296011a) | Pure Rust, handwritten recursive descent | 14 dedicated dialects |
| **[sqlglot-rust](https://crates.io/crates/sqlglot-rust)** | 0.10.29 | crates.io | Pure Rust, standalone port of Python sqlglot | 30 (parser currently dialect-agnostic) |
| **[polyglot-sql](https://github.com/tobilg/polyglot)** | 0.11.0 | git [`47342ba6`](https://github.com/tobilg/polyglot/commit/47342ba6) | Pure Rust, transpiler | 32 |
| **[pg_query.rs](https://github.com/pganalyze/pg_query.rs)** | 6.2.0 | git [`0e382742`](https://github.com/pganalyze/pg_query.rs/commit/0e382742b36ee9592438c6791b91ce14f32f6233) | Rust FFI to C (libpg_query) | PostgreSQL |
| **[qusql-parse](https://crates.io/crates/qusql-parse)** | 0.11.0 | crates.io | Pure Rust, zero-copy | PostgreSQL, MariaDB/MySQL, SQLite |
| **[databend-common-ast](https://github.com/datafuselabs/databend)** | 0.2.5 | crates.io | Pure Rust, zero-copy, Pratt | PostgreSQL, MySQL, Hive |
| **[sqlite3-parser](https://crates.io/crates/sqlite3-parser)** (lemon-rs) | 0.16.0 | crates.io | Generated from SQLite's Lemon grammar | SQLite |
| **[turso_parser](https://crates.io/crates/turso_parser)** | 0.6.1 | crates.io | Pure Rust, handwritten recursive descent over a Lemon token table | SQLite |
| **[orql](https://codeberg.org/xitep/orql)** | 0.1.0 | git [`6a5391b`](https://codeberg.org/xitep/orql/commit/6a5391b1b11f5771ab15e4ba519bdf00fdacc021) | Pure Rust, early-stage | Oracle (SELECT only) |
| **[sqlite3-parser](https://crates.io/crates/sqlite3-parser)** (lemon-rs) | 0.17.0 | crates.io | Generated from SQLite's Lemon grammar | SQLite |
| **[turso_parser](https://crates.io/crates/turso_parser)** | 0.7.2 | crates.io | Pure Rust, handwritten recursive descent over a Lemon token table | SQLite |

Per-parser repository metadata (stars, contributors, fuzzing, test and benchmark suites, license) is shown on each parser page in the [explorer](https://sql-ast-benchmark.luca.phd).

Expand Down Expand Up @@ -63,7 +62,7 @@ cargo run --release --bin sqlbench export # regenerate web

`cargo bench` runs both the per-statement (`parsing`) and whole-script (`batch_parsing`) timing benches. Add `--bench batch_parsing` to run only the batch one. `export` reads whatever the benches left under `target/`, warning rather than failing for any missing source, so the memory and batch columns stay empty until their producers have run.

The `timemachine` crate benchmarks several historical versions of each pure-Rust parser at once (via `package`-rename aliases in `timemachine/Cargo.toml`) and writes a compressed `web/assets/history.json.zst` that the explorer embeds and decompresses in the browser. Each parser page then shows how that library's time, memory, and correctness changed across releases, with a version picker. Cargo can only host semver-incompatible versions side by side, so the milestones are the latest patch of every `0.x` minor the shared adapter compiles against: `sqlparser-rs` gets 33 points (every minor from 0.30, January 2023, through 0.62), `sqlite3-parser` eight, `qusql-parse` seven (its 0.1.0 release parses pathologically slowly on parts of the corpus and is excluded), `polyglot-sql` four, `databend-common-ast` three, `sqlglot-rust` two, while `turso_parser` and `orql` have a single published release and so show one point. The FFI parsers (`pg_query`) are excluded because two builds of libpg_query collide at link. Without `--full` the runners use a small per-dialect sample, which is a fast pipeline check rather than publishable numbers.
The `timemachine` crate benchmarks several historical versions of each pure-Rust parser at once (via `package`-rename aliases in `timemachine/Cargo.toml`) and writes a compressed `web/assets/history.json.zst` that the explorer embeds and decompresses in the browser. Each parser page then shows how that library's time, memory, and correctness changed across releases, with a version picker. Cargo can only host semver-incompatible versions side by side, so the milestones are the latest patch of every `0.x` minor the shared adapter compiles against: `sqlparser-rs` gets 58 points (every minor from 0.6.1, July 2020, through 0.63), `sqlite3-parser` nine, `qusql-parse` ten (its 0.1.0 release parses pathologically slowly on parts of the corpus and is excluded), `polyglot-sql` eleven, `databend-common-ast` three, `sqlglot-rust` two, `turso_parser` two. The FFI parsers (`pg_query`) are excluded because two builds of libpg_query collide at link. Without `--full` the runners use a small per-dialect sample, which is a fast pipeline check rather than publishable numbers.

Validity labels for the reference dialects are produced by the `oracle` crate (real engines in Docker via testcontainers) and committed under `oracle/labels`, so `correctness` and `export` need no Docker. Regenerate them with `cargo run --release -p oracle`.

Expand Down
1 change: 1 addition & 0 deletions badgegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2021"
description = "Writes the static per-parser README badges into web/static/badges"
license = "MIT"
publish = false
rust-version.workspace = true

[lints.clippy]
all = "warn"
Expand Down
5 changes: 5 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Refresh diffs are dominated by macro-generated release adapters and
# regenerated data, which patch coverage cannot measure honestly.
coverage:
status:
patch: off
1 change: 1 addition & 0 deletions featurescan/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2021"
description = "Static source-feature scan of the benchmarked SQL parsers (panic discipline, unsafe, lint policy)"
license = "MIT"
publish = false
rust-version.workspace = true
default-run = "featurescan"

# Offline analysis crate. It locates each benchmarked parser's source on disk via
Expand Down
29 changes: 10 additions & 19 deletions featurescan/data/depth.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"dialect": "postgresql",
"guarded": true,
"shape_rejected": false,
"limit_depth": 48,
"limit_depth": 47,
"crash_depth": null,
"ceil": 50000
},
Expand Down Expand Up @@ -36,16 +36,16 @@
"guarded": false,
"shape_rejected": false,
"limit_depth": null,
"crash_depth": 8578,
"crash_depth": 6976,
"ceil": 50000
},
{
"parser": "polyglot-sql",
"dialect": "postgresql",
"guarded": false,
"guarded": true,
"shape_rejected": false,
"limit_depth": null,
"crash_depth": 232,
"limit_depth": 512,
"crash_depth": null,
"ceil": 50000
},
{
Expand All @@ -54,16 +54,7 @@
"guarded": false,
"shape_rejected": false,
"limit_depth": null,
"crash_depth": 421,
"ceil": 50000
},
{
"parser": "orql",
"dialect": "oracle",
"guarded": false,
"shape_rejected": true,
"limit_depth": null,
"crash_depth": 5184,
"crash_depth": 354,
"ceil": 50000
},
{
Expand All @@ -72,7 +63,7 @@
"guarded": false,
"shape_rejected": false,
"limit_depth": null,
"crash_depth": 412,
"crash_depth": 403,
"ceil": 50000
},
{
Expand All @@ -87,10 +78,10 @@
{
"parser": "turso_parser",
"dialect": "sqlite",
"guarded": false,
"guarded": true,
"shape_rejected": false,
"limit_depth": null,
"crash_depth": 2199,
"limit_depth": 100,
"crash_depth": null,
"ceil": 50000
}
]
Expand Down
Loading