sql_insight/normalizer.rs
1//! SQL normalization — rewrite the AST so structurally identical
2//! queries hash to the same string. See [`normalize`] as the entry
3//! point.
4//!
5//! The base pass replaces every literal `Value` with a `?`
6//! placeholder, so queries that differ only in their parameter
7//! values collapse to the same string.
8//!
9//! "Every literal" is meant literally: it includes `Value`s in
10//! structurally significant positions, not just bound-parameter slots.
11//! A JSON path (`JSON_TABLE(data, '$.a')`, `JSON_EXTRACT(data, '$.a')`),
12//! a `CAST(x AS DATE FORMAT 'YYYY-MM-DD')` format string, the
13//! `TABLESAMPLE (BUCKET 3 OUT OF 10)` / `(10 PERCENT)` counts, and
14//! `LIMIT` / `OFFSET` are all rewritten to `?`. So two queries differing
15//! only in such a literal — e.g. selecting a different JSON field or
16//! sampling a different bucket — collapse to the same normalized string.
17//!
18//! Three opt-in toggles ([`NormalizerOptions`]) further collapse
19//! repetitive shapes:
20//!
21//! - [`unify_in_list`](NormalizerOptions::unify_in_list):
22//! `IN (1, 2, 3)` → `IN (...)`.
23//! - [`unify_values`](NormalizerOptions::unify_values):
24//! `VALUES (1, 2, 3), (4, 5, 6)` → `VALUES (...)`.
25//! - [`alphabetize_insert_columns`](NormalizerOptions::alphabetize_insert_columns):
26//! `INSERT INTO t (c, b, a) VALUES (...)` →
27//! `INSERT INTO t (a, b, c) VALUES (...)`, only when VALUES is
28//! unified.
29//!
30//! Output is one `String` per parsed statement, formatted by
31//! sqlparser's `Display` after the rewrite.
32
33use std::ops::{ControlFlow, Deref};
34
35use crate::error::Error;
36use sqlparser::ast::{Expr, Insert, Statement, VisitMut, VisitorMut};
37use sqlparser::ast::{Parens, Query, SetExpr, TopQuantity, Value, ValueWithSpan};
38use sqlparser::dialect::Dialect;
39use sqlparser::parser::Parser;
40use std::ops::DerefMut;
41
42/// Parse `sql` under `dialect` and normalize each statement with
43/// default options (literal-to-`?` placeholder substitution only).
44///
45/// ## Example
46///
47/// ```rust
48/// use sql_insight::sqlparser::dialect::GenericDialect;
49///
50/// let dialect = GenericDialect {};
51/// let sql = "SELECT a FROM t1 WHERE b = 1 AND c in (2, 3) AND d LIKE '%foo'";
52/// let result = sql_insight::normalizer::normalize(&dialect, sql).unwrap();
53/// assert_eq!(result, ["SELECT a FROM t1 WHERE b = ? AND c IN (?, ?) AND d LIKE ?"]);
54/// ```
55pub fn normalize(dialect: &dyn Dialect, sql: &str) -> Result<Vec<String>, Error> {
56 Normalizer::normalize(dialect, sql, NormalizerOptions::new())
57}
58
59/// Parse `sql` under `dialect` and normalize each statement,
60/// applying any extra collapses enabled in `options`.
61///
62/// ## Example
63///
64/// ```rust
65/// use sql_insight::sqlparser::dialect::GenericDialect;
66/// use sql_insight::normalizer::{normalize_with_options, NormalizerOptions};
67///
68/// let dialect = GenericDialect {};
69/// let sql = "SELECT a FROM t1 WHERE b = 1 AND c in (2, 3, 4)";
70/// let result = normalize_with_options(&dialect, sql, NormalizerOptions::new().with_unify_in_list(true)).unwrap();
71/// assert_eq!(result, ["SELECT a FROM t1 WHERE b = ? AND c IN (...)"]);
72/// ```
73pub fn normalize_with_options(
74 dialect: &dyn Dialect,
75 sql: &str,
76 options: NormalizerOptions,
77) -> Result<Vec<String>, Error> {
78 Normalizer::normalize(dialect, sql, options)
79}
80
81/// Toggles for [`normalize_with_options`]. Defaults to all `false`
82/// (placeholder substitution only).
83#[derive(Default, Clone)]
84pub struct NormalizerOptions {
85 /// Unify IN lists to a single form when all elements are literal values.
86 /// For example, `IN (1, 2, 3)` becomes `IN (...)`.
87 pub unify_in_list: bool,
88 /// Unify VALUES lists to a single form when all elements are literal values.
89 /// For example, `VALUES (1, 2, 3), (4, 5, 6)` becomes `VALUES (...)`.
90 pub unify_values: bool,
91 /// Alphabetize column lists for INSERT statements with a VALUES expression
92 /// that gets unified.
93 /// For example, `INSERT INTO t(c, b, a)` becomes `INSERT INTO t(a, b, c)`.
94 pub alphabetize_insert_columns: bool,
95}
96
97impl NormalizerOptions {
98 pub fn new() -> Self {
99 Self::default()
100 }
101
102 pub fn with_unify_in_list(mut self, unify_in_list: bool) -> Self {
103 self.unify_in_list = unify_in_list;
104 self
105 }
106
107 pub fn with_unify_values(mut self, unify_values: bool) -> Self {
108 self.unify_values = unify_values;
109 self
110 }
111
112 pub fn with_alphabetize_insert_columns(mut self, alphabetize_insert_columns: bool) -> Self {
113 self.alphabetize_insert_columns = alphabetize_insert_columns;
114 self
115 }
116}
117
118/// `VisitorMut` impl that performs the normalization rewrite.
119/// Most callers go through [`normalize`] / [`normalize_with_options`]
120/// or [`Normalizer::normalize`] (which constructs and drives this
121/// visitor internally). Use the struct directly only when you want
122/// to integrate the rewrite into a larger AST traversal.
123#[derive(Default)]
124pub struct Normalizer {
125 pub options: NormalizerOptions,
126}
127
128impl VisitorMut for Normalizer {
129 type Break = ();
130
131 fn post_visit_query(&mut self, query: &mut Query) -> ControlFlow<Self::Break> {
132 // `SELECT TOP 10`'s quantity is a bare `u64` (`TopQuantity::Constant`),
133 // not a `Value`, so the visitor's literal pass doesn't reach it — unlike
134 // `TOP (expr)` / `LIMIT`. Normalize it here so `TOP 10` becomes `TOP ?`.
135 normalize_top(query.body.deref_mut());
136 if let SetExpr::Values(values) = query.body.deref_mut() {
137 if self.options.unify_values {
138 let rows = &mut values.rows;
139 if rows.is_empty()
140 || rows.iter().all(|row| {
141 row.is_empty() || row.iter().all(|expr| matches!(expr, Expr::Value(_)))
142 })
143 {
144 // `Values::rows` is `Vec<Parens<Vec<Expr>>>` (each row
145 // tracks its own parentheses tokens); wrap the collapsed
146 // sentinel row accordingly.
147 *rows = vec![Parens::with_empty_span(vec![Expr::Value(
148 Value::Placeholder("...".into()).with_empty_span(),
149 )])];
150 }
151 }
152 }
153 ControlFlow::Continue(())
154 }
155
156 fn post_visit_statement(
157 &mut self,
158 stmt: &mut sqlparser::ast::Statement,
159 ) -> ControlFlow<Self::Break> {
160 if self.options.alphabetize_insert_columns {
161 if let Statement::Insert(Insert {
162 columns,
163 after_columns,
164 source,
165 ..
166 }) = stmt
167 {
168 if let Some(Query { body, .. }) = source.as_deref() {
169 // A parenthesized source (`INSERT … (VALUES …)`) nests
170 // the VALUES under a `SetExpr::Query` — peel to it, so
171 // both spellings alphabetize alike.
172 let mut body = body.deref();
173 while let SetExpr::Query(q) = body {
174 body = q.body.deref();
175 }
176 if let SetExpr::Values(v) = body {
177 // `Parens` equality ignores its parenthesis tokens
178 // (their `PartialEq` is always-equal), so this compares
179 // the row content alone — matching the sentinel above.
180 if v.rows
181 == vec![Parens::with_empty_span(vec![Expr::Value(
182 Value::Placeholder("...".into()).with_empty_span(),
183 )])]
184 {
185 if columns.len() > 1 {
186 // `Insert::columns` is now `Vec<ObjectName>`;
187 // sort by the (unquoted) final identifier part,
188 // preserving the old `Ident::value` key.
189 columns.sort_by_key(|s| {
190 s.0.last()
191 .and_then(|p| p.as_ident())
192 .map(|i| i.value.to_lowercase())
193 .unwrap_or_default()
194 });
195 }
196 if after_columns.len() > 1 {
197 after_columns.sort_by_key(|s| s.value.to_lowercase());
198 }
199 }
200 }
201 }
202 }
203 }
204 ControlFlow::Continue(())
205 }
206
207 fn pre_visit_expr(&mut self, expr: &mut Expr) -> ControlFlow<Self::Break> {
208 // A unary op over a literal — directly (`-9`) or through a chain of
209 // unary ops (`- -9`, `+ -9`) — collapses to a *single* placeholder
210 // (`?`, not `-?`). A parenthesised operand (`NOT (TRUE)`) is an
211 // `Expr::Nested`, not a chain, so it isn't collapsed — only its inner
212 // value is, by `pre_visit_value` on descent. Every other literal —
213 // including a plain `Expr::Value` — is normalized by `pre_visit_value`,
214 // so it needs no arm here.
215 if let Expr::UnaryOp { op: _, expr: child } = expr {
216 if Self::is_unary_chain_over_value(child) {
217 *expr = Expr::Value(Value::Placeholder("?".into()).with_empty_span());
218 }
219 }
220 ControlFlow::Continue(())
221 }
222
223 fn pre_visit_value(&mut self, value: &mut ValueWithSpan) -> ControlFlow<Self::Break> {
224 // The base contract: *every* literal `Value` becomes `?`, wherever the
225 // AST holds it. `pre_visit_expr` only catches an `Expr::Value`; a
226 // literal kept in a bare `Value` field — `DATE '…'` / `TIMESTAMP '…'`
227 // (`TypedString`), a `LIKE … ESCAPE '!'` char, a `MATCH … AGAINST '…'`
228 // search string — is reached only through this hook. The visitor now
229 // hands us a `ValueWithSpan`; rewrite the inner `value`, keeping the span.
230 value.value = Value::Placeholder("?".into());
231 ControlFlow::Continue(())
232 }
233
234 fn post_visit_expr(&mut self, expr: &mut Expr) -> ControlFlow<Self::Break> {
235 match expr {
236 Expr::InList { list, .. }
237 if self.options.unify_in_list
238 && list.iter().all(Self::contains_only_tuples_of_values) =>
239 {
240 *list = vec![Expr::Value(
241 Value::Placeholder("...".into()).with_empty_span(),
242 )];
243 }
244 _ => {}
245 }
246 ControlFlow::Continue(())
247 }
248}
249
250impl Normalizer {
251 pub fn new() -> Self {
252 Self::default()
253 }
254
255 pub fn with_options(mut self, options: NormalizerOptions) -> Self {
256 self.options = options;
257 self
258 }
259
260 /// Parse and normalize `sql`. [`normalize`] / [`normalize_with_options`]
261 /// are thin free-function wrappers around this.
262 pub fn normalize(
263 dialect: &dyn Dialect,
264 sql: &str,
265 options: NormalizerOptions,
266 ) -> Result<Vec<String>, Error> {
267 let mut statements = Parser::parse_sql(dialect, sql)?;
268 let _ = statements.visit(&mut Self::new().with_options(options));
269 Ok(statements
270 .into_iter()
271 .map(|statement| statement.to_string())
272 .collect::<Vec<String>>())
273 }
274
275 /// Whether `expr` is a literal `Value`, or a chain of unary ops bottoming
276 /// out in one (`-9`, `- -9`, `+ -9`). Such a chain collapses to a single
277 /// `?`; a parenthesised operand (`Expr::Nested`) is *not* a chain, so it
278 /// stops the recursion and its inner value is placeholdered separately.
279 fn is_unary_chain_over_value(expr: &Expr) -> bool {
280 match expr {
281 Expr::Value(_) => true,
282 Expr::UnaryOp { expr: child, .. } => Self::is_unary_chain_over_value(child),
283 _ => false,
284 }
285 }
286
287 /// Check if an expression contains only tuples of constants, recursively.
288 fn contains_only_tuples_of_values(expr: &Expr) -> bool {
289 match expr {
290 Expr::Value(_) => true,
291 Expr::Tuple(v) => v.iter().all(Self::contains_only_tuples_of_values),
292 _ => false,
293 }
294 }
295}
296
297/// Replace a `SELECT TOP 10`-style constant quantity with a `?` placeholder
298/// (rendered `TOP ?`), recursing into set-operation branches. A bare
299/// `TopQuantity::Constant(u64)` isn't a `Value`, so the visitor's literal pass
300/// misses it; `TopQuantity::Expr` is already normalized by that pass.
301fn normalize_top(body: &mut SetExpr) {
302 match body {
303 SetExpr::Select(select) => {
304 if let Some(top) = &mut select.top {
305 if matches!(top.quantity, Some(TopQuantity::Constant(_))) {
306 top.quantity = Some(TopQuantity::Expr(Expr::Value(
307 Value::Placeholder("?".into()).with_empty_span(),
308 )));
309 }
310 }
311 }
312 SetExpr::SetOperation { left, right, .. } => {
313 normalize_top(left);
314 normalize_top(right);
315 }
316 _ => {}
317 }
318}