From 9fa15a1f29ed58d964f30c41bde7bdbaefa74682 Mon Sep 17 00:00:00 2001 From: Luca Barbato Date: Wed, 15 Oct 2025 23:14:16 +0200 Subject: [PATCH 1/8] wip: Use a builder --- Cargo.toml | 1 + src/lib.rs | 513 ++++++++++++++++++++++++----------------------------- 2 files changed, 233 insertions(+), 281 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bb2f3e3..c0d8f0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ debug = ["dep:anstream", "dep:anstyle", "dep:okhsl"] [dependencies] anstream = { version = "0.6.21", optional = true } anstyle = { version = "1.0.13", optional = true } +bon = "3.8.1" okhsl = { version = "1.0.1", optional = true } unicode_categories = "0.1.1" winnow = { version = "0.7.0", features = ["simd"] } diff --git a/src/lib.rs b/src/lib.rs index e29617e..7475dd7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! This crate is a port of https://github.com/kufii/sql-formatter-plus +//! This crate is a port of //! written in Rust. It is intended to be usable as a pure-Rust library //! for formatting SQL queries. @@ -9,6 +9,8 @@ // This lint is overly pedantic and annoying #![allow(clippy::needless_lifetimes)] +use bon::{builder, Builder}; + mod formatter; mod indentation; mod inline_block; @@ -18,19 +20,11 @@ mod tokenizer; #[cfg(feature = "debug")] mod debug; -/// Formats whitespace in a SQL string to make it easier to read. -/// Optionally replaces parameter placeholders with `params`. -pub fn format(query: &str, params: &QueryParams, options: &FormatOptions) -> String { - let named_placeholders = matches!(params, QueryParams::Named(_)); - - let tokens = tokenizer::tokenize(query, named_placeholders, options); - formatter::format(&tokens, params, options) -} - /// The SQL dialect to use. This affects parsing of special characters. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Dialect { /// Generic SQL syntax, most dialect-specific constructs are disabled + #[default] Generic, /// Enables array syntax (`[`, `]`) and operators PostgreSql, @@ -38,67 +32,63 @@ pub enum Dialect { SQLServer, } -/// Options for controlling how the library formats SQL -#[derive(Debug, Clone)] +/// SQL FormatOptions +#[derive(Debug, Clone, Builder)] pub struct FormatOptions<'a> { /// Controls the type and length of indentation to use /// - /// Default: 2 spaces - pub indent: Indent, + #[builder(default)] + indent: Indent, /// When set, changes reserved keywords to ALL CAPS - /// - /// Default: false - pub uppercase: Option, + uppercase: Option, /// Controls the number of line breaks after a query - /// - /// Default: 1 - pub lines_between_queries: u8, + #[builder(default = 1)] + lines_between_queries: u8, /// Ignore case conversion for specified strings in the array. - /// - /// Default: None - pub ignore_case_convert: Option>, + ignore_case_convert: Option>, /// Keep the query in a single line - /// - /// Default: false - pub inline: bool, + #[builder(default)] + inline: bool, /// Maximum length of an inline block - /// - /// Default: 50 - pub max_inline_block: usize, + #[builder(default = 50)] + max_inline_block: usize, /// Maximum length of inline arguments /// /// If unset keep every argument in a separate line - /// - /// Default: None - pub max_inline_arguments: Option, + max_inline_arguments: Option, /// Inline the argument at the top level if they would fit a line of this length - /// - /// Default: None - pub max_inline_top_level: Option, + max_inline_top_level: Option, /// Consider any JOIN statement as a top level keyword instead of a reserved keyword - /// - /// Default: false, - pub joins_as_top_level: bool, + #[builder(default)] + joins_as_top_level: bool, /// Tell the SQL dialect to use - /// - /// Default: Generic - pub dialect: Dialect, + #[builder(default)] + dialect: Dialect, + /// Replacements for the placeholders in the query + #[builder(default, into)] + params: QueryParams, +} + +impl<'a> FormatOptions<'a> { + /// Format the SQL query string + fn format(&self, query: &str) -> String { + let tokens = tokenizer::tokenize(query, self.params.is_named(), self); + formatter::format(&tokens, &self.params, self) + } +} + +use format_options_builder::State; + +impl<'a, S: State> FormatOptionsBuilder<'a, S> { + /// Build and format the SQL query string in a single step + pub fn format(self, query: &str) -> String { + self.build().format(query) + } } impl<'a> Default for FormatOptions<'a> { fn default() -> Self { - FormatOptions { - indent: Indent::Spaces(2), - uppercase: None, - lines_between_queries: 1, - ignore_case_convert: None, - inline: false, - max_inline_block: 50, - max_inline_arguments: None, - max_inline_top_level: None, - joins_as_top_level: false, - dialect: Dialect::Generic, - } + Self::builder().build() } } @@ -108,6 +98,12 @@ pub enum Indent { Tabs, } +impl Default for Indent { + fn default() -> Self { + Self::Spaces(2) + } +} + #[derive(Debug, Clone, Default)] pub enum QueryParams { Named(Vec<(String, String)>), @@ -116,6 +112,24 @@ pub enum QueryParams { None, } +impl From> for QueryParams { + fn from(value: Vec<(String, String)>) -> Self { + Self::Named(value) + } +} + +impl From> for QueryParams { + fn from(value: Vec) -> Self { + Self::Indexed(value) + } +} + +impl QueryParams { + fn is_named(&self) -> bool { + matches!(self, QueryParams::Named(_)) + } +} + #[derive(Default, Debug, Clone)] pub(crate) struct SpanInfo { pub full_span: usize, @@ -141,7 +155,7 @@ mod tests { SELECT x'73716c69676874' AS BLOB_VAL;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); let input = "SELECT X'73716c69676874' AS BLOB_VAL;"; let expected = indoc!( @@ -149,16 +163,13 @@ mod tests { SELECT X'73716c69676874' AS BLOB_VAL;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] fn it_uses_given_indent_config_for_indentation() { let input = "SELECT count(*),Column1 FROM Table1;"; - let options = FormatOptions { - indent: Indent::Spaces(4), - ..FormatOptions::default() - }; + let options = FormatOptions::builder().indent(Indent::Spaces(4)); let expected = indoc!( " SELECT @@ -168,7 +179,7 @@ mod tests { Table1;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -183,7 +194,7 @@ mod tests { schema2;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -199,7 +210,7 @@ mod tests { Table1;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -218,7 +229,7 @@ mod tests { foo;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -244,7 +255,7 @@ mod tests { );" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -260,7 +271,7 @@ mod tests { foo;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -278,10 +289,7 @@ mod tests { h FROM foo;" }; - let options = FormatOptions { - max_inline_arguments: Some(50), - ..Default::default() - }; + let options = FormatOptions::builder().max_inline_arguments(50); let expected = indoc! { " SELECT @@ -289,7 +297,7 @@ mod tests { FROM foo;" }; - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -307,17 +315,15 @@ mod tests { h FROM foo;" }; - let options = FormatOptions { - max_inline_arguments: Some(50), - max_inline_top_level: Some(50), - ..Default::default() - }; + let options = FormatOptions::builder() + .max_inline_arguments(50) + .max_inline_top_level(50); let expected = indoc! { " SELECT a, b, c, d, e, f, g, h FROM foo;" }; - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -335,28 +341,24 @@ mod tests { h FROM foo;" }; - let options = FormatOptions { - max_inline_arguments: Some(50), - max_inline_top_level: Some(20), - ..Default::default() - }; + let options = FormatOptions::builder() + .max_inline_arguments(50) + .max_inline_top_level(20); let expected = indoc! { " SELECT a, b, c, d, e, f, g, h FROM foo;" }; - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] fn inline_single_block_argument() { let input = "SELECT a, b, c FROM ( SELECT (e+f) AS a, (m+o) AS b FROM d) WHERE (a != b) OR (c IS NULL AND a == b)"; - let options = FormatOptions { - max_inline_arguments: Some(10), - max_inline_top_level: Some(20), - ..Default::default() - }; + let options = FormatOptions::builder() + .max_inline_arguments(10) + .max_inline_top_level(20); let expected = indoc! { " SELECT a, b, c @@ -373,7 +375,7 @@ mod tests { AND a == b )" }; - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -401,7 +403,7 @@ mod tests { );" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -412,10 +414,7 @@ mod tests { AND ( (Column2 = Column3 OR Column4 >= NOW()) ); " ); - let options = FormatOptions { - max_inline_arguments: Some(100), - ..Default::default() - }; + let options = FormatOptions::builder().max_inline_arguments(100); let expected = indoc!( " SELECT @@ -426,7 +425,7 @@ mod tests { Column1 = 'testing' AND ((Column2 = Column3 OR Column4 >= NOW()));" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -456,7 +455,7 @@ mod tests { 5;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -477,7 +476,7 @@ mod tests { 1" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -490,7 +489,7 @@ mod tests { 5, 10;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -506,16 +505,13 @@ mod tests { bar;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] fn it_formats_type_specifiers() { let input = "SELECT id, ARRAY [] :: UUID [] FROM UNNEST($1 :: UUID []) WHERE $1::UUID[] IS NOT NULL;"; - let options = FormatOptions { - dialect: Dialect::PostgreSql, - ..Default::default() - }; + let options = FormatOptions::builder().dialect(Dialect::PostgreSql); let expected = indoc!( " SELECT @@ -527,17 +523,14 @@ mod tests { $1::UUID[] IS NOT NULL;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] fn it_formats_arrays_as_function_arguments() { let input = "SELECT array_position(ARRAY['sun','mon','tue', 'wed', 'thu','fri', 'sat'], 'mon');"; - let options = FormatOptions { - dialect: Dialect::PostgreSql, - ..Default::default() - }; + let options = FormatOptions::builder().dialect(Dialect::PostgreSql); let expected = indoc!( " SELECT @@ -547,18 +540,16 @@ mod tests { );" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] fn it_formats_arrays_as_values() { let input = " INSERT INTO t VALUES('a', ARRAY[0, 1,2,3], ARRAY[['a','b'], ['c' ,'d']]);"; - let options = FormatOptions { - dialect: Dialect::PostgreSql, - max_inline_block: 10, - max_inline_top_level: Some(50), - ..Default::default() - }; + let options = FormatOptions::builder() + .dialect(Dialect::PostgreSql) + .max_inline_block(10) + .max_inline_top_level(50); let expected = indoc!( " INSERT INTO t @@ -572,23 +563,20 @@ mod tests { );" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] fn it_formats_array_index_notation() { let input = "SELECT a [ 1 ] + b [ 2 ] [ 5+1 ] > c [3] ;"; - let options = FormatOptions { - dialect: Dialect::PostgreSql, - ..Default::default() - }; + let options = FormatOptions::builder().dialect(Dialect::PostgreSql); let expected = indoc!( " SELECT a[1] + b[2][5 + 1] > c[3];" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] fn it_formats_limit_of_single_value_and_offset() { @@ -600,7 +588,7 @@ mod tests { 5 OFFSET 8;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -613,7 +601,7 @@ mod tests { 5, 10;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -632,7 +620,7 @@ mod tests { and b = 3" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -657,7 +645,7 @@ mod tests { a > b" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -681,7 +669,7 @@ mod tests { idx_b;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); let input = indoc!( r#" @@ -697,7 +685,7 @@ mod tests { "public"."table_name";"# ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -718,7 +706,7 @@ mod tests { INNER JOIN orders ON customers.customer_id = orders.customer_id;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -746,7 +734,7 @@ mod tests { PASTE JOIN bar;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -761,12 +749,10 @@ mod tests { JOIN bar ;" ); - let options = FormatOptions { - joins_as_top_level: true, - max_inline_top_level: Some(40), - max_inline_arguments: Some(40), - ..Default::default() - }; + let options = FormatOptions::builder() + .joins_as_top_level(true) + .max_inline_top_level(40) + .max_inline_arguments(40); let expected = indoc!( " SELECT @@ -779,7 +765,7 @@ mod tests { PASTE JOIN bar;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -810,7 +796,7 @@ mod tests { 1 = 2;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -829,7 +815,7 @@ mod tests { ); let options = FormatOptions::default(); - assert_eq!(format(input, &QueryParams::None, &options), input); + assert_eq!(options.format(input), input); } #[test] @@ -844,7 +830,7 @@ mod tests { (12, -123.4, 'Skagen 2111', 'Stv');" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -852,12 +838,10 @@ mod tests { let input = " INSERT INTO t(id, a, min, max) SELECT input.id, input.a, input.min, input.max FROM ( SELECT id, a, min, max FROM foo WHERE a IN ('a', 'b') ) AS input WHERE (SELECT true FROM condition) ON CONFLICT ON CONSTRAINT a_id_key DO UPDATE SET id = EXCLUDED.id, a = EXCLUDED.severity, min = EXCLUDED.min, max = EXCLUDED.max RETURNING *; "; let max_line = 50; - let options = FormatOptions { - max_inline_block: max_line, - max_inline_arguments: Some(max_line), - max_inline_top_level: Some(max_line), - ..Default::default() - }; + let options = FormatOptions::builder() + .max_inline_block(max_line) + .max_inline_arguments(max_line) + .max_inline_top_level(max_line); let expected = indoc!( " @@ -877,7 +861,7 @@ mod tests { RETURNING *;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -890,7 +874,7 @@ mod tests { (a + b * (c - NOW()));" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -928,7 +912,7 @@ mod tests { );" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -939,10 +923,7 @@ mod tests { SELECT IF (dq.id_discounter_shopping = 2, dq.value, dq.value / 100), IF (dq.id_discounter_shopping = 2, 'amount', 'percentage') FROM foo);" ); - let options = FormatOptions { - max_inline_block: 100, - ..Default::default() - }; + let options = FormatOptions::builder().max_inline_block(100); let expected = indoc!( " INSERT INTO @@ -955,7 +936,7 @@ mod tests { );" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -973,17 +954,15 @@ mod tests { CustomerName = 'Alfreds Futterkiste';" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] fn it_formats_simple_update_query_inlining_set() { let input = "UPDATE Customers SET ContactName='Alfred Schmidt', City='Hamburg' WHERE CustomerName='Alfreds Futterkiste';"; - let options = FormatOptions { - max_inline_top_level: Some(20), - max_inline_arguments: Some(10), - ..Default::default() - }; + let options = FormatOptions::builder() + .max_inline_top_level(20) + .max_inline_arguments(10); let expected = indoc!( " UPDATE Customers SET @@ -993,7 +972,7 @@ mod tests { CustomerName = 'Alfreds Futterkiste';" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1009,7 +988,7 @@ mod tests { AND Phone = 5002132;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1028,7 +1007,7 @@ mod tests { AND Phone = 5002132;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1040,7 +1019,7 @@ mod tests { DROP TABLE IF EXISTS admin_role;" ); - assert_eq!(format(input, &QueryParams::None, &options), output); + assert_eq!(options.format(input), output); } #[test] @@ -1053,7 +1032,7 @@ mod tests { count(" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1071,7 +1050,7 @@ mod tests { /*Comment" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1093,15 +1072,12 @@ mod tests { ) AS order_summary" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] fn it_formats_update_query_with_as_part_inline() { - let options = FormatOptions { - inline: true, - ..Default::default() - }; + let options = FormatOptions::builder().inline(true); let expected = "UPDATE customers SET total_orders = order_summary.total FROM ( SELECT * FROM bank ) AS order_summary"; let input = indoc!( " @@ -1118,7 +1094,7 @@ mod tests { ) AS order_summary" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1136,7 +1112,7 @@ mod tests { blah" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1152,7 +1128,7 @@ mod tests { )" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1160,7 +1136,7 @@ mod tests { let input = "((foo = 'bar'))"; let options = FormatOptions::default(); - assert_eq!(format(input, &QueryParams::None, &options), input); + assert_eq!(options.format(input), input); } #[test] @@ -1177,7 +1153,7 @@ mod tests { ]; let options = FormatOptions::default(); for input in &inputs { - assert_eq!(&format(input, &QueryParams::None, &options), input); + assert_eq!(&options.format(input), input); } } @@ -1195,7 +1171,7 @@ mod tests { ]; let options = FormatOptions::default(); for input in &inputs { - assert_eq!(&format(input, &QueryParams::None, &options), input); + assert_eq!(&options.format(input), input); } } @@ -1212,7 +1188,7 @@ mod tests { ]; let options = FormatOptions::default(); for input in &inputs { - assert_eq!(&format(input, &QueryParams::None, &options), input); + assert_eq!(&options.format(input), input); } } @@ -1226,7 +1202,7 @@ mod tests { ]; let options = FormatOptions::default(); for (input, output) in &strings { - assert_eq!(&format(input, &QueryParams::None, &options), output); + assert_eq!(&options.format(input), output); } } @@ -1235,7 +1211,7 @@ mod tests { let inputs = ["\"foo JOIN bar\"", "'foo JOIN bar'", "`foo JOIN bar`"]; let options = FormatOptions::default(); for input in &inputs { - assert_eq!(&format(input, &QueryParams::None, &options), input); + assert_eq!(&options.format(input), input); } } @@ -1252,7 +1228,7 @@ mod tests { ]; let options = FormatOptions::default(); for input in &inputs { - assert_eq!(&format(input, &QueryParams::None, &options), input); + assert_eq!(&options.format(input), input); } } @@ -1272,7 +1248,7 @@ mod tests { ]; let options = FormatOptions::default(); for (input, output) in &strings { - assert_eq!(&format(input, &QueryParams::None, &options), output); + assert_eq!(&options.format(input), output); } } @@ -1285,7 +1261,7 @@ mod tests { ]; let options = FormatOptions::default(); for (input, output) in &strings { - assert_eq!(&format(input, &QueryParams::None, &options), output); + assert_eq!(&options.format(input), output); } let input = indoc!( @@ -1308,7 +1284,7 @@ mod tests { Table2;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1324,7 +1300,7 @@ mod tests { table;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1346,7 +1322,7 @@ mod tests { AND colb = 3" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1369,7 +1345,7 @@ mod tests { bar;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1394,7 +1370,7 @@ mod tests { );" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1402,7 +1378,7 @@ mod tests { let input = "CREATE TABLE items (a INT PRIMARY KEY, b TEXT);"; let options = FormatOptions::default(); - assert_eq!(format(input, &QueryParams::None, &options), input); + assert_eq!(options.format(input), input); } #[test] @@ -1420,7 +1396,7 @@ mod tests { );" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1436,7 +1412,7 @@ mod tests { (12, -123.4, 'Skagen 2111', 'Stv');" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1451,7 +1427,7 @@ mod tests { supplier_name char(100) NOT NULL;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1465,7 +1441,7 @@ mod tests { ALTER COLUMN supplier_name VARCHAR(100) NOT NULL;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1488,7 +1464,7 @@ mod tests { );"# ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1499,7 +1475,7 @@ mod tests { ..Default::default() }; for input in &inputs { - assert_eq!(&format(input, &QueryParams::None, &options), input); + assert_eq!(&options.format(input), input); } } @@ -1522,7 +1498,7 @@ mod tests { @[var name];" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1535,10 +1511,9 @@ mod tests { ("var name".to_string(), "'var value'".to_string()), ("var\\name".to_string(), "'var\\ value'".to_string()), ]; - let options = FormatOptions { - dialect: Dialect::SQLServer, - ..Default::default() - }; + let options = FormatOptions::builder() + .dialect(Dialect::SQLServer) + .params(params); let expected = indoc!( " SELECT @@ -1551,10 +1526,7 @@ mod tests { 'var\\ value';" ); - assert_eq!( - format(input, &QueryParams::Named(params), &options), - expected - ); + assert_eq!(options.format(input), expected); } #[test] @@ -1576,7 +1548,7 @@ mod tests { :[var name];" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1597,10 +1569,9 @@ mod tests { "'super weird value'".to_string(), ), ]; - let options = FormatOptions { - dialect: Dialect::SQLServer, - ..Default::default() - }; + let options = FormatOptions::builder() + .dialect(Dialect::SQLServer) + .params(params); let expected = indoc!( " SELECT @@ -1614,10 +1585,7 @@ mod tests { 'super weird value';" ); - assert_eq!( - format(input, &QueryParams::Named(params), &options), - expected - ); + assert_eq!(options.format(input), expected); } #[test] @@ -1632,7 +1600,7 @@ mod tests { ?;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1643,7 +1611,7 @@ mod tests { "second".to_string(), "third".to_string(), ]; - let options = FormatOptions::default(); + let options = FormatOptions::builder().params(params); let expected = indoc!( " SELECT @@ -1652,12 +1620,7 @@ mod tests { first;" ); - assert_eq!( - format(input, &QueryParams::Indexed(params), &options), - expected - ); - - format("?62666666121266666612", &QueryParams::None, &options); + assert_eq!(options.format(input), expected); } #[test] @@ -1668,7 +1631,7 @@ mod tests { "second".to_string(), "third".to_string(), ]; - let options = FormatOptions::default(); + let options = FormatOptions::builder().params(params); let expected = indoc!( " SELECT @@ -1677,10 +1640,7 @@ mod tests { third;" ); - assert_eq!( - format(input, &QueryParams::Indexed(params), &options), - expected - ); + assert_eq!(options.format(input), expected); } #[test] @@ -1694,7 +1654,7 @@ mod tests { $2;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1709,7 +1669,7 @@ mod tests { $bar;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1721,7 +1681,7 @@ mod tests { "third".to_string(), "4th".to_string(), ]; - let options = FormatOptions::default(); + let options = FormatOptions::builder().params(params); let expected = indoc!( " SELECT @@ -1733,10 +1693,7 @@ mod tests { $alias;" ); - assert_eq!( - format(input, &QueryParams::Indexed(params), &options), - expected - ); + assert_eq!(options.format(input), expected); } #[test] @@ -1748,7 +1705,7 @@ mod tests { ("1".to_string(), "number 1".to_string()), ("2".to_string(), "number 2".to_string()), ]; - let options = FormatOptions::default(); + let options = FormatOptions::builder().params(params); let expected = indoc!( " SELECT @@ -1758,10 +1715,7 @@ mod tests { number 2;" ); - assert_eq!( - format(input, &QueryParams::Named(params), &options), - expected - ); + assert_eq!(options.format(input), expected); } #[test] @@ -1795,7 +1749,7 @@ mod tests { "second".to_string(), "third".to_string(), ]; - let options = FormatOptions::default(); + let options = FormatOptions::builder().params(params); let expected = indoc!( " SELECT @@ -1805,10 +1759,7 @@ mod tests { 2" ); - assert_eq!( - format(input, &QueryParams::Indexed(params), &options), - expected - ); + assert_eq!(options.format(input), expected); } #[test] @@ -1825,7 +1776,7 @@ mod tests { CROSS JOIN t2 on t.id = t2.id_t" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1842,7 +1793,7 @@ mod tests { CROSS APPLY fn(t.id)" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1858,7 +1809,7 @@ mod tests { t" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1871,7 +1822,7 @@ mod tests { N'value'" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1888,7 +1839,7 @@ mod tests { OUTER APPLY fn(t.id)" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1903,7 +1854,7 @@ mod tests { 2 ROWS ONLY;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1920,7 +1871,7 @@ mod tests { END;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1943,7 +1894,7 @@ mod tests { table" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1968,7 +1919,7 @@ mod tests { FROM table" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -1986,7 +1937,7 @@ mod tests { END;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2010,7 +1961,7 @@ mod tests { (CASE $3 WHEN 'created_at_desc' THEN created_at END) DESC;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2025,7 +1976,7 @@ mod tests { end;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2041,7 +1992,7 @@ mod tests { table1;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2056,7 +2007,7 @@ mod tests { b --comment" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2077,7 +2028,7 @@ mod tests { ;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2096,7 +2047,7 @@ mod tests { b" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2111,7 +2062,7 @@ mod tests { )" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2125,7 +2076,7 @@ mod tests { ()" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2133,7 +2084,7 @@ mod tests { let input = ";"; let options = FormatOptions::default(); - assert_eq!(format(input, &QueryParams::None, &options), input); + assert_eq!(options.format(input), input); } #[test] @@ -2142,7 +2093,7 @@ mod tests { let options = FormatOptions::default(); let expected = "SELECT\n 'главная'"; - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2160,7 +2111,7 @@ mod tests { t" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2179,7 +2130,7 @@ mod tests { LANGUAGE plpgsql;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2202,7 +2153,7 @@ mod tests { LANGUAGE plpgsql;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2288,7 +2239,7 @@ mod tests { ADD PARTITION (sale_year = '2024', sale_month = '08') LOCATION '/user/hive/warehouse/sales_data/2024/08';" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2313,7 +2264,7 @@ mod tests { email" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2329,7 +2280,7 @@ mod tests { other integer );" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2354,7 +2305,7 @@ mod tests { table_0" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2374,7 +2325,7 @@ mod tests { Table1;" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2413,7 +2364,7 @@ mod tests { -- json:first_key.second_key = 1" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2435,7 +2386,7 @@ mod tests { and colb = 3" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2462,7 +2413,7 @@ mod tests { b, field FROM a, aa;" }; - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2482,7 +2433,7 @@ mod tests { SELECT b, field FROM a, aa;" }; - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2525,7 +2476,7 @@ mod tests { SELECT b, field FROM a, aa;" }; - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2547,7 +2498,7 @@ mod tests { and colb = 3" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] fn it_correctly_parses_all_operators() { @@ -2570,7 +2521,7 @@ mod tests { }; assert_eq!( - format(&input, &QueryParams::None, &options), + options.format(&input), expected, "Failed to parse operator: {}", operator @@ -2632,7 +2583,7 @@ SELECT left ~= right" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] fn it_formats_double_colons() { @@ -2652,7 +2603,7 @@ from foo" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2693,7 +2644,7 @@ from WHERE id = $1" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } #[test] @@ -2715,6 +2666,6 @@ from c = $12 AND f" ); - assert_eq!(format(input, &QueryParams::None, &options), expected); + assert_eq!(options.format(input), expected); } } From 9f374cd27f817deb625ab433c3193273991477c5 Mon Sep 17 00:00:00 2001 From: Luca Barbato Date: Fri, 17 Oct 2025 06:42:31 +0200 Subject: [PATCH 2/8] Format with different params --- src/lib.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 7475dd7..84163d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ // This lint is overly pedantic and annoying #![allow(clippy::needless_lifetimes)] -use bon::{builder, Builder}; +use bon::{bon, builder, Builder}; mod formatter; mod indentation; @@ -77,6 +77,27 @@ impl<'a> FormatOptions<'a> { } } +#[bon] +impl<'a> FormatOptions<'a> { + /// Use the FormatOptions with different params + #[builder( + finish_fn( + name = format, + doc { + /// Format the SQL query string + } + ) + )] + pub fn with_params( + &self, + #[builder(start_fn, into)] params: QueryParams, + #[builder(finish_fn)] query: &str, + ) -> String { + let tokens = tokenizer::tokenize(query, params.is_named(), self); + formatter::format(&tokens, ¶ms, self) + } +} + use format_options_builder::State; impl<'a, S: State> FormatOptionsBuilder<'a, S> { From 16aa9149de9dcfc29c9c73af63d13062232a20e6 Mon Sep 17 00:00:00 2001 From: Luca Barbato Date: Fri, 17 Oct 2025 06:43:38 +0200 Subject: [PATCH 3/8] Make setting the indentation spaces even more ergonomic --- src/lib.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 84163d6..b15c303 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,7 @@ pub enum Dialect { pub struct FormatOptions<'a> { /// Controls the type and length of indentation to use /// - #[builder(default)] + #[builder(default, into)] indent: Indent, /// When set, changes reserved keywords to ALL CAPS uppercase: Option, @@ -119,6 +119,12 @@ pub enum Indent { Tabs, } +impl From for Indent { + fn from(value: u8) -> Self { + Self::Spaces(value) + } +} + impl Default for Indent { fn default() -> Self { Self::Spaces(2) @@ -190,7 +196,7 @@ mod tests { #[test] fn it_uses_given_indent_config_for_indentation() { let input = "SELECT count(*),Column1 FROM Table1;"; - let options = FormatOptions::builder().indent(Indent::Spaces(4)); + let options = FormatOptions::builder().indent(4); let expected = indoc!( " SELECT From 5adffcc4b36f06f9217b9ff0867cc7ab59de8764 Mon Sep 17 00:00:00 2001 From: Luca Barbato Date: Fri, 17 Oct 2025 07:15:36 +0200 Subject: [PATCH 4/8] Use a Cow in QueryParams --- src/lib.rs | 44 +++++++++++++++++++++++++++++--------------- src/params.rs | 2 +- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b15c303..ebf9594 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ // This lint is overly pedantic and annoying #![allow(clippy::needless_lifetimes)] +use std::borrow::Cow; + use bon::{bon, builder, Builder}; mod formatter; @@ -66,7 +68,7 @@ pub struct FormatOptions<'a> { dialect: Dialect, /// Replacements for the placeholders in the query #[builder(default, into)] - params: QueryParams, + params: QueryParams<'a>, } impl<'a> FormatOptions<'a> { @@ -88,9 +90,9 @@ impl<'a> FormatOptions<'a> { } ) )] - pub fn with_params( + pub fn with_params<'b>( &self, - #[builder(start_fn, into)] params: QueryParams, + #[builder(start_fn, into)] params: QueryParams<'b>, #[builder(finish_fn)] query: &str, ) -> String { let tokens = tokenizer::tokenize(query, params.is_named(), self); @@ -132,26 +134,38 @@ impl Default for Indent { } #[derive(Debug, Clone, Default)] -pub enum QueryParams { - Named(Vec<(String, String)>), - Indexed(Vec), +pub enum QueryParams<'a> { + Named(Cow<'a, [(String, String)]>), + Indexed(Cow<'a, [String]>), #[default] None, } -impl From> for QueryParams { +impl<'a> From> for QueryParams<'a> { fn from(value: Vec<(String, String)>) -> Self { - Self::Named(value) + Self::Named(Cow::Owned(value)) } } -impl From> for QueryParams { +impl<'a> From> for QueryParams<'a> { fn from(value: Vec) -> Self { - Self::Indexed(value) + Self::Indexed(Cow::Owned(value)) + } +} + +impl<'a> From<&'a [(String, String)]> for QueryParams<'a> { + fn from(value: &'a [(String, String)]) -> Self { + Self::Named(Cow::Borrowed(value)) } } -impl QueryParams { +impl<'a> From<&'a [String]> for QueryParams<'a> { + fn from(value: &'a [String]) -> Self { + Self::Indexed(Cow::Borrowed(value)) + } +} + +impl<'a> QueryParams<'a> { fn is_named(&self) -> bool { matches!(self, QueryParams::Named(_)) } @@ -1532,7 +1546,7 @@ mod tests { fn it_recognizes_at_variables_with_param_values() { let input = "SELECT @variable, @a1_2.3$, @'var name', @\"var name\", @`var name`, @[var name], @'var\\name';"; - let params = vec![ + let params = [ ("variable".to_string(), "\"variable value\"".to_string()), ("a1_2.3$".to_string(), "'weird value'".to_string()), ("var name".to_string(), "'var value'".to_string()), @@ -1540,7 +1554,7 @@ mod tests { ]; let options = FormatOptions::builder() .dialect(Dialect::SQLServer) - .params(params); + .params(params.as_ref()); let expected = indoc!( " SELECT @@ -1653,12 +1667,12 @@ mod tests { #[test] fn it_recognizes_question_indexed_placeholders_with_param_values() { let input = "SELECT ?, ?, ?;"; - let params = vec![ + let params = [ "first".to_string(), "second".to_string(), "third".to_string(), ]; - let options = FormatOptions::builder().params(params); + let options = FormatOptions::builder().params(params.as_ref()); let expected = indoc!( " SELECT diff --git a/src/params.rs b/src/params.rs index 16e08ba..341a06c 100644 --- a/src/params.rs +++ b/src/params.rs @@ -3,7 +3,7 @@ use crate::QueryParams; pub(crate) struct Params<'a> { index: usize, - params: &'a QueryParams, + params: &'a QueryParams<'a>, } impl<'a> Params<'a> { From 951b118f5a28227f32e2541435f431b512475602 Mon Sep 17 00:00:00 2001 From: Luca Barbato Date: Sat, 18 Oct 2025 11:53:14 +0200 Subject: [PATCH 5/8] More API updates --- benches/bench.rs | 70 ++++++++++-------------------------------------- src/lib.rs | 19 +++++++++---- 2 files changed, 28 insertions(+), 61 deletions(-) diff --git a/benches/bench.rs b/benches/bench.rs index 68dfd4d..10e25c2 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -1,46 +1,34 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use std::hint::black_box; + +use criterion::{criterion_group, criterion_main, Criterion}; use sqlformat::*; fn simple_query(c: &mut Criterion) { let input = "SELECT * FROM my_table WHERE id = 1"; c.bench_function("simple query", |b| { - b.iter(|| { - format( - black_box(input), - black_box(&QueryParams::None), - black_box(&FormatOptions::default()), - ) - }) + b.iter(|| FormatOptions::default().format(input)) }); } fn complex_query(c: &mut Criterion) { let input = "SELECT t1.id, t1.name, t1.title, t1.description, t2.mothers_maiden_name, t2.first_girlfriend\nFROM my_table t1 LEFT JOIN other_table t2 ON t1.id = t2.other_id WHERE t2.order BETWEEN 17 AND 30"; c.bench_function("complex query", |b| { - b.iter(|| { - format( - black_box(input), - black_box(&QueryParams::None), - black_box(&FormatOptions::default()), - ) - }) + b.iter(|| FormatOptions::default().format(input)) }); } fn query_with_named_params(c: &mut Criterion) { let input = "SELECT * FROM my_table WHERE id = :first OR id = :second OR id = :third"; - let params = vec![ + let params = &[ ("first".to_string(), "1".to_string()), ("second".to_string(), "2".to_string()), ("third".to_string(), "3".to_string()), ]; c.bench_function("named params", |b| { b.iter(|| { - format( - black_box(input), - black_box(&QueryParams::Named(params.clone())), - black_box(&FormatOptions::default()), - ) + FormatOptions::default() + .with_params(params.as_ref()) + .format(input) }) }); } @@ -49,13 +37,7 @@ fn query_with_explicit_indexed_params(c: &mut Criterion) { let input = "SELECT * FROM my_table WHERE id = ?1 OR id = ?2 OR id = ?0"; let params = vec!["0".to_string(), "1".to_string(), "2".to_string()]; c.bench_function("explicit indexed params", |b| { - b.iter(|| { - format( - black_box(input), - black_box(&QueryParams::Indexed(params.clone())), - black_box(&FormatOptions::default()), - ) - }) + b.iter(|| FormatOptions::default().with_params(¶ms).format(input)) }); } @@ -63,13 +45,7 @@ fn query_with_implicit_indexed_params(c: &mut Criterion) { let input = "SELECT * FROM my_table WHERE id = ? OR id = ? OR id = ?"; let params = vec!["0".to_string(), "1".to_string(), "2".to_string()]; c.bench_function("implicit indexed params", |b| { - b.iter(|| { - format( - black_box(input), - black_box(&QueryParams::Indexed(params.clone())), - black_box(&FormatOptions::default()), - ) - }) + b.iter(|| FormatOptions::default().with_params(¶ms).format(input)) }); } @@ -131,13 +107,7 @@ VALUES let input = generate_insert_query(); c.bench_function("issue 633", |b| { - b.iter(|| { - format( - black_box(&input), - black_box(&QueryParams::None), - black_box(&FormatOptions::default()), - ) - }) + b.iter(|| FormatOptions::default().format(&input)) }); } @@ -145,13 +115,7 @@ fn issue_633_2(c: &mut Criterion) { let input = "SELECT\n d.uuid AS uuid,\n\td.name_of_document AS name,\n\td.slug_name AS slug,\n\td.default_contract_uuid AS default_contract_uuid,\n\ta.uuid AS parent_uuid,\n\ta.name_of_agreement AS agreement_name,\n\td.icon_name AS icon\nFROM `documents` d\nLEFT JOIN agreements a ON a.uuid = d.parent_uuid\n WHERE d.uuid = ? LIMIT 1"; let params = vec!["0".to_string()]; c.bench_function("issue 633 query 2", |b| { - b.iter(|| { - format( - black_box(input), - black_box(&QueryParams::Indexed(params.clone())), - black_box(&FormatOptions::default()), - ) - }) + b.iter(|| FormatOptions::default().with_params(¶ms).format(input)) }); } @@ -171,13 +135,7 @@ fn issue_633_3(c: &mut Criterion) { } c.bench_function("issue 633 query 3", |b| { - b.iter(|| { - format( - black_box(&input), - black_box(&QueryParams::None), - black_box(&FormatOptions::default()), - ) - }) + b.iter(|| FormatOptions::default().format(&input)) }); } diff --git a/src/lib.rs b/src/lib.rs index ebf9594..36d3067 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,7 +73,7 @@ pub struct FormatOptions<'a> { impl<'a> FormatOptions<'a> { /// Format the SQL query string - fn format(&self, query: &str) -> String { + pub fn format(&self, query: &str) -> String { let tokens = tokenizer::tokenize(query, self.params.is_named(), self); formatter::format(&tokens, &self.params, self) } @@ -153,6 +153,18 @@ impl<'a> From> for QueryParams<'a> { } } +impl<'a> From<&'a Vec<(String, String)>> for QueryParams<'a> { + fn from(value: &'a Vec<(String, String)>) -> Self { + Self::Named(Cow::Borrowed(value.as_ref())) + } +} + +impl<'a> From<&'a Vec> for QueryParams<'a> { + fn from(value: &'a Vec) -> Self { + Self::Indexed(Cow::Borrowed(value.as_ref())) + } +} + impl<'a> From<&'a [(String, String)]> for QueryParams<'a> { fn from(value: &'a [(String, String)]) -> Self { Self::Named(Cow::Borrowed(value)) @@ -1776,10 +1788,7 @@ mod tests { third;" ); - assert_eq!( - format(input, &QueryParams::Named(params), &options), - expected - ); + assert_eq!(options.with_params(params).format(input), expected); } #[test] From 2fa8de75c2229390e944f41df96d57213f08d93e Mon Sep 17 00:00:00 2001 From: Luca Barbato Date: Sat, 18 Oct 2025 16:21:04 +0200 Subject: [PATCH 6/8] More ergonomicity for params --- src/lib.rs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 36d3067..8a12cb6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -177,6 +177,18 @@ impl<'a> From<&'a [String]> for QueryParams<'a> { } } +impl<'a, const N: usize> From<&'a [(String, String); N]> for QueryParams<'a> { + fn from(value: &'a [(String, String); N]) -> Self { + Self::Named(Cow::Borrowed(value)) + } +} + +impl<'a, const N: usize> From<&'a [String; N]> for QueryParams<'a> { + fn from(value: &'a [String; N]) -> Self { + Self::Indexed(Cow::Borrowed(value)) + } +} + impl<'a> QueryParams<'a> { fn is_named(&self) -> bool { matches!(self, QueryParams::Named(_)) @@ -1566,7 +1578,7 @@ mod tests { ]; let options = FormatOptions::builder() .dialect(Dialect::SQLServer) - .params(params.as_ref()); + .params(¶ms); let expected = indoc!( " SELECT @@ -1622,9 +1634,7 @@ mod tests { "'super weird value'".to_string(), ), ]; - let options = FormatOptions::builder() - .dialect(Dialect::SQLServer) - .params(params); + let options = FormatOptions::builder().dialect(Dialect::SQLServer).build(); let expected = indoc!( " SELECT @@ -1638,7 +1648,7 @@ mod tests { 'super weird value';" ); - assert_eq!(options.format(input), expected); + assert_eq!(options.with_params(params).format(input), expected); } #[test] @@ -1664,7 +1674,7 @@ mod tests { "second".to_string(), "third".to_string(), ]; - let options = FormatOptions::builder().params(params); + let options = FormatOptions::builder().params(¶ms); let expected = indoc!( " SELECT @@ -1734,7 +1744,7 @@ mod tests { "third".to_string(), "4th".to_string(), ]; - let options = FormatOptions::builder().params(params); + let options = FormatOptions::builder().params(¶ms); let expected = indoc!( " SELECT @@ -1752,13 +1762,13 @@ mod tests { #[test] fn it_recognizes_dollar_sign_alphanumeric_placeholders_with_param_values() { let input = "SELECT $hash, $salt, $1, $2;"; - let params = vec![ + let params = [ ("hash".to_string(), "hash value".to_string()), ("salt".to_string(), "salt value".to_string()), ("1".to_string(), "number 1".to_string()), ("2".to_string(), "number 2".to_string()), ]; - let options = FormatOptions::builder().params(params); + let options = FormatOptions::builder().params(¶ms); let expected = indoc!( " SELECT From a796bf9885f699917ede77c65959f6d44601ab68 Mon Sep 17 00:00:00 2001 From: Luca Barbato Date: Sat, 18 Oct 2025 17:14:15 +0200 Subject: [PATCH 7/8] fixup: try to cover more --- src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8a12cb6..fa95213 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1578,7 +1578,7 @@ mod tests { ]; let options = FormatOptions::builder() .dialect(Dialect::SQLServer) - .params(¶ms); + .params(params.as_ref()); let expected = indoc!( " SELECT @@ -1624,7 +1624,7 @@ mod tests { :[var name], :'escaped \\'var\\'', :\"^*& weird \\\" var \"; " ); - let params = vec![ + let params = [ ("variable".to_string(), "\"variable value\"".to_string()), ("a1_2.3$".to_string(), "'weird value'".to_string()), ("var name".to_string(), "'var value'".to_string()), @@ -1648,7 +1648,7 @@ mod tests { 'super weird value';" ); - assert_eq!(options.with_params(params).format(input), expected); + assert_eq!(options.with_params(¶ms).format(input), expected); } #[test] @@ -1744,7 +1744,7 @@ mod tests { "third".to_string(), "4th".to_string(), ]; - let options = FormatOptions::builder().params(¶ms); + let options = FormatOptions::builder().params(params); let expected = indoc!( " SELECT @@ -1762,7 +1762,7 @@ mod tests { #[test] fn it_recognizes_dollar_sign_alphanumeric_placeholders_with_param_values() { let input = "SELECT $hash, $salt, $1, $2;"; - let params = [ + let params = vec![ ("hash".to_string(), "hash value".to_string()), ("salt".to_string(), "salt value".to_string()), ("1".to_string(), "number 1".to_string()), @@ -1804,12 +1804,12 @@ mod tests { #[test] fn it_formats_query_with_go_batch_separator() { let input = "SELECT 1 GO SELECT 2"; - let params = vec![ + let params = [ "first".to_string(), "second".to_string(), "third".to_string(), ]; - let options = FormatOptions::builder().params(params); + let options = FormatOptions::builder().params(¶ms); let expected = indoc!( " SELECT From 0bb35b85e7aee23167da648c6cca6dbcf1cf7c70 Mon Sep 17 00:00:00 2001 From: Luca Barbato Date: Sun, 19 Oct 2025 12:04:34 +0200 Subject: [PATCH 8/8] Update all the examples --- README.md | 88 +++++++++++++++++++++++++++++------------------------- src/lib.rs | 9 +++--- 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 37ec2cb..21c5ee9 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,12 @@ This crate is a Rust port of [sql-formatter-plus](https://github.com/kufii/sql-f ## Quick start ```rust -use sqlformat::{format, FormatOptions, Indent, QueryParams}; +use sqlformat::FormatOptions; fn main() { let sql = "SELECT id, name FROM users WHERE created_at > NOW();"; let options = FormatOptions::default(); - let formatted = format(sql, &QueryParams::None, &options); + let formatted = options.format(sql); println!("{}", formatted); } ``` @@ -66,44 +66,43 @@ Minimum Supported Rust Version (MSRV): `1.84`. ### Basic formatting ```rust -use sqlformat::{format, FormatOptions, QueryParams}; +use sqlformat::{FormatOptions}; let sql = "SELECT count(*), col FROM t WHERE a = 1 AND b = 2;"; -let out = format(sql, &QueryParams::None, &FormatOptions::default()); +let out = FormatOptions::default().format(sql); ``` ### Indentation ```rust -use sqlformat::{format, FormatOptions, Indent, QueryParams}; +use sqlformat::{FormatOptions, Indent}; -let options = FormatOptions { indent: Indent::Spaces(4), ..Default::default() }; -let out = format("SELECT a, b FROM t;", &QueryParams::None, &options); +let options = FormatOptions::builder().ident(4); +let out = options.format("SELECT a, b FROM t;"); -let options = FormatOptions { indent: Indent::Tabs, ..Default::default() }; -let out = format("SELECT a, b FROM t;", &QueryParams::None, &options); +let options = FormatOptions::builder().indent(Indent::Tabs); +let out = options.format("SELECT a, b FROM t;"); ``` ### Keyword case conversion ```rust -use sqlformat::{format, FormatOptions, QueryParams}; +use sqlformat::FormatOptions; // Uppercase reserved keywords -let options = FormatOptions { uppercase: Some(true), ..Default::default() }; -let out = format("select distinct * from foo where bar = 1", &QueryParams::None, &options); +let options = FormatOptions::builder().uppercase(true); +let out = options.format("select distinct * from foo where bar = 1"); // Lowercase reserved keywords -let options = FormatOptions { uppercase: Some(false), ..Default::default() }; -let out = format("SELECT DISTINCT * FROM FOO WHERE BAR = 1", &QueryParams::None, &options); +let options = FormatOptions::builder().uppercase(false); +let out = options.format("SELECT DISTINCT * FROM FOO WHERE BAR = 1"); // Preserve case with exceptions -let options = FormatOptions { - uppercase: Some(true), - ignore_case_convert: Some(vec!["from", "where"]), - ..Default::default() -}; -let out = format("select * from foo where bar = 1", &QueryParams::None, &options); +let options = FormatOptions::builder() + .uppercase(true) + .ignore_case_convert(vec!["from", "where"]); + +let out = options.format("select * from foo where bar = 1"); ``` ### Inline/compact formatting @@ -111,16 +110,15 @@ let out = format("select * from foo where bar = 1", &QueryParams::None, &options Control how aggressively short blocks and argument lists are kept on one line. ```rust -use sqlformat::{format, FormatOptions, QueryParams}; - -let options = FormatOptions { - inline: false, // when true, forces single-line output - max_inline_block: 50, // characters allowed to keep a parenthesized block inline - max_inline_arguments: Some(40), - max_inline_top_level: Some(40), - ..Default::default() -}; -let out = format("SELECT a, b, c, d, e, f, g, h FROM t;", &QueryParams::None, &options); +use sqlformat::FormatOptions; + +let options = FormatOptions::builder() + .inline(false) // when true, forces single-line output + .max_inline_block(50) // characters allowed to keep a parenthesized block inline + .max_inline_arguments(40), + .max_inline_top_level(40); + +let out = options.format("SELECT a, b, c, d, e, f, g, h FROM t;"); ``` ### JOIN layout @@ -128,10 +126,10 @@ let out = format("SELECT a, b, c, d, e, f, g, h FROM t;", &QueryParams::None, &o Treat any JOIN as a top-level keyword (affects line breaks): ```rust -use sqlformat::{format, FormatOptions, QueryParams}; +use sqlformat::FormatOptions; -let options = FormatOptions { joins_as_top_level: true, ..Default::default() }; -let out = format("SELECT * FROM a INNER JOIN b ON a.id = b.a_id", &QueryParams::None, &options); +let options = FormatOptions::builder().joins_as_top_level(true); +let out = options.format("SELECT * FROM a INNER JOIN b ON a.id = b.a_id"); ``` ### Parameter interpolation @@ -139,30 +137,38 @@ let out = format("SELECT * FROM a INNER JOIN b ON a.id = b.a_id", &QueryParams:: `sqlformat` can substitute placeholders using `QueryParams`: ```rust -use sqlformat::{format, FormatOptions, QueryParams}; +use sqlformat::FormatOptions; + +// Set the options only once +let options = FormatOptions::default(); // Numbered / positional (e.g., ?, ?1, $1) let sql = "SELECT ?1, ?, $2;"; -let params = QueryParams::Indexed(vec!["first".to_string(), "second".to_string(), "third".to_string()]); -let out = format(sql, ¶ms, &FormatOptions::default()); +let params = ["first".to_string(), "second".to_string(), "third".to_string()]; +// Pass params by reference +let out = options.with_params(¶ms).format(sql); // Named (e.g., $name, :name, @name, :\"weird name\") let sql = "SELECT $hash, :name, @`var name`;"; -let params = QueryParams::Named(vec![ +let params = vec![ ("hash".to_string(), "hash value".to_string()), ("name".to_string(), "Alice".to_string()), ("var name".to_string(), "Bob".to_string()), ]); -let out = format(sql, ¶ms, &FormatOptions::default()); +let out = options.with_params(¶ms).format(sql); + +// Named using a format-like syntax +let sql = "SELECT {hash}, {name}, {var name};" +let out = options.with_params(¶ms).format(sql); ``` ### Controlling blank lines between statements ```rust -use sqlformat::{format, FormatOptions, QueryParams}; +use sqlformat::FormatOptions; -let options = FormatOptions { lines_between_queries: 2, ..Default::default() }; -let out = format("SELECT 1; SELECT 2;", &QueryParams::None, &options); +let options = FormatOptions::builder().lines_between_queries(2); +let out = options.format("SELECT 1; SELECT 2;"); ``` ### Temporarily disabling the formatter diff --git a/src/lib.rs b/src/lib.rs index fa95213..485e999 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2371,11 +2371,10 @@ mod tests { #[test] fn it_uses_given_ignore_case_convert_config() { let input = "select count(*),Column1 from Table1;"; - let options = FormatOptions { - uppercase: Some(true), - ignore_case_convert: Some(vec!["from"]), - ..FormatOptions::default() - }; + let options = FormatOptions::builder() + .uppercase(true) + .ignore_case_convert(vec!["from"]); + let expected = indoc!( " SELECT