Skip to content

Commit

Permalink
Add unify_values option to normalization (and update tarpaulin to v0.…
Browse files Browse the repository at this point in the history
…30.0) (#6)

* add unify_values option to normalization

* update tarpaulin to v0.30.0
  • Loading branch information
takaebato authored Jul 4, 2024
1 parent 0a6a661 commit 03adc44
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/rust.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
uses: actions-rs/[email protected]
with:
crate: cargo-tarpaulin
version: 0.27.3
version: 0.30.0
use-tool-cache: true
- name: Checkout
uses: actions/checkout@v4
Expand Down
10 changes: 8 additions & 2 deletions sql-insight-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ struct NormalizeCommandOptions {
/// Unify IN lists to a single form when all elements are literal values. For example, `IN (1, 2, 3)` becomes `IN (...)`.
#[clap(long)]
unify_in_list: bool,
/// Unify VALUES lists to a single form when all elements are literal values. For example, `VALUES (1, 2, 3), (4, 5, 6)` becomes `VALUES (...)`.
#[clap(long)]
unify_values: bool,
}

enum ProcessType {
Expand Down Expand Up @@ -172,8 +175,11 @@ impl Commands {
match self {
Commands::Format(opts) => Box::new(FormatExecutor::new(sql, opts.dialect.clone())),
Commands::Normalize(opts) => Box::new(
NormalizeExecutor::new(sql, opts.common_options.dialect.clone())
.with_options(NormalizerOptions::new().with_unify_in_list(opts.unify_in_list)),
NormalizeExecutor::new(sql, opts.common_options.dialect.clone()).with_options(
NormalizerOptions::new()
.with_unify_in_list(opts.unify_in_list)
.with_unify_values(opts.unify_values),
),
),
Commands::ExtractCrud(opts) => {
Box::new(CrudTableExtractExecutor::new(sql, opts.dialect.clone()))
Expand Down
29 changes: 29 additions & 0 deletions sql-insight-cli/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,35 @@ mod integration {
.stderr("");
}

#[test]
fn test_normalize_with_unify_values_option() {
sql_insight_cmd()
.arg("normalize")
.arg("--unify-values")
.arg("select * from t1 where a = 1 and b in (2, 3); insert into t2 (a) values (4), (5), (6);")
.assert()
.success()
.stdout(
"SELECT * FROM t1 WHERE a = ? AND b IN (?, ?)\nINSERT INTO t2 (a) VALUES (...)\n",
)
.stderr("");
}

#[test]
fn test_normalize_with_all_options() {
sql_insight_cmd()
.arg("normalize")
.arg("--unify-in-list")
.arg("--unify-values")
.arg("select * from t1 where a = 1 and b in (2, 3); insert into t2 (a) values (4), (5), (6);")
.assert()
.success()
.stdout(
"SELECT * FROM t1 WHERE a = ? AND b IN (...)\nINSERT INTO t2 (a) VALUES (...)\n",
)
.stderr("");
}

#[test]
fn test_normalize_with_dialect() {
sql_insight_cmd()
Expand Down
71 changes: 70 additions & 1 deletion sql-insight/src/normalizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
use std::ops::ControlFlow;

use crate::error::Error;
use sqlparser::ast::Value;
use sqlparser::ast::{Expr, VisitMut, VisitorMut};
use sqlparser::ast::{Query, SetExpr, Value};
use sqlparser::dialect::Dialect;
use sqlparser::parser::Parser;
use std::ops::DerefMut;

/// Convenience function to normalize SQL with default options.
///
Expand Down Expand Up @@ -53,6 +54,9 @@ pub struct NormalizerOptions {
/// Unify IN lists to a single form when all elements are literal values.
/// For example, `IN (1, 2, 3)` becomes `IN (...)`.
pub unify_in_list: bool,
/// Unify VALUES lists to a single form when all elements are literal values.
/// For example, `VALUES (1, 2, 3), (4, 5, 6)` becomes `VALUES (...)`.
pub unify_values: bool,
}

impl NormalizerOptions {
Expand All @@ -64,6 +68,11 @@ impl NormalizerOptions {
self.unify_in_list = unify_in_list;
self
}

pub fn with_unify_values(mut self, unify_values: bool) -> Self {
self.unify_values = unify_values;
self
}
}

/// A visitor for SQL AST nodes that normalizes SQL queries.
Expand All @@ -75,6 +84,22 @@ pub struct Normalizer {
impl VisitorMut for Normalizer {
type Break = ();

fn post_visit_query(&mut self, query: &mut Query) -> ControlFlow<Self::Break> {
if let SetExpr::Values(values) = query.body.deref_mut() {
if self.options.unify_values {
let rows = &mut values.rows;
if rows.is_empty()
|| rows.iter().all(|row| {
row.is_empty() || row.iter().all(|expr| matches!(expr, Expr::Value(_)))
})
{
*rows = vec![vec![Expr::Value(Value::Placeholder("...".into()))]];
}
}
}
ControlFlow::Continue(())
}

fn pre_visit_expr(&mut self, expr: &mut Expr) -> ControlFlow<Self::Break> {
if let Expr::Value(value) = expr {
*value = Value::Placeholder("?".into());
Expand Down Expand Up @@ -187,4 +212,48 @@ mod tests {
NormalizerOptions::new().with_unify_in_list(true),
);
}

#[test]
fn test_sql_with_values_without_unify_values_option() {
let sql = "INSERT INTO t1 (a, b, c) VALUES (1, 2, 3), (4, 5, 6), (7, 8, 9)";
let expected =
vec!["INSERT INTO t1 (a, b, c) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)".into()];
assert_normalize(sql, expected, all_dialects(), NormalizerOptions::new());
}

#[test]
fn test_sql_with_values_with_unify_values_option() {
let sql = "INSERT INTO t1 (a, b, c) VALUES (1, 2, 3), (4, 5, 6), (7, 8, 9)";
let expected = vec!["INSERT INTO t1 (a, b, c) VALUES (...)".into()];
assert_normalize(
sql,
expected,
all_dialects(),
NormalizerOptions::new().with_unify_values(true),
);
}

#[test]
fn test_sql_with_values_with_row_constructor_with_unify_values_option() {
let sql = "INSERT INTO t1 (a, b, c) VALUES ROW(1, 2, 3), ROW(4, 5, 6), ROW(7, 8, 9)";
let expected = vec!["INSERT INTO t1 (a, b, c) VALUES ROW(...)".into()];
assert_normalize(
sql,
expected,
all_dialects(),
NormalizerOptions::new().with_unify_values(true),
);
}

#[test]
fn test_sql_with_values_with_unify_values_option_when_not_all_elements_are_literal_values() {
let sql = "INSERT INTO t1 (a, b, c) VALUES (1, 2, 3), (4, 5, 6), (7, (SELECT * FROM t2 WHERE d = 9))";
let expected = vec!["INSERT INTO t1 (a, b, c) VALUES (?, ?, ?), (?, ?, ?), (?, (SELECT * FROM t2 WHERE d = ?))".into()];
assert_normalize(
sql,
expected,
all_dialects(),
NormalizerOptions::new().with_unify_values(true),
);
}
}
11 changes: 8 additions & 3 deletions sql-insight/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,22 @@ mod integration {

#[test]
fn test_normalize_with_options() {
let sql = "SELECT a FROM t1 WHERE b = 1 AND c in (2, 3, 4)";
let sql = "SELECT a FROM t1 WHERE b = 1 AND c in (2, 3, 4); INSERT INTO t2 (a, b, c) VALUES (1, 2, 3), (4, 5, 6)";
for dialect in all_dialects() {
let result = sql_insight::normalize_with_options(
dialect.as_ref(),
sql,
NormalizerOptions::new().with_unify_in_list(true),
NormalizerOptions::new()
.with_unify_in_list(true)
.with_unify_values(true),
)
.unwrap();
assert_eq!(
result,
["SELECT a FROM t1 WHERE b = ? AND c IN (...)"],
[
"SELECT a FROM t1 WHERE b = ? AND c IN (...)",
"INSERT INTO t2 (a, b, c) VALUES (...)"
],
"Failed for dialect: {dialect:?}"
)
}
Expand Down

0 comments on commit 03adc44

Please sign in to comment.