From 5b4d7abc2df95c7f5b3828be5eab1ef4089cbdec Mon Sep 17 00:00:00 2001 From: Sage Griffin Date: Thu, 16 Jul 2026 09:32:01 -0600 Subject: [PATCH] Implement our own version of `normalize` This introduces a reimplementation of normalize using our primitives instead of the implementation in libpg_query. In addition to reducing our reliance on that library, this implementation takes advantage of the fact that for our use case we will already have a parsed AST to pass in. libpg_query takes a string, parses it, records the location of any constants, then copies and manipulates that string to replace the constants. In contrast, this implementation just copies the AST, walks it, replaces any constants with parameters, and changes the numbers on any parameters it encounters. In an effort to do this change in a single iteration over the parse tree, we don't preserve any bind parameters that were in the original query, which you can see reflected in the test that was updated. On an extremely large query, the new implementation is 4.5x faster. If you include deparse time it is about twice as fast, and including parse + deparse time it is roughly 10% slower. While it isn't ideal that the full parse + normalize + deparse flow is slightly slower, in pgdog we'd only be using the normalize + deparse flow, so this results in a significant performance win as well as a reduction in unsafe code. --- Cargo.lock | 431 +---------------------------------------------- Cargo.toml | 2 +- benches/bench.rs | 22 ++- build.rs | 2 - src/normalize.rs | 64 ++++--- 5 files changed, 61 insertions(+), 460 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1281665..a20e052 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,12 +32,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - [[package]] name = "autocfg" version = "1.5.1" @@ -53,7 +47,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools", "log", "prettyplease", "proc-macro2", @@ -76,12 +70,6 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - [[package]] name = "cast" version = "0.3.0" @@ -197,7 +185,7 @@ dependencies = [ "ciborium", "clap", "criterion-plot", - "itertools 0.13.0", + "itertools", "num-traits", "oorandom", "page_size", @@ -217,7 +205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", - "itertools 0.13.0", + "itertools", ] [[package]] @@ -263,52 +251,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" -[[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", -] - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures-core" version = "0.3.32" @@ -339,19 +287,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2c81fb5260e37854d09d5c87183309fd8c555b75289427884b25660bc87a85e" -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - [[package]] name = "glob" version = "0.3.3" @@ -369,54 +304,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[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 = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[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" @@ -443,12 +330,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.186" @@ -465,12 +346,6 @@ dependencies = [ "windows-link", ] -[[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.32" @@ -489,12 +364,6 @@ 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" @@ -536,33 +405,6 @@ dependencies = [ "winapi", ] -[[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 = "git+https://github.com/pgdogdev/pg_query.rs.git#97019d0c13ad0b888fe91ee5bed5448b5f409cdd" -dependencies = [ - "bindgen", - "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" @@ -574,12 +416,11 @@ dependencies = [ "generativity", "glob", "libc", - "pg_query", "pretty_assertions", "prettyplease", "regex", "syn", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -645,58 +486,6 @@ 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.13.0", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn", - "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.13.0", - "proc-macro2", - "quote", - "syn", -] - -[[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.45" @@ -706,12 +495,6 @@ 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 = "rayon" version = "1.12.0" @@ -767,19 +550,6 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" -[[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", - "windows-sys", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -795,12 +565,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - [[package]] name = "serde" version = "1.0.228" @@ -873,46 +637,13 @@ dependencies = [ "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", - "windows-sys", -] - -[[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.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[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", + "thiserror-impl", ] [[package]] @@ -948,12 +679,6 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "walkdir" version = "2.5.0" @@ -964,24 +689,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - [[package]] name = "wasm-bindgen" version = "0.2.125" @@ -1027,40 +734,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" version = "0.3.102" @@ -1117,100 +790,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index d4154f4..bdf57e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ syn = { version = "2.0.118", features = ["full", "visit", "extra-traits"] } [dev-dependencies] criterion = "0.8.2" -pg_query = { version = "6.1.1", git = "https://github.com/pgdogdev/pg_query.rs.git" } +# pg_query = { version = "6.1.1", git = "https://github.com/pgdogdev/pg_query.rs.git" } pretty_assertions = "1.4.1" syn = { version = "2.0.118", features = ["full", "extra-traits"] } diff --git a/benches/bench.rs b/benches/bench.rs index 29af48e..14c5308 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -11,10 +11,26 @@ fn benchmark(c: &mut Criterion) { group.bench_function("pg_raw_parse::parse", |b| { b.iter(|| pg_raw_parse::parse(black_box(&*sql)).unwrap()) }); - group.bench_function("pg_query::parse", |b| { - b.iter(|| pg_query::parse(black_box(&*sql)).unwrap()) - }); + // group.bench_function("pg_query::parse", |b| { + // b.iter(|| pg_query::parse_raw(black_box(&*sql)).unwrap()) + // }); group.finish(); + + let parsed = pg_raw_parse::parse(&sql).unwrap(); + let stmt = parsed.first().unwrap(); + let mut group = c.benchmark_group("normalize"); + // group.bench_function("pg_query_normalize", |b| { + // b.iter(|| pg_query::normalize(black_box(&*sql))); + // }); + group.bench_function("custom normalize", |b| { + b.iter(|| pg_raw_parse::normalize::normalize(black_box(stmt))); + }); + group.bench_function("custom normalize + deparse", |b| { + b.iter(|| pg_raw_parse::deparse(&*pg_raw_parse::normalize::normalize(black_box(stmt)))); + }); + group.bench_function("parse + custom normalize + deparse", |b| { + b.iter(|| pg_raw_parse::normalize::normalize_str(black_box(&*sql))); + }); } criterion_group!(benches, benchmark); diff --git a/build.rs b/build.rs index dbb61dd..89233f3 100644 --- a/build.rs +++ b/build.rs @@ -129,8 +129,6 @@ fn main() { .allowlist_item("list_concat") .allowlist_item("wrapped_copy_object") .allowlist_item("newNode") - .allowlist_item("pg_query_normalize") - .allowlist_item("pg_query_free_normalize_result") .wrap_static_fns(true) .wrap_static_fns_path(out_dir.join("wrap_static_fns")); for struct_name in &node_structs { diff --git a/src/normalize.rs b/src/normalize.rs index c4c5c66..b2645ce 100644 --- a/src/normalize.rs +++ b/src/normalize.rs @@ -1,39 +1,47 @@ -use crate::{Error, raw}; -use libc::free; -use std::ffi::{CStr, CString}; -use std::ptr::NonNull; +use crate::transform::{Transform, TransformClosure}; +use crate::{DeparseResult, Node, NodeMut, Owned, deparse, make, nodes, parse}; -pub fn normalize(query: &str) -> crate::Result { - let input = CString::new(query)?; - // SAFETY: No documented invariants, so we assume it's safe as long as it - // gets a valid nul terminated string - let result = unsafe { raw::pg_query_normalize(input.as_ptr()) }; +pub fn normalize(query: &nodes::RawStmt) -> Owned { + make::owned(|mem| { + let mut copy = mem.make_unique(query); + let mut param_count = 0; + TransformClosure::new(|mut node| match &mut *node { + NodeMut::A_Const(_) => { + param_count += 1; + node.replace(mem.make_param_ref(param_count).uncast()); + None + } + NodeMut::ParamRef(p) => { + param_count += 1; + p.set_number(param_count); + None + } + _ => Some(node), + }) + .transform_raw_stmt(copy.as_mut()); + copy + }) +} - match NonNull::new(result.error) { - Some(err) => { - // SAFETY: This is either a malloc'd pointer or NULL - unsafe { free(result.normalized_query.cast()) }; - Err(Error::from_pg_query_error(err)) - } - None => { - // SAFETY: This is always valid if no error was returned - let c_str = unsafe { CStr::from_ptr(result.normalized_query) }; - let string = c_str.to_string_lossy().into_owned(); - // SAFETY: We aren't holding onto any pointers from this now - unsafe { raw::pg_query_free_normalize_result(result) }; - Ok(string) - } +pub fn normalize_str(query: &str) -> crate::Result { + let tree = parse(query)?; + if let Some(stmt) = tree.first() { + deparse(&*normalize(stmt)) + } else { + deparse(Node::None) } } #[test] fn test_normalize_does_the_thing() { - let normalized = normalize("SELECT * FROM users WHERE id = 1").unwrap(); - assert_eq!(normalized, "SELECT * FROM users WHERE id = $1"); + let normalized = normalize_str("SELECT * FROM users WHERE id = 1").unwrap(); + assert_eq!(normalized.as_str(), "SELECT * FROM users WHERE id = $1"); - let normalized = normalize("SELECT * FROM users WHERE id = 1 AND name = $1").unwrap(); + let normalized = normalize_str("SELECT * FROM users WHERE id = 1 AND name = $1").unwrap(); assert_eq!( - normalized, - "SELECT * FROM users WHERE id = $2 AND name = $1" + normalized.as_str(), + "SELECT * FROM users WHERE id = $1 AND name = $2" ); + + assert!(normalize_str("").is_err()); }