diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5aa69af --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,21 @@ +# Contribution guidelines + +This library's API surface is primarily driven by the needs of +[PgDog](https://github.com/pgdogdev/pgdog). It is not intended to be a complete, +one-size-fits-all solution to PostgreSQL ASTs. + +Contributions are welcome, but pull requests adding large and complex features are unlikely to be accepted +unless they align with PgDog's needs. + +## LLM Policy + +This library was primarily written by humans, without much LLM assistance. While LLM +assisted pull requests are allowed, any code written by an LLM must be +disclosed. Regardless of whether they were written by a human or an LLM, pull +requests are expected to be of a reasonable size to be reviewed by a human. + +Your pull request description, and any communication with maintainers must be +written by a human. Do not copy/paste output from an LLM into a PR comment. Pull +requests that violate this policy will be closed and locked. + + diff --git a/README.md b/README.md index 6a8d088..487452d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,143 @@ -# PG Raw Parse -## Safe bindings to libpg_query +# pg_raw_parse -PG Raw Parse provides a low level wrapper around the PostgreSQL backend parser. -These bindings, as well as some additional functionality are provided by -[libpg\_query]. +`pg_raw_parse` is a Rust library that provides direct access to the PostgreSQL parser. It's 20-60x faster than [`pg_query.rs`](https://docs.rs/pg_query/latest/pg_query/) and uses 90% less memory (see [benchmarks](#benchmarks)). -In addition to parsing, we provide mechanisms to [traverse an AST], [construct -new ASTs], and [transform ASTs]. See the API docs for more details. +The library is primarily used in [PgDog](https://github.com/pgdogdev/pgdog), but has no dependencies +except [`libpg_query`](https://github.com/pganalyze/libpg_query), so it can be used in any Rust application to quickly parse and manipulate PgSQL. + +## Quick start + +We don't regularly publish the crate to crates.io, so you should install it via git dependency instead: + +```toml +# Cargo.toml +pg_raw_parse = { git = "https://github.com/pgdogdev/pg_raw_parse" } +``` + +This crate has a very similar API to `pg_query.rs`, e.g., to parse a query and get its AST, you can: + +```rust +use pg_raw_parse::{parse, deparse, normalize}; + +let ast = parse("SELECT * FROM users WHERE id = $1").unwrap(); +let query = deparse(&ast).unwrap(); +let normalized = normalize(&ast).unwrap(); // Doesn't require parsing the query again! +``` + +## Why another crate + +`libpg_query` uses Protobuf to provide access to its API to non-C languages, e.g., Rust, Ruby, Python, etc. This makes it very slow at runtime because it requires (de)serialization and additional memory allocations to pass the AST data structure across the FFI boundary. + +`pg_raw_parse` uses macros to generate Rust structs directly on top of the PostgreSQL arena allocator. This ensures that calls to its API require much fewer memory allocations, performed by the PostgreSQL memory context. + +Since most code is generated, upgrading major PostgreSQL versions only requires bumping up the `postgres` and `libpg_query` submodules. This allows us to stay current with upstream changes without much effort. + +You can read more about the crate's internals [below](#design). + +## Benchmarks + +You can reproduce our benchmarks [here](benchmarks). The following numbers are from my Mac M1 Max. + +![Benchmark](benchmark_parse.svg) + +### Parse + +```rust +let ast = pg_raw_parse::parse("SELECT 1").unwrap(); +``` + +| Query size (nodes) | `pg_query.rs` | `pg_raw_parse` | Speedup | +| -----------------: | ------------: | -------------: | ------: | +| 10 | 20.415 µs | 1.1200 µs | 18.23× | +| 100 | 107.14 µs | 5.6657 µs | 18.91× | +| 1,000 | 1.1002 ms | 51.615 µs | 21.32× | +| 2,000 | 2.5137 ms | 104.65 µs | 24.02× | +| 5,000 | 9.1901 ms | 275.39 µs | 33.37× | +| 10,000 | 32.179 ms | 541.03 µs | 59.48× | + +### Deparse + +```rust +let query = pg_raw_parse::deparse(&st).unwrap(); +``` + +| Query length (nodes) | `pg_query.rs` | `pg_raw_parse` | Speedup | +| -------------------: | ------------: | -------------: | ------: | +| 10 | 11.715 µs | 777.91 ns | 15.06× | +| 100 | 66.007 µs | 3.6580 µs | 18.04× | +| 1,000 | 613.31 µs | 35.260 µs | 17.39× | +| 2,000 | 1.2209 ms | 70.296 µs | 17.37× | +| 5,000 | 3.0952 ms | 178.90 µs | 17.30× | +| 10,000 | 6.3492 ms | 355.46 µs | 17.86× | + +### Normalize + +```rust +let normalized = pg_raw_parse::normalize("SELECT 1").unwrap(); // SELECT $1 +``` + +| Query length (nodes) | `pg_query.rs` | `pg_raw_parse` | Speedup | +| -------------------: | ------------: | -------------: | ------: | +| 10 | 3.1349 µs | 2.4581 µs | 1.28× | +| 100 | 16.974 µs | 11.776 µs | 1.44× | +| 1,000 | 144.84 µs | 108.65 µs | 1.33× | +| 2,000 | 289.67 µs | 221.25 µs | 1.31× | +| 5,000 | 767.01 µs | 550.07 µs | 1.39× | +| 10,000 | 1.4547 ms | 1.1976 ms | 1.21× | + +## Working with ASTs + +In addition to parsing queries, we provide mechanisms to [traverse an AST], [construct +new ASTs], and [transform ASTs]. + +### Examples + +Traverse a query to find its parameters: + +```rust +use pg_raw_parse::{Node, parse, walk}; + +let ast = parse("SELECT $1, $2").unwrap(); +walk::walk(ast.stmts().next().unwrap(), |node| { + if let Node::ParamRef(param) = node { + println!("${}", param.number); + } +}); +``` + +Construct a `SELECT $1` AST without parsing SQL: + +```rust +use pg_raw_parse::{deparse, make, nodes}; + +let ast = make::owned(|mem| { + let mut select = mem.make_node::(); + let target = mem.make_res_target(None, mem.empty(), mem.make_param_ref(1).uncast()); + select.as_mut().set_target_list(mem.make_list(&[target])); + select +}); +assert_eq!(deparse(&*ast).unwrap().as_str(), "SELECT $1"); +``` + +Transform a copy of an AST, replacing a literal with a parameter: + +```rust +use pg_raw_parse::{NodeMut, deparse, make, parse, transform}; + +let ast = parse("SELECT 42").unwrap(); +let changed = make::owned(|mem| { + let mut copy = mem.make_unique(ast.stmts().next().unwrap()); + transform::transform(&mut copy, |node| match &*node { + NodeMut::A_Const(_) => { + node.replace(mem.make_param_ref(1).uncast()); + None + } + _ => Some(node), + }); + mem.make_raw_stmt(copy) +}); +assert_eq!(deparse(&*changed).unwrap().as_str(), "SELECT $1"); +``` [traverse an AST]: https://docs.rs/pg_raw_parse/latest/pg_raw_parse/walk/index.html [construct new ASTs]: https://docs.rs/pg_raw_parse/latest/pg_raw_parse/make/index.html @@ -21,32 +152,49 @@ copies of data. PostgreSQL does not publish any header files or libraries to expose its backend functions. We use [libpg\_query], which embeds those files in a form that is -easy to compile without going through cmake, as well as makes a few changes +easy to compile without going through CMake, as well as makes a few changes to enable multithreaded usage. We also use this library for its `deparse` implementation, turning an AST back into a string. +### Structs + When possible, the structures in this library are cast directly from a pointer -to the C structure. The main exception to this is `Node *`, which is +to the C structure. + +The main exception to this is `Node *`, which is semantically an unsized enum. There is no way to represent an enum with -different sizes for each variant in Rust, so we need our own wrapper enum. The -tag is identical to the tag of the C enum, so LLVM *should* be able to optimize +different sizes for each variant in Rust, so we need our own wrapper enum. + +The tag is identical to the tag of the C enum, so LLVM _should_ be able to optimize this away in many cases but it is not guaranteed. -Everything in pg\_raw\_parse makes use of PostgreSQLs allocator, both for +### Memory architecture + +Everything in pg\_raw\_parse makes use of PostgreSQL's allocator, both for manipulating the structures returned by `parse`, and for [constructors provided -by this library][construct new ASTs]. It is assumed that ASTs are retained at -the scope of a single query. Each call to `parse` will return an AST with its -own arena. Individual nodes do not implement `Drop`, and are not freed until the +by this library][construct new ASTs]. + +It is assumed that ASTs are retained at the scope of a single query. Each call to `parse` will return an AST with its +own arena. + +Individual nodes do not implement `Drop`, and are not freed until the entire arena is dropped. This can result in slightly higher memory usage when -mutating ASTs, as nodes that are replaced will still occupy memory. But the -result is much less overhead from `palloc`/`pfree` in the most common usage +mutating ASTs, as nodes that are replaced will still occupy memory. + +The result is much less overhead from `palloc`/`pfree` in the most common usage patterns. +### Memory safety + To ensure that fields of an AST node are always allocated on the same arena as -its parent, we make use of [lifetime branding]. [`MemoryToken`] is a type that +its parent, we make use of [lifetime branding]. + +[`MemoryToken`] is a type that is used for constructing node allocated on a specific arena. Constructors require all fields to be [`Unique`], which represents a node allocated on that -same arena and is not assigned anywhere else. Once all construction/mutation is +same arena and is not assigned anywhere else. + +Once all construction/mutation is complete, the result is wrapped in [`Owned`], which is responsible for freeing the arena in its destructor. @@ -57,8 +205,9 @@ the arena in its destructor. Because individual nodes are never freed on their own, once an arena is inside of an `Owned`, it is frozen. It is only possible to get shared references to -fields within it, and its arena can never be used for allocations again. This -decision was made to make it impossible to cause a memory leak by holding a long +fields within it, and its arena can never be used for allocations again. + +This decision was made to make it impossible to cause a memory leak by holding a long lived reference to an AST, and then mutating it repeatedly. Instead, to mutate an `Owned` node, it must first be copied onto a new memory arena using [`make_unique`]. @@ -68,26 +217,32 @@ an `Owned` node, it must first be copied onto a new memory arena using The majority of the code in this library is generated from C header files, with the exception of extremely generic code such as list manipulation. We first run these header files through [bindgen], and then operate on the resulting code as -if it were a procedural macro. Although this code lives in -[build.rs](blob/main/build.rs), its patterns should be familiar to developers +if it were a procedural macro. + +Although this code lives in [build.rs](build.rs), its patterns should be familiar to developers familiar with writing procedural macros. [bindgen]: https://github.com/rust-lang/rust-bindgen +### Memory layout + We create our own layout compatible structs rather than directly exposing the structs generated by bindgen. This is to give us control over the visibility of -fields, as we don't want raw pointer fields to be public. We generate accessor -methods that convert to our custom type, and check the tag so an invalid node +fields, as we don't want raw pointer fields to be public. + +We generate accessor methods that convert to our custom type, and check the tag so an invalid node assignment results in a panic rather than undefined behavior. In particular, this is required for `Node*`, which cannot be represented in Rust as a simple pointer cast for the reasons mentioned above. +### Compatibility + C has no concept of generics, so all lists are untyped lists of nodes. However, many of those fields have documentation stating that they are a list of a single type of node. We look for those comments, and change the type of the field to a typed list if we find one. -[AST traversal][walk an AST] is done using PostgreSQL's internal +[AST traversal](#working-with-asts) is done using PostgreSQL's internal `raw_expression_tree_walker` function, with a thin wrapper to handle passing a Rust closure to C and transform PostgreSQL's exceptions into Rust panics. [AST transformation][transform ASTs] is done with generated code. @@ -104,7 +259,7 @@ by the same team who maintains [libpg\_query]. While both libraries depend on [libpg\_query] to get access to PostgreSQL's internal parser, [pg\_query.rs] uses [libpg\_query]'s protobuf serialization layer to somewhat decouple it from PostgreSQL's internal details. This type of approach makes sense when you're -maintaining bindings for multiple languages. But Rust's strong C FFI means a +maintaining bindings for multiple languages, but Rust's strong C FFI means a lower level binding allows us to avoid many of the drawbacks of that approach. We are able to avoid the overhead of protobuf de/serialization, as well as @@ -118,52 +273,23 @@ And the cost of "constructing" the Rust structures is at most a pointer cast and a tag check. These two factors result in pg\_raw\_parse performing significantly better, with the gap increasing as the size of the AST increases. -#### Parse time - -![Speed benchmark graph](raw/main/benchmark_time.png) - -#### Parse time (log scale) - -![Speed benchmark graph (log scale)](raw/main/benchmark_time_log.png) - -#### Memory Usage - -![Memory benchmark graph](raw/main/benchmark_mem.png) - -#### Memory Usage (log scale) - -![Memory benchmark graph (log scale)](raw/main/benchmark_mem_log.png) - ## Contributing -This library's API surface is primarily driven by the needs of -[PgDog](https://github.com/pgdogdev/pgdog). It is not intended to be a complete, -one-size-fits-all solution to PostgreSQL ASTs. Contributions are welcome, but -pull requests adding large and complex features are unlikely to be accepted -unless they align with PgDog's needs. For a more general purpose library, -consider [pg\_query.rs]. +See [Contribution Guidelines](CONTRIBUTING.md). ## License Licensed under either of these: - * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or - https://www.apache.org/licenses/LICENSE-2.0) - * MIT license ([LICENSE-MIT](LICENSE-MIT) or - https://opensource.org/licenses/MIT) - -[libpg\_query]: https://github.com/pganalyze/libpg_query -[pg\_query.rs]: https://github.com/pganalyze/pg_query.rs - -## LLM Policy +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + https://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or + https://opensource.org/licenses/MIT) -This library was primarily written by humans, without LLM assistance. While LLM -assisted pull requests are allowed, any code written by an LLM must be -disclosed. Regardless of whether they were written by a human or an LLM, pull -requests are expected to be of a reasonable size to be reviewed by a human. +## Prior art -Your pull request description, and any communication with maintainers must be -written by a human. Do not copy/paste output from an LLM into a PR comment. Pull -requests that violate this policy will be closed and locked. +- [libpg_query](https://github.com/pganalyze/libpg_query) +- [pg_query.rs](https://github.com/pganalyze/pg_query.rs) - +[libpg_query]: https://github.com/pganalyze/libpg_query +[pg_query.rs]: https://github.com/pganalyze/pg_query.rs diff --git a/benchmark_parse.svg b/benchmark_parse.svg new file mode 100644 index 0000000..753b85b --- /dev/null +++ b/benchmark_parse.svg @@ -0,0 +1,455 @@ + + + + PostgreSQL parse and deparse performance + + + + + PostgreSQL parse and deparse performance + Parse and deparse timings for pg_raw_parse and pg_query.rs on Apple M1 Max. Query sizes are equally spaced categories. Both charts use the same logarithmic time scale. + image/svg+xml + + + Matplotlib v3.11.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + 10 + + + + + + 100 + + + + + + 1,000 + + + + + + 2,000 + + + + + + 5,000 + + + + + + 10,000 + + + + Parse query length + + + + + + + + + + 1 µs + + + + + + + + + 10 µs + + + + + + + + + 100 µs + + + + + + + + + 1 ms + + + + + + + + + 10 ms + + + + + + + + + 100 ms + + + + Time per execution + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.1200 µs + + + 5.6657 µs + + + 51.615 µs + + + 104.65 µs + + + 275.39 µs + + + 541.03 µs + + + 20.415 µs + + + 107.14 µs + + + 1.1002 ms + + + 2.5137 ms + + + 9.1901 ms + + + 32.179 ms + + + + + + + + + + + 10 + + + + + + 100 + + + + + + 1,000 + + + + + + 2,000 + + + + + + 5,000 + + + + + + 10,000 + + + + Deparse query length + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 777.91 ns + + + 3.6580 µs + + + 35.260 µs + + + 70.296 µs + + + 178.90 µs + + + 355.46 µs + + + 11.715 µs + + + 66.007 µs + + + 613.31 µs + + + 1.2209 ms + + + 3.0952 ms + + + 6.3492 ms + + + Log scale + + + + + + + + + + + pg_raw_parse + + + + + + + + + pg_query.rs + + + + + + + + + + + + diff --git a/benchmarks/.gitignore b/benchmarks/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/benchmarks/.gitignore @@ -0,0 +1 @@ +/target diff --git a/benchmarks/Cargo.lock b/benchmarks/Cargo.lock new file mode 100644 index 0000000..3c00cc8 --- /dev/null +++ b/benchmarks/Cargo.lock @@ -0,0 +1,1031 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "benchmarks" +version = "0.1.0" +dependencies = [ + "criterion", + "pg_query", + "pg_raw_parse", +] + +[[package]] +name = "bindgen" +version = "0.66.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex 1.3.0", + "syn 2.0.119", + "which", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.3", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "page_size", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools 0.13.0", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "generativity" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c81fb5260e37854d09d5c87183309fd8c555b75289427884b25660bc87a85e" + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pg_query" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ca6fdb8f9d32182abf17328789f87f305dd8c8ce5bf48c5aa2b5cffc94e1c04" +dependencies = [ + "bindgen 0.66.1", + "cc", + "fs_extra", + "glob", + "itertools 0.10.5", + "prost", + "prost-build", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "pg_raw_parse" +version = "0.1.0" +dependencies = [ + "bindgen 0.72.1", + "cc", + "convert_case", + "generativity", + "glob", + "libc", + "prettyplease", + "regex", + "syn 2.0.119", + "thiserror 2.0.20", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.119", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml new file mode 100644 index 0000000..381e6e2 --- /dev/null +++ b/benchmarks/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "benchmarks" +version = "0.1.0" +edition = "2024" + +[features] +default = [] +pg_query = ["dep:pg_query"] +pg_raw_parse = ["dep:pg_raw_parse"] + +[dependencies] +criterion = { version = "0.8.2", default-features = false } +pg_query = { version = "6.1.1", optional = true } +pg_raw_parse = { path = "../", optional = true } + +[[bin]] +name = "benchmarks" +path = "src/main.rs" +bench = true +harness = false diff --git a/benchmarks/plot_parse.py b/benchmarks/plot_parse.py new file mode 100644 index 0000000..5806b19 --- /dev/null +++ b/benchmarks/plot_parse.py @@ -0,0 +1,102 @@ +"""Generate benchmark_parse.svg. Requires matplotlib: pip install matplotlib.""" + +from pathlib import Path + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from matplotlib.ticker import FixedLocator, FuncFormatter + + +# Criterion central estimates from the README, retaining their displayed units. +SIZES = (10, 100, 1_000, 2_000, 5_000, 10_000) +BENCHMARKS = ( + ( + "parse", + ("20.415 µs", "107.14 µs", "1.1002 ms", "2.5137 ms", "9.1901 ms", "32.179 ms"), + ("1.1200 µs", "5.6657 µs", "51.615 µs", "104.65 µs", "275.39 µs", "541.03 µs"), + ), + ( + "deparse", + ("11.715 µs", "66.007 µs", "613.31 µs", "1.2209 ms", "3.0952 ms", "6.3492 ms"), + ("777.91 ns", "3.6580 µs", "35.260 µs", "70.296 µs", "178.90 µs", "355.46 µs"), + ), +) + + +def create_figure(): + plt.rcParams.update({ + "font.family": "DejaVu Sans", + "font.size": 16.5, + "text.color": "#24352f", + "axes.labelcolor": "#43554d", + "xtick.color": "#43554d", + "ytick.color": "#43554d", + "axes.spines.top": False, + "axes.spines.right": False, + "axes.spines.left": False, + "axes.spines.bottom": False, + "svg.fonttype": "none", + "svg.hashsalt": "pg-raw-parse-benchmark", + }) + fig, axes = plt.subplots(1, 2, figsize=(20, 6)) + fig.subplots_adjust(left=0.09, right=0.98, top=0.85, bottom=0.20, wspace=0.08) + for times, (operation, query_labels, raw_labels) in zip(axes, BENCHMARKS): + draw_chart(times, operation, query_labels, raw_labels) + handles, labels = axes[0].get_legend_handles_labels() + center = (axes[0].get_position().x0 + axes[-1].get_position().x1) / 2 + fig.legend(handles, labels, loc="lower center", bbox_to_anchor=(center, 0.92), + ncol=2, frameon=False, borderaxespad=0) + return fig + + +def draw_chart(times, operation, query_labels, raw_labels): + x = list(range(len(SIZES))) + for labels, color, marker, linestyle, name, offset in ( + (raw_labels, "#355ba9", "o", "-", "pg_raw_parse", -36), + (query_labels, "#b96a47", "s", "--", "pg_query.rs", 26), + ): + values = [] + for label in labels: + value, unit = label.split() + values.append(float(value) * {"ns": 0.001, "µs": 1, "ms": 1_000}[unit]) + times.plot(x, values, label=name, color=color, marker=marker, + linestyle=linestyle, linewidth=2.4, markersize=6) + for position, value, label in zip(x, values, labels): + times.annotate(label, (position, value), xytext=(0, offset), + textcoords="offset points", ha="center", fontsize=16.5, + color=color) + + times.set_yscale("log") + times.set_ylim(0.08, 200_000) + times.set_xlim(-0.45, 5.45) + times.yaxis.set_major_locator(FixedLocator([1, 10, 100, 1_000, 10_000, 100_000])) + times.yaxis.set_major_formatter(FuncFormatter( + lambda value, _: f"{value / 1_000:g} ms" if value >= 1_000 else f"{value:g} µs" + )) + times.minorticks_off() + if operation != "deparse": + times.set_ylabel("Time per execution", labelpad=14) + times.grid(axis="y", color="#e2e8e3", linewidth=0.8) + times.set_axisbelow(True) + times.tick_params(axis="both", length=0, pad=16, labelsize=16.5) + if operation == "deparse": + times.tick_params(axis="y", labelleft=False) + if operation != "parse": + times.text(1, 1.10, "Log scale", transform=times.transAxes, + ha="right", fontsize=15, color="#63746b") + times.set_xticks(x, [f"{size:,}" for size in SIZES]) + times.set_xlabel(f"{operation.capitalize()} query length", labelpad=24) + + +if __name__ == "__main__": + output = Path(__file__).resolve().parents[1] / "benchmark_parse.svg" + figure = create_figure() + figure.savefig(output, format="svg", metadata={ + "Date": None, + "Title": "PostgreSQL parse and deparse performance", + "Description": "Parse and deparse timings for pg_raw_parse and pg_query.rs on Apple M1 Max. Query sizes are equally spaced categories. Both charts use the same logarithmic time scale.", + }) + plt.close(figure) + print(output) diff --git a/benchmarks/src/main.rs b/benchmarks/src/main.rs new file mode 100644 index 0000000..1b3f898 --- /dev/null +++ b/benchmarks/src/main.rs @@ -0,0 +1,103 @@ +#[cfg(all(feature = "pg_query", feature = "pg_raw_parse"))] +compile_error!("features `pg_query` and `pg_raw_parse` are mutually exclusive; enable only one"); + +#[cfg(any(feature = "pg_query", feature = "pg_raw_parse"))] +fn main() { + use criterion::{BenchmarkId, Criterion, Throughput}; + use std::fmt::Write; + use std::hint::black_box; + + #[cfg(feature = "pg_query")] + use pg_query as parser; + #[cfg(all(feature = "pg_raw_parse", not(feature = "pg_query")))] + use pg_raw_parse as parser; + + // Reuse Criterion's positional filter as the required benchmark selector. + let benchmark = match std::env::args().nth(1).as_deref() { + Some("parse") => "parse", + Some("deparse") => "deparse", + Some("normalize") => "normalize", + Some("normalize_str") => "normalize_str", + _ => { + eprintln!( + "Usage: benchmarks [Criterion options]" + ); + std::process::exit(2); + } + }; + + let parser_name = if cfg!(feature = "pg_query") { + "pg_query" + } else { + "pg_raw_parse" + }; + let mut criterion = Criterion::default().configure_from_args(); + let mut group = criterion.benchmark_group(format!("{parser_name}::{benchmark}")); + // One element is one complete operation, so elem/s is operations per second. + group.throughput(Throughput::Elements(1)); + + for nodes in [10, 100, 1_000, 2_000, 5_000, 10_000] { + // Each IN-list value adds an A_Const node; the rest of the AST is fixed. + // Generate the SQL once, outside the timed loop. + let mut sql = String::from("SELECT * FROM users WHERE id IN ("); + for id in 0..nodes { + if id > 0 { + sql.push(','); + } + write!(sql, "{id}").unwrap(); + } + sql.push(')'); + + if benchmark == "parse" { + group.bench_with_input(BenchmarkId::from_parameter(nodes), &sql, |b, sql| { + b.iter(|| parser::parse(black_box(sql.as_str())).unwrap()); + }); + } else if benchmark == "normalize_str" { + // Include parsing, normalization, and SQL output in each iteration. + group.bench_with_input(BenchmarkId::from_parameter(nodes), &sql, |b, sql| { + #[cfg(feature = "pg_query")] + b.iter(|| parser::normalize(black_box(sql.as_str())).unwrap()); + + #[cfg(all(feature = "pg_raw_parse", not(feature = "pg_query")))] + b.iter(|| parser::normalize::normalize_str(black_box(sql.as_str())).unwrap()); + }); + } else if benchmark == "normalize" { + #[cfg(feature = "pg_query")] + group.bench_with_input(BenchmarkId::from_parameter(nodes), &sql, |b, sql| { + b.iter(|| parser::normalize(black_box(sql.as_str())).unwrap()); + }); + + #[cfg(all(feature = "pg_raw_parse", not(feature = "pg_query")))] + { + // Normalize the AST directly, excluding parsing and deparsing. + let parsed = parser::parse(&sql).unwrap(); + let stmt = parsed.first().unwrap(); + group.bench_function(BenchmarkId::from_parameter(nodes), |b| { + b.iter(|| parser::normalize::normalize(black_box(stmt))); + }); + } + } else { + // Parse once so deparse timing excludes constructing the AST. + let parsed = parser::parse(&sql).unwrap(); + group.bench_with_input(BenchmarkId::from_parameter(nodes), &parsed, |b, parsed| { + #[cfg(feature = "pg_query")] + b.iter(|| parser::deparse(black_box(&parsed.protobuf)).unwrap()); + + #[cfg(all(feature = "pg_raw_parse", not(feature = "pg_query")))] + { + let stmt = parsed.first().unwrap(); + b.iter(|| parser::deparse(black_box(stmt)).unwrap()); + } + }); + } + } + + group.finish(); + criterion.final_summary(); +} + +#[cfg(not(any(feature = "pg_query", feature = "pg_raw_parse")))] +fn main() { + eprintln!("Enable one parser feature: --features pg_query or --features pg_raw_parse"); + std::process::exit(1); +}