From 322ca2c8cc2d096db56a7ca6b685608a521910c0 Mon Sep 17 00:00:00 2001 From: Belchior Oliveira Date: Mon, 11 Nov 2024 09:19:46 -0300 Subject: [PATCH 01/19] Adds MySQL syntax to select builder --- .github/workflows/pull-requests.yml | 5 ++ Cargo.toml | 14 +++- README.md | 1 + scripts/coverage_test.sh | 1 + scripts/test.sh | 3 +- scripts/watch_doc.sh | 2 +- scripts/watch_test.sh | 4 +- src/behavior.rs | 2 +- src/fmt.rs | 1 + src/select/select.rs | 54 +++++++++++--- src/select/select_internal.rs | 51 +++++++++++-- src/structure.rs | 37 ++++++---- tests/clause_except_spec.rs | 2 +- tests/clause_intersect_spec.rs | 2 +- tests/clause_limit_spec.rs | 2 +- tests/clause_offset_spec.rs | 2 +- tests/clause_select_spec.rs | 2 +- tests/clause_union_spec.rs | 2 +- tests/clause_with_spec.rs | 2 +- tests/command_select_spec.rs | 109 ++++++++++++++++++++++++++++ 20 files changed, 254 insertions(+), 44 deletions(-) diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 22ade34..44cae3c 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -25,3 +25,8 @@ jobs: with: command: test args: --features sqlite + - uses: actions-rs/cargo@v1 + name: Test MySQL syntax + with: + command: test + args: --features mysql diff --git a/Cargo.toml b/Cargo.toml index caf7d24..279de1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,20 +4,28 @@ description = "Write SQL queries in a simple and composable way" documentation = "https://docs.rs/sql_query_builder" repository = "https://github.com/belchior/sql_query_builder" authors = ["Belchior Oliveira "] -version = "2.4.2" +version = "2.5.1" edition = "2021" rust-version = "1.62" license = "MIT" -keywords = ["sql", "query", "database", "postgres", "sqlite"] +keywords = ["sql", "query", "postgres", "sqlite", "mysql"] [features] +#! SQL Query Builder comes with the following optional features: + +## enable Postgres syntax postgresql = [] + +## enable SQLite syntax sqlite = [] +## enable MySQL syntax +mysql = [] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] -features = ["postgresql", "sqlite"] +features = ["postgresql", "sqlite", "mysql"] [dev-dependencies] pretty_assertions = "=1.4.0" diff --git a/README.md b/README.md index bc51c9c..210098c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ SELECT id, login FROM users WHERE login = $1 AND is_admin = true SQL Query Builder comes with the following optional features: - `postgresql` enable Postgres syntax - `sqlite` enable SQLite syntax +- `mysql` enable MySQL syntax You can enable features like diff --git a/scripts/coverage_test.sh b/scripts/coverage_test.sh index 085860d..828cd70 100755 --- a/scripts/coverage_test.sh +++ b/scripts/coverage_test.sh @@ -16,6 +16,7 @@ clear RUSTFLAGS="-C instrument-coverage" LLVM_PROFILE_FILE="$COVERAGE_TARGET/$PKG_NAME-%m.profraw" cargo test --target-dir $COVERAGE_TARGET; RUSTFLAGS="-C instrument-coverage" LLVM_PROFILE_FILE="$COVERAGE_TARGET/$PKG_NAME-%m.profraw" cargo test --target-dir $COVERAGE_TARGET --features postgresql; RUSTFLAGS="-C instrument-coverage" LLVM_PROFILE_FILE="$COVERAGE_TARGET/$PKG_NAME-%m.profraw" cargo test --target-dir $COVERAGE_TARGET --features sqlite; +RUSTFLAGS="-C instrument-coverage" LLVM_PROFILE_FILE="$COVERAGE_TARGET/$PKG_NAME-%m.profraw" cargo test --target-dir $COVERAGE_TARGET --features mysql; cargo profdata -- merge -sparse $COVERAGE_TARGET/$PKG_NAME-*.profraw -o $COVERAGE_TARGET/$PKG_NAME.profdata; diff --git a/scripts/test.sh b/scripts/test.sh index 578a3bf..18074bc 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,11 +1,12 @@ #!/bin/sh -test_names=$(git status -s | grep tests/ | sed -e 's/.* //' -e 's/tests\//--test /' -e 's/.rs//' | tr '\n' ' ') +test_names=$(git status -s | grep tests/ | sed -e 's/.* //' -e 's/tests\//--test /' -e 's/\.rs//' | tr '\n' ' ') clear cargo test $test_names cargo test $test_names --features postgresql cargo test $test_names --features sqlite +cargo test $test_names --features mysql # run only one test # cargo test --features sqlite --test name_of_the_test_file name_of_the_test -- --nocapture --color always diff --git a/scripts/watch_doc.sh b/scripts/watch_doc.sh index 7204c42..4a19400 100755 --- a/scripts/watch_doc.sh +++ b/scripts/watch_doc.sh @@ -1,6 +1,6 @@ #!/bin/sh -doc_path=$(realpath ./target/doc/sql_query_builder/index.html) +doc_path=$(pwd)/target/doc/sql_query_builder/index.html c_blue='\033[34;1m' c_no='\033[0m' diff --git a/scripts/watch_test.sh b/scripts/watch_test.sh index c1bc033..141bb7c 100755 --- a/scripts/watch_test.sh +++ b/scripts/watch_test.sh @@ -8,9 +8,9 @@ # ./scripts/watch_test.sh all # will enable all feature # ./scripts/watch_test.sh postgresql # will enable only the postgresql feature -all_features='postgresql sqlite' +all_features='postgresql sqlite mysql' features='' -test_names=$(git status -s | grep tests/ | sed -e 's/.* //' -e 's/tests\//--test /' -e 's/.rs//' | tr '\n' ' ') +test_names=$(git status -s | grep tests/ | sed -e 's/.* //' -e 's/tests\//--test /' -e 's/\.rs//' | tr '\n' ' ') case "$@" in "") features="";; diff --git a/src/behavior.rs b/src/behavior.rs index 659c6c2..a6ad9c8 100644 --- a/src/behavior.rs +++ b/src/behavior.rs @@ -4,5 +4,5 @@ use crate::concat::Concat; pub trait TransactionQuery: Concat {} /// Represents all commands that can be used inside the with method -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] pub trait WithQuery: Concat {} diff --git a/src/fmt.rs b/src/fmt.rs index 100364e..60fdaf7 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -62,6 +62,7 @@ pub fn colorize(query: String) -> String { (blue, "ONLY ", "only "), (blue, "ORDER BY", "order by"), (blue, "OVERRIDING", "overriding"), + (blue, "PARTITION", "partition"), (blue, "PRIMARY", "primary"), (blue, "READ ONLY", "read only"), (blue, "READ WRITE", "read write"), diff --git a/src/select/select.rs b/src/select/select.rs index 7127608..6f527f5 100644 --- a/src/select/select.rs +++ b/src/select/select.rs @@ -565,22 +565,23 @@ impl Select { } } -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] use crate::behavior::WithQuery; -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] impl WithQuery for Select {} -#[cfg(any(doc, feature = "postgresql", feature = "sqlite"))] +#[cfg(any(doc, feature = "postgresql", feature = "sqlite", feature = "mysql"))] #[cfg_attr(docsrs, doc(cfg(feature = "postgresql")))] #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] +#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] impl Select { /// The `except` clause /// /// # Example /// /// ``` - /// # #[cfg(any(feature = "postgresql", feature = "sqlite"))] + /// # #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] /// # { /// # use sql_query_builder as sql; /// let select_users = sql::Select::new() @@ -620,7 +621,7 @@ impl Select { /// # Example /// /// ``` - /// # #[cfg(any(feature = "postgresql", feature = "sqlite"))] + /// # #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] /// # { /// # use sql_query_builder as sql; /// let select_users = sql::Select::new() @@ -660,7 +661,7 @@ impl Select { /// # Example /// /// ``` - /// # #[cfg(any(feature = "postgresql", feature = "sqlite"))] + /// # #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] /// # { /// # use sql_query_builder as sql; /// let select = sql::Select::new() @@ -684,7 +685,7 @@ impl Select { /// # Example /// /// ``` - /// # #[cfg(any(feature = "postgresql", feature = "sqlite"))] + /// # #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] /// # { /// # use sql_query_builder as sql; /// let select = sql::Select::new() @@ -708,7 +709,7 @@ impl Select { /// # Example /// /// ``` - /// # #[cfg(any(feature = "postgresql", feature = "sqlite"))] + /// # #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] /// # { /// # use sql_query_builder as sql; /// let select_users = sql::Select::new() @@ -748,7 +749,7 @@ impl Select { /// # Example /// /// ``` - /// # #[cfg(any(feature = "postgresql", feature = "sqlite"))] + /// # #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] /// # { /// # use sql_query_builder as sql; /// let logins = sql::Select::new() @@ -792,13 +793,46 @@ impl Select { /// WHERE owner_login in (select * from logins) /// -- ------------------------------------------------------------------------------ /// ``` - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] pub fn with(mut self, name: &str, query: impl WithQuery + Send + Sync + 'static) -> Self { self._with.push((name.trim().to_string(), std::sync::Arc::new(query))); self } } +#[cfg(any(doc, feature = "mysql"))] +#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] +impl Select { + /// The `partition` clause + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Select::new() + /// .select("*") + /// .from("employees") + /// .partition("p1") + /// .to_string(); + /// + /// # let expected_query = "SELECT * FROM employees PARTITION (p1)"; + /// # assert_eq!(expected_query, query); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// SELECT * FROM employees PARTITION (p1) + /// ``` + pub fn partition(mut self, name: &str) -> Self { + push_unique(&mut self._partition, name.trim().to_string()); + self + } +} + impl std::fmt::Display for Select { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.as_string()) diff --git a/src/select/select_internal.rs b/src/select/select_internal.rs index d039d1c..0c773dd 100644 --- a/src/select/select_internal.rs +++ b/src/select/select_internal.rs @@ -19,7 +19,7 @@ impl Concat for Select { query = self.concat_raw(query, &fmts, &self._raw); - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] { query = self.concat_with( &self._raw_before, @@ -49,6 +49,11 @@ impl Concat for Select { &self._join, ); + #[cfg(feature = "mysql")] + { + query = self.concat_partition(query, &fmts); + } + query = self.concat_where( &self._raw_before, &self._raw_after, @@ -69,7 +74,7 @@ impl Concat for Select { &self._order_by, ); - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] { query = self.concat_limit( &self._raw_before, @@ -82,7 +87,7 @@ impl Concat for Select { query = self.concat_offset(query, &fmts); } - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] { use crate::structure::Combinator; query = self.concat_combinator(query, &fmts, Combinator::Except); @@ -199,12 +204,12 @@ impl Select { #[cfg(any(feature = "postgresql", feature = "sqlite"))] use crate::concat::non_standard::{ConcatLimit, ConcatWith}; -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] impl ConcatWith for Select {} #[cfg(any(feature = "postgresql", feature = "sqlite"))] impl ConcatLimit for Select {} -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] impl Select { fn concat_combinator( &self, @@ -253,7 +258,7 @@ impl Select { } } -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] impl Select { fn concat_offset(&self, query: String, fmts: &fmt::Formatter) -> String { let fmt::Formatter { lb, space, .. } = fmts; @@ -274,3 +279,37 @@ impl Select { ) } } + +#[cfg(feature = "mysql")] +impl Select { + fn concat_partition(&self, query: String, fmts: &fmt::Formatter) -> String { + let fmt::Formatter { comma, lb, space, .. } = fmts; + + let sql = if self._partition.is_empty() == false { + let column_names = self + ._partition + .iter() + .filter(|column| column.is_empty() == false) + .map(|column| column.as_str()) + .collect::>() + .join(comma); + + if column_names.is_empty() == false { + format!("PARTITION{space}({column_names}){space}{lb}") + } else { + "".to_string() + } + } else { + "".to_string() + }; + + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + SelectClause::Partition, + sql, + ) + } +} diff --git a/src/structure.rs b/src/structure.rs index 2b0d0ce..6d17e68 100644 --- a/src/structure.rs +++ b/src/structure.rs @@ -1,9 +1,9 @@ use crate::behavior::TransactionQuery; -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] use crate::behavior::WithQuery; -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] use std::sync::Arc; /// Builder to contruct a [AlterTable] command. @@ -77,7 +77,7 @@ pub enum AlterTableAction { Drop, } -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] pub(crate) enum Combinator { Except, Intersect, @@ -492,23 +492,26 @@ pub struct Select { pub(crate) _where: Vec<(LogicalOperator, String)>, pub(crate) _window: Vec, - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] pub(crate) _except: Vec, - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] pub(crate) _intersect: Vec, - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] pub(crate) _limit: String, - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] pub(crate) _offset: String, - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] pub(crate) _union: Vec, - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] pub(crate) _with: Vec<(String, Arc)>, + + #[cfg(feature = "mysql")] + pub(crate) _partition: Vec, } /// All available clauses to be used in [Select::raw_before] and [Select::raw_after] methods on [Select] builder @@ -525,25 +528,33 @@ pub enum SelectClause { Where, Window, - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] #[cfg_attr(docsrs, doc(cfg(feature = "postgresql")))] #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] + #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] Except, - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] #[cfg_attr(docsrs, doc(cfg(feature = "postgresql")))] #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] + #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] Intersect, - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] #[cfg_attr(docsrs, doc(cfg(feature = "postgresql")))] #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] + #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] Union, - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] #[cfg_attr(docsrs, doc(cfg(feature = "postgresql")))] #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] + #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] With, + + #[cfg(feature = "mysql")] + #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] + Partition, } /// Builder to contruct a [Transaction] block. diff --git a/tests/clause_except_spec.rs b/tests/clause_except_spec.rs index c789129..2a7ea89 100644 --- a/tests/clause_except_spec.rs +++ b/tests/clause_except_spec.rs @@ -1,4 +1,4 @@ -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] mod select_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; diff --git a/tests/clause_intersect_spec.rs b/tests/clause_intersect_spec.rs index b806ff8..5e5da5d 100644 --- a/tests/clause_intersect_spec.rs +++ b/tests/clause_intersect_spec.rs @@ -1,4 +1,4 @@ -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] mod select_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; diff --git a/tests/clause_limit_spec.rs b/tests/clause_limit_spec.rs index ee425b8..1d92bac 100644 --- a/tests/clause_limit_spec.rs +++ b/tests/clause_limit_spec.rs @@ -1,4 +1,4 @@ -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] mod select_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; diff --git a/tests/clause_offset_spec.rs b/tests/clause_offset_spec.rs index d5b0724..76646f7 100644 --- a/tests/clause_offset_spec.rs +++ b/tests/clause_offset_spec.rs @@ -1,4 +1,4 @@ -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] mod select_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; diff --git a/tests/clause_select_spec.rs b/tests/clause_select_spec.rs index c6987cc..996eead 100644 --- a/tests/clause_select_spec.rs +++ b/tests/clause_select_spec.rs @@ -106,7 +106,7 @@ mod select_command { } #[test] - fn method_select_by_should_trim_space_of_the_argument() { + fn method_select_should_trim_space_of_the_argument() { let query = sql::Select::new().select(" login, name ").as_string(); let expected_query = "SELECT login, name"; diff --git a/tests/clause_union_spec.rs b/tests/clause_union_spec.rs index 681e2fb..688193e 100644 --- a/tests/clause_union_spec.rs +++ b/tests/clause_union_spec.rs @@ -1,4 +1,4 @@ -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] mod select_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; diff --git a/tests/clause_with_spec.rs b/tests/clause_with_spec.rs index da848c3..dc47edf 100644 --- a/tests/clause_with_spec.rs +++ b/tests/clause_with_spec.rs @@ -280,7 +280,7 @@ mod insert_command { } } -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] mod select_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; diff --git a/tests/command_select_spec.rs b/tests/command_select_spec.rs index 3a2678d..f57d4ac 100644 --- a/tests/command_select_spec.rs +++ b/tests/command_select_spec.rs @@ -289,3 +289,112 @@ mod builder_methods { assert_eq!(query, expected_query); } } + +#[cfg(feature = "mysql")] +mod partition_method { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_partition_should_define_the_partition_clause() { + let query = sql::Select::new().partition("p0").as_string(); + let expected_query = "PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_not_defined_the_clause_without_partition_names() { + let query = sql::Select::new().partition("").as_string(); + let expected_query = ""; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_accumulate_names_on_consecutive_calls() { + let query = sql::Select::new().partition("p0").partition("p1").as_string(); + + let expected_query = "PARTITION (p0, p1)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_not_accumulate_values_when_expression_is_empty() { + let query = sql::Select::new() + .partition("") + .partition("p0") + .partition("") + .as_string(); + + let expected_query = "PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_not_accumulate_names_with_the_same_content() { + let query = sql::Select::new().partition("p0").partition("p0").as_string(); + let expected_query = "PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_trim_space_of_the_argument() { + let query = sql::Select::new().partition(" p0 ").as_string(); + let expected_query = "PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_be_defined_after_from_clause() { + let query = sql::Select::new().from("employees").partition("p0").as_string(); + let expected_query = "FROM employees PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_be_defined_after_join_clauses() { + let query = sql::Select::new() + .from("employees") + .inner_join("addresses ON employees.login = addresses.login") + .partition("p0") + .as_string(); + + let expected_query = "\ + FROM employees \ + INNER JOIN addresses ON employees.login = addresses.login \ + PARTITION (p0)\ + "; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_after_should_add_raw_sql_after_partition_parameter() { + let query = sql::Select::new() + .partition("name") + .raw_after(sql::SelectClause::Partition, "/* uncommon parameter */") + .as_string(); + + let expected_query = "PARTITION (name) /* uncommon parameter */"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_before_should_add_raw_sql_before_partition_parameter() { + let query = sql::Select::new() + .raw_before(sql::SelectClause::Partition, "/* uncommon parameter */") + .partition("name") + .as_string(); + + let expected_query = "/* uncommon parameter */ PARTITION (name)"; + + assert_eq!(expected_query, query); + } +} From e0b7f58e8dbcfd46d27eb264a51fba2375a64817 Mon Sep 17 00:00:00 2001 From: Belchior Oliveira Date: Tue, 12 Nov 2024 16:44:43 -0300 Subject: [PATCH 02/19] refactor mysql concat_partition --- src/concat/mod.rs | 1 + src/concat/mysql.rs | 36 +++++++++++ src/concat/non_standard.rs | 4 +- src/select/select_internal.rs | 43 ++++--------- tests/clause_partition_spec.rs | 108 ++++++++++++++++++++++++++++++++ tests/command_select_spec.rs | 109 --------------------------------- 6 files changed, 158 insertions(+), 143 deletions(-) create mode 100644 src/concat/mysql.rs create mode 100644 tests/clause_partition_spec.rs diff --git a/src/concat/mod.rs b/src/concat/mod.rs index acd9247..5fb430d 100644 --- a/src/concat/mod.rs +++ b/src/concat/mod.rs @@ -1,5 +1,6 @@ use crate::fmt; +pub(crate) mod mysql; pub(crate) mod non_standard; pub(crate) mod sql_standard; pub(crate) mod sqlite; diff --git a/src/concat/mysql.rs b/src/concat/mysql.rs new file mode 100644 index 0000000..9a84834 --- /dev/null +++ b/src/concat/mysql.rs @@ -0,0 +1,36 @@ +#[cfg(feature = "mysql")] +use crate::{concat::concat_raw_before_after, fmt}; + +#[cfg(feature = "mysql")] +pub(crate) trait ConcatPartition { + fn concat_partition( + &self, + items_raw_before: &Vec<(Clause, String)>, + items_raw_after: &Vec<(Clause, String)>, + query: String, + fmts: &fmt::Formatter, + clause: Clause, + items: &Vec, + ) -> String { + let fmt::Formatter { comma, lb, space, .. } = fmts; + + let sql = if items.is_empty() == false { + let column_names = items + .iter() + .filter(|column| column.is_empty() == false) + .map(|column| column.as_str()) + .collect::>() + .join(comma); + + if column_names.is_empty() == false { + format!("PARTITION{space}({column_names}){space}{lb}") + } else { + "".to_string() + } + } else { + "".to_string() + }; + + concat_raw_before_after(items_raw_before, items_raw_after, query, fmts, clause, sql) + } +} diff --git a/src/concat/non_standard.rs b/src/concat/non_standard.rs index 828b58d..89c61ca 100644 --- a/src/concat/non_standard.rs +++ b/src/concat/non_standard.rs @@ -1,4 +1,4 @@ -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] use crate::{behavior::WithQuery, concat::concat_raw_before_after, fmt}; #[cfg(any(feature = "postgresql", feature = "sqlite"))] @@ -51,7 +51,7 @@ pub(crate) trait ConcatReturning { } } -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] pub(crate) trait ConcatWith { fn concat_with( &self, diff --git a/src/select/select_internal.rs b/src/select/select_internal.rs index 0c773dd..37b2b2b 100644 --- a/src/select/select_internal.rs +++ b/src/select/select_internal.rs @@ -51,7 +51,14 @@ impl Concat for Select { #[cfg(feature = "mysql")] { - query = self.concat_partition(query, &fmts); + query = self.concat_partition( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::Partition, + &self._partition, + ); } query = self.concat_where( @@ -281,35 +288,7 @@ impl Select { } #[cfg(feature = "mysql")] -impl Select { - fn concat_partition(&self, query: String, fmts: &fmt::Formatter) -> String { - let fmt::Formatter { comma, lb, space, .. } = fmts; +use crate::concat::mysql::ConcatPartition; - let sql = if self._partition.is_empty() == false { - let column_names = self - ._partition - .iter() - .filter(|column| column.is_empty() == false) - .map(|column| column.as_str()) - .collect::>() - .join(comma); - - if column_names.is_empty() == false { - format!("PARTITION{space}({column_names}){space}{lb}") - } else { - "".to_string() - } - } else { - "".to_string() - }; - - concat_raw_before_after( - &self._raw_before, - &self._raw_after, - query, - fmts, - SelectClause::Partition, - sql, - ) - } -} +#[cfg(feature = "mysql")] +impl ConcatPartition for Select {} diff --git a/tests/clause_partition_spec.rs b/tests/clause_partition_spec.rs new file mode 100644 index 0000000..1229c00 --- /dev/null +++ b/tests/clause_partition_spec.rs @@ -0,0 +1,108 @@ +#[cfg(feature = "mysql")] +mod select_command { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_partition_should_define_the_partition_clause() { + let query = sql::Select::new().partition("p0").as_string(); + let expected_query = "PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_not_defined_the_clause_without_partition_names() { + let query = sql::Select::new().partition("").as_string(); + let expected_query = ""; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_accumulate_names_on_consecutive_calls() { + let query = sql::Select::new().partition("p0").partition("p1").as_string(); + + let expected_query = "PARTITION (p0, p1)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_not_accumulate_values_when_expression_is_empty() { + let query = sql::Select::new() + .partition("") + .partition("p0") + .partition("") + .as_string(); + + let expected_query = "PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_not_accumulate_names_with_the_same_content() { + let query = sql::Select::new().partition("p0").partition("p0").as_string(); + let expected_query = "PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_trim_space_of_the_argument() { + let query = sql::Select::new().partition(" p0 ").as_string(); + let expected_query = "PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_be_defined_after_from_clause() { + let query = sql::Select::new().from("employees").partition("p0").as_string(); + let expected_query = "FROM employees PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_be_defined_after_join_clauses() { + let query = sql::Select::new() + .from("employees") + .inner_join("addresses ON employees.login = addresses.login") + .partition("p0") + .as_string(); + + let expected_query = "\ + FROM employees \ + INNER JOIN addresses ON employees.login = addresses.login \ + PARTITION (p0)\ + "; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_after_should_add_raw_sql_after_partition_parameter() { + let query = sql::Select::new() + .partition("name") + .raw_after(sql::SelectClause::Partition, "/* uncommon parameter */") + .as_string(); + + let expected_query = "PARTITION (name) /* uncommon parameter */"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_before_should_add_raw_sql_before_partition_parameter() { + let query = sql::Select::new() + .raw_before(sql::SelectClause::Partition, "/* uncommon parameter */") + .partition("name") + .as_string(); + + let expected_query = "/* uncommon parameter */ PARTITION (name)"; + + assert_eq!(expected_query, query); + } +} diff --git a/tests/command_select_spec.rs b/tests/command_select_spec.rs index f57d4ac..3a2678d 100644 --- a/tests/command_select_spec.rs +++ b/tests/command_select_spec.rs @@ -289,112 +289,3 @@ mod builder_methods { assert_eq!(query, expected_query); } } - -#[cfg(feature = "mysql")] -mod partition_method { - use pretty_assertions::assert_eq; - use sql_query_builder as sql; - - #[test] - fn method_partition_should_define_the_partition_clause() { - let query = sql::Select::new().partition("p0").as_string(); - let expected_query = "PARTITION (p0)"; - - assert_eq!(expected_query, query); - } - - #[test] - fn method_partition_should_not_defined_the_clause_without_partition_names() { - let query = sql::Select::new().partition("").as_string(); - let expected_query = ""; - - assert_eq!(expected_query, query); - } - - #[test] - fn method_partition_should_accumulate_names_on_consecutive_calls() { - let query = sql::Select::new().partition("p0").partition("p1").as_string(); - - let expected_query = "PARTITION (p0, p1)"; - - assert_eq!(expected_query, query); - } - - #[test] - fn method_partition_should_not_accumulate_values_when_expression_is_empty() { - let query = sql::Select::new() - .partition("") - .partition("p0") - .partition("") - .as_string(); - - let expected_query = "PARTITION (p0)"; - - assert_eq!(expected_query, query); - } - - #[test] - fn method_partition_should_not_accumulate_names_with_the_same_content() { - let query = sql::Select::new().partition("p0").partition("p0").as_string(); - let expected_query = "PARTITION (p0)"; - - assert_eq!(expected_query, query); - } - - #[test] - fn method_partition_should_trim_space_of_the_argument() { - let query = sql::Select::new().partition(" p0 ").as_string(); - let expected_query = "PARTITION (p0)"; - - assert_eq!(expected_query, query); - } - - #[test] - fn method_partition_should_be_defined_after_from_clause() { - let query = sql::Select::new().from("employees").partition("p0").as_string(); - let expected_query = "FROM employees PARTITION (p0)"; - - assert_eq!(expected_query, query); - } - - #[test] - fn method_partition_should_be_defined_after_join_clauses() { - let query = sql::Select::new() - .from("employees") - .inner_join("addresses ON employees.login = addresses.login") - .partition("p0") - .as_string(); - - let expected_query = "\ - FROM employees \ - INNER JOIN addresses ON employees.login = addresses.login \ - PARTITION (p0)\ - "; - - assert_eq!(expected_query, query); - } - - #[test] - fn method_raw_after_should_add_raw_sql_after_partition_parameter() { - let query = sql::Select::new() - .partition("name") - .raw_after(sql::SelectClause::Partition, "/* uncommon parameter */") - .as_string(); - - let expected_query = "PARTITION (name) /* uncommon parameter */"; - - assert_eq!(expected_query, query); - } - - #[test] - fn method_raw_before_should_add_raw_sql_before_partition_parameter() { - let query = sql::Select::new() - .raw_before(sql::SelectClause::Partition, "/* uncommon parameter */") - .partition("name") - .as_string(); - - let expected_query = "/* uncommon parameter */ PARTITION (name)"; - - assert_eq!(expected_query, query); - } -} From fe3b3edee727c704e193944bec8f2408ac1c84ac Mon Sep 17 00:00:00 2001 From: Belchior Oliveira Date: Sun, 17 Nov 2024 18:00:07 -0300 Subject: [PATCH 03/19] Adds MySQL syntax to Delete builder --- scripts/coverage_test.sh | 17 +- scripts/test.sh | 17 +- scripts/watch_test.sh | 2 +- src/concat/non_standard.rs | 2 +- src/delete/delete.rs | 449 +++++++++++++++++++++++++++---- src/delete/delete_internal.rs | 218 ++++++++++++++- src/select/select.rs | 6 +- src/select/select_internal.rs | 37 +-- src/structure.rs | 149 ++++++---- src/utils.rs | 9 + tests/clause_delete_from_spec.rs | 53 ---- tests/clause_from_spec.rs | 108 ++++++-- tests/clause_join_spec.rs | 311 +++++++++++++++++++++ tests/clause_limit_spec.rs | 72 ++++- tests/clause_order_by_spec.rs | 115 +++++++- tests/clause_partition_spec.rs | 109 ++++++++ tests/command_delete_spec.rs | 196 ++++++++++++-- 17 files changed, 1622 insertions(+), 248 deletions(-) delete mode 100644 tests/clause_delete_from_spec.rs diff --git a/scripts/coverage_test.sh b/scripts/coverage_test.sh index 828cd70..f30a3a7 100755 --- a/scripts/coverage_test.sh +++ b/scripts/coverage_test.sh @@ -3,6 +3,7 @@ # Prerequisites # cargo install rustfilt cargo-binutils # rustup component add llvm-tools-preview +clear PKG_NAME="$(grep 'name\s*=\s*"' Cargo.toml | sed -E 's/.*"(.*)"/\1/')" COVERAGE_OUTPUT="coverage" @@ -11,11 +12,25 @@ COVERAGE_TARGET="target/coverage" rm -fr "$COVERAGE_TARGET" mkdir -p "$COVERAGE_OUTPUT" mkdir -p "$COVERAGE_TARGET" -clear +echo "\n-- ------------------------------------------------------------------------------" +echo "-- Testing SQL Standard" +echo "-- ------------------------------------------------------------------------------\n" RUSTFLAGS="-C instrument-coverage" LLVM_PROFILE_FILE="$COVERAGE_TARGET/$PKG_NAME-%m.profraw" cargo test --target-dir $COVERAGE_TARGET; + +echo "\n-- ------------------------------------------------------------------------------" +echo "-- Testing PostgreSQL syntax" +echo "-- ------------------------------------------------------------------------------\n" RUSTFLAGS="-C instrument-coverage" LLVM_PROFILE_FILE="$COVERAGE_TARGET/$PKG_NAME-%m.profraw" cargo test --target-dir $COVERAGE_TARGET --features postgresql; + +echo "\n-- ------------------------------------------------------------------------------" +echo "-- Testing SQLite syntax" +echo "-- ------------------------------------------------------------------------------\n" RUSTFLAGS="-C instrument-coverage" LLVM_PROFILE_FILE="$COVERAGE_TARGET/$PKG_NAME-%m.profraw" cargo test --target-dir $COVERAGE_TARGET --features sqlite; + +echo "\n-- ------------------------------------------------------------------------------" +echo "-- Testing MySQL syntax" +echo "-- ------------------------------------------------------------------------------\n" RUSTFLAGS="-C instrument-coverage" LLVM_PROFILE_FILE="$COVERAGE_TARGET/$PKG_NAME-%m.profraw" cargo test --target-dir $COVERAGE_TARGET --features mysql; cargo profdata -- merge -sparse $COVERAGE_TARGET/$PKG_NAME-*.profraw -o $COVERAGE_TARGET/$PKG_NAME.profdata; diff --git a/scripts/test.sh b/scripts/test.sh index 18074bc..9c96da9 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,11 +1,26 @@ #!/bin/sh -test_names=$(git status -s | grep tests/ | sed -e 's/.* //' -e 's/tests\//--test /' -e 's/\.rs//' | tr '\n' ' ') +test_names=$(git status -s | grep 'A tests/\|M tests/' | sed -e 's/.* //' -e 's/tests\//--test /' -e 's/\.rs//' | tr '\n' ' ') clear +echo "\n-- ------------------------------------------------------------------------------" +echo "-- Testing SQL Standard" +echo "-- ------------------------------------------------------------------------------\n" cargo test $test_names + +echo "\n-- ------------------------------------------------------------------------------" +echo "-- Testing PostgreSQL syntax" +echo "-- ------------------------------------------------------------------------------\n" cargo test $test_names --features postgresql + +echo "\n-- ------------------------------------------------------------------------------" +echo "-- Testing SQLite syntax" +echo "-- ------------------------------------------------------------------------------\n" cargo test $test_names --features sqlite + +echo "\n-- ------------------------------------------------------------------------------" +echo "-- Testing MySQL syntax" +echo "-- ------------------------------------------------------------------------------\n" cargo test $test_names --features mysql # run only one test diff --git a/scripts/watch_test.sh b/scripts/watch_test.sh index 141bb7c..ddb7839 100755 --- a/scripts/watch_test.sh +++ b/scripts/watch_test.sh @@ -10,7 +10,7 @@ all_features='postgresql sqlite mysql' features='' -test_names=$(git status -s | grep tests/ | sed -e 's/.* //' -e 's/tests\//--test /' -e 's/\.rs//' | tr '\n' ' ') +test_names=$(git status -s | grep 'A tests/\|M tests/' | sed -e 's/.* //' -e 's/tests\//--test /' -e 's/\.rs//' | tr '\n' ' ') case "$@" in "") features="";; diff --git a/src/concat/non_standard.rs b/src/concat/non_standard.rs index 89c61ca..858dff7 100644 --- a/src/concat/non_standard.rs +++ b/src/concat/non_standard.rs @@ -1,7 +1,7 @@ #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] use crate::{behavior::WithQuery, concat::concat_raw_before_after, fmt}; -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] pub(crate) trait ConcatLimit { fn concat_limit( &self, diff --git a/src/delete/delete.rs b/src/delete/delete.rs index 7915e61..9ca23dd 100644 --- a/src/delete/delete.rs +++ b/src/delete/delete.rs @@ -21,7 +21,7 @@ impl Delete { /// .as_string(); /// /// # let expected = "DELETE FROM users WHERE id = $1"; - /// # assert_eq!(query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Output @@ -41,11 +41,11 @@ impl Delete { /// /// ``` /// # use sql_query_builder as sql; - /// let delete_query = sql::Delete::new() + /// let query = sql::Delete::new() /// .delete_from("users") /// .where_clause("login = 'foo'") - /// .debug() /// .where_clause("name = 'Foo'") + /// .debug() /// .as_string(); /// ``` /// @@ -54,7 +54,9 @@ impl Delete { /// ```sql /// -- ------------------------------------------------------------------------------ /// DELETE FROM users - /// WHERE login = 'foo' + /// WHERE + /// login = 'foo' + /// AND name = 'Foo' /// -- ------------------------------------------------------------------------------ /// ``` pub fn debug(self) -> Self { @@ -63,7 +65,7 @@ impl Delete { self } - /// The `delete` clause. This method overrides the previous value + /// The `delete` and `from` clauses. This method overrides the previous value /// /// # Example /// @@ -73,17 +75,23 @@ impl Delete { /// .delete_from("orders"); /// # /// # let expected = "DELETE FROM orders"; - /// # assert_eq!(delete.to_string(), expected); + /// # assert_eq!(expected, delete.to_string()); /// /// let delete = sql::Delete::new() /// .delete_from("addresses") /// .delete_from("orders"); /// /// # let expected = "DELETE FROM orders"; - /// # assert_eq!(delete.to_string(), expected); + /// # assert_eq!(expected, delete.to_string()); + /// ``` + /// + /// Output + /// + /// ```sql + /// DELETE FROM orders /// ``` - pub fn delete_from(mut self, table_name: &str) -> Self { - self._delete_from = table_name.trim().to_string(); + pub fn delete_from(mut self, table: &str) -> Self { + self._delete_from = table.trim().to_string(); self } @@ -108,13 +116,13 @@ impl Delete { /// # use sql_query_builder as sql; /// let raw_query = "delete from users"; /// - /// let delete_query = sql::Delete::new() + /// let query = sql::Delete::new() /// .raw(raw_query) /// .where_clause("login = 'foo'") /// .as_string(); /// /// # let expected = "delete from users WHERE login = 'foo'"; - /// # assert_eq!(delete_query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Output @@ -135,13 +143,13 @@ impl Delete { /// # use sql_query_builder as sql; /// let raw = "where name = 'Foo'"; /// - /// let delete_query = sql::Delete::new() + /// let query = sql::Delete::new() /// .delete_from("users") /// .raw_after(sql::DeleteClause::DeleteFrom, raw) /// .as_string(); /// /// # let expected = "DELETE FROM users where name = 'Foo'"; - /// # assert_eq!(delete_query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Output @@ -162,13 +170,13 @@ impl Delete { /// # use sql_query_builder as sql; /// let raw = "delete from users"; /// - /// let delete_query = sql::Delete::new() + /// let query = sql::Delete::new() /// .raw_before(sql::DeleteClause::Where, raw) /// .where_clause("name = 'Bar'") /// .as_string(); /// /// # let expected = "delete from users WHERE name = 'Bar'"; - /// # assert_eq!(delete_query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Output @@ -187,14 +195,14 @@ impl Delete { /// /// ``` /// # use sql_query_builder as sql; - /// let delete_query = sql::Delete::new() + /// let query = sql::Delete::new() /// .where_clause("login = $1") /// .where_and("product_id = $2") /// .where_and("created_at >= current_date") /// .as_string(); /// /// # let expected = "WHERE login = $1 AND product_id = $2 AND created_at >= current_date"; - /// # assert_eq!(delete_query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Outputs @@ -216,13 +224,13 @@ impl Delete { /// /// ``` /// # use sql_query_builder as sql; - /// let delete_query = sql::Delete::new() + /// let query = sql::Delete::new() /// .where_clause("login = $1") /// .where_clause("status = 'deactivated'") /// .as_string(); /// /// # let expected = "WHERE login = $1 AND status = 'deactivated'"; - /// # assert_eq!(delete_query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Outputs @@ -244,13 +252,13 @@ impl Delete { /// /// ``` /// # use sql_query_builder as sql; - /// let delete_query = sql::Delete::new() + /// let query = sql::Delete::new() /// .where_clause("login = 'foo'") /// .where_or("login = 'bar'") /// .as_string(); /// /// # let expected = "WHERE login = 'foo' OR login = 'bar'"; - /// # assert_eq!(delete_query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Output @@ -266,12 +274,67 @@ impl Delete { } } -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] use crate::behavior::WithQuery; -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] impl WithQuery for Delete {} +#[cfg(any(doc, feature = "postgresql", feature = "sqlite", feature = "mysql"))] +#[cfg_attr(docsrs, doc(cfg(feature = "postgresql")))] +#[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] +#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] +impl Delete { + /// The `with` clause + /// + /// # Example + /// + /// ``` + /// # #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] + /// # { + /// # use sql_query_builder as sql; + /// let deactivated_users = sql::Select::new() + /// .select("id") + /// .from("users") + /// .where_clause("ative = false"); + /// + /// let delete = sql::Delete::new() + /// .with("deactivated_users", deactivated_users) + /// .delete_from("users") + /// .where_clause("id in (select * from deactivated_users)") + /// .debug(); + /// + /// # let expected = "\ + /// # WITH deactivated_users AS (\ + /// # SELECT id \ + /// # FROM users \ + /// # WHERE ative = false\ + /// # ) \ + /// # DELETE FROM users \ + /// # WHERE id in (select * from deactivated_users)\ + /// # "; + /// # assert_eq!(expected, delete.to_string()); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// WITH deactivated_users AS ( + /// SELECT id + /// FROM users + /// WHERE ative = false + /// ) + /// DELETE FROM users + /// WHERE id in (select * from deactivated_users) + /// ``` + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] + pub fn with(mut self, name: &str, query: impl WithQuery + 'static + Send + Sync) -> Self { + self._with.push((name.trim().to_string(), std::sync::Arc::new(query))); + self + } +} + #[cfg(any(doc, feature = "postgresql", feature = "sqlite"))] #[cfg_attr(docsrs, doc(cfg(feature = "postgresql")))] #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] @@ -290,7 +353,7 @@ impl Delete { /// .returning("login"); /// /// # let expected = "DELETE FROM users RETURNING id, login"; - /// # assert_eq!(delete.to_string(), expected); + /// # assert_eq!(expected, delete.to_string()); /// # } /// ``` /// @@ -303,49 +366,333 @@ impl Delete { push_unique(&mut self._returning, output_name.trim().to_string()); self } +} - /// The `with` clause +#[cfg(any(doc, feature = "mysql"))] +#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] +impl Delete { + /// The `delete` clause. MySQL allow single and multi-table deletes /// - /// # Example + /// # Single-table delete /// /// ``` - /// # #[cfg(any(feature = "postgresql", feature = "sqlite"))] + /// # #[cfg(feature = "mysql")] /// # { /// # use sql_query_builder as sql; - /// let deactivated_users = sql::Select::new().select("id").from("users").where_clause("ative = false"); /// let delete = sql::Delete::new() - /// .with("deactivated_users", deactivated_users) - /// .delete_from("users") - /// .where_clause("id in (select * from deactivated_users)") - /// .debug(); + /// .delete("low_priority") + /// .from("t1") + /// .where_clause("t1.id = '123'"); + /// + /// # let expected = "DELETE low_priority FROM t1 WHERE t1.id = '123'"; + /// # assert_eq!(expected, delete.to_string()); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// DELETE low_priority FROM t1 WHERE t1.id = '123' + /// ``` + /// + /// If the delete clause has no argument you can use the [delete_from](Delete::delete_from) method + /// + /// # Single-table delete + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let delete = sql::Delete::new() + /// .delete_from("t1") + /// .where_clause("t1.id = '123'"); + /// + /// # let expected = "DELETE FROM t1 WHERE t1.id = '123'"; + /// # assert_eq!(expected, delete.to_string()); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// DELETE FROM t1 WHERE t1.id = '123' + /// ``` + /// + /// # Multi-table deletes + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let delete = sql::Delete::new() + /// .delete("t1") + /// .delete("t2") + /// .from("t1") + /// .inner_join("t2") + /// .inner_join("t3") + /// .where_clause("t1.id = t2.id") + /// .where_and("t2.id = t3.id"); /// /// # let expected = "\ - /// WITH deactivated_users AS (\ - /// SELECT id \ - /// FROM users \ - /// WHERE ative = false\ - /// ) \ - /// DELETE FROM users \ - /// WHERE id in (select * from deactivated_users)\ - /// "; - /// # assert_eq!(delete.to_string(), expected); + /// # DELETE t1, t2 \ + /// # FROM t1 \ + /// # INNER JOIN t2 \ + /// # INNER JOIN t3 \ + /// # WHERE \ + /// # t1.id = t2.id \ + /// # AND t2.id = t3.id\ + /// # "; + /// # assert_eq!(expected, delete.to_string()); /// # } /// ``` /// /// Output /// /// ```sql - /// WITH deactivated_users AS ( - /// SELECT id - /// FROM users - /// WHERE ative = false - /// ) - /// DELETE FROM users - /// WHERE id in (select * from deactivated_users) + /// DELETE t1, t2 + /// FROM t1 + /// INNER JOIN t2 + /// INNER JOIN t3 + /// WHERE + /// t1.id = t2.id + /// AND t2.id = t3.id /// ``` - #[cfg(any(feature = "postgresql", feature = "sqlite"))] - pub fn with(mut self, name: &str, query: impl WithQuery + 'static + Send + Sync) -> Self { - self._with.push((name.trim().to_string(), std::sync::Arc::new(query))); + pub fn delete(mut self, table: &str) -> Self { + push_unique(&mut self._delete, table.trim().to_string()); + self + } + + /// The `from` clause + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let select = sql::Delete::new() + /// .from("users"); + /// + /// # let expected = "FROM users"; + /// # assert_eq!(expected, select.as_string()); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// FROM users + /// ``` + pub fn from(mut self, table: &str) -> Self { + push_unique(&mut self._from, table.trim().to_string()); + self + } + + /// The `cross join` clause + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Delete::new() + /// .cross_join("addresses") + /// .as_string(); + /// + /// # let expected = "CROSS JOIN addresses"; + /// # assert_eq!(expected, query); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// CROSS JOIN addresses + /// ``` + pub fn cross_join(mut self, table: &str) -> Self { + let table = table.trim(); + if table.is_empty() == false { + let join = format!("CROSS JOIN {table}"); + push_unique(&mut self._join, join); + } + self + } + + /// The `inner join` clause + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Delete::new() + /// .inner_join("addresses on addresses.user_login = users.login") + /// .as_string(); + /// + /// # let expected = "INNER JOIN addresses on addresses.user_login = users.login"; + /// # assert_eq!(query, expected); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// INNER JOIN addresses on addresses.user_login = users.login + /// ``` + pub fn inner_join(mut self, table: &str) -> Self { + let table = table.trim(); + if table.is_empty() == false { + let join = format!("INNER JOIN {table}"); + push_unique(&mut self._join, join); + } + self + } + + /// The `left join` clause + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Delete::new() + /// .left_join("addresses on addresses.user_login = users.login") + /// .as_string(); + /// + /// # let expected = "LEFT JOIN addresses on addresses.user_login = users.login"; + /// # assert_eq!(query, expected); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// LEFT JOIN addresses on addresses.user_login = users.login + /// ``` + pub fn left_join(mut self, table: &str) -> Self { + let table = table.trim(); + if table.is_empty() == false { + let join = format!("LEFT JOIN {table}"); + push_unique(&mut self._join, join); + } + self + } + + /// The `right join` clause + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Delete::new() + /// .right_join("addresses on addresses.user_login = users.login") + /// .as_string(); + /// + /// # let expected = "RIGHT JOIN addresses on addresses.user_login = users.login"; + /// # assert_eq!(query, expected); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// RIGHT JOIN addresses on addresses.user_login = users.login + /// ``` + pub fn right_join(mut self, table: &str) -> Self { + let table = table.trim(); + if table.is_empty() == false { + let join = format!("RIGHT JOIN {table}"); + push_unique(&mut self._join, join); + } + self + } + + /// The `limit` clause, this method overrides the previous value + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let delete = sql::Delete::new() + /// .limit("123"); + /// + /// let delete = sql::Delete::new() + /// .limit("1000") + /// .limit("123"); + /// + /// # let expected = "LIMIT 123"; + /// # assert_eq!(expected, delete.as_string()); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// LIMIT 123 + /// ``` + pub fn limit(mut self, num: &str) -> Self { + self._limit = num.trim().to_string(); + self + } + + /// The `order by` clause + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let delete = sql::Delete::new() + /// .order_by("created_at asc"); + /// + /// # let expected = "ORDER BY created_at asc"; + /// # assert_eq!(expected, delete.as_string()); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// ORDER BY created_at asc + /// ``` + pub fn order_by(mut self, column: &str) -> Self { + push_unique(&mut self._order_by, column.trim().to_string()); + self + } + + /// The `partition` clause + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Delete::new() + /// .delete_from("employees") + /// .partition("p1") + /// .to_string(); + /// + /// # let expected = "DELETE FROM employees PARTITION (p1)"; + /// # assert_eq!(expected, query); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// DELETE FROM employees PARTITION (p1) + /// ``` + pub fn partition(mut self, name: &str) -> Self { + push_unique(&mut self._partition, name.trim().to_string()); self } } diff --git a/src/delete/delete_internal.rs b/src/delete/delete_internal.rs index dfa745a..4f7f71d 100644 --- a/src/delete/delete_internal.rs +++ b/src/delete/delete_internal.rs @@ -11,7 +11,8 @@ impl Concat for Delete { let mut query = "".to_string(); query = self.concat_raw(query, &fmts, &self._raw); - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] { query = self.concat_with( &self._raw_before, @@ -22,7 +23,33 @@ impl Concat for Delete { &self._with, ); } - query = self.concat_delete_from(query, &fmts); + + #[cfg(not(feature = "mysql"))] + { + query = self.concat_delete_from(query, &fmts); + } + + #[cfg(feature = "mysql")] + { + query = self.concat_delete_from_mysql(query, &fmts); + query = self.concat_join( + &self._raw_before, + &self._raw_after, + query, + &fmts, + DeleteClause::Join, + &self._join, + ); + query = self.concat_partition( + &self._raw_before, + &self._raw_after, + query, + &fmts, + DeleteClause::Partition, + &self._partition, + ); + } + query = self.concat_where( &self._raw_before, &self._raw_after, @@ -31,6 +58,27 @@ impl Concat for Delete { DeleteClause::Where, &self._where, ); + + #[cfg(feature = "mysql")] + { + query = self.concat_order_by( + &self._raw_before, + &self._raw_after, + query, + &fmts, + DeleteClause::OrderBy, + &self._order_by, + ); + query = self.concat_limit( + &self._raw_before, + &self._raw_after, + query, + &fmts, + DeleteClause::Limit, + &self._limit, + ); + } + #[cfg(any(feature = "postgresql", feature = "sqlite"))] { query = self.concat_returning( @@ -48,6 +96,7 @@ impl Concat for Delete { } impl Delete { + #[cfg(not(feature = "mysql"))] fn concat_delete_from(&self, query: String, fmts: &fmt::Formatter) -> String { let fmt::Formatter { lb, space, .. } = fmts; let sql = if self._delete_from.is_empty() == false { @@ -68,10 +117,169 @@ impl Delete { } } +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] +use crate::concat::non_standard::ConcatWith; + +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] +impl ConcatWith for Delete {} + #[cfg(any(feature = "postgresql", feature = "sqlite"))] -use crate::concat::non_standard::{ConcatReturning, ConcatWith}; +use crate::concat::non_standard::ConcatReturning; #[cfg(any(feature = "postgresql", feature = "sqlite"))] impl ConcatReturning for Delete {} -#[cfg(any(feature = "postgresql", feature = "sqlite"))] -impl ConcatWith for Delete {} + +#[cfg(feature = "mysql")] +use crate::{ + concat::{ + mysql::ConcatPartition, + non_standard::ConcatLimit, + sql_standard::{ConcatFrom, ConcatJoin, ConcatOrderBy}, + }, + utils, +}; + +#[cfg(feature = "mysql")] +impl ConcatFrom for Delete {} +#[cfg(feature = "mysql")] +impl ConcatJoin for Delete {} +#[cfg(feature = "mysql")] +impl ConcatLimit for Delete {} +#[cfg(feature = "mysql")] +impl ConcatOrderBy for Delete {} +#[cfg(feature = "mysql")] +impl ConcatPartition for Delete {} + +#[cfg(feature = "mysql")] +impl Delete { + fn concat_delete_from_mysql(&self, query: String, fmts: &fmt::Formatter) -> String { + let fmt::Formatter { comma, lb, space, .. } = fmts; + let delete_values = utils::join(&self._delete, comma); + let from_values = utils::join(&self._from, comma); + + match (&self._delete_from, delete_values, from_values) { + (del_from, del, from) if del_from.is_empty() == false && del.is_empty() == false && from.is_empty() == false => { + let delete_clause = concat_raw_before_after( + &self._raw_before, + &self._raw_after, + "".to_string(), + fmts, + DeleteClause::Delete, + format!("DELETE{space}{del}{space}"), + ); + + let from_clause = concat_raw_before_after( + &self._raw_before, + &self._raw_after, + "".to_string(), + fmts, + DeleteClause::From, + format!("FROM{space}{del_from}{comma}{from}{space}{lb}"), + ); + + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + DeleteClause::DeleteFrom, + format!("{delete_clause}{from_clause}"), + ) + } + (del_from, del, from) if del_from.is_empty() == false && del.is_empty() == false && from.is_empty() => { + let delete_clause = concat_raw_before_after( + &self._raw_before, + &self._raw_after, + "".to_string(), + fmts, + DeleteClause::Delete, + format!("DELETE{space}{del}{space}"), + ); + + let sql = format!("{delete_clause}FROM{space}{del_from}{space}{lb}"); + + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + DeleteClause::DeleteFrom, + sql, + ) + } + (del_from, del, from) if del_from.is_empty() == false && del.is_empty() && from.is_empty() == false => { + let from_clause = concat_raw_before_after( + &self._raw_before, + &self._raw_after, + "".to_string(), + fmts, + DeleteClause::From, + format!("FROM{space}{del_from}{comma}{from}{space}{lb}"), + ); + + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + DeleteClause::DeleteFrom, + format!("DELETE{space}{from_clause}"), + ) + } + (del_from, del, from) if del_from.is_empty() == false && del.is_empty() && from.is_empty() => { + let sql = format!("DELETE{space}FROM{space}{del_from}{space}{lb}"); + + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + DeleteClause::DeleteFrom, + sql, + ) + } + (del_from, del, from) if del_from.is_empty() && del.is_empty() == false && from.is_empty() == false => { + let delete_clause = concat_raw_before_after( + &self._raw_before, + &self._raw_after, + "".to_string(), + fmts, + DeleteClause::Delete, + format!("DELETE{space}{del}{space}"), + ); + + let from_clause = concat_raw_before_after( + &self._raw_before, + &self._raw_after, + "".to_string(), + fmts, + DeleteClause::From, + format!("FROM{space}{from}{space}{lb}"), + ); + + format!("{delete_clause}{from_clause}") + } + (del_from, del, from) if del_from.is_empty() && del.is_empty() == false && from.is_empty() => { + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + DeleteClause::Delete, + format!("DELETE{space}{del}{space}"), + ) + } + (del_from, del, from) if del_from.is_empty() && del.is_empty() && from.is_empty() == false => { + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + DeleteClause::From, + format!("FROM{space}{from}{space}{lb}"), + ) + } + (_, _, _) => query, + } + } +} diff --git a/src/select/select.rs b/src/select/select.rs index 6f527f5..5d05f73 100644 --- a/src/select/select.rs +++ b/src/select/select.rs @@ -99,10 +99,10 @@ impl Select { /// .from("users"); /// /// # let expected = "FROM users"; - /// # assert_eq!(select.as_string(), expected); + /// # assert_eq!(expected, select.as_string()); /// ``` - pub fn from(mut self, tables: &str) -> Self { - push_unique(&mut self._from, tables.trim().to_string()); + pub fn from(mut self, table: &str) -> Self { + push_unique(&mut self._from, table.trim().to_string()); self } diff --git a/src/select/select_internal.rs b/src/select/select_internal.rs index 37b2b2b..4b6238a 100644 --- a/src/select/select_internal.rs +++ b/src/select/select_internal.rs @@ -6,6 +6,7 @@ use crate::{ }, fmt, structure::{Select, SelectClause}, + utils, }; impl ConcatFrom for Select {} @@ -110,13 +111,7 @@ impl Select { fn concat_group_by(&self, query: String, fmts: &fmt::Formatter) -> String { let fmt::Formatter { comma, lb, space, .. } = fmts; let sql = if self._group_by.is_empty() == false { - let columns = self - ._group_by - .iter() - .filter(|column| column.is_empty() == false) - .map(|column| column.as_str()) - .collect::>() - .join(comma); + let columns = utils::join(&self._group_by, comma); format!("GROUP BY{space}{columns}{space}{lb}") } else { "".to_string() @@ -135,13 +130,7 @@ impl Select { fn concat_having(&self, query: String, fmts: &fmt::Formatter) -> String { let fmt::Formatter { lb, space, .. } = fmts; let sql = if self._having.is_empty() == false { - let conditions = self - ._having - .iter() - .filter(|item| item.is_empty() == false) - .map(|item| item.as_str()) - .collect::>() - .join(" AND "); + let conditions = utils::join(&self._having, " AND "); format!("HAVING{space}{conditions}{space}{lb}") } else { "".to_string() @@ -160,13 +149,7 @@ impl Select { fn concat_select(&self, query: String, fmts: &fmt::Formatter) -> String { let fmt::Formatter { comma, lb, space, .. } = fmts; let sql = if self._select.is_empty() == false { - let columns = self - ._select - .iter() - .filter(|item| item.is_empty() == false) - .map(|item| item.as_str()) - .collect::>() - .join(comma); + let columns = utils::join(&self._select, comma); format!("SELECT{space}{columns}{space}{lb}") } else { "".to_string() @@ -185,13 +168,7 @@ impl Select { fn concat_window(&self, query: String, fmts: &fmt::Formatter) -> String { let fmt::Formatter { comma, lb, space, .. } = fmts; let sql = if self._window.is_empty() == false { - let columns = self - ._window - .iter() - .filter(|item| item.is_empty() == false) - .map(|item| item.as_str()) - .collect::>() - .join(comma); + let columns = utils::join(&self._window, comma); format!("WINDOW{space}{columns}{space}{lb}") } else { "".to_string() @@ -208,12 +185,12 @@ impl Select { } } -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] use crate::concat::non_standard::{ConcatLimit, ConcatWith}; #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] impl ConcatWith for Select {} -#[cfg(any(feature = "postgresql", feature = "sqlite"))] +#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] impl ConcatLimit for Select {} #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] diff --git a/src/structure.rs b/src/structure.rs index 6d17e68..8cbd2c4 100644 --- a/src/structure.rs +++ b/src/structure.rs @@ -214,6 +214,102 @@ pub enum CreateTableParams { PrimaryKey, } +/// Builder to contruct a [Delete] command. +/// +/// Basic API +/// +/// ``` +/// use sql_query_builder as sql; +/// +/// let query = sql::Delete::new() +/// .delete_from("users") +/// .where_clause("id = $1") +/// .as_string(); +/// +/// # let expected = "DELETE FROM users WHERE id = $1"; +/// # assert_eq!(expected, query); +/// ``` +/// +/// Output +/// +/// ```sql +/// DELETE FROM users WHERE id = $1 +/// ``` +#[derive(Default, Clone)] +pub struct Delete { + pub(crate) _delete_from: String, + pub(crate) _raw_after: Vec<(DeleteClause, String)>, + pub(crate) _raw_before: Vec<(DeleteClause, String)>, + pub(crate) _raw: Vec, + pub(crate) _where: Vec<(LogicalOperator, String)>, + + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] + pub(crate) _with: Vec<(String, std::sync::Arc)>, + + #[cfg(any(feature = "postgresql", feature = "sqlite"))] + pub(crate) _returning: Vec, + + #[cfg(feature = "mysql")] + pub(crate) _delete: Vec, + + #[cfg(feature = "mysql")] + pub(crate) _from: Vec, + + #[cfg(feature = "mysql")] + pub(crate) _join: Vec, + + #[cfg(feature = "mysql")] + pub(crate) _limit: String, + + #[cfg(feature = "mysql")] + pub(crate) _order_by: Vec, + + #[cfg(feature = "mysql")] + pub(crate) _partition: Vec, +} + +/// All available clauses to be used in [Delete::raw_before] and [Delete::raw_after] methods on [Delete] builder +#[derive(PartialEq, Clone)] +pub enum DeleteClause { + DeleteFrom, + Where, + + #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] + #[cfg_attr(docsrs, doc(cfg(feature = "postgresql")))] + #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] + #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] + With, + + #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg_attr(docsrs, doc(cfg(feature = "postgresql")))] + #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] + Returning, + + #[cfg(feature = "mysql")] + #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] + Limit, + + #[cfg(feature = "mysql")] + #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] + Delete, + + #[cfg(feature = "mysql")] + #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] + From, + + #[cfg(feature = "mysql")] + #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] + Join, + + #[cfg(feature = "mysql")] + #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] + OrderBy, + + #[cfg(feature = "mysql")] + #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] + Partition, +} + /// Builder to contruct a [DropIndex] command. Available only for the crate features `postgresql` and `sqlite`. /// /// Basic API @@ -291,59 +387,6 @@ pub enum DropTableParams { DropTable, } -/// Builder to contruct a [Delete] command. -/// -/// Basic API -/// -/// ``` -/// use sql_query_builder as sql; -/// -/// let query = sql::Delete::new() -/// .delete_from("users") -/// .where_clause("id = $1") -/// .as_string(); -/// -/// # let expected = "DELETE FROM users WHERE id = $1"; -/// # assert_eq!(expected, query); -/// ``` -/// -/// Output -/// -/// ```sql -/// DELETE FROM users WHERE id = $1 -/// ``` -#[derive(Default, Clone)] -pub struct Delete { - pub(crate) _delete_from: String, - pub(crate) _raw_after: Vec<(DeleteClause, String)>, - pub(crate) _raw_before: Vec<(DeleteClause, String)>, - pub(crate) _raw: Vec, - pub(crate) _where: Vec<(LogicalOperator, String)>, - - #[cfg(any(feature = "postgresql", feature = "sqlite"))] - pub(crate) _returning: Vec, - - #[cfg(any(feature = "postgresql", feature = "sqlite"))] - pub(crate) _with: Vec<(String, std::sync::Arc)>, -} - -/// All available clauses to be used in [Delete::raw_before] and [Delete::raw_after] methods on [Delete] builder -#[derive(PartialEq, Clone)] -pub enum DeleteClause { - DeleteFrom, - Where, - - #[cfg(any(feature = "postgresql", feature = "sqlite"))] - #[cfg_attr(docsrs, doc(cfg(feature = "postgresql")))] - #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] - Returning, - - #[cfg(any(feature = "postgresql", feature = "sqlite"))] - #[cfg_attr(docsrs, doc(cfg(feature = "postgresql")))] - #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] - With, -} - /// Builder to contruct a [Insert] command. /// /// Basic API diff --git a/src/utils.rs b/src/utils.rs index b0f699c..471b90c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,3 +6,12 @@ pub(crate) fn push_unique(list: &mut Vec, value: T) { list.push(value); } } + +pub(crate) fn join(list: &Vec, sep: &str) -> String { + list + .iter() + .filter(|item| item.is_empty() == false) + .map(|item| item.as_str()) + .collect::>() + .join(sep) +} diff --git a/tests/clause_delete_from_spec.rs b/tests/clause_delete_from_spec.rs deleted file mode 100644 index 234e21a..0000000 --- a/tests/clause_delete_from_spec.rs +++ /dev/null @@ -1,53 +0,0 @@ -mod delete_command { - use pretty_assertions::assert_eq; - use sql_query_builder as sql; - - #[test] - fn method_delete_should_add_a_delete_clause() { - let query = sql::Delete::new().delete_from("users").as_string(); - let expected_query = "DELETE FROM users"; - - assert_eq!(query, expected_query); - } - - #[test] - fn method_delete_should_override_value_on_consecutive_calls() { - let query = sql::Delete::new() - .delete_from("users") - .delete_from("orders") - .as_string(); - let expected_query = "DELETE FROM orders"; - - assert_eq!(query, expected_query); - } - - #[test] - fn method_delete_should_trim_space_of_the_argument() { - let query = sql::Delete::new().delete_from(" orders ").as_string(); - let expected_query = "DELETE FROM orders"; - - assert_eq!(query, expected_query); - } - - #[test] - fn method_raw_before_should_add_raw_sql_before_delete_clause() { - let query = sql::Delete::new() - .raw_before(sql::DeleteClause::DeleteFrom, "/* delete users */") - .delete_from("users") - .as_string(); - let expected_query = "/* delete users */ DELETE FROM users"; - - assert_eq!(query, expected_query); - } - - #[test] - fn method_raw_after_should_add_raw_sql_after_delete_clause() { - let query = sql::Delete::new() - .delete_from("users") - .raw_after(sql::DeleteClause::DeleteFrom, "where login = 'foo'") - .as_string(); - let expected_query = "DELETE FROM users where login = 'foo'"; - - assert_eq!(query, expected_query); - } -} diff --git a/tests/clause_from_spec.rs b/tests/clause_from_spec.rs index 5d57ce6..8ca113a 100644 --- a/tests/clause_from_spec.rs +++ b/tests/clause_from_spec.rs @@ -1,3 +1,79 @@ +#[cfg(feature = "mysql")] +mod delete_command { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_from_should_add_the_from_clause() { + let query = sql::Delete::new().from("users").as_string(); + let expected_query = "FROM users"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_from_should_accumulate_values_on_consecutive_calls() { + let query = sql::Delete::new().from("users").from("addresses").as_string(); + let expected_query = "FROM users, addresses"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_from_should_not_accumulate_values_when_table_name_is_empty() { + let query = sql::Delete::new().from("").from("users").from("").as_string(); + let expected_query = "FROM users"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_from_should_trim_space_of_the_argument() { + let query = sql::Delete::new().from(" users ").as_string(); + let expected_query = "FROM users"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_from_should_not_accumulate_arguments_with_the_same_content() { + let query = sql::Delete::new().from("addresses").from("addresses").as_string(); + let expected_query = "FROM addresses"; + + assert_eq!(expected_query, query); + } + + #[test] + fn clause_from_should_be_after_select_clause() { + let query = sql::Delete::new().delete("QUICK").from("users").as_string(); + let expected_query = "DELETE QUICK FROM users"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_before_should_add_raw_sql_before_from_clause() { + let query = sql::Delete::new() + .raw_before(sql::DeleteClause::From, "DELETE QUICK") + .from("orders") + .as_string(); + let expected_query = "DELETE QUICK FROM orders"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_after_should_add_raw_sql_after_from_clause() { + let query = sql::Delete::new() + .from("users") + .raw_after(sql::DeleteClause::From, "WHERE id = '123'") + .as_string(); + let expected_query = "FROM users WHERE id = '123'"; + + assert_eq!(expected_query, query); + } +} + mod select_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; @@ -7,7 +83,7 @@ mod select_command { let query = sql::Select::new().from("users").as_string(); let expected_query = "FROM users"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -15,7 +91,7 @@ mod select_command { let query = sql::Select::new().from("users").from("addresses").as_string(); let expected_query = "FROM users, addresses"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -23,7 +99,7 @@ mod select_command { let query = sql::Select::new().from("").from("users").from("").as_string(); let expected_query = "FROM users"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -31,7 +107,7 @@ mod select_command { let query = sql::Select::new().from(" users ").as_string(); let expected_query = "FROM users"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -39,7 +115,7 @@ mod select_command { let query = sql::Select::new().from("addresses").from("addresses").as_string(); let expected_query = "FROM addresses"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -47,7 +123,7 @@ mod select_command { let query = sql::Select::new().select("*").from("users").as_string(); let expected_query = "SELECT * FROM users"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -58,7 +134,7 @@ mod select_command { .as_string(); let expected_query = "select amount FROM orders"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -72,7 +148,7 @@ mod select_command { .as_string(); let expected_query = "FROM users inner join addresses on users.login = addresses.login"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } } @@ -86,7 +162,7 @@ mod update_command { let query = sql::Update::new().from("users").as_string(); let expected_query = "FROM users"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -94,7 +170,7 @@ mod update_command { let query = sql::Update::new().from("users").from("addresses").as_string(); let expected_query = "FROM users, addresses"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -102,7 +178,7 @@ mod update_command { let query = sql::Update::new().from("").from("users").from("").as_string(); let expected_query = "FROM users"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -110,7 +186,7 @@ mod update_command { let query = sql::Update::new().from(" users ").as_string(); let expected_query = "FROM users"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -118,7 +194,7 @@ mod update_command { let query = sql::Update::new().from("addresses").from("addresses").as_string(); let expected_query = "FROM addresses"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -126,7 +202,7 @@ mod update_command { let query = sql::Update::new().set("country = 'Bar'").from("addresses").as_string(); let expected_query = "SET country = 'Bar' FROM addresses"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -137,7 +213,7 @@ mod update_command { .as_string(); let expected_query = "set country = 'Bar' FROM addresses"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -148,6 +224,6 @@ mod update_command { .as_string(); let expected_query = "FROM users where login = $1"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } } diff --git a/tests/clause_join_spec.rs b/tests/clause_join_spec.rs index b3b66b0..23824d3 100644 --- a/tests/clause_join_spec.rs +++ b/tests/clause_join_spec.rs @@ -1,4 +1,32 @@ mod raw_methods { + #[cfg(feature = "mysql")] + mod delete_command { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_raw_after_should_add_raw_sql_after_join_clause() { + let query = sql::Delete::new() + .inner_join("orders ON orders.user_id = user.id") + .raw_after(sql::DeleteClause::Join, "WHERE user.id = $1") + .as_string(); + let expected_query = "INNER JOIN orders ON orders.user_id = user.id WHERE user.id = $1"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_before_should_add_raw_sql_before_join_clause() { + let query = sql::Delete::new() + .raw_before(sql::DeleteClause::Join, "FROM users") + .inner_join("orders ON orders.user_id = user.id") + .as_string(); + let expected_query = "FROM users INNER JOIN orders ON orders.user_id = user.id"; + + assert_eq!(expected_query, query); + } + } + mod select_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; @@ -56,6 +84,73 @@ mod raw_methods { } mod cross_join_clause { + #[cfg(feature = "mysql")] + mod delete_command { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_cross_join_should_add_the_cross_join_clause() { + let query = sql::Delete::new().cross_join("addresses").as_string(); + let expected_query = "CROSS JOIN addresses"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_cross_join_should_accumulate_values_on_consecutive_calls() { + let query = sql::Delete::new() + .cross_join("addresses") + .cross_join("orders") + .as_string(); + let expected_query = "\ + CROSS JOIN addresses \ + CROSS JOIN orders\ + "; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_cross_join_should_not_accumulate_values_when_table_name_is_empty() { + let query = sql::Delete::new() + .cross_join("") + .cross_join("orders") + .cross_join("") + .as_string(); + let expected_query = "CROSS JOIN orders"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_cross_join_by_should_trim_space_of_the_argument() { + let query = sql::Delete::new().cross_join(" orders ").as_string(); + let expected_query = "CROSS JOIN orders"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_cross_join_should_not_accumulate_arguments_with_the_same_content() { + let query = sql::Delete::new() + .cross_join("addresses") + .cross_join("addresses") + .as_string(); + let expected_query = "CROSS JOIN addresses"; + + assert_eq!(expected_query, query); + } + + #[test] + fn clause_cross_join_should_be_after_from_clause() { + let query = sql::Delete::new().from("users").cross_join("addresses").as_string(); + let expected_query = "FROM users CROSS JOIN addresses"; + + assert_eq!(expected_query, query); + } + } + mod select_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; @@ -191,6 +286,78 @@ mod cross_join_clause { } mod inner_join_clause { + #[cfg(feature = "mysql")] + mod delete_command { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_inner_join_should_add_the_inner_join_clause() { + let query = sql::Delete::new() + .inner_join("addresses ON users.login = addresses.login") + .as_string(); + let expected_query = "INNER JOIN addresses ON users.login = addresses.login"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_inner_join_should_accumulate_values_on_consecutive_calls() { + let query = sql::Delete::new() + .inner_join("addresses ON users.login = addresses.login") + .inner_join("orders ON users.login = orders.login") + .as_string(); + let expected_query = "\ + INNER JOIN addresses ON users.login = addresses.login \ + INNER JOIN orders ON users.login = orders.login\ + "; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_inner_join_should_not_accumulate_values_when_table_expression_is_empty() { + let query = sql::Delete::new() + .inner_join("") + .inner_join("orders ON users.login = orders.login") + .inner_join("") + .as_string(); + let expected_query = "INNER JOIN orders ON users.login = orders.login"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_inner_join_by_should_trim_space_of_the_argument() { + let query = sql::Delete::new().inner_join(" orders ").as_string(); + let expected_query = "INNER JOIN orders"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_inner_join_should_not_accumulate_arguments_with_the_same_content() { + let query = sql::Delete::new() + .inner_join("addresses") + .inner_join("addresses") + .as_string(); + let expected_query = "INNER JOIN addresses"; + + assert_eq!(expected_query, query); + } + + #[test] + fn clause_inner_join_should_be_after_from_clause() { + let query = sql::Delete::new() + .from("users") + .inner_join("addresses ON users.login = addresses.login") + .as_string(); + let expected_query = "FROM users INNER JOIN addresses ON users.login = addresses.login"; + + assert_eq!(expected_query, query); + } + } + mod select_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; @@ -336,6 +503,78 @@ mod inner_join_clause { } mod left_join_clause { + #[cfg(feature = "mysql")] + mod delete_command { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_left_join_should_add_the_left_join_clause() { + let query = sql::Delete::new() + .left_join("addresses ON users.login = addresses.login") + .as_string(); + let expected_query = "LEFT JOIN addresses ON users.login = addresses.login"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_left_join_should_accumulate_values_on_consecutive_calls() { + let query = sql::Delete::new() + .left_join("addresses ON users.login = addresses.login") + .left_join("orders ON users.login = orders.login") + .as_string(); + let expected_query = "\ + LEFT JOIN addresses ON users.login = addresses.login \ + LEFT JOIN orders ON users.login = orders.login\ + "; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_left_join_should_not_accumulate_values_when_table_expression_is_empty() { + let query = sql::Delete::new() + .left_join("") + .left_join("orders ON users.login = orders.login") + .left_join("") + .as_string(); + let expected_query = "LEFT JOIN orders ON users.login = orders.login"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_left_join_by_should_trim_space_of_the_argument() { + let query = sql::Delete::new().left_join(" orders ").as_string(); + let expected_query = "LEFT JOIN orders"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_left_join_should_not_accumulate_arguments_with_the_same_content() { + let query = sql::Delete::new() + .left_join("addresses") + .left_join("addresses") + .as_string(); + let expected_query = "LEFT JOIN addresses"; + + assert_eq!(expected_query, query); + } + + #[test] + fn clause_left_join_should_be_after_from_clause() { + let query = sql::Delete::new() + .from("users") + .left_join("addresses ON users.login = addresses.login") + .as_string(); + let expected_query = "FROM users LEFT JOIN addresses ON users.login = addresses.login"; + + assert_eq!(expected_query, query); + } + } + mod select_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; @@ -481,6 +720,78 @@ mod left_join_clause { } mod right_join_clause { + #[cfg(feature = "mysql")] + mod delete_command { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_right_join_should_add_the_right_join_clause() { + let query = sql::Delete::new() + .right_join("addresses ON users.login = addresses.login") + .as_string(); + let expected_query = "RIGHT JOIN addresses ON users.login = addresses.login"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_right_join_should_accumulate_values_on_consecutive_calls() { + let query = sql::Delete::new() + .right_join("addresses ON users.login = addresses.login") + .right_join("orders ON users.login = orders.login") + .as_string(); + let expected_query = "\ + RIGHT JOIN addresses ON users.login = addresses.login \ + RIGHT JOIN orders ON users.login = orders.login\ + "; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_right_join_should_not_accumulate_values_when_table_expression_is_empty() { + let query = sql::Delete::new() + .right_join("") + .right_join("orders ON users.login = orders.login") + .right_join("") + .as_string(); + let expected_query = "RIGHT JOIN orders ON users.login = orders.login"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_right_join_by_should_trim_space_of_the_argument() { + let query = sql::Delete::new().right_join(" orders ").as_string(); + let expected_query = "RIGHT JOIN orders"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_right_join_should_not_accumulate_arguments_with_the_same_content() { + let query = sql::Delete::new() + .right_join("addresses") + .right_join("addresses") + .as_string(); + let expected_query = "RIGHT JOIN addresses"; + + assert_eq!(expected_query, query); + } + + #[test] + fn clause_right_join_should_be_after_from_clause() { + let query = sql::Delete::new() + .from("users") + .right_join("addresses ON users.login = addresses.login") + .as_string(); + let expected_query = "FROM users RIGHT JOIN addresses ON users.login = addresses.login"; + + assert_eq!(expected_query, query); + } + } + mod select_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; diff --git a/tests/clause_limit_spec.rs b/tests/clause_limit_spec.rs index 1d92bac..a6f888b 100644 --- a/tests/clause_limit_spec.rs +++ b/tests/clause_limit_spec.rs @@ -8,7 +8,7 @@ mod select_command { let query = sql::Select::new().limit("3").as_string(); let expected_query = "LIMIT 3"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -16,7 +16,7 @@ mod select_command { let query = sql::Select::new().limit("3").limit("4").as_string(); let expected_query = "LIMIT 4"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -24,7 +24,7 @@ mod select_command { let query = sql::Select::new().limit(" 50 ").as_string(); let expected_query = "LIMIT 50"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -32,7 +32,7 @@ mod select_command { let query = sql::Select::new().order_by("created_at desc").limit("42").as_string(); let expected_query = "ORDER BY created_at desc LIMIT 42"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -43,7 +43,7 @@ mod select_command { .as_string(); let expected_query = "group by id LIMIT 10"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -54,6 +54,66 @@ mod select_command { .as_string(); let expected_query = "LIMIT 10 except select id, login"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); + } +} + +#[cfg(feature = "mysql")] +mod delete_command { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_limit_should_add_the_limit_clause() { + let query = sql::Delete::new().limit("3").as_string(); + let expected_query = "LIMIT 3"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_limit_should_override_the_current_value() { + let query = sql::Delete::new().limit("3").limit("4").as_string(); + let expected_query = "LIMIT 4"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_limit_should_trim_space_of_the_argument() { + let query = sql::Delete::new().limit(" 50 ").as_string(); + let expected_query = "LIMIT 50"; + + assert_eq!(expected_query, query); + } + + #[test] + fn clause_limit_should_be_after_order_by_clause() { + let query = sql::Delete::new().order_by("created_at desc").limit("42").as_string(); + let expected_query = "ORDER BY created_at desc LIMIT 42"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_before_should_add_raw_sql_before_limit_clause() { + let query = sql::Delete::new() + .raw_before(sql::DeleteClause::Limit, "order by id") + .limit("10") + .as_string(); + let expected_query = "order by id LIMIT 10"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_after_should_add_raw_sql_after_limit_clause() { + let query = sql::Delete::new() + .limit("10") + .raw_after(sql::DeleteClause::Limit, "/* uncommon argument */") + .as_string(); + let expected_query = "LIMIT 10 /* uncommon argument */"; + + assert_eq!(expected_query, query); } } diff --git a/tests/clause_order_by_spec.rs b/tests/clause_order_by_spec.rs index 9212f99..cd5815c 100644 --- a/tests/clause_order_by_spec.rs +++ b/tests/clause_order_by_spec.rs @@ -7,7 +7,7 @@ mod select_command { let query = sql::Select::new().order_by("id asc").as_string(); let expected_query = "ORDER BY id asc"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -18,7 +18,7 @@ mod select_command { .as_string(); let expected_query = "ORDER BY login asc, created_at desc"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -30,7 +30,7 @@ mod select_command { .as_string(); let expected_query = "ORDER BY created_at desc"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -38,7 +38,7 @@ mod select_command { let query = sql::Select::new().order_by(" id desc ").as_string(); let expected_query = "ORDER BY id desc"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -46,7 +46,7 @@ mod select_command { let query = sql::Select::new().order_by("id desc").order_by("id desc").as_string(); let expected_query = "ORDER BY id desc"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -57,7 +57,7 @@ mod select_command { .as_string(); let expected_query = "HAVING active = true ORDER BY created_at desc"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -65,7 +65,7 @@ mod select_command { let query = sql::Select::new().window("foo").order_by("created_at desc").as_string(); let expected_query = "WINDOW foo ORDER BY created_at desc"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -76,7 +76,7 @@ mod select_command { .as_string(); let expected_query = "where orders.user_login = $1 ORDER BY id desc"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -87,6 +87,103 @@ mod select_command { .as_string(); let expected_query = "ORDER BY id desc limit 20"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); + } +} + +#[cfg(feature = "mysql")] +mod delete_command { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_order_by_should_add_the_order_by_clause() { + let query = sql::Delete::new().order_by("id asc").as_string(); + let expected_query = "ORDER BY id asc"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_order_by_should_accumulate_values_on_consecutive_calls() { + let query = sql::Delete::new() + .order_by("login asc") + .order_by("created_at desc") + .as_string(); + let expected_query = "ORDER BY login asc, created_at desc"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_order_by_should_not_accumulate_values_when_column_name_is_empty() { + let query = sql::Delete::new() + .order_by("") + .order_by("created_at desc") + .order_by("") + .as_string(); + let expected_query = "ORDER BY created_at desc"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_order_by_should_trim_space_of_the_argument() { + let query = sql::Delete::new().order_by(" id desc ").as_string(); + let expected_query = "ORDER BY id desc"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_order_by_should_not_accumulate_arguments_with_the_same_content() { + let query = sql::Delete::new().order_by("id desc").order_by("id desc").as_string(); + let expected_query = "ORDER BY id desc"; + + assert_eq!(expected_query, query); + } + + #[test] + fn clause_order_by_should_be_after_where_clause() { + let query = sql::Delete::new() + .where_clause("active = true") + .order_by("created_at desc") + .as_string(); + let expected_query = "WHERE active = true ORDER BY created_at desc"; + + assert_eq!(expected_query, query); + } + + #[test] + fn clause_order_by_should_be_after_delete_from_clause() { + let query = sql::Delete::new() + .delete_from("foo") + .order_by("created_at desc") + .as_string(); + let expected_query = "DELETE FROM foo ORDER BY created_at desc"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_before_should_add_raw_sql_before_order_by_clause() { + let query = sql::Delete::new() + .raw_before(sql::DeleteClause::OrderBy, "where user_login = $1") + .order_by("id desc") + .as_string(); + let expected_query = "where user_login = $1 ORDER BY id desc"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_after_should_add_raw_sql_after_order_by_clause() { + let query = sql::Delete::new() + .order_by("id desc") + .raw_after(sql::DeleteClause::OrderBy, "limit 20") + .as_string(); + let expected_query = "ORDER BY id desc limit 20"; + + assert_eq!(expected_query, query); } } diff --git a/tests/clause_partition_spec.rs b/tests/clause_partition_spec.rs index 1229c00..a4f3498 100644 --- a/tests/clause_partition_spec.rs +++ b/tests/clause_partition_spec.rs @@ -1,3 +1,112 @@ +#[cfg(feature = "mysql")] +mod delete_command { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_partition_should_define_the_partition_clause() { + let query = sql::Delete::new().partition("p0").as_string(); + let expected_query = "PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_not_defined_the_clause_without_partition_names() { + let query = sql::Delete::new().partition("").as_string(); + let expected_query = ""; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_accumulate_names_on_consecutive_calls() { + let query = sql::Delete::new().partition("p0").partition("p1").as_string(); + + let expected_query = "PARTITION (p0, p1)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_not_accumulate_values_when_expression_is_empty() { + let query = sql::Delete::new() + .partition("") + .partition("p0") + .partition("") + .as_string(); + + let expected_query = "PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_not_accumulate_names_with_the_same_content() { + let query = sql::Delete::new().partition("p0").partition("p0").as_string(); + let expected_query = "PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_trim_space_of_the_argument() { + let query = sql::Delete::new().partition(" p0 ").as_string(); + let expected_query = "PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_partition_should_be_defined_after_from_clause() { + let query = sql::Delete::new().delete_from("employees").partition("p0").as_string(); + let expected_query = "DELETE FROM employees PARTITION (p0)"; + + assert_eq!(expected_query, query); + } + + // #[test] + // fn method_partition_should_be_defined_after_join_clauses() { + // let query = sql::Delete::new() + // .delete_from("employees") + // .inner_join("addresses ON employees.login = addresses.login") + // .partition("p0") + // .as_string(); + + // let expected_query = "\ + // FROM employees \ + // INNER JOIN addresses ON employees.login = addresses.login \ + // PARTITION (p0)\ + // "; + + // assert_eq!(expected_query, query); + // } + + #[test] + fn method_raw_after_should_add_raw_sql_after_partition_parameter() { + let query = sql::Delete::new() + .partition("name") + .raw_after(sql::DeleteClause::Partition, "/* uncommon parameter */") + .as_string(); + + let expected_query = "PARTITION (name) /* uncommon parameter */"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_before_should_add_raw_sql_before_partition_parameter() { + let query = sql::Delete::new() + .raw_before(sql::DeleteClause::Partition, "/* uncommon parameter */") + .partition("name") + .as_string(); + + let expected_query = "/* uncommon parameter */ PARTITION (name)"; + + assert_eq!(expected_query, query); + } +} + #[cfg(feature = "mysql")] mod select_command { use pretty_assertions::assert_eq; diff --git a/tests/command_delete_spec.rs b/tests/command_delete_spec.rs index af2541a..c3cffe1 100644 --- a/tests/command_delete_spec.rs +++ b/tests/command_delete_spec.rs @@ -11,7 +11,7 @@ mod builder_features { let query = delete.as_string(); let expected_query = "DELETE FROM users WHERE login = 'foo'"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -26,7 +26,7 @@ mod builder_features { let expected_query = "DELETE FROM users WHERE name = 'Foo' AND login = 'foo'"; let query = delete.as_string(); - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -73,7 +73,7 @@ mod builder_features { let query = delete.as_string(); let expected_query = "DELETE FROM users WHERE name = 'Bar' AND login = 'bar'"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -107,7 +107,7 @@ mod builder_features { AND created_at::date = current_date\ "; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -122,7 +122,7 @@ mod builder_features { WHERE users.login = $1\ "; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } /** This test can fail only at compile time @@ -144,7 +144,7 @@ mod builder_methods { let query = sql::Delete::new().as_string(); let expected_query = ""; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -152,7 +152,7 @@ mod builder_methods { let query = sql::Delete::new().delete_from("users").debug().as_string(); let expected_query = "DELETE FROM users"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -160,7 +160,7 @@ mod builder_methods { let query = sql::Delete::new().as_string(); let expected_query = ""; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -168,7 +168,7 @@ mod builder_methods { let query = sql::Delete::new().delete_from("users").print().as_string(); let expected_query = "DELETE FROM users"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -176,7 +176,7 @@ mod builder_methods { let query = sql::Delete::new().raw("delete from addresses").as_string(); let expected_query = "delete from addresses"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -187,7 +187,7 @@ mod builder_methods { .as_string(); let expected_query = "delete from addresses where city = 'Foo'"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -199,7 +199,7 @@ mod builder_methods { .as_string(); let expected_query = "delete from addresses"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -210,7 +210,7 @@ mod builder_methods { .as_string(); let expected_query = "delete from addresses WHERE country = 'Bar'"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -218,7 +218,7 @@ mod builder_methods { let query = sql::Delete::new().raw(" delete from users ").as_string(); let expected_query = "delete from users"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -229,17 +229,18 @@ mod builder_methods { .as_string(); let expected_query = "delete from users"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] fn method_raw_after_should_trim_space_of_the_argument() { let query = sql::Delete::new() + .delete_from("users") .raw_after(sql::DeleteClause::DeleteFrom, " where name = 'Bar' ") .as_string(); - let expected_query = "where name = 'Bar'"; + let expected_query = "DELETE FROM users where name = 'Bar'"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -249,6 +250,165 @@ mod builder_methods { .as_string(); let expected_query = "where name = 'Bar'"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); + } +} + +mod delete_from_method { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_delete_should_add_a_delete_clause() { + let query = sql::Delete::new().delete_from("users").as_string(); + let expected_query = "DELETE FROM users"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_delete_should_override_value_on_consecutive_calls() { + let query = sql::Delete::new() + .delete_from("users") + .delete_from("orders") + .as_string(); + let expected_query = "DELETE FROM orders"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_delete_should_trim_space_of_the_argument() { + let query = sql::Delete::new().delete_from(" orders ").as_string(); + let expected_query = "DELETE FROM orders"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_before_should_add_raw_sql_before_delete_clause() { + let query = sql::Delete::new() + .raw_before(sql::DeleteClause::DeleteFrom, "/* delete users */") + .delete_from("users") + .as_string(); + let expected_query = "/* delete users */ DELETE FROM users"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_after_should_add_raw_sql_after_delete_clause() { + let query = sql::Delete::new() + .delete_from("users") + .raw_after(sql::DeleteClause::DeleteFrom, "where login = 'foo'") + .as_string(); + let expected_query = "DELETE FROM users where login = 'foo'"; + + assert_eq!(expected_query, query); + } +} + +#[cfg(feature = "mysql")] +mod delete_method { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_delete_should_add_the_delete_clause() { + let query = sql::Delete::new().delete("users").as_string(); + let expected_query = "DELETE users"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_delete_should_accumulate_values_on_consecutive_calls() { + let query = sql::Delete::new().delete("users").delete("employees").as_string(); + let expected_query = "DELETE users, employees"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_delete_should_not_accumulate_values_when_table_name_is_empty() { + let query = sql::Delete::new().delete("").delete("users").delete("").as_string(); + let expected_query = "DELETE users"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_delete_should_trim_space_of_the_argument() { + let query = sql::Delete::new().delete(" users ").as_string(); + let expected_query = "DELETE users"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_delete_should_not_accumulate_arguments_with_the_same_content() { + let query = sql::Delete::new().delete("employees").delete("employees").as_string(); + let expected_query = "DELETE employees"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_before_should_add_raw_sql_before_delete_clause() { + let query = sql::Delete::new() + .raw_before(sql::DeleteClause::Delete, "/* uncommon parameter */") + .delete("orders") + .as_string(); + let expected_query = "/* uncommon parameter */ DELETE orders"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_after_should_add_raw_sql_after_delete_clause() { + let query = sql::Delete::new() + .delete("LOW_PRIORITY") + .raw_after(sql::DeleteClause::Delete, "FROM users") + .as_string(); + let expected_query = "DELETE LOW_PRIORITY FROM users"; + + assert_eq!(expected_query, query); + } +} + +#[cfg(feature = "mysql")] +mod relation_between_delete_and_from { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn when_delete_from_method_was_called_with_delete_method_the_values_of_delete_method_should_be_preserved() { + let query = sql::Delete::new().delete("quick").delete_from("users").as_string(); + let expected_query = "DELETE quick FROM users"; + + assert_eq!(expected_query, query); + } + + #[test] + fn when_delete_from_method_was_called_with_from_method_the_values_both_methods_should_be_preserved() { + // the value of delete_from should be concatenated first + let query = sql::Delete::new().from("t1").delete_from("t2").from("t3").as_string(); + let expected_query = "DELETE FROM t2, t1, t3"; + + assert_eq!(expected_query, query); + } + + #[test] + fn when_delete_from_method_was_called_with_delete_and_from_method_the_values_of_all_methods_should_be_preserved() { + // the value of delete_from should be concatenated first + let query = sql::Delete::new() + .delete("low_priority") + .from("t1") + .delete_from("t2") + .from("t3") + .as_string(); + let expected_query = "DELETE low_priority FROM t2, t1, t3"; + + assert_eq!(expected_query, query); } } From 66aec0a7c726019556b65d78bf14662e2d45b800 Mon Sep 17 00:00:00 2001 From: Belchior Oliveira Date: Fri, 10 Jan 2025 16:21:56 -0300 Subject: [PATCH 04/19] Adds MySQL syntax to Insert builder --- src/concat/mysql.rs | 9 +- src/concat/non_standard.rs | 37 +- src/concat/sql_standard.rs | 38 +- src/concat/sqlite.rs | 32 +- src/insert/insert.rs | 330 +++++++++--- src/insert/insert_internal.rs | 240 +++++++-- src/structure.rs | 68 ++- src/update/update_internal.rs | 36 +- tests/clause_default_values_spec.rs | 53 -- tests/clause_insert_into_spec.rs | 108 ---- tests/clause_insert_or_spec.rs | 64 --- tests/clause_on_conflict_spec.rs | 64 --- tests/clause_overriding_spec.rs | 65 --- tests/clause_partition_spec.rs | 138 ++++- tests/clause_set_spec.rs | 100 +++- tests/command_insert_spec.rs | 774 +++++++++++++++++++++++++++- 16 files changed, 1563 insertions(+), 593 deletions(-) delete mode 100644 tests/clause_default_values_spec.rs delete mode 100644 tests/clause_insert_into_spec.rs delete mode 100644 tests/clause_insert_or_spec.rs delete mode 100644 tests/clause_on_conflict_spec.rs delete mode 100644 tests/clause_overriding_spec.rs diff --git a/src/concat/mysql.rs b/src/concat/mysql.rs index 9a84834..f1e0ad7 100644 --- a/src/concat/mysql.rs +++ b/src/concat/mysql.rs @@ -1,5 +1,5 @@ #[cfg(feature = "mysql")] -use crate::{concat::concat_raw_before_after, fmt}; +use crate::{concat::concat_raw_before_after, fmt, utils}; #[cfg(feature = "mysql")] pub(crate) trait ConcatPartition { @@ -15,12 +15,7 @@ pub(crate) trait ConcatPartition { let fmt::Formatter { comma, lb, space, .. } = fmts; let sql = if items.is_empty() == false { - let column_names = items - .iter() - .filter(|column| column.is_empty() == false) - .map(|column| column.as_str()) - .collect::>() - .join(comma); + let column_names = utils::join(items, comma); if column_names.is_empty() == false { format!("PARTITION{space}({column_names}){space}{lb}") diff --git a/src/concat/non_standard.rs b/src/concat/non_standard.rs index 858dff7..0dc86e2 100644 --- a/src/concat/non_standard.rs +++ b/src/concat/non_standard.rs @@ -1,5 +1,5 @@ #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] -use crate::{behavior::WithQuery, concat::concat_raw_before_after, fmt}; +use crate::{behavior::WithQuery, concat::concat_raw_before_after, fmt, utils}; #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] pub(crate) trait ConcatLimit { @@ -36,12 +36,7 @@ pub(crate) trait ConcatReturning { ) -> String { let fmt::Formatter { lb, space, comma, .. } = fmts; let sql = if items.is_empty() == false { - let output_names = items - .iter() - .filter(|item| item.is_empty() == false) - .map(|item| item.as_str()) - .collect::>() - .join(comma); + let output_names = utils::join(items, comma); format!("RETURNING{space}{output_names}{space}{lb}") } else { "".to_string() @@ -98,3 +93,31 @@ pub(crate) trait ConcatWith { concat_raw_before_after(items_raw_before, items_raw_after, query, fmts, clause, sql) } } + +#[cfg(feature = "mysql")] +pub(crate) trait ConcatColumn { + fn concat_column( + &self, + items_raw_before: &Vec<(Clause, String)>, + items_raw_after: &Vec<(Clause, String)>, + query: String, + fmts: &fmt::Formatter, + clause: Clause, + items: &Vec, + ) -> String { + let fmt::Formatter { lb, comma, space, .. } = fmts; + + let sql = if items.is_empty() == false { + let column_names = utils::join(&items, comma); + if column_names.is_empty() == false { + format!("({column_names}){space}{lb}") + } else { + "".to_string() + } + } else { + "".to_string() + }; + + concat_raw_before_after(items_raw_before, items_raw_after, query, fmts, clause, sql) + } +} diff --git a/src/concat/sql_standard.rs b/src/concat/sql_standard.rs index 2a0ab79..58f9de4 100644 --- a/src/concat/sql_standard.rs +++ b/src/concat/sql_standard.rs @@ -1,4 +1,4 @@ -use crate::{concat::concat_raw_before_after, fmt, structure::LogicalOperator}; +use crate::{concat::concat_raw_before_after, fmt, structure::LogicalOperator, utils}; pub(crate) trait ConcatFrom { fn concat_from( @@ -12,12 +12,7 @@ pub(crate) trait ConcatFrom { ) -> String { let fmt::Formatter { comma, lb, space, .. } = fmts; let sql = if items.is_empty() == false { - let tables = items - .iter() - .filter(|item| item.is_empty() == false) - .map(|item| item.as_str()) - .collect::>() - .join(comma); + let tables = utils::join(items, comma); format!("FROM{space}{tables}{space}{lb}") } else { "".to_string() @@ -61,12 +56,7 @@ pub(crate) trait ConcatOrderBy { ) -> String { let fmt::Formatter { comma, lb, space, .. } = fmts; let sql = if items.is_empty() == false { - let columns = items - .iter() - .filter(|item| item.is_empty() == false) - .map(|item| item.as_str()) - .collect::>() - .join(comma); + let columns = utils::join(items, comma); format!("ORDER BY{space}{columns}{space}{lb}") } else { @@ -77,6 +67,28 @@ pub(crate) trait ConcatOrderBy { } } +pub(crate) trait ConcatSet { + fn concat_set( + &self, + items_raw_before: &Vec<(Clause, String)>, + items_raw_after: &Vec<(Clause, String)>, + query: String, + fmts: &fmt::Formatter, + clause: Clause, + items: &Vec, + ) -> String { + let fmt::Formatter { comma, lb, space, .. } = fmts; + let sql = if items.is_empty() == false { + let values = utils::join(items, comma); + format!("SET{space}{values}{space}{lb}") + } else { + "".to_string() + }; + + concat_raw_before_after(items_raw_before, items_raw_after, query, fmts, clause, sql) + } +} + pub(crate) trait ConcatWhere { fn concat_where( &self, diff --git a/src/concat/sqlite.rs b/src/concat/sqlite.rs index e939387..5080435 100644 --- a/src/concat/sqlite.rs +++ b/src/concat/sqlite.rs @@ -2,39 +2,9 @@ use crate::{ concat::concat_raw_before_after, fmt, - structure::{InsertClause, InsertVars, UpdateClause, UpdateVars}, + structure::{UpdateClause, UpdateVars}, }; -#[cfg(feature = "sqlite")] -pub(crate) trait ConcatInsert { - fn concat_insert( - &self, - items_raw_before: &Vec<(InsertClause, String)>, - items_raw_after: &Vec<(InsertClause, String)>, - query: String, - fmts: &fmt::Formatter, - insert: &(InsertVars, String), - ) -> String { - let fmt::Formatter { lb, space, .. } = fmts; - - let (clause, sql) = match insert { - (InsertVars::InsertInto, exp) if exp.is_empty() => (InsertClause::InsertInto, "".to_string()), - (InsertVars::InsertInto, exp) => (InsertClause::InsertInto, format!("INSERT INTO{space}{exp}{space}{lb}")), - - (InsertVars::InsertOr, exp) if exp.is_empty() => (InsertClause::InsertOr, "".to_string()), - (InsertVars::InsertOr, exp) => (InsertClause::InsertOr, format!("INSERT OR{space}{exp}{space}{lb}")), - - (InsertVars::ReplaceInto, exp) if exp.is_empty() => (InsertClause::ReplaceInto, "".to_string()), - (InsertVars::ReplaceInto, exp) => ( - InsertClause::ReplaceInto, - format!("REPLACE INTO{space}{exp}{space}{lb}"), - ), - }; - - concat_raw_before_after(items_raw_before, items_raw_after, query, fmts, clause, sql) - } -} - #[cfg(feature = "sqlite")] pub(crate) trait ConcatUpdate { fn concat_update( diff --git a/src/insert/insert.rs b/src/insert/insert.rs index e009f0c..3c87e54 100644 --- a/src/insert/insert.rs +++ b/src/insert/insert.rs @@ -21,7 +21,7 @@ impl Insert { /// .as_string(); /// /// # let expected = "INSERT INTO users (login) VALUES ('foo')"; - /// # assert_eq!(query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Output @@ -41,7 +41,7 @@ impl Insert { /// /// ``` /// # use sql_query_builder as sql; - /// let insert_query = sql::Insert::new() + /// let query = sql::Insert::new() /// .insert_into("users (login, name)") /// .values("('foo', 'Foo')") /// .debug() @@ -68,14 +68,17 @@ impl Insert { /// # Example /// /// ``` + /// # #[cfg(not(feature = "mysql"))] + /// # { /// # use sql_query_builder as sql; - /// let insert_query = sql::Insert::new() + /// let query = sql::Insert::new() /// .insert_into("users") /// .default_values() /// .to_string(); /// /// # let expected = "INSERT INTO users DEFAULT VALUES"; - /// # assert_eq!(insert_query, expected); + /// # assert_eq!(expected, query); + /// # } /// ``` /// /// Output @@ -83,8 +86,10 @@ impl Insert { /// ```sql /// INSERT INTO users DEFAULT VALUES /// ``` + #[cfg(not(feature = "mysql"))] pub fn default_values(mut self) -> Self { self._default_values = true; + self._values = vec![]; self } @@ -98,18 +103,32 @@ impl Insert { /// .insert_into("users (login, name)"); /// # /// # let expected = "INSERT INTO users (login, name)"; - /// # assert_eq!(insert.to_string(), expected); + /// # assert_eq!(expected, insert.to_string()); /// /// let insert = sql::Insert::new() /// .insert_into("addresses (state, country)") /// .insert_into("users (login, name)"); /// /// # let expected = "INSERT INTO users (login, name)"; - /// # assert_eq!(insert.to_string(), expected); + /// # assert_eq!(expected, insert.to_string()); /// ``` - #[cfg(not(feature = "sqlite"))] pub fn insert_into(mut self, table_name: &str) -> Self { self._insert_into = table_name.trim().to_string(); + + #[cfg(feature = "sqlite")] + { + self._insert_or = "".to_string(); + self._replace_into = "".to_string(); + } + + #[cfg(feature = "mysql")] + { + self._insert = "".to_string(); + self._into = "".to_string(); + self._partition = vec![]; + self._column = vec![]; + } + self } @@ -118,31 +137,6 @@ impl Insert { Self::default() } - /// The `on conflict` clause. This method overrides the previous value - /// - /// # Example - /// - /// ``` - /// # use sql_query_builder as sql; - /// let query = sql::Insert::new() - /// .insert_into("users (login)") - /// .on_conflict("do nothing") - /// .as_string(); - /// - /// # let expected = "INSERT INTO users (login) ON CONFLICT do nothing"; - /// # assert_eq!(query, expected); - /// ``` - /// - /// Output - /// - /// ```sql - /// INSERT INTO users (login) ON CONFLICT do nothing - /// ``` - pub fn on_conflict(mut self, conflict: &str) -> Self { - self._on_conflict = conflict.trim().to_string(); - self - } - /// The `overriding` clause. This method overrides the previous value /// /// # Example @@ -155,7 +149,7 @@ impl Insert { /// .as_string(); /// /// # let expected = "INSERT INTO users (login) OVERRIDING user value"; - /// # assert_eq!(query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Output @@ -163,7 +157,7 @@ impl Insert { /// ```sql /// INSERT INTO users (login) OVERRIDING user value /// ``` - #[cfg(not(feature = "sqlite"))] + #[cfg(not(any(feature = "sqlite", feature = "mysql")))] pub fn overriding(mut self, option: &str) -> Self { self._overriding = option.trim().to_string(); self @@ -183,7 +177,7 @@ impl Insert { /// /// ``` /// # use sql_query_builder as sql; - /// let insert_query = sql::Insert::new() + /// let query = sql::Insert::new() /// .insert_into("users (login, name)") /// .select( /// sql::Select::new() @@ -199,7 +193,7 @@ impl Insert { /// # FROM users_bk \ /// # WHERE active = true\ /// # "; - /// # assert_eq!(insert_query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Output @@ -223,13 +217,13 @@ impl Insert { /// # use sql_query_builder as sql; /// let raw_query = "insert into users (login, name)"; /// - /// let insert_query = sql::Insert::new() + /// let query = sql::Insert::new() /// .raw(raw_query) /// .values("('foo', 'Foo')") /// .as_string(); /// /// # let expected = "insert into users (login, name) VALUES ('foo', 'Foo')"; - /// # assert_eq!(insert_query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Output @@ -250,13 +244,13 @@ impl Insert { /// # use sql_query_builder as sql; /// let raw = "values ('foo', 'Foo')"; /// - /// let insert_query = sql::Insert::new() + /// let query = sql::Insert::new() /// .insert_into("users (login, name)") /// .raw_after(sql::InsertClause::InsertInto, raw) /// .as_string(); /// /// # let expected = "INSERT INTO users (login, name) values ('foo', 'Foo')"; - /// # assert_eq!(insert_query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Output @@ -277,13 +271,13 @@ impl Insert { /// # use sql_query_builder as sql; /// let raw = "insert into users (login, name)"; /// - /// let insert_query = sql::Insert::new() + /// let query = sql::Insert::new() /// .raw_before(sql::InsertClause::Values, raw) /// .values("('bar', 'Bar')") /// .as_string(); /// /// # let expected = "insert into users (login, name) VALUES ('bar', 'Bar')"; - /// # assert_eq!(insert_query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Output @@ -309,7 +303,7 @@ impl Insert { /// .as_string(); /// /// # let expected = "INSERT INTO users (login, name) VALUES ('foo', 'Foo'), ('bar', 'Bar')"; - /// # assert_eq!(query, expected); + /// # assert_eq!(expected, query); /// ``` /// /// Output @@ -319,6 +313,10 @@ impl Insert { /// ``` pub fn values(mut self, value: &str) -> Self { push_unique(&mut self._values, value.trim().to_string()); + #[cfg(not(feature = "mysql"))] + { + self._default_values = false + } self } } @@ -333,6 +331,34 @@ impl WithQuery for Insert {} #[cfg_attr(docsrs, doc(cfg(feature = "postgresql")))] #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] impl Insert { + /// The `on conflict` clause. This method overrides the previous value + /// + /// # Example + /// + /// ``` + /// # #[cfg(any(feature = "postgresql", feature = "sqlite"))] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Insert::new() + /// .insert_into("users (login)") + /// .on_conflict("do nothing") + /// .as_string(); + /// + /// # let expected = "INSERT INTO users (login) ON CONFLICT do nothing"; + /// # assert_eq!(expected, query); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// INSERT INTO users (login) ON CONFLICT do nothing + /// ``` + pub fn on_conflict(mut self, conflict: &str) -> Self { + self._on_conflict = conflict.trim().to_string(); + self + } + /// The `returning` clause /// /// # Example @@ -341,14 +367,14 @@ impl Insert { /// # #[cfg(any(feature = "postgresql", feature = "sqlite"))] /// # { /// # use sql_query_builder as sql; - /// let insert_query = sql::Insert::new() + /// let query = sql::Insert::new() /// .insert_into("users") /// .returning("id") /// .returning("login") /// .to_string(); /// /// # let expected = "INSERT INTO users RETURNING id, login"; - /// # assert_eq!(insert_query, expected); + /// # assert_eq!(expected, query); /// # } /// ``` /// @@ -375,7 +401,7 @@ impl Insert { /// .from("users_bk") /// .where_clause("ative = true"); /// - /// let insert_query = sql::Insert::new() + /// let query = sql::Insert::new() /// .with("active_users", active_users) /// .insert_into("users") /// .select(sql::Select::new().select("*").from("active_users")) @@ -391,7 +417,7 @@ impl Insert { /// # SELECT * \ /// # FROM active_users\ /// # "; - /// # assert_eq!(insert_query, expected); + /// # assert_eq!(expected, query); /// # } /// ``` /// @@ -414,19 +440,9 @@ impl Insert { } } -#[cfg(feature = "sqlite")] -use crate::structure::InsertVars; - #[cfg(any(doc, feature = "sqlite"))] #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] impl Insert { - /// The `insert into` clause, this method overrides the previous value - #[cfg(not(doc))] - pub fn insert_into(mut self, expression: &str) -> Self { - self._insert = (InsertVars::InsertInto, expression.trim().to_string()); - self - } - /// The `insert or into` clause /// /// # Example @@ -439,14 +455,14 @@ impl Insert { /// .insert_or("abort into users (login, name)"); /// # /// # let expected = "INSERT OR abort into users (login, name)"; - /// # assert_eq!(insert.to_string(), expected); + /// # assert_eq!(expected, insert.to_string()); /// /// let insert = sql::Insert::new() /// .insert_or("fail into addresses (state, country)") /// .insert_or("abort into users (login, name)"); /// /// # let expected = "INSERT OR abort into users (login, name)"; - /// # assert_eq!(insert.to_string(), expected); + /// # assert_eq!(expected, insert.to_string()); /// # } /// ``` /// @@ -456,7 +472,9 @@ impl Insert { /// INSERT OR abort into users (login, name) /// ``` pub fn insert_or(mut self, expression: &str) -> Self { - self._insert = (InsertVars::InsertOr, expression.trim().to_string()); + self._insert_or = expression.trim().to_string(); + self._insert_into = "".to_string(); + self._replace_into = "".to_string(); self } @@ -472,7 +490,7 @@ impl Insert { /// .replace_into("users (login, name)"); /// # /// # let expected = "REPLACE INTO users (login, name)"; - /// # assert_eq!(insert.to_string(), expected); + /// # assert_eq!(expected, insert.to_string()); /// # } /// ``` /// @@ -481,8 +499,194 @@ impl Insert { /// ```sql /// REPLACE INTO users (login, name) /// ``` - pub fn replace_into(mut self, expression: &str) -> Self { - self._insert = (InsertVars::ReplaceInto, expression.trim().to_string()); + pub fn replace_into(mut self, table_name: &str) -> Self { + self._replace_into = table_name.trim().to_string(); + self._insert_into = "".to_string(); + self._insert_or = "".to_string(); + self + } +} + +#[cfg(any(doc, feature = "mysql"))] +#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] +impl Insert { + /// Defines the columns of the table used to insert values. + /// + /// ### Example + /// + ///``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Insert::new() + /// .into("users") + /// .column("login") + /// .column("name") + /// .as_string(); + /// + /// # let expected = "INTO users (login, name)"; + /// # assert_eq!(expected, query); + /// # } + /// ``` + /// + /// Outputs + /// + /// ```sql + /// INTO users (login, name) + /// ``` + pub fn column(mut self, column_name: &str) -> Self { + push_unique(&mut self._column, column_name.trim().to_string()); + self._insert_into = "".to_string(); + self + } + + /// The `insert` clause, used to defined modifiers to change de insert execution + /// + /// ### Example + /// + ///``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Insert::new() + /// .insert("LOW_PRIORITY") + /// .into("users") + /// .column("login") + /// .as_string(); + /// + /// # let expected = "INSERT LOW_PRIORITY INTO users (login)"; + /// # assert_eq!(expected, query); + /// # } + /// ``` + /// + /// Outputs + /// + /// ```sql + /// INSERT LOW_PRIORITY INTO users (login) + /// ``` + pub fn insert(mut self, modifier: &str) -> Self { + self._insert = modifier.trim().to_string(); + self._insert_into = "".to_string(); + self + } + + /// The `into` clause, defines the name of the table to be used + /// + /// ### Example + /// + ///``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Insert::new() + /// .into("users") + /// .column("login") + /// .as_string(); + /// + /// # let expected = "INTO users (login)"; + /// # assert_eq!(expected, query); + /// # } + /// ``` + /// + /// Outputs + /// + /// ```sql + /// INTO users (login) + /// ``` + pub fn into(mut self, table: &str) -> Self { + self._into = table.trim().to_string(); + self._insert_into = "".to_string(); + self + } + + /// The `partition` clause + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Insert::new() + /// .into("employees") + /// .partition("p1") + /// .to_string(); + /// + /// # let expected = "INTO employees PARTITION (p1)"; + /// # assert_eq!(expected, query); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// INTO employees PARTITION (p1) + /// ``` + pub fn partition(mut self, name: &str) -> Self { + push_unique(&mut self._partition, name.trim().to_string()); + self._insert_into = "".to_string(); + self + } + + /// The `ON DUPLICATE KEY UPDATE` clause + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Insert::new() + /// .insert_into("t1 (a, b, c)") + /// .values("(1, 2, 3)") + /// .on_duplicate_key_update("c = c+1") + /// .as_string(); + /// + /// # let expected = "\ + /// # INSERT INTO t1 (a, b, c) \ + /// # VALUES (1, 2, 3) \ + /// # ON DUPLICATE KEY UPDATE c = c+1\ + /// # "; + /// # assert_eq!(expected, query); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// INSERT INTO t1 (a, b, c) + /// VALUES (1, 2, 3) + /// ON DUPLICATE KEY UPDATE c = c+1 + /// ``` + pub fn on_duplicate_key_update(mut self, assignment: &str) -> Self { + push_unique(&mut self._on_duplicate_key_update, assignment.trim().to_string()); + self + } + + /// The `set` clause + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let update_query = sql::Insert::new() + /// .set("name = 'Bar'") + /// .as_string(); + /// + /// # let expected = "SET name = 'Bar'"; + /// # assert_eq!(expected, update_query); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// SET name = 'Bar' + /// ``` + pub fn set(mut self, assignment: &str) -> Self { + push_unique(&mut self._set, assignment.trim().to_string()); self } } diff --git a/src/insert/insert_internal.rs b/src/insert/insert_internal.rs index 5ab978b..68c8246 100644 --- a/src/insert/insert_internal.rs +++ b/src/insert/insert_internal.rs @@ -2,6 +2,7 @@ use crate::{ concat::{concat_raw_before_after, Concat}, fmt, structure::{Insert, InsertClause}, + utils, }; impl Concat for Insert { @@ -22,25 +23,67 @@ impl Concat for Insert { ); } - #[cfg(not(feature = "sqlite"))] + query = self.concat_insert_into(query, &fmts); + + #[cfg(feature = "sqlite")] { - query = self.concat_insert_into(query, &fmts); + query = self.concat_insert_or(query, &fmts); + query = self.concat_replace_into(query, &fmts); } - #[cfg(feature = "sqlite")] + + #[cfg(feature = "mysql")] { - query = ConcatInsert::concat_insert(self, &self._raw_before, &self._raw_after, query, &fmts, &self._insert); + query = self.concat_insert(query, &fmts); + query = self.concat_into(query, &fmts); + query = self.concat_partition( + &self._raw_before, + &self._raw_after, + query, + &fmts, + InsertClause::Partition, + &self._partition, + ); + query = self.concat_column( + &self._raw_before, + &self._raw_after, + query, + &fmts, + InsertClause::Column, + &self._column, + ); + query = self.concat_set( + &self._raw_before, + &self._raw_after, + query, + &fmts, + InsertClause::Set, + &self._set, + ); } - query = self.concat_overriding(query, &fmts); + #[cfg(not(any(feature = "sqlite", feature = "mysql")))] + { + query = self.concat_overriding(query, &fmts); + } + + #[cfg(not(any(feature = "mysql")))] + { + query = self.concat_default_values(query, &fmts); + } query = self.concat_values(query, &fmts); query = self.concat_select(query, &fmts); - query = self.concat_on_conflict(query, &fmts); + #[cfg(feature = "mysql")] + { + query = self.concat_on_duplicate_key_update(query, &fmts); + } #[cfg(any(feature = "postgresql", feature = "sqlite"))] { + query = self.concat_on_conflict(query, &fmts); + query = self.concat_returning( &self._raw_before, &self._raw_after, @@ -56,12 +99,11 @@ impl Concat for Insert { } impl Insert { - #[cfg(not(feature = "sqlite"))] fn concat_insert_into(&self, query: String, fmts: &fmt::Formatter) -> String { let fmt::Formatter { lb, space, .. } = fmts; let sql = if self._insert_into.is_empty() == false { - let insert_into = &self._insert_into; - format!("INSERT INTO{space}{insert_into}{space}{lb}") + let expression = &self._insert_into; + format!("INSERT INTO{space}{expression}{space}{lb}") } else { "".to_string() }; @@ -76,6 +118,7 @@ impl Insert { ) } + #[cfg(not(any(feature = "sqlite", feature = "mysql")))] fn concat_overriding(&self, query: String, fmts: &fmt::Formatter) -> String { let fmt::Formatter { lb, space, .. } = fmts; let sql = if self._overriding.is_empty() == false { @@ -95,11 +138,11 @@ impl Insert { ) } - fn concat_on_conflict(&self, query: String, fmts: &fmt::Formatter) -> String { + fn concat_select(&self, query: String, fmts: &fmt::Formatter) -> String { let fmt::Formatter { lb, space, .. } = fmts; - let sql = if self._on_conflict.is_empty() == false { - let overriding = &self._on_conflict; - format!("ON CONFLICT{space}{overriding}{space}{lb}") + let sql = if let Some(select) = &self._select { + let select_string = select.concat(fmts); + format!("{select_string}{space}{lb}") } else { "".to_string() }; @@ -109,16 +152,16 @@ impl Insert { &self._raw_after, query, fmts, - InsertClause::OnConflict, + InsertClause::Select, sql, ) } - fn concat_select(&self, query: String, fmts: &fmt::Formatter) -> String { + #[cfg(not(feature = "mysql"))] + fn concat_default_values(&self, query: String, fmts: &fmt::Formatter) -> String { let fmt::Formatter { lb, space, .. } = fmts; - let sql = if let Some(select) = &self._select { - let select_string = select.concat(fmts); - format!("{select_string}{space}{lb}") + let sql = if self._default_values { + format!("DEFAULT VALUES{space}{lb}") } else { "".to_string() }; @@ -128,31 +171,29 @@ impl Insert { &self._raw_after, query, fmts, - InsertClause::Select, + InsertClause::DefaultValues, sql, ) } fn concat_values(&self, query: String, fmts: &fmt::Formatter) -> String { let fmt::Formatter { comma, lb, space, .. } = fmts; - - let (clause, sql) = if self._default_values { - (InsertClause::DefaultValues, format!("DEFAULT VALUES{space}{lb}")) - } else if self._values.is_empty() == false { + let sql = if self._values.is_empty() == false { let sep = format!("{comma}{lb}"); - let values = self - ._values - .iter() - .filter(|item| item.is_empty() == false) - .map(|item| item.as_str()) - .collect::>() - .join(&sep); - (InsertClause::Values, format!("VALUES{space}{lb}{values}{space}{lb}")) + let values = utils::join(&self._values, &sep); + format!("VALUES{space}{lb}{values}{space}{lb}") } else { - (InsertClause::Values, "".to_string()) + "".to_string() }; - concat_raw_before_after(&self._raw_before, &self._raw_after, query, fmts, clause, sql) + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + InsertClause::Values, + sql, + ) } } @@ -164,8 +205,135 @@ impl ConcatReturning for Insert {} #[cfg(any(feature = "postgresql", feature = "sqlite"))] impl ConcatWith for Insert {} -#[cfg(feature = "sqlite")] -use crate::concat::sqlite::ConcatInsert; +#[cfg(any(feature = "postgresql", feature = "sqlite"))] +impl Insert { + fn concat_on_conflict(&self, query: String, fmts: &fmt::Formatter) -> String { + let fmt::Formatter { lb, space, .. } = fmts; + let sql = if self._on_conflict.is_empty() == false { + let overriding = &self._on_conflict; + format!("ON CONFLICT{space}{overriding}{space}{lb}") + } else { + "".to_string() + }; + + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + InsertClause::OnConflict, + sql, + ) + } +} #[cfg(feature = "sqlite")] -impl ConcatInsert for Insert {} +impl Insert { + fn concat_insert_or(&self, query: String, fmts: &fmt::Formatter) -> String { + let fmt::Formatter { lb, space, .. } = fmts; + let sql = if self._insert_or.is_empty() == false { + let expression = &self._insert_or; + format!("INSERT OR{space}{expression}{space}{lb}") + } else { + "".to_string() + }; + + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + InsertClause::InsertOr, + sql, + ) + } + + fn concat_replace_into(&self, query: String, fmts: &fmt::Formatter) -> String { + let fmt::Formatter { lb, space, .. } = fmts; + let sql = if self._replace_into.is_empty() == false { + let table_name = &self._replace_into; + format!("REPLACE INTO{space}{table_name}{space}{lb}") + } else { + "".to_string() + }; + + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + InsertClause::ReplaceInto, + sql, + ) + } +} + +#[cfg(feature = "mysql")] +use crate::concat::{mysql::ConcatPartition, non_standard::ConcatColumn, sql_standard::ConcatSet}; + +#[cfg(feature = "mysql")] +impl ConcatColumn for Insert {} +#[cfg(feature = "mysql")] +impl ConcatPartition for Insert {} +#[cfg(feature = "mysql")] +impl ConcatSet for Insert {} + +#[cfg(feature = "mysql")] +impl Insert { + fn concat_insert(&self, query: String, fmts: &fmt::Formatter) -> String { + let fmt::Formatter { space, .. } = fmts; + let sql = if self._insert.is_empty() == false { + let modifiers = &self._insert; + format!("INSERT{space}{modifiers}{space}") + } else { + "".to_string() + }; + + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + InsertClause::Insert, + sql, + ) + } + + fn concat_into(&self, query: String, fmts: &fmt::Formatter) -> String { + let fmt::Formatter { space, .. } = fmts; + let sql = if self._into.is_empty() == false { + let table_name = &self._into; + format!("INTO{space}{table_name}{space}") + } else { + "".to_string() + }; + + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + InsertClause::Into, + sql, + ) + } + + fn concat_on_duplicate_key_update(&self, query: String, fmts: &fmt::Formatter) -> String { + let fmt::Formatter { comma, lb, space, .. } = fmts; + let sql = if self._on_duplicate_key_update.is_empty() == false { + let values = utils::join(&self._on_duplicate_key_update, comma); + format!("ON DUPLICATE KEY UPDATE{space}{values}{space}{lb}") + } else { + "".to_string() + }; + + concat_raw_before_after( + &self._raw_before, + &self._raw_after, + query, + fmts, + InsertClause::OnDuplicateKeyUpdate, + sql, + ) + } +} diff --git a/src/structure.rs b/src/structure.rs index 8cbd2c4..763f09e 100644 --- a/src/structure.rs +++ b/src/structure.rs @@ -411,35 +411,51 @@ pub enum DropTableParams { /// ``` #[derive(Default, Clone)] pub struct Insert { - pub(crate) _default_values: bool, - pub(crate) _on_conflict: String, - pub(crate) _overriding: String, + pub(crate) _insert_into: String, pub(crate) _raw_after: Vec<(InsertClause, String)>, pub(crate) _raw_before: Vec<(InsertClause, String)>, pub(crate) _raw: Vec, pub(crate) _select: Option, pub(crate) _values: Vec, + pub(crate) _use_row: bool, #[cfg(any(feature = "postgresql", feature = "sqlite"))] pub(crate) _on_conflict: String, @@ -858,6 +859,8 @@ pub enum UpdateClause { /// Basic API /// /// ``` +/// # #[cfg(not(feature = "mysql"))] +/// # { /// use sql_query_builder as sql; /// /// let query = sql::Values::new() @@ -867,6 +870,7 @@ pub enum UpdateClause { /// /// # let expected = "VALUES ('foo', 'Foo'), ('bar', 'Bar')"; /// # assert_eq!(expected, query); +/// # } /// ``` /// /// Output diff --git a/src/values/values.rs b/src/values/values.rs index dda410a..f339375 100644 --- a/src/values/values.rs +++ b/src/values/values.rs @@ -17,13 +17,16 @@ impl Values { /// # Example /// /// ``` + /// # #[cfg(not(feature = "mysql"))] + /// # { /// # use sql_query_builder as sql; /// let values_query = sql::Values::new() /// .values("('foo', 'Foo')") /// .as_string(); /// /// # let expected = "VALUES ('foo', 'Foo')"; - /// # assert_eq!(values_query, expected); + /// # assert_eq!(expected, values_query); + /// # } /// ``` /// /// Output @@ -42,6 +45,8 @@ impl Values { /// # Example /// /// ``` + /// # #[cfg(not(feature = "mysql"))] + /// # { /// # use sql_query_builder as sql; /// let values = sql::Values::new() /// .values("(1, 'one'), (2, 'two')") @@ -49,7 +54,8 @@ impl Values { /// .debug(); /// /// # let expected = "VALUES (1, 'one'), (2, 'two'), (3, 'three')"; - /// # assert_eq!(values.as_string(), expected); + /// # assert_eq!(expected, values.as_string()); + /// # } /// ``` /// /// Prints to the standard output @@ -83,6 +89,8 @@ impl Values { /// # Example /// /// ``` + /// # #[cfg(not(feature = "mysql"))] + /// # { /// # use sql_query_builder as sql; /// let raw_query = "insert into my_table(num, txt)"; /// @@ -92,7 +100,8 @@ impl Values { /// .as_string(); /// /// # let expected = "insert into my_table(num, txt) VALUES (1, 'one'), (2, 'two')"; - /// # assert_eq!(values_query, expected); + /// # assert_eq!(expected, values_query); + /// # } /// ``` /// /// Output @@ -111,6 +120,8 @@ impl Values { /// # Example /// /// ``` + /// # #[cfg(not(feature = "mysql"))] + /// # { /// # use sql_query_builder as sql; /// let raw_query = ", (3, 'three')"; /// @@ -120,7 +131,8 @@ impl Values { /// .as_string(); /// /// # let expected = "VALUES (1, 'one'), (2, 'two') , (3, 'three')"; - /// # assert_eq!(values_query, expected); + /// # assert_eq!(expected, values_query); + /// # } /// ``` /// /// Output @@ -138,6 +150,8 @@ impl Values { /// # Example /// /// ``` + /// # #[cfg(not(feature = "mysql"))] + /// # { /// # use sql_query_builder as sql; /// let raw_query = "/* the values command */"; /// @@ -147,7 +161,8 @@ impl Values { /// .as_string(); /// /// # let expected = "/* the values command */ VALUES (1, 'one'), (2, 'two')"; - /// # assert_eq!(values_query, expected); + /// # assert_eq!(expected, values_query); + /// # } /// ``` /// /// Output @@ -166,6 +181,8 @@ impl Values { /// # Example /// /// ``` + /// # #[cfg(not(feature = "mysql"))] + /// # { /// # use sql_query_builder as sql; /// let values_query = sql::Values::new() /// .values("(1, 'one'), (2, 'two')") @@ -173,7 +190,8 @@ impl Values { /// .as_string(); /// /// # let expected = "VALUES (1, 'one'), (2, 'two'), (3, 'three')"; - /// # assert_eq!(values_query, expected); + /// # assert_eq!(expected, values_query); + /// # } /// ``` /// /// Output @@ -181,12 +199,42 @@ impl Values { /// ```sql /// VALUES (1, 'one'), (2, 'two'), (3, 'three') /// ``` + #[cfg(not(feature = "mysql"))] pub fn values(mut self, expression: &str) -> Self { push_unique(&mut self._values, expression.trim().to_string()); self } } +#[cfg(feature = "mysql")] +#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] +impl Values { + /// The `values` clause with the `row` constructor clause + /// + /// # Example + /// + /// ``` + /// # use sql_query_builder as sql; + /// let values_query = sql::Values::new() + /// .row("(1, 'one'), row(2, 'two')") + /// .row("(3, 'three')") + /// .as_string(); + /// + /// # let expected = "VALUES ROW(1, 'one'), row(2, 'two'), ROW(3, 'three')"; + /// # assert_eq!(expected, values_query); + /// ``` + /// + /// Output + /// + /// ```sql + /// VALUES ROW(1, 'one'), row(2, 'two'), ROW(3, 'three') + /// ``` + pub fn row(mut self, expression: &str) -> Self { + push_unique(&mut self._values, expression.trim().to_string()); + self + } +} + impl std::fmt::Display for Values { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.as_string()) diff --git a/src/values/values_internal.rs b/src/values/values_internal.rs index 63880f6..ec451d6 100644 --- a/src/values/values_internal.rs +++ b/src/values/values_internal.rs @@ -20,14 +20,28 @@ impl Values { let fmt::Formatter { comma, lb, space, .. } = fmts; let sql = if self._values.is_empty() == false { let sep = format!("{comma}{lb}"); - let values = self + let rows = self ._values .iter() .filter(|item| item.is_empty() == false) - .map(|item| item.as_str()) + .map(|item| { + #[cfg(not(feature = "mysql"))] + { + item.clone() + } + #[cfg(feature = "mysql")] + { + format!("ROW{item}") + } + }) .collect::>() .join(&sep); - format!("VALUES{space}{lb}{values}{space}{lb}") + + if rows.is_empty() == true { + return "".to_string(); + }; + + format!("VALUES{space}{lb}{rows}{space}{lb}") } else { "".to_string() }; diff --git a/tests/clause_row_spec.rs b/tests/clause_row_spec.rs new file mode 100644 index 0000000..98ebc0c --- /dev/null +++ b/tests/clause_row_spec.rs @@ -0,0 +1,179 @@ +#[cfg(feature = "mysql")] +mod insert_command { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_row_should_add_a_row_clause() { + let query = sql::Insert::new().row("('foo', 'Foo')").as_string(); + let expected_query = "VALUES ROW('foo', 'Foo')"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_row_should_accumulate_row_on_consecutive_calls() { + let query = sql::Insert::new() + .row("('foo', 'Foo')") + .row("('bar', 'Bar')") + .as_string(); + let expected_query = "VALUES ROW('foo', 'Foo'), ROW('bar', 'Bar')"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_row_should_not_accumulate_row_when_expression_is_empty() { + let query = sql::Insert::new().row("").row("('bar', 'Bar')").row("").as_string(); + let expected_query = "VALUES ROW('bar', 'Bar')"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_row_should_not_produce_the_values_clause_when_has_only_calls_with_empty_argument() { + let query = sql::Insert::new().row("").row("").as_string(); + let expected_query = ""; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_row_should_trim_space_of_the_argument() { + let query = sql::Insert::new().row(" ('Bar') ").as_string(); + let expected_query = "VALUES ROW('Bar')"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_row_should_not_accumulate_arguments_with_the_same_content() { + let query = sql::Insert::new() + .row("('bar', 'Bar')") + .row("('bar', 'Bar')") + .as_string(); + let expected_query = "VALUES ROW('bar', 'Bar')"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_before_should_add_raw_sql_before_values_clause() { + let query = sql::Insert::new() + .raw_before(sql::InsertClause::Values, "insert into users (login, name)") + .row("('foo', 'Foo')") + .as_string(); + let expected_query = "insert into users (login, name) VALUES ROW('foo', 'Foo')"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_raw_after_should_add_raw_sql_after_values_clause() { + let query = sql::Insert::new() + .row("('baz', 'Baz')") + .raw_after(sql::InsertClause::Values, ", ROW('foo', 'Foo')") + .as_string(); + let expected_query = "VALUES ROW('baz', 'Baz') , ROW('foo', 'Foo')"; + + assert_eq!(expected_query, query); + } + + #[test] + fn clause_row_should_be_after_insert_into_clause() { + let query = sql::Insert::new() + .row("('bar', 'Bar')") + .insert_into("users (login, name)") + .as_string(); + let expected_query = "INSERT INTO users (login, name) VALUES ROW('bar', 'Bar')"; + + assert_eq!(expected_query, query); + } +} + +#[cfg(feature = "mysql")] +mod values_command { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn method_row_should_add_a_values_statement_with_the_row_constructor_clause() { + let query = sql::Values::new().row("('foo', 'Foo')").as_string(); + let expected_query = "VALUES ROW('foo', 'Foo')"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_row_should_accumulate_rows_on_consecutive_calls() { + let query = sql::Values::new() + .row("('foo', 'Foo')") + .row("('bar', 'Bar')") + .as_string(); + let expected_query = "VALUES ROW('foo', 'Foo'), ROW('bar', 'Bar')"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_row_should_not_accumulate_row_when_expression_is_empty() { + let query = sql::Values::new().row("").row("('foo', 'Foo')").row("").as_string(); + let expected_query = "VALUES ROW('foo', 'Foo')"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_row_should_not_produce_values_statement_when_has_only_empty_rows() { + let query = sql::Values::new().row("").row("").as_string(); + let expected_query = ""; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_row_should_trim_space_of_the_argument() { + let query = sql::Values::new().row(" ('Bar') ").as_string(); + let expected_query = "VALUES ROW('Bar')"; + + assert_eq!(expected_query, query); + } + + #[test] + fn method_row_should_not_accumulate_arguments_with_the_same_content() { + let query = sql::Values::new() + .row("('bar', 'Bar')") + .row("('bar', 'Bar')") + .as_string(); + let expected_query = "VALUES ROW('bar', 'Bar')"; + + assert_eq!(expected_query, query); + } + + #[test] + fn when_call_raw_after_method_the_content_should_be_added_after_the_statement() { + let query = sql::Values::new() + .row("('foo', 'Foo')") + .row("('bar', 'Bar')") + .raw_before(sql::ValuesClause::Values, "insert into users (login, name)") + .as_string(); + let expected_query = "\ + insert into users (login, name) \ + VALUES ROW('foo', 'Foo'), ROW('bar', 'Bar')\ + "; + + assert_eq!(expected_query, query); + } + + #[test] + fn when_call_raw_before_method_the_content_should_be_added_before_the_statement() { + let query = sql::Values::new() + .row("('foo', 'Foo')") + .row("('bar', 'Bar')") + .raw_after(sql::ValuesClause::Values, ", ROW('ros', 'Ros')") + .as_string(); + let expected_query = "VALUES ROW('foo', 'Foo'), ROW('bar', 'Bar') , ROW('ros', 'Ros')"; + + assert_eq!(expected_query, query); + } +} diff --git a/tests/clause_values_spec.rs b/tests/clause_values_spec.rs index a66a63b..df3b264 100644 --- a/tests/clause_values_spec.rs +++ b/tests/clause_values_spec.rs @@ -7,7 +7,7 @@ mod insert_command { let query = sql::Insert::new().values("('foo', 'Foo')").as_string(); let expected_query = "VALUES ('foo', 'Foo')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -18,7 +18,7 @@ mod insert_command { .as_string(); let expected_query = "VALUES ('foo', 'Foo'), ('bar', 'Bar')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -30,7 +30,15 @@ mod insert_command { .as_string(); let expected_query = "VALUES ('bar', 'Bar')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); + } + + #[test] + fn method_values_should_not_produce_the_values_clause_when_has_only_calls_with_empty_argument() { + let query = sql::Insert::new().values("").values("").as_string(); + let expected_query = ""; + + assert_eq!(expected_query, query); } #[test] @@ -38,7 +46,7 @@ mod insert_command { let query = sql::Insert::new().values(" ('Bar') ").as_string(); let expected_query = "VALUES ('Bar')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -49,7 +57,7 @@ mod insert_command { .as_string(); let expected_query = "VALUES ('bar', 'Bar')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -60,7 +68,7 @@ mod insert_command { .as_string(); let expected_query = "insert into users (login, name) VALUES ('foo', 'Foo')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -71,7 +79,7 @@ mod insert_command { .as_string(); let expected_query = "VALUES ('baz', 'Baz') , ('foo', 'Foo')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -82,10 +90,11 @@ mod insert_command { .as_string(); let expected_query = "INSERT INTO users (login, name) VALUES ('bar', 'Bar')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } } +#[cfg(not(feature = "mysql"))] mod values_command { use pretty_assertions::assert_eq; use sql_query_builder as sql; @@ -95,7 +104,7 @@ mod values_command { let query = sql::Values::new().values("('foo', 'Foo')").as_string(); let expected_query = "VALUES ('foo', 'Foo')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -106,11 +115,11 @@ mod values_command { .as_string(); let expected_query = "VALUES ('foo', 'Foo'), ('bar', 'Bar')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] - fn method_values_should_not_accumulate_values_when_table_name_is_empty() { + fn method_values_should_not_accumulate_values_when_expression_is_empty() { let query = sql::Values::new() .values("") .values("('foo', 'Foo')") @@ -118,7 +127,15 @@ mod values_command { .as_string(); let expected_query = "VALUES ('foo', 'Foo')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); + } + + #[test] + fn method_values_should_not_produce_the_statement_when_has_only_calls_with_empty_argument() { + let query = sql::Values::new().values("").values("").as_string(); + let expected_query = ""; + + assert_eq!(expected_query, query); } #[test] @@ -126,7 +143,7 @@ mod values_command { let query = sql::Values::new().values(" ('Bar') ").as_string(); let expected_query = "VALUES ('Bar')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -137,7 +154,7 @@ mod values_command { .as_string(); let expected_query = "VALUES ('bar', 'Bar')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -148,7 +165,7 @@ mod values_command { .as_string(); let expected_query = "insert into users (login, name) VALUES ('foo', 'Foo')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -159,6 +176,6 @@ mod values_command { .as_string(); let expected_query = "VALUES ('baz', 'Baz') , ('foo', 'Foo')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } } diff --git a/tests/command_insert_spec.rs b/tests/command_insert_spec.rs index e360420..42e1693 100644 --- a/tests/command_insert_spec.rs +++ b/tests/command_insert_spec.rs @@ -1,3 +1,126 @@ +mod full_api { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[cfg(not(any(feature = "sqlite", feature = "mysql")))] + #[test] + fn sql_standard_with_all_methods() { + let query = sql::Insert::new() + // required + .insert_into("users (login, name)") + // one of is required + .default_values() + .values("(1, 'one')") + .select(sql::Select::new().select("login, name")) + // optional + .overriding("user value") + .as_string(); + + let expected_query = "\ + INSERT INTO users (login, name) \ + OVERRIDING user value \ + VALUES (1, 'one') \ + SELECT login, name\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "postgresql")] + #[test] + fn postgres_with_all_methods() { + let query = sql::Insert::new() + // required + .insert_into("users (login, name)") + // one of is required + .default_values() + .values("(1, 'one')") + .select(sql::Select::new().select("login, name")) + // optional + .on_conflict("do nothing") + .overriding("user value") + .returning("login, name") + .with("foo", sql::Select::new().select("login, name")) + .as_string(); + + let expected_query = "\ + WITH foo AS (SELECT login, name) \ + INSERT INTO users (login, name) \ + OVERRIDING user value \ + VALUES (1, 'one') \ + SELECT login, name \ + ON CONFLICT do nothing \ + RETURNING login, name\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "sqlite")] + #[test] + fn sqlite_with_all_methods() { + let query = sql::Insert::new() + // one of is required + .insert_into("users (login, name)") + .insert_or("abort into users (login, name)") + .replace_into("users (login, name)") + // one of is required + .default_values() + .values("(1, 'one')") + .select(sql::Select::new().select("login, name")) + // optional + .on_conflict("do nothing") + .returning("login, name") + .with("foo", sql::Select::new().select("login, name")) + .as_string(); + + let expected_query = "\ + WITH foo AS (SELECT login, name) \ + REPLACE INTO users (login, name) \ + VALUES (1, 'one') \ + SELECT login, name \ + ON CONFLICT do nothing \ + RETURNING login, name\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "mysql")] + #[test] + fn mysql_with_all_methods() { + let query = sql::Insert::new() + // one of is required + .insert_into("users (login, name)") + // methods below as a group + .insert("LOW_PRIORITY") + .into("users") + .column("login, name") + // one of is required + .values("(1, 'one')") + .row("('foo', 'Foo')") + .select(sql::Select::new().select("login, name")) + .set("name = 'Bar'") + // optional + .partition("p1") + .on_duplicate_key_update("c = c+1") + .as_string(); + + let expected_query = "\ + INSERT LOW_PRIORITY \ + INTO users \ + PARTITION (p1) \ + (login, name) \ + SET name = 'Bar' \ + VALUES ROW(1, 'one'), ROW('foo', 'Foo') \ + SELECT login, name \ + ON DUPLICATE KEY UPDATE c = c+1\ + "; + + assert_eq!(expected_query, query); + } +} + mod builder_features { use pretty_assertions::assert_eq; use sql_query_builder as sql; @@ -57,8 +180,8 @@ mod builder_features { /* test raw_after */\ "; - assert_eq!(query_foo, expected_query_foo); - assert_eq!(query_foo_bar, expected_query_foo_bar); + assert_eq!(expected_query_foo, query_foo); + assert_eq!(expected_query_foo_bar, query_foo_bar); } #[test] @@ -1018,3 +1141,21 @@ mod mysql_insert_variances { assert_eq!(expected_query, query); } } + +#[cfg(feature = "mysql")] +mod values_and_row_methods_mixed { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn when_has_at_least_one_call_to_row_method_all_lines_should_receive_the_constructor_clause_row() { + let query = sql::Insert::new() + .values("('baz', 'Baz')") + .values("('foo', 'Foo')") + .row("('max', 'Max')") + .as_string(); + let expected_query = "VALUES ROW('baz', 'Baz'), ROW('foo', 'Foo'), ROW('max', 'Max')"; + + assert_eq!(expected_query, query); + } +} diff --git a/tests/command_values_spec.rs b/tests/command_values_spec.rs index f572266..dd88f30 100644 --- a/tests/command_values_spec.rs +++ b/tests/command_values_spec.rs @@ -1,100 +1,241 @@ +mod full_api { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[cfg(not(feature = "mysql"))] + #[test] + fn sql_standard_with_all_methods() { + let query = sql::Values::new().values("(1, 'one')").as_string(); + + let expected_query = "VALUES (1, 'one')"; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "postgresql")] + #[test] + fn postgres_with_all_methods() { + let query = sql::Values::new().values("(1, 'one')").as_string(); + + let expected_query = "VALUES (1, 'one')"; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "mysql")] + #[test] + fn mysql_with_all_methods() { + let query = sql::Values::new().row("(1, 'one')").as_string(); + + let expected_query = "VALUES ROW(1, 'one')"; + + assert_eq!(expected_query, query); + } +} + mod builder_features { use pretty_assertions::assert_eq; use sql_query_builder as sql; #[test] fn values_builder_should_be_displayable() { - let values = sql::Values::new().values("('foo', 'Foo')").values("('bar', 'Bar')"); + #[cfg(not(feature = "mysql"))] + { + let values = sql::Values::new().values("('foo', 'Foo')").values("('bar', 'Bar')"); + + println!("{}", values); + + let query = values.as_string(); + let expected_query = "VALUES ('foo', 'Foo'), ('bar', 'Bar')"; + + assert_eq!(expected_query, query); + } - println!("{}", values); + #[cfg(feature = "mysql")] + { + let values = sql::Values::new().row("('foo', 'Foo')").row("('bar', 'Bar')"); - let query = values.as_string(); - let expected_query = "VALUES ('foo', 'Foo'), ('bar', 'Bar')"; + println!("{}", values); - assert_eq!(query, expected_query); + let query = values.as_string(); + let expected_query = "VALUES ROW('foo', 'Foo'), ROW('bar', 'Bar')"; + + assert_eq!(expected_query, query); + } } #[test] fn values_builder_should_be_debuggable() { - let values = sql::Values::new().values("('foo', 'Foo')").values("('bar', 'Bar')"); + #[cfg(not(feature = "mysql"))] + { + let values = sql::Values::new().values("('foo', 'Foo')").values("('bar', 'Bar')"); - println!("{:?}", values); + println!("{:?}", values); - let expected_query = "VALUES ('foo', 'Foo'), ('bar', 'Bar')"; - let query = values.as_string(); + let expected_query = "VALUES ('foo', 'Foo'), ('bar', 'Bar')"; + let query = values.as_string(); - assert_eq!(query, expected_query); - } + assert_eq!(expected_query, query); + } - #[test] - fn values_builder_should_be_cloneable() { - let values_foo = sql::Values::new() - .raw("/* test raw */") - .raw_before(sql::ValuesClause::Values, "/* test raw_before */") - .values("('foo', 'Foo')") - .raw_after(sql::ValuesClause::Values, "/* test raw_after */"); + #[cfg(feature = "mysql")] + { + let values = sql::Values::new().row("('foo', 'Foo')").row("('bar', 'Bar')"); - let values_foo_bar = values_foo.clone().values("('bar', 'Bar')"); + println!("{:?}", values); - let query_foo = values_foo.as_string(); - let query_foo_bar = values_foo_bar.as_string(); + let expected_query = "VALUES ROW('foo', 'Foo'), ROW('bar', 'Bar')"; + let query = values.as_string(); + + assert_eq!(expected_query, query); + } + } - let expected_query_foo = "\ - /* test raw */ \ - /* test raw_before */ \ - VALUES ('foo', 'Foo') \ - /* test raw_after */\ - "; - let expected_query_foo_bar = "\ - /* test raw */ \ - /* test raw_before */ \ - VALUES ('foo', 'Foo'), ('bar', 'Bar') \ - /* test raw_after */\ - "; + #[test] + fn values_builder_should_be_cloneable() { + #[cfg(not(feature = "mysql"))] + { + let values_foo = sql::Values::new() + .raw("/* test raw */") + .raw_before(sql::ValuesClause::Values, "/* test raw_before */") + .values("('foo', 'Foo')") + .raw_after(sql::ValuesClause::Values, "/* test raw_after */"); + + let values_foo_bar = values_foo.clone().values("('bar', 'Bar')"); + + let query_foo = values_foo.as_string(); + let query_foo_bar = values_foo_bar.as_string(); + + let expected_query_foo = "\ + /* test raw */ \ + /* test raw_before */ \ + VALUES ('foo', 'Foo') \ + /* test raw_after */\ + "; + let expected_query_foo_bar = "\ + /* test raw */ \ + /* test raw_before */ \ + VALUES ('foo', 'Foo'), ('bar', 'Bar') \ + /* test raw_after */\ + "; + + assert_eq!(expected_query_foo, query_foo); + assert_eq!(expected_query_foo_bar, query_foo_bar); + } - assert_eq!(query_foo, expected_query_foo); - assert_eq!(query_foo_bar, expected_query_foo_bar); + #[cfg(feature = "mysql")] + { + let values_foo = sql::Values::new() + .raw("/* test raw */") + .raw_before(sql::ValuesClause::Values, "/* test raw_before */") + .row("('foo', 'Foo')") + .raw_after(sql::ValuesClause::Values, "/* test raw_after */"); + + let values_foo_bar = values_foo.clone().row("('bar', 'Bar')"); + + let query_foo = values_foo.as_string(); + let query_foo_bar = values_foo_bar.as_string(); + + let expected_query_foo = "\ + /* test raw */ \ + /* test raw_before */ \ + VALUES ROW('foo', 'Foo') \ + /* test raw_after */\ + "; + let expected_query_foo_bar = "\ + /* test raw */ \ + /* test raw_before */ \ + VALUES ROW('foo', 'Foo'), ROW('bar', 'Bar') \ + /* test raw_after */\ + "; + + assert_eq!(expected_query_foo, query_foo); + assert_eq!(expected_query_foo_bar, query_foo_bar); + } } #[test] fn values_builder_should_be_able_to_conditionally_add_clauses() { - let mut values = sql::Values::new().values("('foo', 'Foo')"); + #[cfg(not(feature = "mysql"))] + { + let mut values = sql::Values::new().values("('foo', 'Foo')"); + + if true { + values = values.values("('bar', 'Bar')"); + } + + let query = values.as_string(); + let expected_query = "VALUES ('foo', 'Foo'), ('bar', 'Bar')"; - if true { - values = values.values("('bar', 'Bar')"); + assert_eq!(expected_query, query); } - let query = values.as_string(); - let expected_query = "VALUES ('foo', 'Foo'), ('bar', 'Bar')"; + #[cfg(feature = "mysql")] + { + let mut values = sql::Values::new().row("('foo', 'Foo')"); + + if true { + values = values.row("('bar', 'Bar')"); + } + + let query = values.as_string(); + let expected_query = "VALUES ROW('foo', 'Foo'), ROW('bar', 'Bar')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); + } } #[test] fn values_builder_should_be_composable() { - fn value_foo(values: sql::Values) -> sql::Values { - values.values("('foo', 'Foo')") - } + #[cfg(not(feature = "mysql"))] + { + fn value_foo(values: sql::Values) -> sql::Values { + values.values("('foo', 'Foo')") + } - fn value_bar(values: sql::Values) -> sql::Values { - values.values("('bar', 'Bar')") - } + fn value_bar(values: sql::Values) -> sql::Values { + values.values("('bar', 'Bar')") + } + + fn as_string(values: sql::Values) -> String { + values.as_string() + } - fn as_string(values: sql::Values) -> String { - values.as_string() + let query = Some(sql::Values::new()) + .map(value_foo) + .map(value_bar) + .map(as_string) + .unwrap(); + + let expected_query = "VALUES ('foo', 'Foo'), ('bar', 'Bar')"; + + assert_eq!(expected_query, query); } - let query = Some(sql::Values::new()) - .map(value_foo) - .map(value_bar) - .map(as_string) - .unwrap(); + #[cfg(feature = "mysql")] + { + fn value_foo(values: sql::Values) -> sql::Values { + values.row("('foo', 'Foo')") + } + + fn value_bar(values: sql::Values) -> sql::Values { + values.row("('bar', 'Bar')") + } + + fn as_string(values: sql::Values) -> String { + values.as_string() + } + + let query = Some(sql::Values::new()) + .map(value_foo) + .map(value_bar) + .map(as_string) + .unwrap(); - let expected_query = "\ - VALUES ('foo', 'Foo'), ('bar', 'Bar')\ - "; + let expected_query = "VALUES ROW('foo', 'Foo'), ROW('bar', 'Bar')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); + } } } @@ -107,7 +248,7 @@ mod builder_methods { let query = sql::Values::new().as_string(); let expected_query = ""; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -115,42 +256,86 @@ mod builder_methods { let query = sql::Values::new().as_string(); let expected_query = ""; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] fn method_debug_should_print_at_console_in_a_human_readable_format() { - let query = sql::Values::new() - .values("(1, 'one')") - .values("(2, 'two')") - .debug() - .as_string(); - let expected_query = "VALUES (1, 'one'), (2, 'two')"; + #[cfg(not(feature = "mysql"))] + { + let query = sql::Values::new() + .values("(1, 'one')") + .values("(2, 'two')") + .debug() + .as_string(); + let expected_query = "VALUES (1, 'one'), (2, 'two')"; + + assert_eq!(expected_query, query); + } - assert_eq!(query, expected_query); + #[cfg(feature = "mysql")] + { + let query = sql::Values::new() + .row("(1, 'one')") + .row("(2, 'two')") + .debug() + .as_string(); + let expected_query = "VALUES ROW(1, 'one'), ROW(2, 'two')"; + + assert_eq!(expected_query, query); + } } #[test] fn method_print_should_print_in_one_line_the_current_state_of_builder() { - let query = sql::Values::new() - .values("(1, 'one')") - .values("(2, 'two')") - .print() - .as_string(); - let expected_query = "VALUES (1, 'one'), (2, 'two')"; + #[cfg(not(feature = "mysql"))] + { + let query = sql::Values::new() + .values("(1, 'one')") + .values("(2, 'two')") + .print() + .as_string(); + let expected_query = "VALUES (1, 'one'), (2, 'two')"; + + assert_eq!(expected_query, query); + } - assert_eq!(query, expected_query); + #[cfg(feature = "mysql")] + { + let query = sql::Values::new() + .row("(1, 'one')") + .row("(2, 'two')") + .print() + .as_string(); + let expected_query = "VALUES ROW(1, 'one'), ROW(2, 'two')"; + + assert_eq!(expected_query, query); + } } #[test] fn method_raw_should_add_raw_sql_on_top_of_the_command() { - let query = sql::Values::new() - .raw("/* the values command */") - .values("(1, 'one')") - .as_string(); - let expected_query = "/* the values command */ VALUES (1, 'one')"; + #[cfg(not(feature = "mysql"))] + { + let query = sql::Values::new() + .raw("/* the values command */") + .values("(1, 'one')") + .as_string(); + let expected_query = "/* the values command */ VALUES (1, 'one')"; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "mysql")] + { + let query = sql::Values::new() + .raw("/* the values command */") + .row("(1, 'one')") + .as_string(); + let expected_query = "/* the values command */ VALUES ROW(1, 'one')"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); + } } #[test] @@ -158,7 +343,7 @@ mod builder_methods { let query = sql::Values::new().raw("/* raw one */").raw("/* raw two */").as_string(); let expected_query = "/* raw one */ /* raw two */"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -166,18 +351,32 @@ mod builder_methods { let query = sql::Values::new().raw("").raw("/* raw one */").raw("").as_string(); let expected_query = "/* raw one */"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] fn method_raw_should_be_the_first_to_be_concatenated() { - let query = sql::Values::new() - .values("(1, 'one')") - .raw("insert into my_table(num, txt)") - .as_string(); - let expected_query = "insert into my_table(num, txt) VALUES (1, 'one')"; + #[cfg(not(feature = "mysql"))] + { + let query = sql::Values::new() + .values("(1, 'one')") + .raw("insert into my_table(num, txt)") + .as_string(); + let expected_query = "insert into my_table(num, txt) VALUES (1, 'one')"; + + assert_eq!(expected_query, query); + } - assert_eq!(query, expected_query); + #[cfg(feature = "mysql")] + { + let query = sql::Values::new() + .row("(1, 'one')") + .raw("insert into my_table(num, txt)") + .as_string(); + let expected_query = "insert into my_table(num, txt) VALUES ROW(1, 'one')"; + + assert_eq!(expected_query, query); + } } #[test] @@ -185,7 +384,7 @@ mod builder_methods { let query = sql::Values::new().raw(" /* raw one */ ").as_string(); let expected_query = "/* raw one */"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -196,7 +395,7 @@ mod builder_methods { .as_string(); let expected_query = "/* should not be repeat */"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -206,7 +405,7 @@ mod builder_methods { .as_string(); let expected_query = "/* raw one */"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } #[test] @@ -216,6 +415,6 @@ mod builder_methods { .as_string(); let expected_query = "/* raw one */"; - assert_eq!(query, expected_query); + assert_eq!(expected_query, query); } } From 29c166b14da21ade82a21851d18242534621a277 Mon Sep 17 00:00:00 2001 From: Belchior Oliveira Date: Wed, 23 Jul 2025 09:01:43 -0300 Subject: [PATCH 15/19] Insert builder: fix mutually exclusive methods --- src/insert/insert.rs | 67 ++++++++++- src/insert/insert_internal.rs | 148 +++++++++++++++-------- src/structure.rs | 14 ++- tests/command_insert_spec.rs | 220 ++++++++++++++++++++++++++-------- 4 files changed, 348 insertions(+), 101 deletions(-) diff --git a/src/insert/insert.rs b/src/insert/insert.rs index 19d679c..407becc 100644 --- a/src/insert/insert.rs +++ b/src/insert/insert.rs @@ -6,6 +6,9 @@ use crate::{ utils::push_unique, }; +#[cfg(feature = "mysql")] +use crate::structure::MySqlVariance; + impl TransactionQuery for Insert {} impl Insert { @@ -206,6 +209,12 @@ impl Insert { /// ``` pub fn select(mut self, select: Select) -> Self { self._select = Some(select); + + #[cfg(feature = "mysql")] + { + self._mysql_variance = MySqlVariance::InsertSelect; + } + self } @@ -311,12 +320,67 @@ impl Insert { /// ```sql /// INSERT INTO users (login, name) VALUES ('foo', 'Foo'), ('bar', 'Bar') /// ``` + /// + /// # Composable methods + /// The methods [Insert::values], [Insert::row] are composable, when both was used each line will receive the contructor clause row. + /// + /// # Example + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Insert::new() + /// .values("('foo', 'Foo')") + /// .row("('bar', 'Bar')") + /// .as_string(); + /// # let expected = "VALUES ROW('foo', 'Foo'), ROW('bar', 'Bar')"; + /// # assert_eq!(expected, query); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// VALUES ROW('foo', 'Foo'), ROW('bar', 'Bar') + /// ``` + /// + /// # Mutually exclusive methods + /// The methods [Insert::values]/[Insert::row], [Insert::select] and [Insert::set] are mutually exclusive, the last called will overrides the previous ones. + /// [Insert::row] and [Insert::set] are available on crate feature `mysql` only. + /// + /// # Example + /// ``` + /// # #[cfg(feature = "mysql")] + /// # { + /// # use sql_query_builder as sql; + /// let query = sql::Insert::new() + /// .values("('foo', 'Foo')") + /// .row("('bar', 'Bar')") + /// .set("login = 'foo'") + /// .set("name = 'Foo'") + /// .as_string(); + /// # let expected = "SET login = 'foo', name = 'Foo'"; + /// # assert_eq!(expected, query); + /// # } + /// ``` + /// + /// Output + /// + /// ```sql + /// SET login = 'foo', name = 'Foo' + /// ``` pub fn values(mut self, expression: &str) -> Self { push_unique(&mut self._values, expression.trim().to_string()); + #[cfg(not(feature = "mysql"))] { self._default_values = false } + + #[cfg(feature = "mysql")] + { + self._mysql_variance = MySqlVariance::InsertValues; + } self } } @@ -689,7 +753,7 @@ impl Insert { /// ``` pub fn row(mut self, expression: &str) -> Self { push_unique(&mut self._values, expression.trim().to_string()); - self._use_row = true; + self._mysql_variance = MySqlVariance::InsertValuesRow; self } @@ -717,6 +781,7 @@ impl Insert { /// ``` pub fn set(mut self, assignment: &str) -> Self { push_unique(&mut self._set, assignment.trim().to_string()); + self._mysql_variance = MySqlVariance::InsertSet; self } } diff --git a/src/insert/insert_internal.rs b/src/insert/insert_internal.rs index 20bab9f..cd2fbc4 100644 --- a/src/insert/insert_internal.rs +++ b/src/insert/insert_internal.rs @@ -4,14 +4,26 @@ use crate::{ structure::{Insert, InsertClause}, }; +#[cfg(feature = "mysql")] +use crate::structure::MySqlVariance; + impl Concat for Insert { fn concat(&self, fmts: &fmt::Formatter) -> String { let mut query = "".to_string(); - query = self.concat_raw(query, &fmts, &self._raw); + #[cfg(not(any(feature = "postgresql", feature = "sqlite", feature = "mysql")))] + { + query = self.concat_raw(query, &fmts, &self._raw); + query = self.concat_insert_into(query, &fmts); + query = self.concat_overriding(query, &fmts); + query = self.concat_default_values(query, &fmts); + query = self.concat_values(query, &fmts); + query = self.concat_select(query, &fmts); + } - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(feature = "postgresql")] { + query = self.concat_raw(query, &fmts, &self._raw); query = self.concat_with( &self._raw_before, &self._raw_after, @@ -20,77 +32,101 @@ impl Concat for Insert { InsertClause::With, &self._with, ); - } - - query = self.concat_insert_into(query, &fmts); - - #[cfg(feature = "sqlite")] - { - query = self.concat_insert_or(query, &fmts); - query = self.concat_replace_into(query, &fmts); - } - - #[cfg(feature = "mysql")] - { - query = self.concat_insert(query, &fmts); - query = self.concat_into(query, &fmts); - query = self.concat_partition( + query = self.concat_insert_into(query, &fmts); + query = self.concat_overriding(query, &fmts); + query = self.concat_default_values(query, &fmts); + query = self.concat_values(query, &fmts); + query = self.concat_select(query, &fmts); + query = self.concat_on_conflict(query, &fmts); + query = self.concat_returning( &self._raw_before, &self._raw_after, query, &fmts, - InsertClause::Partition, - &self._partition, + InsertClause::Returning, + &self._returning, ); - query = self.concat_column( + } + + #[cfg(feature = "sqlite")] + { + query = self.concat_raw(query, &fmts, &self._raw); + query = self.concat_with( &self._raw_before, &self._raw_after, query, &fmts, - InsertClause::Column, - &self._column, + InsertClause::With, + &self._with, ); - query = self.concat_set( + query = self.concat_insert_into(query, &fmts); + query = self.concat_insert_or(query, &fmts); + query = self.concat_replace_into(query, &fmts); + query = self.concat_default_values(query, &fmts); + query = self.concat_values(query, &fmts); + query = self.concat_select(query, &fmts); + query = self.concat_on_conflict(query, &fmts); + query = self.concat_returning( &self._raw_before, &self._raw_after, query, &fmts, - InsertClause::Set, - &self._set, + InsertClause::Returning, + &self._returning, ); } - #[cfg(not(any(feature = "sqlite", feature = "mysql")))] - { - query = self.concat_overriding(query, &fmts); - } - - #[cfg(not(any(feature = "mysql")))] - { - query = self.concat_default_values(query, &fmts); - } - - query = self.concat_values(query, &fmts); - - query = self.concat_select(query, &fmts); - #[cfg(feature = "mysql")] { - query = self.concat_on_duplicate_key_update(query, &fmts); - } - - #[cfg(any(feature = "postgresql", feature = "sqlite"))] - { - query = self.concat_on_conflict(query, &fmts); - - query = self.concat_returning( + query = self.concat_raw(query, &fmts, &self._raw); + query = self.concat_insert_into(query, &fmts); + query = self.concat_insert(query, &fmts); + query = self.concat_into(query, &fmts); + query = self.concat_partition( &self._raw_before, &self._raw_after, query, &fmts, - InsertClause::Returning, - &self._returning, + InsertClause::Partition, + &self._partition, ); + + match self._mysql_variance { + MySqlVariance::InsertSelect => { + query = self.concat_column( + &self._raw_before, + &self._raw_after, + query, + &fmts, + InsertClause::Column, + &self._column, + ); + query = self.concat_select(query, &fmts); + } + MySqlVariance::InsertSet => { + query = self.concat_set( + &self._raw_before, + &self._raw_after, + query, + &fmts, + InsertClause::Set, + &self._set, + ); + } + MySqlVariance::InsertValues | MySqlVariance::InsertValuesRow => { + query = self.concat_column( + &self._raw_before, + &self._raw_after, + query, + &fmts, + InsertClause::Column, + &self._column, + ); + query = self.concat_values(query, &fmts); + } + } + + query = self.concat_on_duplicate_key_update(query, &fmts); } query.trim_end().to_string() @@ -184,11 +220,19 @@ impl Insert { .iter() .filter(|item| item.is_empty() == false) .map(|item| { - if self._use_row { - format!("ROW{item}") - } else { + #[cfg(not(feature = "mysql"))] + { item.clone() } + + #[cfg(feature = "mysql")] + { + if self._mysql_variance == MySqlVariance::InsertValuesRow { + format!("ROW{item}") + } else { + item.clone() + } + } }) .collect::>() .join(&sep); diff --git a/src/structure.rs b/src/structure.rs index 3d736a3..07e6c99 100644 --- a/src/structure.rs +++ b/src/structure.rs @@ -437,7 +437,6 @@ pub struct Insert { pub(crate) _raw: Vec, pub(crate) _select: Option, pub(crate) _values: Vec, + pub(crate) _values_variance: ValuesVariance, #[cfg(any(feature = "postgresql", feature = "sqlite"))] pub(crate) _on_conflict: String, @@ -456,9 +458,6 @@ pub struct Insert { #[cfg(feature = "sqlite")] pub(crate) _replace_into: String, - #[cfg(not(feature = "mysql"))] - pub(crate) _default_values: bool, - #[cfg(feature = "mysql")] pub(crate) _column: Vec, @@ -468,9 +467,6 @@ pub struct Insert { #[cfg(feature = "mysql")] pub(crate) _into: String, - #[cfg(feature = "mysql")] - pub(crate) _mysql_variance: MySqlVariance, - #[cfg(feature = "mysql")] pub(crate) _on_duplicate_key_update: Vec, @@ -534,13 +530,34 @@ pub enum InsertClause { Set, } -#[cfg(feature = "mysql")] #[derive(Default, PartialEq, Clone)] -pub enum MySqlVariance { +pub enum InsertVariance { + #[default] + InsertInto, + + #[cfg(feature = "sqlite")] + InsertOr, + + #[cfg(feature = "sqlite")] + ReplaceInto, + + #[cfg(feature = "mysql")] + InsertSplitted, +} + +#[derive(Default, PartialEq, Clone)] +pub enum ValuesVariance { #[default] InsertValues, InsertSelect, + + #[cfg(not(feature = "mysql"))] + InsertDefaultValues, + + #[cfg(feature = "mysql")] InsertSet, + + #[cfg(feature = "mysql")] InsertValuesRow, } diff --git a/tests/command_insert_spec.rs b/tests/command_insert_spec.rs index ac95357..56214dd 100644 --- a/tests/command_insert_spec.rs +++ b/tests/command_insert_spec.rs @@ -19,7 +19,6 @@ mod full_api { let expected_query = "\ INSERT INTO users (login, name) \ OVERRIDING user value \ - VALUES (1, 'one') \ SELECT login, name\ "; @@ -47,7 +46,6 @@ mod full_api { WITH foo AS (SELECT login, name) \ INSERT INTO users (login, name) \ OVERRIDING user value \ - VALUES (1, 'one') \ SELECT login, name \ ON CONFLICT do nothing \ RETURNING login, name\ @@ -77,7 +75,6 @@ mod full_api { let expected_query = "\ WITH foo AS (SELECT login, name) \ REPLACE INTO users (login, name) \ - VALUES (1, 'one') \ SELECT login, name \ ON CONFLICT do nothing \ RETURNING login, name\ From 91bcd259906c714d9cb9eace0ed3609adf345a8e Mon Sep 17 00:00:00 2001 From: Belchior Oliveira Date: Sat, 6 Sep 2025 16:53:58 -0300 Subject: [PATCH 18/19] adds tests with all query methods --- src/delete/delete_internal.rs | 103 ++++++++---- src/select/select_internal.rs | 249 +++++++++++++++++++++++------ src/update/update_internal.rs | 150 ++++++++++++----- tests/command_alter_table_spec.rs | 97 +++++++++++ tests/command_create_table_spec.rs | 40 ++++- tests/command_delete_spec.rs | 137 ++++++++++++++++ tests/command_select_spec.rs | 222 +++++++++++++++++++++++++ tests/command_update_spec.rs | 126 +++++++++++++++ 8 files changed, 997 insertions(+), 127 deletions(-) diff --git a/src/delete/delete_internal.rs b/src/delete/delete_internal.rs index 4f7f71d..589e69b 100644 --- a/src/delete/delete_internal.rs +++ b/src/delete/delete_internal.rs @@ -10,10 +10,23 @@ impl Concat for Delete { fn concat(&self, fmts: &fmt::Formatter) -> String { let mut query = "".to_string(); - query = self.concat_raw(query, &fmts, &self._raw); + #[cfg(not(any(feature = "postgresql", feature = "sqlite", feature = "mysql")))] + { + query = self.concat_raw(query, &fmts, &self._raw); + query = self.concat_delete_from(query, &fmts); + query = self.concat_where( + &self._raw_before, + &self._raw_after, + query, + &fmts, + DeleteClause::Where, + &self._where, + ); + } - #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] + #[cfg(feature = "postgresql")] { + query = self.concat_raw(query, &fmts, &self._raw); query = self.concat_with( &self._raw_before, &self._raw_after, @@ -22,15 +35,66 @@ impl Concat for Delete { DeleteClause::With, &self._with, ); + query = self.concat_delete_from(query, &fmts); + query = self.concat_where( + &self._raw_before, + &self._raw_after, + query, + &fmts, + DeleteClause::Where, + &self._where, + ); + query = self.concat_returning( + &self._raw_before, + &self._raw_after, + query, + &fmts, + DeleteClause::Returning, + &self._returning, + ); } - #[cfg(not(feature = "mysql"))] + #[cfg(feature = "sqlite")] { + query = self.concat_raw(query, &fmts, &self._raw); + query = self.concat_with( + &self._raw_before, + &self._raw_after, + query, + &fmts, + DeleteClause::With, + &self._with, + ); query = self.concat_delete_from(query, &fmts); + query = self.concat_where( + &self._raw_before, + &self._raw_after, + query, + &fmts, + DeleteClause::Where, + &self._where, + ); + query = self.concat_returning( + &self._raw_before, + &self._raw_after, + query, + &fmts, + DeleteClause::Returning, + &self._returning, + ); } #[cfg(feature = "mysql")] { + query = self.concat_raw(query, &fmts, &self._raw); + query = self.concat_with( + &self._raw_before, + &self._raw_after, + query, + &fmts, + DeleteClause::With, + &self._with, + ); query = self.concat_delete_from_mysql(query, &fmts); query = self.concat_join( &self._raw_before, @@ -48,19 +112,14 @@ impl Concat for Delete { DeleteClause::Partition, &self._partition, ); - } - - query = self.concat_where( - &self._raw_before, - &self._raw_after, - query, - &fmts, - DeleteClause::Where, - &self._where, - ); - - #[cfg(feature = "mysql")] - { + query = self.concat_where( + &self._raw_before, + &self._raw_after, + query, + &fmts, + DeleteClause::Where, + &self._where, + ); query = self.concat_order_by( &self._raw_before, &self._raw_after, @@ -79,18 +138,6 @@ impl Concat for Delete { ); } - #[cfg(any(feature = "postgresql", feature = "sqlite"))] - { - query = self.concat_returning( - &self._raw_before, - &self._raw_after, - query, - &fmts, - DeleteClause::Returning, - &self._returning, - ); - } - query.trim_end().to_string() } } diff --git a/src/select/select_internal.rs b/src/select/select_internal.rs index 4b6238a..7d82fb4 100644 --- a/src/select/select_internal.rs +++ b/src/select/select_internal.rs @@ -18,10 +18,52 @@ impl Concat for Select { fn concat(&self, fmts: &fmt::Formatter) -> String { let mut query = "".to_string(); - query = self.concat_raw(query, &fmts, &self._raw); + #[cfg(not(any(feature = "postgresql", feature = "sqlite", feature = "mysql")))] + { + query = self.concat_raw(query, &fmts, &self._raw); + query = self.concat_select(query, &fmts); + query = self.concat_from( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::From, + &self._from, + ); + query = self.concat_join( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::Join, + &self._join, + ); + query = self.concat_where( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::Where, + &self._where, + ); + query = self.concat_group_by(query, &fmts); + query = self.concat_having(query, &fmts); + query = self.concat_window(query, &fmts); + query = self.concat_order_by( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::OrderBy, + &self._order_by, + ); + } - #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] + #[cfg(feature = "postgresql")] { + use crate::structure::Combinator; + + query = self.concat_raw(query, &fmts, &self._raw); query = self.concat_with( &self._raw_before, &self._raw_after, @@ -30,28 +72,149 @@ impl Concat for Select { SelectClause::With, &self._with, ); + query = self.concat_select(query, &fmts); + query = self.concat_from( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::From, + &self._from, + ); + query = self.concat_join( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::Join, + &self._join, + ); + query = self.concat_where( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::Where, + &self._where, + ); + query = self.concat_group_by(query, &fmts); + query = self.concat_having(query, &fmts); + query = self.concat_window(query, &fmts); + query = self.concat_order_by( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::OrderBy, + &self._order_by, + ); + query = self.concat_limit( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::Limit, + &self._limit, + ); + query = self.concat_offset(query, &fmts); + query = self.concat_combinator(query, &fmts, Combinator::Except); + query = self.concat_combinator(query, &fmts, Combinator::Intersect); + query = self.concat_combinator(query, &fmts, Combinator::Union); } - query = self.concat_select(query, &fmts); - query = self.concat_from( - &self._raw_before, - &self._raw_after, - query, - &fmts, - SelectClause::From, - &self._from, - ); - query = self.concat_join( - &self._raw_before, - &self._raw_after, - query, - &fmts, - SelectClause::Join, - &self._join, - ); + #[cfg(feature = "sqlite")] + { + use crate::structure::Combinator; + + query = self.concat_raw(query, &fmts, &self._raw); + query = self.concat_with( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::With, + &self._with, + ); + query = self.concat_select(query, &fmts); + query = self.concat_from( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::From, + &self._from, + ); + query = self.concat_join( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::Join, + &self._join, + ); + query = self.concat_where( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::Where, + &self._where, + ); + query = self.concat_group_by(query, &fmts); + query = self.concat_having(query, &fmts); + query = self.concat_window(query, &fmts); + query = self.concat_order_by( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::OrderBy, + &self._order_by, + ); + query = self.concat_limit( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::Limit, + &self._limit, + ); + query = self.concat_offset(query, &fmts); + query = self.concat_combinator(query, &fmts, Combinator::Except); + query = self.concat_combinator(query, &fmts, Combinator::Intersect); + query = self.concat_combinator(query, &fmts, Combinator::Union); + } #[cfg(feature = "mysql")] { + use crate::structure::Combinator; + + query = self.concat_raw(query, &fmts, &self._raw); + query = self.concat_with( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::With, + &self._with, + ); + query = self.concat_select(query, &fmts); + query = self.concat_from( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::From, + &self._from, + ); + query = self.concat_join( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::Join, + &self._join, + ); query = self.concat_partition( &self._raw_before, &self._raw_after, @@ -60,30 +223,25 @@ impl Concat for Select { SelectClause::Partition, &self._partition, ); - } - - query = self.concat_where( - &self._raw_before, - &self._raw_after, - query, - &fmts, - SelectClause::Where, - &self._where, - ); - query = self.concat_group_by(query, &fmts); - query = self.concat_having(query, &fmts); - query = self.concat_window(query, &fmts); - query = self.concat_order_by( - &self._raw_before, - &self._raw_after, - query, - &fmts, - SelectClause::OrderBy, - &self._order_by, - ); - - #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] - { + query = self.concat_where( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::Where, + &self._where, + ); + query = self.concat_group_by(query, &fmts); + query = self.concat_having(query, &fmts); + query = self.concat_window(query, &fmts); + query = self.concat_order_by( + &self._raw_before, + &self._raw_after, + query, + &fmts, + SelectClause::OrderBy, + &self._order_by, + ); query = self.concat_limit( &self._raw_before, &self._raw_after, @@ -93,11 +251,6 @@ impl Concat for Select { &self._limit, ); query = self.concat_offset(query, &fmts); - } - - #[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] - { - use crate::structure::Combinator; query = self.concat_combinator(query, &fmts, Combinator::Except); query = self.concat_combinator(query, &fmts, Combinator::Intersect); query = self.concat_combinator(query, &fmts, Combinator::Union); diff --git a/src/update/update_internal.rs b/src/update/update_internal.rs index db2427a..7b67b51 100644 --- a/src/update/update_internal.rs +++ b/src/update/update_internal.rs @@ -16,10 +16,31 @@ impl Concat for Update { fn concat(&self, fmts: &fmt::Formatter) -> String { let mut query = "".to_string(); - query = self.concat_raw(query, &fmts, &self._raw); + #[cfg(not(any(feature = "postgresql", feature = "sqlite", feature = "mysql")))] + { + query = self.concat_raw(query, &fmts, &self._raw); + query = self.concat_update(query, &fmts); + query = self.concat_set( + &self._raw_before, + &self._raw_after, + query, + &fmts, + UpdateClause::Set, + &self._set, + ); + query = self.concat_where( + &self._raw_before, + &self._raw_after, + query, + &fmts, + UpdateClause::Where, + &self._where, + ); + } - #[cfg(any(feature = "postgresql", feature = "sqlite"))] + #[cfg(feature = "postgresql")] { + query = self.concat_raw(query, &fmts, &self._raw); query = self.concat_with( &self._raw_before, &self._raw_after, @@ -28,29 +49,61 @@ impl Concat for Update { UpdateClause::With, &self._with, ); - } - - #[cfg(not(feature = "sqlite"))] - { query = self.concat_update(query, &fmts); + query = self.concat_set( + &self._raw_before, + &self._raw_after, + query, + &fmts, + UpdateClause::Set, + &self._set, + ); + query = self.concat_from( + &self._raw_before, + &self._raw_after, + query, + &fmts, + UpdateClause::From, + &self._from, + ); + query = self.concat_where( + &self._raw_before, + &self._raw_after, + query, + &fmts, + UpdateClause::Where, + &self._where, + ); + query = self.concat_returning( + &self._raw_before, + &self._raw_after, + query, + &fmts, + UpdateClause::Returning, + &self._returning, + ); } #[cfg(feature = "sqlite")] { + query = self.concat_raw(query, &fmts, &self._raw); + query = self.concat_with( + &self._raw_before, + &self._raw_after, + query, + &fmts, + UpdateClause::With, + &self._with, + ); query = self.concat_update(&self._raw_before, &self._raw_after, query, &fmts, &self._update); - } - - query = self.concat_set( - &self._raw_before, - &self._raw_after, - query, - &fmts, - UpdateClause::Set, - &self._set, - ); - - #[cfg(any(feature = "postgresql", feature = "sqlite"))] - { + query = self.concat_set( + &self._raw_before, + &self._raw_after, + query, + &fmts, + UpdateClause::Set, + &self._set, + ); query = self.concat_from( &self._raw_before, &self._raw_after, @@ -59,10 +112,6 @@ impl Concat for Update { UpdateClause::From, &self._from, ); - } - - #[cfg(feature = "sqlite")] - { query = self.concat_join( &self._raw_before, &self._raw_after, @@ -71,19 +120,44 @@ impl Concat for Update { UpdateClause::Join, &self._join, ); + query = self.concat_where( + &self._raw_before, + &self._raw_after, + query, + &fmts, + UpdateClause::Where, + &self._where, + ); + query = self.concat_returning( + &self._raw_before, + &self._raw_after, + query, + &fmts, + UpdateClause::Returning, + &self._returning, + ); } - query = self.concat_where( - &self._raw_before, - &self._raw_after, - query, - &fmts, - UpdateClause::Where, - &self._where, - ); - #[cfg(feature = "mysql")] { + query = self.concat_raw(query, &fmts, &self._raw); + query = self.concat_update(query, &fmts); + query = self.concat_set( + &self._raw_before, + &self._raw_after, + query, + &fmts, + UpdateClause::Set, + &self._set, + ); + query = self.concat_where( + &self._raw_before, + &self._raw_after, + query, + &fmts, + UpdateClause::Where, + &self._where, + ); query = self.concat_order_by( &self._raw_before, &self._raw_after, @@ -102,18 +176,6 @@ impl Concat for Update { ); } - #[cfg(any(feature = "postgresql", feature = "sqlite"))] - { - query = self.concat_returning( - &self._raw_before, - &self._raw_after, - query, - &fmts, - UpdateClause::Returning, - &self._returning, - ); - } - query.trim_end().to_string() } } diff --git a/tests/command_alter_table_spec.rs b/tests/command_alter_table_spec.rs index babcc45..c7d3532 100644 --- a/tests/command_alter_table_spec.rs +++ b/tests/command_alter_table_spec.rs @@ -1,3 +1,100 @@ +mod full_api { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[cfg(not(any(feature = "postgresql", feature = "mysql")))] + #[test] + fn sql_standard_with_all_methods() { + let query = sql::AlterTable::new() + // required + .alter_table("users") + // at least one + .add("COLUMN age int not null") + .drop("column login") + .as_string(); + + let expected_query = "\ + ALTER TABLE users \ + DROP column login\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "postgresql")] + #[test] + fn postgres_with_all_methods() { + let query = sql::AlterTable::new() + // required + .alter_table("users") + // at least one + .add("COLUMN age int not null") + .alter("COLUMN created_at SET DEFAULT now()") + .drop("column login") + .rename("COLUMN address TO city") + .rename_to("users_old") + .as_string(); + + let expected_query = "\ + ALTER TABLE users \ + RENAME COLUMN address TO city \ + RENAME TO users_old \ + ADD COLUMN age int not null, \ + ALTER COLUMN created_at SET DEFAULT now(), \ + DROP column login\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "sqlite")] + #[test] + fn sqlite_with_all_methods() { + let query = sql::AlterTable::new() + // required + .alter_table("users") + // at least one + .add("COLUMN age int not null") + .drop("column login") + .rename("COLUMN address TO city") + .rename_to("users_old") + .as_string(); + + let expected_query = "\ + ALTER TABLE users \ + RENAME COLUMN address TO city \ + RENAME TO users_old \ + DROP column login\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "mysql")] + #[test] + fn mysql_with_all_methods() { + let query = sql::AlterTable::new() + // required + .alter_table("users") + // at least one + .add("COLUMN age int not null") + .alter("COLUMN created_at SET DEFAULT now()") + .drop("column login") + .rename("COLUMN address TO city") + .as_string(); + + let expected_query = "\ + ALTER TABLE users \ + ADD COLUMN age int not null, \ + ALTER COLUMN created_at SET DEFAULT now(), \ + DROP column login, \ + RENAME COLUMN address TO city\ + "; + + assert_eq!(expected_query, query); + } +} + mod builder_features { use pretty_assertions::assert_eq; use sql_query_builder as sql; diff --git a/tests/command_create_table_spec.rs b/tests/command_create_table_spec.rs index f90a55e..6d570f9 100644 --- a/tests/command_create_table_spec.rs +++ b/tests/command_create_table_spec.rs @@ -1,16 +1,41 @@ -#[cfg(any(feature = "postgresql", feature = "sqlite", feature = "mysql"))] mod full_api { use pretty_assertions::assert_eq; use sql_query_builder as sql; + #[test] + fn sql_standard_with_all_methods() { + let query = sql::CreateTable::new() + // at least one + .create_table("users") + .create_table_if_not_exists("users") + // required + .column("id serial, login varchar(100) not null") + // optional + .primary_key("(id)") + .foreign_key("(address_id) references addresses(id)") + .constraint("login users_login_key unique(login)") + .as_string(); + + let expected_query = "\ + CREATE TABLE IF NOT EXISTS users (\ + id serial, login varchar(100) not null, \ + PRIMARY KEY(id), \ + CONSTRAINT login users_login_key unique(login), \ + FOREIGN KEY(address_id) references addresses(id)\ + )\ + "; + + assert_eq!(expected_query, query); + } + #[cfg(feature = "postgresql")] #[test] fn postgres_with_all_methods() { let query = sql::CreateTable::new() - // at least one of methods + // at least one .create_table("users") .create_table_if_not_exists("users") - // optional methods + // optional .column("id serial, login varchar(100) not null") .primary_key("(id)") .foreign_key("(address_id) references addresses(id)") @@ -33,12 +58,12 @@ mod full_api { #[test] fn sqlite_with_all_methods() { let query = sql::CreateTable::new() - // at least one of methods + // at least one .create_table("users") .create_table_if_not_exists("users") - // required methods + // required .column("id integer, login varchar(100) not null") - // optional methods + // optional .primary_key("(id)") .foreign_key("(address_id) references addresses(id)") .constraint("login users_login_key unique(login)") @@ -63,8 +88,9 @@ mod full_api { // at least one of methods .create_table("users") .create_table_if_not_exists("users") - // optional methods + // required .column("id int not null auto_increment, login varchar(100) not null") + // optional methods .primary_key("(id)") .foreign_key("(address_id) references addresses(id)") .constraint("login users_login_key unique(login)") diff --git a/tests/command_delete_spec.rs b/tests/command_delete_spec.rs index c3cffe1..f5f1164 100644 --- a/tests/command_delete_spec.rs +++ b/tests/command_delete_spec.rs @@ -1,3 +1,140 @@ +mod full_api { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn sql_standard_with_all_methods() { + let query = sql::Delete::new() + // required + .delete_from("orders") + // at least one + .where_clause("login = $1") + .where_and("product_id = $2") + .where_or("login = 'bar'") + .as_string(); + + let expected_query = "\ + DELETE FROM orders \ + WHERE login = $1 \ + AND product_id = $2 \ + OR login = 'bar'\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "postgresql")] + #[test] + fn postgres_with_all_methods() { + let deactivated_users = sql::Select::new() + .select("id") + .from("users") + .where_clause("ative = false"); + + let query = sql::Delete::new() + // required + .delete_from("orders") + // at least one is required + .where_clause("login = $1") + .where_and("product_id = $2") + .where_or("login = 'bar'") + // optional + .returning("id") + .with("deactivated_users", deactivated_users) + .as_string(); + + let expected_query = "\ + WITH deactivated_users AS (SELECT id FROM users WHERE ative = false) \ + DELETE FROM orders \ + WHERE login = $1 \ + AND product_id = $2 \ + OR login = 'bar' \ + RETURNING id\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "sqlite")] + #[test] + fn sqlite_with_all_methods() { + let deactivated_users = sql::Select::new() + .select("id") + .from("users") + .where_clause("ative = false"); + + let query = sql::Delete::new() + // required + .delete_from("orders") + // at least one is required + .where_clause("login = $1") + .where_and("product_id = $2") + .where_or("login = 'bar'") + // optional + .returning("id") + .with("deactivated_users", deactivated_users) + .as_string(); + + let expected_query = "\ + WITH deactivated_users AS (SELECT id FROM users WHERE ative = false) \ + DELETE FROM orders \ + WHERE login = $1 \ + AND product_id = $2 \ + OR login = 'bar' \ + RETURNING id\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "mysql")] + #[test] + fn mysql_with_all_methods() { + let deactivated_users = sql::Select::new() + .select("id") + .from("users") + .where_clause("ative = false"); + + let query = sql::Delete::new() + // one of is required + .delete_from("orders") + // methods below as a group + .delete("low_priority") + .from("address") + // at least one is required + .where_clause("login = $1") + .where_and("product_id = $2") + .where_or("login = 'bar'") + // optional + .cross_join("addresses") + .inner_join("addresses on addresses.user_login = users.login") + .left_join("addresses on addresses.user_login = users.login") + .limit("123") + .order_by("created_at asc") + .partition("p1") + .right_join("addresses on addresses.user_login = users.login") + .with("deactivated_users", deactivated_users) + .as_string(); + + let expected_query = "\ + WITH deactivated_users AS (SELECT id FROM users WHERE ative = false) \ + DELETE low_priority FROM orders, address \ + CROSS JOIN addresses \ + INNER JOIN addresses on addresses.user_login = users.login \ + LEFT JOIN addresses on addresses.user_login = users.login \ + RIGHT JOIN addresses on addresses.user_login = users.login \ + PARTITION (p1) \ + WHERE login = $1 \ + AND product_id = $2 \ + OR login = 'bar' \ + ORDER BY created_at asc \ + LIMIT 123\ + "; + + assert_eq!(expected_query, query); + } +} + mod builder_features { use pretty_assertions::assert_eq; use sql_query_builder as sql; diff --git a/tests/command_select_spec.rs b/tests/command_select_spec.rs index f57d4ac..122640c 100644 --- a/tests/command_select_spec.rs +++ b/tests/command_select_spec.rs @@ -1,3 +1,225 @@ +mod full_api { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn sql_standard_with_all_methods() { + let query = sql::Select::new() + // required + .select("login, name, status") + // optional + .from("users") + .group_by("login") + .having("status != 'disabled'") + .cross_join("addresses") + .inner_join("addresses on addresses.user_login = users.login") + .left_join("addresses on addresses.user_login = users.login") + .right_join("addresses on addresses.user_login = users.login") + .order_by("login asc") + .where_clause("login = $1") + .where_and("login in ($2)") + .where_or("login in ($3)") + .window("win as (partition by department)") + .as_string(); + + let expected_query = "\ + SELECT login, name, status \ + FROM users \ + CROSS JOIN addresses \ + INNER JOIN addresses on addresses.user_login = users.login \ + LEFT JOIN addresses on addresses.user_login = users.login \ + RIGHT JOIN addresses on addresses.user_login = users.login \ + WHERE login = $1 \ + AND login in ($2) \ + OR login in ($3) \ + GROUP BY login \ + HAVING status != 'disabled' \ + WINDOW win as (partition by department) \ + ORDER BY login asc\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "postgresql")] + #[test] + fn postgres_with_all_methods() { + let query = sql::Select::new() + // required + .select("login, name, status") + // optional + .from("users") + .group_by("login") + .having("status != 'disabled'") + .cross_join("addresses") + .inner_join("addresses on addresses.user_login = users.login") + .left_join("addresses on addresses.user_login = users.login") + .right_join("addresses on addresses.user_login = users.login") + .order_by("login asc") + .where_clause("login = $1") + .where_and("login in ($2)") + .where_or("login in ($3)") + .window("win as (partition by department)") + .limit("1") + .offset("10") + .except(sql::Select::new().select("login, name, status")) + .intersect(sql::Select::new().select("login, name, status")) + .union(sql::Select::new().select("login, name, status")) + .with("foo", sql::Select::new().select("login, name, status")) + .as_string(); + + let expected_query = "\ + (((\ + WITH foo AS (SELECT login, name, status) \ + SELECT login, name, status \ + FROM users \ + CROSS JOIN addresses \ + INNER JOIN addresses on addresses.user_login = users.login \ + LEFT JOIN addresses on addresses.user_login = users.login \ + RIGHT JOIN addresses on addresses.user_login = users.login \ + WHERE login = $1 \ + AND login in ($2) \ + OR login in ($3) \ + GROUP BY login \ + HAVING status != 'disabled' \ + WINDOW win as (partition by department) \ + ORDER BY login asc \ + LIMIT 1 \ + OFFSET 10\ + ) \ + EXCEPT \ + (SELECT login, name, status)\ + ) \ + INTERSECT \ + (SELECT login, name, status)\ + ) \ + UNION \ + (SELECT login, name, status)\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "sqlite")] + #[test] + fn sqlite_with_all_methods() { + let query = sql::Select::new() + // required + .select("login, name, status") + // optional + .from("users") + .group_by("login") + .having("status != 'disabled'") + .cross_join("addresses") + .inner_join("addresses on addresses.user_login = users.login") + .left_join("addresses on addresses.user_login = users.login") + .right_join("addresses on addresses.user_login = users.login") + .order_by("login asc") + .where_clause("login = $1") + .where_and("login in ($2)") + .where_or("login in ($3)") + .window("win as (partition by department)") + .limit("1") + .offset("10") + .except(sql::Select::new().select("login, name, status")) + .intersect(sql::Select::new().select("login, name, status")) + .union(sql::Select::new().select("login, name, status")) + .with("foo", sql::Select::new().select("login, name, status")) + .as_string(); + + let expected_query = "\ + (((\ + WITH foo AS (SELECT login, name, status) \ + SELECT login, name, status \ + FROM users \ + CROSS JOIN addresses \ + INNER JOIN addresses on addresses.user_login = users.login \ + LEFT JOIN addresses on addresses.user_login = users.login \ + RIGHT JOIN addresses on addresses.user_login = users.login \ + WHERE login = $1 \ + AND login in ($2) \ + OR login in ($3) \ + GROUP BY login \ + HAVING status != 'disabled' \ + WINDOW win as (partition by department) \ + ORDER BY login asc \ + LIMIT 1 \ + OFFSET 10\ + ) \ + EXCEPT \ + (SELECT login, name, status)\ + ) \ + INTERSECT \ + (SELECT login, name, status)\ + ) \ + UNION \ + (SELECT login, name, status)\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "mysql")] + #[test] + fn mysql_with_all_methods() { + let query = sql::Select::new() + // required + .select("login, name, status") + // optional + .from("users") + .group_by("login") + .having("status != 'disabled'") + .cross_join("addresses") + .inner_join("addresses on addresses.user_login = users.login") + .left_join("addresses on addresses.user_login = users.login") + .right_join("addresses on addresses.user_login = users.login") + .partition("p1") + .order_by("login asc") + .where_clause("login = $1") + .where_and("login in ($2)") + .where_or("login in ($3)") + .window("win as (partition by department)") + .limit("1") + .offset("10") + .except(sql::Select::new().select("login, name, status")) + .intersect(sql::Select::new().select("login, name, status")) + .union(sql::Select::new().select("login, name, status")) + .with("foo", sql::Select::new().select("login, name, status")) + .as_string(); + + let expected_query = "\ + (((\ + WITH foo AS (SELECT login, name, status) \ + SELECT login, name, status \ + FROM users \ + CROSS JOIN addresses \ + INNER JOIN addresses on addresses.user_login = users.login \ + LEFT JOIN addresses on addresses.user_login = users.login \ + RIGHT JOIN addresses on addresses.user_login = users.login \ + PARTITION (p1) \ + WHERE login = $1 \ + AND login in ($2) \ + OR login in ($3) \ + GROUP BY login \ + HAVING status != 'disabled' \ + WINDOW win as (partition by department) \ + ORDER BY login asc \ + LIMIT 1 \ + OFFSET 10\ + ) \ + EXCEPT \ + (SELECT login, name, status)\ + ) \ + INTERSECT \ + (SELECT login, name, status)\ + ) \ + UNION \ + (SELECT login, name, status)\ + "; + + assert_eq!(expected_query, query); + } +} mod builder_features { use pretty_assertions::assert_eq; use sql_query_builder as sql; diff --git a/tests/command_update_spec.rs b/tests/command_update_spec.rs index 06aae2d..1148c79 100644 --- a/tests/command_update_spec.rs +++ b/tests/command_update_spec.rs @@ -1,3 +1,129 @@ +mod full_api { + use pretty_assertions::assert_eq; + use sql_query_builder as sql; + + #[test] + fn sql_standard_with_all_methods() { + let query = sql::Update::new() + // required + .update("orders") + .set("name = 'Foo'") + // optional + .where_clause("login = $1") + .where_and("product_id = $2") + .where_or("ref_id = $3") + .as_string(); + + let expected_query = "\ + UPDATE orders \ + SET name = 'Foo' \ + WHERE login = $1 \ + AND product_id = $2 \ + OR ref_id = $3\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "postgresql")] + #[test] + fn postgres_with_all_methods() { + let query = sql::Update::new() + // required + .update("orders") + .set("name = 'Foo'") + // optional + .with("foo", sql::Select::new().select("login")) + .from("products p") + .where_clause("login = $1") + .where_and("product_id = $2") + .where_or("p.ref_id = $3") + .returning("*") + .as_string(); + + let expected_query = "\ + WITH foo AS (SELECT login) \ + UPDATE orders \ + SET name = 'Foo' \ + FROM products p \ + WHERE login = $1 \ + AND product_id = $2 \ + OR p.ref_id = $3 \ + RETURNING *\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "sqlite")] + #[test] + fn sqlite_with_all_methods() { + let query = sql::Update::new() + // one of is required + .update("orders") + .update_or("users") + // required + .set("name = 'Foo'") + // optional + .with("foo", sql::Select::new().select("login")) + .from("products p") + .cross_join("addresses") + .inner_join("addresses on addresses.user_login = users.login") + .left_join("addresses on addresses.user_login = users.login") + .right_join("addresses on addresses.user_login = users.login") + .where_clause("login = $1") + .where_and("product_id = $2") + .where_or("p.ref_id = $3") + .returning("*") + .as_string(); + + let expected_query = "\ + WITH foo AS (SELECT login) \ + UPDATE OR users \ + SET name = 'Foo' \ + FROM products p \ + CROSS JOIN addresses \ + INNER JOIN addresses on addresses.user_login = users.login \ + LEFT JOIN addresses on addresses.user_login = users.login \ + RIGHT JOIN addresses on addresses.user_login = users.login \ + WHERE login = $1 \ + AND product_id = $2 \ + OR p.ref_id = $3 \ + RETURNING *\ + "; + + assert_eq!(expected_query, query); + } + + #[cfg(feature = "mysql")] + #[test] + fn mysql_with_all_methods() { + let query = sql::Update::new() + // required + .update("orders") + .set("name = 'Foo'") + // optional + .where_clause("login = $1") + .where_and("product_id = $2") + .where_or("ref_id = $3") + .order_by("id desc") + .limit("1") + .as_string(); + + let expected_query = "\ + UPDATE orders \ + SET name = 'Foo' \ + WHERE login = $1 \ + AND product_id = $2 \ + OR ref_id = $3 \ + ORDER BY id desc \ + LIMIT 1\ + "; + + assert_eq!(expected_query, query); + } +} + mod builder_features { use pretty_assertions::assert_eq; use sql_query_builder as sql; From 20727965d5099dcb7bec0eb8cceb101c2e9be8c9 Mon Sep 17 00:00:00 2001 From: Belchior Oliveira Date: Sun, 21 Sep 2025 15:43:57 -0300 Subject: [PATCH 19/19] fix package version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 279de1d..adc05ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ description = "Write SQL queries in a simple and composable way" documentation = "https://docs.rs/sql_query_builder" repository = "https://github.com/belchior/sql_query_builder" authors = ["Belchior Oliveira "] -version = "2.5.1" +version = "2.5.2" edition = "2021" rust-version = "1.62" license = "MIT"