From f2784f92d2282c75cf010d22d619027053bc01d4 Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Wed, 16 Dec 2020 13:29:53 +0800 Subject: [PATCH] init --- .gitignore | 4 + Cargo.toml | 21 + LICENSE-APACHE | 201 ++++ LICENSE-MIT | 25 + README.md | 496 ++++++++++ src/backend/mod.rs | 134 +++ src/backend/mysql/foreign_key.rs | 80 ++ src/backend/mysql/index.rs | 37 + src/backend/mysql/mod.rs | 21 + src/backend/mysql/query.rs | 460 +++++++++ src/backend/mysql/table.rs | 194 ++++ src/backend/postgres/foreign_key.rs | 76 ++ src/backend/postgres/index.rs | 32 + src/backend/postgres/mod.rs | 21 + src/backend/postgres/query.rs | 460 +++++++++ src/backend/postgres/table.rs | 227 +++++ src/backend/sqlite/foreign_key.rs | 65 ++ src/backend/sqlite/index.rs | 37 + src/backend/sqlite/mod.rs | 21 + src/backend/sqlite/query.rs | 460 +++++++++ src/backend/sqlite/table.rs | 212 ++++ src/expr.rs | 1380 +++++++++++++++++++++++++++ src/foreign_key/common.rs | 79 ++ src/foreign_key/create.rs | 101 ++ src/foreign_key/drop.rs | 71 ++ src/foreign_key/mod.rs | 37 + src/index/common.rs | 38 + src/index/create.rs | 82 ++ src/index/drop.rs | 74 ++ src/index/mod.rs | 37 + src/lib.rs | 513 ++++++++++ src/query/delete.rs | 366 +++++++ src/query/insert.rs | 333 +++++++ src/query/mod.rs | 53 + src/query/select.rs | 1319 +++++++++++++++++++++++++ src/query/update.rs | 508 ++++++++++ src/table/alter.rs | 206 ++++ src/table/common.rs | 295 ++++++ src/table/create.rs | 189 ++++ src/table/drop.rs | 95 ++ src/table/mod.rs | 64 ++ src/table/rename.rs | 68 ++ src/table/truncate.rs | 65 ++ src/tests_cfg.rs | 87 ++ src/types.rs | 210 ++++ src/value.rs | 129 +++ tests/mod.rs | 59 ++ tests/mysql/foreign_key.rs | 31 + tests/mysql/index.rs | 37 + tests/mysql/mod.rs | 7 + tests/mysql/online.rs | 204 ++++ tests/mysql/query.rs | 650 +++++++++++++ tests/mysql/table.rs | 169 ++++ tests/postgres/foreign_key.rs | 30 + tests/postgres/index.rs | 36 + tests/postgres/mod.rs | 7 + tests/postgres/online.rs | 195 ++++ tests/postgres/query.rs | 642 +++++++++++++ tests/postgres/table.rs | 164 ++++ tests/sqlite/foreign_key.rs | 1 + tests/sqlite/index.rs | 37 + tests/sqlite/mod.rs | 7 + tests/sqlite/online.rs | 0 tests/sqlite/query.rs | 642 +++++++++++++ tests/sqlite/table.rs | 153 +++ 65 files changed, 12754 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100755 LICENSE-APACHE create mode 100755 LICENSE-MIT create mode 100644 README.md create mode 100644 src/backend/mod.rs create mode 100644 src/backend/mysql/foreign_key.rs create mode 100644 src/backend/mysql/index.rs create mode 100644 src/backend/mysql/mod.rs create mode 100644 src/backend/mysql/query.rs create mode 100644 src/backend/mysql/table.rs create mode 100644 src/backend/postgres/foreign_key.rs create mode 100644 src/backend/postgres/index.rs create mode 100644 src/backend/postgres/mod.rs create mode 100644 src/backend/postgres/query.rs create mode 100644 src/backend/postgres/table.rs create mode 100644 src/backend/sqlite/foreign_key.rs create mode 100644 src/backend/sqlite/index.rs create mode 100644 src/backend/sqlite/mod.rs create mode 100644 src/backend/sqlite/query.rs create mode 100644 src/backend/sqlite/table.rs create mode 100644 src/expr.rs create mode 100644 src/foreign_key/common.rs create mode 100644 src/foreign_key/create.rs create mode 100644 src/foreign_key/drop.rs create mode 100644 src/foreign_key/mod.rs create mode 100644 src/index/common.rs create mode 100644 src/index/create.rs create mode 100644 src/index/drop.rs create mode 100644 src/index/mod.rs create mode 100644 src/lib.rs create mode 100644 src/query/delete.rs create mode 100644 src/query/insert.rs create mode 100644 src/query/mod.rs create mode 100644 src/query/select.rs create mode 100644 src/query/update.rs create mode 100644 src/table/alter.rs create mode 100644 src/table/common.rs create mode 100644 src/table/create.rs create mode 100644 src/table/drop.rs create mode 100644 src/table/mod.rs create mode 100644 src/table/rename.rs create mode 100644 src/table/truncate.rs create mode 100644 src/tests_cfg.rs create mode 100644 src/types.rs create mode 100644 src/value.rs create mode 100644 tests/mod.rs create mode 100644 tests/mysql/foreign_key.rs create mode 100644 tests/mysql/index.rs create mode 100644 tests/mysql/mod.rs create mode 100644 tests/mysql/online.rs create mode 100644 tests/mysql/query.rs create mode 100644 tests/mysql/table.rs create mode 100644 tests/postgres/foreign_key.rs create mode 100644 tests/postgres/index.rs create mode 100644 tests/postgres/mod.rs create mode 100644 tests/postgres/online.rs create mode 100644 tests/postgres/query.rs create mode 100644 tests/postgres/table.rs create mode 100644 tests/sqlite/foreign_key.rs create mode 100644 tests/sqlite/index.rs create mode 100644 tests/sqlite/mod.rs create mode 100644 tests/sqlite/online.rs create mode 100644 tests/sqlite/query.rs create mode 100644 tests/sqlite/table.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..950c227c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target +Cargo.lock +*.sublime* +.vscode \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..f93cb2d70 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "sea-query" +version = "0.1.0" +authors = ["Billy Chan "] +edition = "2018" +description = "A database agnostic runtime query builder for Rust" +license = "MIT OR Apache-2.0" +documentation = "https://docs.rs/sea-query" +repository = "https://github.com/SeaQL/Sea-Query" +categories = ["database"] +keywords = ["database", "sql", "mysql", "postgres", "sqlite"] + +[dependencies] +serde_json = "1.0" + +[dev-dependencies] +async-std = "1.8" +sqlx = { version = "0.4.0-beta.1", default-features = false, features = [ "runtime-async-std", "macros", "any", "mysql", "sqlite", "postgres", "tls", "migrate", "decimal" ] } + +[lib] +name = "sea_query" \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100755 index 000000000..f8e5e5ea0 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100755 index 000000000..14bf0d7d5 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2020 Tsang Hao Fung + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..92179ac74 --- /dev/null +++ b/README.md @@ -0,0 +1,496 @@ +
+ +

Sea-Query

+ +

+ A database agnostic runtime query builder for Rust +

+ +

+ Install + | + Usage + | + API Docs +

+ + Built with 🦀 + +
+ +## Introduction + +This library is the foundation of upcoming projects on Document ORM (Sea-ORM) and Database Synchor (Sea-Horse). + +## Install + +```toml +# Cargo.toml +[dependencies] +sea-query = "*" +``` + +## Usage + +Construct a SQL statement with the library then execute the statement +with a database connector such as [SQLx](https://github.com/launchbadge/sqlx). + +Later we will release Document ORM (Sea-ORM) which you can load and save document on any supported relational database, +without the need of managing database connection on your own. + +### Iden + +A trait for identifiers used in any query statement. + +Commonly implemented by Enum where each Enum represent a table found in a database, +and its variants include table name and column name. + +[`Iden::unquoted()`] must be implemented to provide a mapping between Enum variant and its corresponding string value. + +```rust +use sea_query::{*, tests_cfg::*}; + +// For example Character table with column id, character, font_size... +pub enum Character { + Table, + Id, + Character, + FontSize, + SizeW, + SizeH, + FontId, +} + +// Mapping between Enum variant and its corresponding string value +impl Iden for Character { + fn unquoted(&self, s: &mut dyn FmtWrite) { + write!(s, "{}", match self { + Self::Table => "character", + Self::Id => "id", + Self::Character => "character", + Self::FontSize => "font_size", + Self::SizeW => "size_w", + Self::SizeH => "size_h", + Self::FontId => "font_id", + }).unwrap(); + } +} +``` + +### Expression + +Use [`Expr`] to construct select, join, where and having expression in query. + +```rust +use sea_query::{*, tests_cfg::*}; + +assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + .and_where( + Expr::expr(Expr::col(Char::SizeW).add(1)).mul(2) + .equals(Expr::expr(Expr::col(Char::SizeH).div(2)).sub(1)) + ) + .and_where(Expr::col(Char::SizeW).in_subquery( + Query::select() + .expr(Expr::cust("3 + 2 * 2")) + .take() + )) + .or_where(Expr::col(Char::Character).like("D").and(Expr::col(Char::Character).like("E"))) + .to_string(MysqlQueryBuilder::new()), + vec![ + "SELECT `character` FROM `character`", + "LEFT JOIN `font` ON `character`.`font_id` = `font`.`id`", + "WHERE ((`size_w` + 1) * 2 = (`size_h` / 2) - 1)", + "AND `size_w` IN (SELECT 3 + 2 * 2)", + "OR ((`character` LIKE 'D') AND (`character` LIKE 'E'))", + ].join(" ") +); +``` + +### Query Select + +```rust +use sea_query::{*, tests_cfg::*}; + +let query = Query::select() + .column(Char::Character) + .table_column(Font::Table, Font::Name) + .from(Char::Table) + .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + .and_where(Expr::col(Char::SizeW).is_in(vec![3, 4])) + .and_where(Expr::col(Char::Character).like("A%")) + .to_owned(); + +assert_eq!( + query.to_string(MysqlQueryBuilder), + r#"SELECT `character`, `font`.`name` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id` WHERE `size_w` IN (3, 4) AND `character` LIKE 'A%'"# +); +assert_eq!( + query.to_string(PostgresQueryBuilder), + r#"SELECT "character", "font"."name" FROM "character" LEFT JOIN "font" ON "character"."font_id" = "font"."id" WHERE "size_w" IN (3, 4) AND "character" LIKE 'A%'"# +); +assert_eq!( + query.to_string(SqliteQueryBuilder), + r#"SELECT `character`, `font`.`name` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id` WHERE `size_w` IN (3, 4) AND `character` LIKE 'A%'"# +); +``` + +### Query Insert + +```rust +use sea_query::{*, tests_cfg::*}; + +let query = Query::insert() + .into_table(Glyph::Table) + .columns(vec![ + Glyph::Aspect, + Glyph::Image, + ]) + .values_panic(vec![ + 5.15.into(), + "12A".into(), + ]) + .json(json!({ + "aspect": 4.21, + "image": "123", + })) + .to_owned(); + +assert_eq!( + query.to_string(MysqlQueryBuilder), + r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (5.15, '12A'), (4.21, '123')"# +); +assert_eq!( + query.to_string(PostgresQueryBuilder), + r#"INSERT INTO "glyph" ("aspect", "image") VALUES (5.15, '12A'), (4.21, '123')"# +); +assert_eq!( + query.to_string(SqliteQueryBuilder), + r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (5.15, '12A'), (4.21, '123')"# +); +``` + +### Query Update + +```rust +use sea_query::{*, tests_cfg::*}; + +let query = Query::update() + .into_table(Glyph::Table) + .values(vec![ + (Glyph::Aspect, 1.23.into()), + (Glyph::Image, "123".into()), + ]) + .and_where(Expr::col(Glyph::Id).eq(1)) + .to_owned(); + +assert_eq!( + query.to_string(MysqlQueryBuilder), + r#"UPDATE `glyph` SET `aspect` = 1.23, `image` = '123' WHERE `id` = 1"# +); +assert_eq!( + query.to_string(PostgresQueryBuilder), + r#"UPDATE "glyph" SET "aspect" = 1.23, "image" = '123' WHERE "id" = 1"# +); +assert_eq!( + query.to_string(SqliteQueryBuilder), + r#"UPDATE `glyph` SET `aspect` = 1.23, `image` = '123' WHERE `id` = 1"# +); +``` + +### Query Delete + +```rust +use sea_query::{*, tests_cfg::*}; + +let query = Query::delete() + .from_table(Glyph::Table) + .or_where(Expr::col(Glyph::Id).lt(1)) + .or_where(Expr::col(Glyph::Id).gt(10)) + .to_owned(); + +assert_eq!( + query.to_string(MysqlQueryBuilder), + r#"DELETE FROM `glyph` WHERE (`id` < 1) OR (`id` > 10)"# +); +assert_eq!( + query.to_string(PostgresQueryBuilder), + r#"DELETE FROM "glyph" WHERE ("id" < 1) OR ("id" > 10)"# +); +assert_eq!( + query.to_string(SqliteQueryBuilder), + r#"DELETE FROM `glyph` WHERE (`id` < 1) OR (`id` > 10)"# +); +``` + +### Table Create + +```rust +use sea_query::{*, tests_cfg::*}; + +let table = Table::create() + .table(Char::Table) + .create_if_not_exists() + .col(ColumnDef::new(Char::Id).integer().not_null().auto_increment().primary_key()) + .col(ColumnDef::new(Char::FontSize).integer().not_null()) + .col(ColumnDef::new(Char::Character).string().not_null()) + .col(ColumnDef::new(Char::SizeW).integer().not_null()) + .col(ColumnDef::new(Char::SizeH).integer().not_null()) + .col(ColumnDef::new(Char::FontId).integer().default(Value::NULL)) + .foreign_key( + ForeignKey::create() + .name("FK_2e303c3a712662f1fc2a4d0aad6") + .table(Char::Table, Font::Table) + .col(Char::FontId, Font::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) + ) + .to_owned(); + +assert_eq!( + table.to_string(MysqlQueryBuilder), + vec![ + r#"CREATE TABLE IF NOT EXISTS `character` ("#, + r#"`id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,"#, + r#"`font_size` int NOT NULL,"#, + r#"`character` varchar NOT NULL,"#, + r#"`size_w` int NOT NULL,"#, + r#"`size_h` int NOT NULL,"#, + r#"`font_id` int DEFAULT NULL,"#, + r#"CONSTRAINT `FK_2e303c3a712662f1fc2a4d0aad6`"#, + r#"FOREIGN KEY `FK_2e303c3a712662f1fc2a4d0aad6` (`font_id`) REFERENCES `font` (`id`)"#, + r#"ON DELETE CASCADE ON UPDATE CASCADE"#, + r#")"#, + ].join(" ") +); +assert_eq!( + table.to_string(PostgresQueryBuilder), + vec![ + r#"CREATE TABLE IF NOT EXISTS "character" ("#, + r#""id" serial NOT NULL PRIMARY KEY,"#, + r#""font_size" integer NOT NULL,"#, + r#""character" varchar NOT NULL,"#, + r#""size_w" integer NOT NULL,"#, + r#""size_h" integer NOT NULL,"#, + r#""font_id" integer DEFAULT NULL,"#, + r#"CONSTRAINT "FK_2e303c3a712662f1fc2a4d0aad6""#, + r#"FOREIGN KEY ("font_id") REFERENCES "font" ("id")"#, + r#"ON DELETE CASCADE ON UPDATE CASCADE"#, + r#")"#, + ].join(" ") +); +assert_eq!( + table.to_string(SqliteQueryBuilder), + vec![ + r#"CREATE TABLE IF NOT EXISTS `character` ("#, + r#"`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,"#, + r#"`font_size` integer NOT NULL,"#, + r#"`character` text NOT NULL,"#, + r#"`size_w` integer NOT NULL,"#, + r#"`size_h` integer NOT NULL,"#, + r#"`font_id` integer DEFAULT NULL,"#, + r#"FOREIGN KEY (`font_id`) REFERENCES `font` (`id`) ON DELETE CASCADE ON UPDATE CASCADE"#, + r#")"#, + ].join(" ") +); +``` + +### Table Alter + +```rust +use sea_query::{*, tests_cfg::*}; + +let table = Table::alter() + .table(Font::Table) + .add_column(ColumnDef::new(Alias::new("new_col")).integer().not_null().default(100)) + .to_owned(); + +assert_eq!( + table.to_string(MysqlQueryBuilder), + r#"ALTER TABLE `font` ADD COLUMN `new_col` int NOT NULL DEFAULT 100"# +); +assert_eq!( + table.to_string(PostgresQueryBuilder), + r#"ALTER TABLE "font" ADD COLUMN "new_col" integer NOT NULL DEFAULT 100"# +); +assert_eq!( + table.to_string(SqliteQueryBuilder), + r#"ALTER TABLE `font` ADD COLUMN `new_col` integer NOT NULL DEFAULT 100"#, +); +``` + +### Table Drop + +```rust +use sea_query::{*, tests_cfg::*}; + +let table = Table::drop() + .table(Glyph::Table) + .table(Char::Table) + .to_owned(); + +assert_eq!( + table.to_string(MysqlQueryBuilder), + r#"DROP TABLE `glyph`, `character`"# +); +assert_eq!( + table.to_string(PostgresQueryBuilder), + r#"DROP TABLE "glyph", "character""# +); +assert_eq!( + table.to_string(SqliteQueryBuilder), + r#"DROP TABLE `glyph`, `character`"# +); +``` + +### Table Rename + +```rust +use sea_query::{*, tests_cfg::*}; + +let table = Table::rename() + .table(Font::Table, Alias::new("font_new")) + .to_owned(); + +assert_eq!( + table.to_string(MysqlQueryBuilder), + r#"RENAME TABLE `font` TO `font_new`"# +); +assert_eq!( + table.to_string(PostgresQueryBuilder), + r#"ALTER TABLE "font" RENAME TO "font_new""# +); +assert_eq!( + table.to_string(SqliteQueryBuilder), + r#"ALTER TABLE `font` RENAME TO `font_new`"# +); +``` + +### Table Truncate + +```rust +use sea_query::{*, tests_cfg::*}; + +let table = Table::truncate() + .table(Font::Table) + .to_owned(); + +assert_eq!( + table.to_string(MysqlQueryBuilder), + r#"TRUNCATE TABLE `font`"# +); +assert_eq!( + table.to_string(PostgresQueryBuilder), + r#"TRUNCATE TABLE "font""# +); +assert_eq!( + table.to_string(SqliteQueryBuilder), + r#"TRUNCATE TABLE `font`"# +); +``` + +### Foreign Key Create + +```rust +use sea_query::{*, tests_cfg::*}; + +let foreign_key = ForeignKey::create() + .name("FK_character_font") + .table(Char::Table, Font::Table) + .col(Char::FontId, Font::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) + .to_owned(); + +assert_eq!( + foreign_key.to_string(MysqlQueryBuilder), + vec![ + r#"ALTER TABLE `character`"#, + r#"ADD CONSTRAINT `FK_character_font`"#, + r#"FOREIGN KEY `FK_character_font` (`font_id`) REFERENCES `font` (`id`)"#, + r#"ON DELETE CASCADE ON UPDATE CASCADE"#, + ].join(" ") +); +assert_eq!( + foreign_key.to_string(PostgresQueryBuilder), + vec![ + r#"ALTER TABLE "character" ADD CONSTRAINT "FK_character_font""#, + r#"FOREIGN KEY ("font_id") REFERENCES "font" ("id")"#, + r#"ON DELETE CASCADE ON UPDATE CASCADE"#, + ].join(" ") +); +// Sqlite does not support modification of foreign key constraints to existing tables +``` + +### Foreign Key Drop + +```rust +use sea_query::{*, tests_cfg::*}; + +let foreign_key = ForeignKey::drop() + .name("FK_character_font") + .table(Char::Table) + .to_owned(); + +assert_eq!( + foreign_key.to_string(MysqlQueryBuilder), + r#"ALTER TABLE `character` DROP FOREIGN KEY `FK_character_font`"# +); +assert_eq!( + foreign_key.to_string(PostgresQueryBuilder), + r#"ALTER TABLE "character" DROP CONSTRAINT "FK_character_font""# +); +// Sqlite does not support modification of foreign key constraints to existing tables +``` + +### Index Create + +```rust +use sea_query::{*, tests_cfg::*}; + +let index = Index::create() + .name("idx-glyph-aspect") + .table(Glyph::Table) + .col(Glyph::Aspect) + .to_owned(); + +assert_eq!( + index.to_string(MysqlQueryBuilder), + r#"CREATE INDEX `idx-glyph-aspect` ON `glyph` (`aspect`)"# +); +assert_eq!( + index.to_string(PostgresQueryBuilder), + r#"CREATE INDEX "idx-glyph-aspect" ON "glyph" ("aspect")"# +); +assert_eq!( + index.to_string(SqliteQueryBuilder), + r#"CREATE INDEX `idx-glyph-aspect` ON `glyph` (`aspect`)"# +); +``` + +### Index Drop + +```rust +use sea_query::{*, tests_cfg::*}; + +let index = Index::drop() + .name("idx-glyph-aspect") + .table(Glyph::Table) + .to_owned(); + +assert_eq!( + index.to_string(MysqlQueryBuilder), + r#"DROP INDEX `idx-glyph-aspect` ON `glyph`"# +); +assert_eq!( + index.to_string(PostgresQueryBuilder), + r#"DROP INDEX "idx-glyph-aspect""# +); +assert_eq!( + index.to_string(SqliteQueryBuilder), + r#"DROP INDEX `idx-glyph-aspect` ON `glyph`"# +); +``` \ No newline at end of file diff --git a/src/backend/mod.rs b/src/backend/mod.rs new file mode 100644 index 000000000..08c9e9e38 --- /dev/null +++ b/src/backend/mod.rs @@ -0,0 +1,134 @@ +//! Translating unified SQL representation into database specific SQL statement. +//! +//! There traits are defined to model the CRUD (create, read, update, delete) behaviours +//! of each type of SQL statement, namely [`query`], [`table`], [`index`], [`foreign_key`]. +//! +//! NOTE: not all operations are support at the time, we will add more functionality in the future. :) + +use crate::*; +use std::fmt::Write as FmtWrite; + +mod mysql; +mod sqlite; +mod postgres; + +pub use mysql::*; +pub use sqlite::*; +pub use postgres::*; + +pub trait QueryBuilder { + /// Translate [`InsertStatement`] into database specific SQL statement. + fn prepare_insert_statement(&mut self, insert: &InsertStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`SelectStatement`] into database specific SQL statement. + fn prepare_select_statement(&mut self, select: &SelectStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`UpdateStatement`] into database specific SQL statement. + fn prepare_update_statement(&mut self, update: &UpdateStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`DeleteStatement`] into database specific SQL statement. + fn prepare_delete_statement(&mut self, delete: &DeleteStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`SimpleExpr`] into database specific SQL statement. + fn prepare_simple_expr(&mut self, simple_expr: &SimpleExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`SelectDistinct`] into database specific SQL statement. + fn prepare_select_distinct(&mut self, select_distinct: &SelectDistinct, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`SelectExpr`] into database specific SQL statement. + fn prepare_select_expr(&mut self, select_expr: &SelectExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`JoinExpr`] into database specific SQL statement. + fn prepare_join_expr(&mut self, join_expr: &JoinExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`TableRef`] into database specific SQL statement. + fn prepare_table_ref(&mut self, table_ref: &TableRef, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`UnOper`] into database specific SQL statement. + fn prepare_un_oper(&mut self, un_oper: &UnOper, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`BinOper`] into database specific SQL statement. + fn prepare_bin_oper(&mut self, bin_oper: &BinOper, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`LogicalChainOper`] into database specific SQL statement. + fn prepare_logical_chain_oper(&mut self, log_chain_oper: &LogicalChainOper, i: usize, length: usize, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`Function`] into database specific SQL statement. + fn prepare_function(&mut self, function: &Function, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`JoinType`] into database specific SQL statement. + fn prepare_join_type(&mut self, join_type: &JoinType, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`OrderExpr`] into database specific SQL statement. + fn prepare_order_expr(&mut self, order_expr: &OrderExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`JoinOn`] into database specific SQL statement. + fn prepare_join_on(&mut self, join_on: &JoinOn, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`Order`] into database specific SQL statement. + fn prepare_order(&mut self, order: &Order, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`Value`] into database specific SQL statement. + fn prepare_value(&mut self, value: &Value, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)); + + /// Translate [`Value`] into database specific SQL statement. + fn prepare_value_param(&mut self, value: &Value, collector: &mut dyn FnMut(Value)); + +} + +pub trait TableBuilder { + /// Translate [`TableCreateStatement`] into database specific SQL statement. + fn prepare_table_create_statement(&mut self, insert: &TableCreateStatement, sql: &mut dyn FmtWrite); + + /// Translate [`ColumnDef`] into database specific SQL statement. + fn prepare_column_def(&mut self, column_def: &ColumnDef, sql: &mut dyn FmtWrite); + + /// Translate [`ColumnType`] into database specific SQL statement. + fn prepare_column_type(&mut self, column_type: &ColumnType, sql: &mut dyn FmtWrite); + + /// Translate [`ColumnSpec`] into database specific SQL statement. + fn prepare_column_spec(&mut self, column_spec: &ColumnSpec, sql: &mut dyn FmtWrite); + + /// Translate [`TableOpt`] into database specific SQL statement. + fn prepare_table_opt(&mut self, table_opt: &TableOpt, sql: &mut dyn FmtWrite); + + /// Translate [`TablePartition`] into database specific SQL statement. + fn prepare_table_partition(&mut self, table_partition: &TablePartition, sql: &mut dyn FmtWrite); + + /// Translate [`TableDropStatement`] into database specific SQL statement. + fn prepare_table_drop_statement(&mut self, drop: &TableDropStatement, sql: &mut dyn FmtWrite); + + /// Translate [`TableDropOpt`] into database specific SQL statement. + fn prepare_table_drop_opt(&mut self, drop_opt: &TableDropOpt, sql: &mut dyn FmtWrite); + + /// Translate [`TableTruncateStatement`] into database specific SQL statement. + fn prepare_table_truncate_statement(&mut self, truncate: &TableTruncateStatement, sql: &mut dyn FmtWrite); + + /// Translate [`TableAlterStatement`] into database specific SQL statement. + fn prepare_table_alter_statement(&mut self, alter: &TableAlterStatement, sql: &mut dyn FmtWrite); + + /// Translate [`TableRenameStatement`] into database specific SQL statement. + fn prepare_table_rename_statement(&mut self, rename: &TableRenameStatement, sql: &mut dyn FmtWrite); + +} + +pub trait IndexBuilder { + /// Translate [`IndexCreateStatement`] into database specific SQL statement. + fn prepare_index_create_statement(&mut self, create: &IndexCreateStatement, sql: &mut dyn FmtWrite); + + /// Translate [`IndexDropStatement`] into database specific SQL statement. + fn prepare_index_drop_statement(&mut self, drop: &IndexDropStatement, sql: &mut dyn FmtWrite); + +} + +pub trait ForeignKeyBuilder { + /// Translate [`ForeignKeyCreateStatement`] into database specific SQL statement. + fn prepare_foreign_key_create_statement(&mut self, create: &ForeignKeyCreateStatement, sql: &mut dyn FmtWrite); + + /// Translate [`ForeignKeyAction`] into database specific SQL statement. + fn prepare_foreign_key_action(&mut self, foreign_key_action: &ForeignKeyAction, sql: &mut dyn FmtWrite); + + /// Translate [`ForeignKeyDropStatement`] into database specific SQL statement. + fn prepare_foreign_key_drop_statement(&mut self, drop: &ForeignKeyDropStatement, sql: &mut dyn FmtWrite); + +} \ No newline at end of file diff --git a/src/backend/mysql/foreign_key.rs b/src/backend/mysql/foreign_key.rs new file mode 100644 index 000000000..7eddd61f1 --- /dev/null +++ b/src/backend/mysql/foreign_key.rs @@ -0,0 +1,80 @@ +use super::*; + +impl ForeignKeyBuilder for MysqlQueryBuilder { + fn prepare_foreign_key_create_statement(&mut self, create: &ForeignKeyCreateStatement, sql: &mut dyn FmtWrite) { + if !create.inside_table_creation { + write!(sql, "ALTER TABLE ").unwrap(); + if let Some(table) = &create.foreign_key.table { + table.prepare(sql, '`'); + } + write!(sql, " ADD ").unwrap(); + } + + write!(sql, "CONSTRAINT ").unwrap(); + if let Some(name) = &create.foreign_key.name { + write!(sql, "`{}`", name).unwrap(); + } + write!(sql, " FOREIGN KEY ").unwrap(); + if let Some(name) = &create.foreign_key.name { + write!(sql, "`{}`", name).unwrap(); + } + + write!(sql, " (").unwrap(); + create.foreign_key.columns.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap(); + } + col.prepare(sql, '`'); + false + }); + write!(sql, ")").unwrap(); + + write!(sql, " REFERENCES ").unwrap(); + if let Some(ref_table) = &create.foreign_key.ref_table { + ref_table.prepare(sql, '`'); + } + write!(sql, " ").unwrap(); + + write!(sql, "(").unwrap(); + create.foreign_key.ref_columns.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap(); + } + col.prepare(sql, '`'); + false + }); + write!(sql, ")").unwrap(); + + if let Some(foreign_key_action) = &create.foreign_key.on_delete { + write!(sql, " ON DELETE ").unwrap(); + self.prepare_foreign_key_action(&foreign_key_action, sql); + } + + if let Some(foreign_key_action) = &create.foreign_key.on_delete { + write!(sql, " ON UPDATE ").unwrap(); + self.prepare_foreign_key_action(&foreign_key_action, sql); + } + } + + fn prepare_foreign_key_action(&mut self, foreign_key_action: &ForeignKeyAction, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match foreign_key_action { + ForeignKeyAction::Restrict => "RESTRICT", + ForeignKeyAction::Cascade => "CASCADE", + ForeignKeyAction::SetNull => "SET NULL", + ForeignKeyAction::NoAction => "NO ACTION", + ForeignKeyAction::SetDefault => "SET DEFAULT", + }).unwrap() + } + + fn prepare_foreign_key_drop_statement(&mut self, drop: &ForeignKeyDropStatement, sql: &mut dyn FmtWrite) { + write!(sql, "ALTER TABLE ").unwrap(); + if let Some(table) = &drop.table { + table.prepare(sql, '`'); + } + + write!(sql, " DROP FOREIGN KEY ").unwrap(); + if let Some(name) = &drop.foreign_key.name { + write!(sql, "`{}`", name).unwrap(); + } + } +} \ No newline at end of file diff --git a/src/backend/mysql/index.rs b/src/backend/mysql/index.rs new file mode 100644 index 000000000..30c080490 --- /dev/null +++ b/src/backend/mysql/index.rs @@ -0,0 +1,37 @@ +use super::*; + +impl IndexBuilder for MysqlQueryBuilder { + fn prepare_index_create_statement(&mut self, create: &IndexCreateStatement, sql: &mut dyn FmtWrite) { + write!(sql, "CREATE INDEX ").unwrap(); + if let Some(name) = &create.index.name { + write!(sql, "`{}`", name).unwrap(); + } + + write!(sql, " ON ").unwrap(); + if let Some(table) = &create.table { + table.prepare(sql, '`'); + } + + write!(sql, " (").unwrap(); + create.index.columns.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap(); + } + col.prepare(sql, '`'); + false + }); + write!(sql, ")").unwrap(); + } + + fn prepare_index_drop_statement(&mut self, drop: &IndexDropStatement, sql: &mut dyn FmtWrite) { + write!(sql, "DROP INDEX ").unwrap(); + if let Some(name) = &drop.index.name { + write!(sql, "`{}`", name).unwrap(); + } + + write!(sql, " ON ").unwrap(); + if let Some(table) = &drop.table { + table.prepare(sql, '`'); + } + } +} \ No newline at end of file diff --git a/src/backend/mysql/mod.rs b/src/backend/mysql/mod.rs new file mode 100644 index 000000000..1730ebafe --- /dev/null +++ b/src/backend/mysql/mod.rs @@ -0,0 +1,21 @@ +pub(crate) mod query; +pub(crate) mod table; +pub(crate) mod index; +pub(crate) mod foreign_key; + +use super::*; + +/// Mysql query builder. +pub struct MysqlQueryBuilder; + +impl Default for MysqlQueryBuilder { + fn default() -> Self { + Self::new() + } +} + +impl MysqlQueryBuilder { + pub fn new() -> Self { + Self + } +} \ No newline at end of file diff --git a/src/backend/mysql/query.rs b/src/backend/mysql/query.rs new file mode 100644 index 000000000..a9467c7fc --- /dev/null +++ b/src/backend/mysql/query.rs @@ -0,0 +1,460 @@ +use super::*; + +impl QueryBuilder for MysqlQueryBuilder { + fn prepare_insert_statement(&mut self, insert: &InsertStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "INSERT").unwrap(); + + if let Some(table) = &insert.table { + write!(sql, " INTO ").unwrap(); + self.prepare_table_ref(table, sql, collector); + write!(sql, " ").unwrap(); + } + + write!(sql, "(").unwrap(); + insert.columns.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap() + } + col.prepare(sql, '`'); + false + }); + write!(sql, ")").unwrap(); + + write!(sql, " VALUES ").unwrap(); + insert.values.iter().fold(true, |first, row| { + if !first { + write!(sql, ", ").unwrap() + } + write!(sql, "(").unwrap(); + row.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap() + } + self.prepare_value(col, sql, collector); + false + }); + write!(sql, ")").unwrap(); + false + }); + } + + fn prepare_select_statement(&mut self, select: &SelectStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "SELECT ").unwrap(); + + if let Some(distinct) = &select.distinct { + write!(sql, " ").unwrap(); + self.prepare_select_distinct(distinct, sql, collector); + write!(sql, " ").unwrap(); + } + + select.selects.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap() + } + self.prepare_select_expr(expr, sql, collector); + false + }); + + if let Some(from) = &select.from { + write!(sql, " FROM ").unwrap(); + self.prepare_table_ref(from, sql, collector); + } + + if !select.join.is_empty() { + for expr in select.join.iter() { + write!(sql, " ").unwrap(); + self.prepare_join_expr(expr, sql, collector); + } + } + + if !select.wherei.is_empty() { + write!(sql, " WHERE ").unwrap(); + } + for (i, log_chain_oper) in select.wherei.iter().enumerate() { + self.prepare_logical_chain_oper(log_chain_oper, i, select.wherei.len(), sql, collector); + } + + if !select.groups.is_empty() { + write!(sql, " GROUP BY ").unwrap(); + select.groups.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap() + } + self.prepare_simple_expr(expr, sql, collector); + false + }); + } + + if !select.having.is_empty() { + write!(sql, " HAVING ").unwrap(); + } + for (i, log_chain_oper) in select.having.iter().enumerate() { + self.prepare_logical_chain_oper(log_chain_oper, i, select.having.len(), sql, collector); + } + + if !select.orders.is_empty() { + write!(sql, " ORDER BY ").unwrap(); + select.orders.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap() + } + self.prepare_order_expr(expr, sql, collector); + false + }); + } + + if let Some(limit) = &select.limit { + write!(sql, " LIMIT ").unwrap(); + self.prepare_value(limit, sql, collector); + } + + if let Some(offset) = &select.offset { + write!(sql, " OFFSET ").unwrap(); + self.prepare_value(offset, sql, collector); + } + } + + fn prepare_update_statement(&mut self, update: &UpdateStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "UPDATE ").unwrap(); + + if let Some(table) = &update.table { + self.prepare_table_ref(table, sql, collector); + } + + write!(sql, " SET ").unwrap(); + + update.values.iter().fold(true, |first, row| { + if !first { + write!(sql, ", ").unwrap() + } + let (k, v) = row; + write!(sql, "`{}` = ", k).unwrap(); + self.prepare_simple_expr(v, sql, collector); + false + }); + + if let Some(wherei) = &update.wherei { + write!(sql, " WHERE ").unwrap(); + self.prepare_simple_expr(wherei, sql, collector); + } + + if !update.orders.is_empty() { + write!(sql, " ORDER BY ").unwrap(); + update.orders.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap(); + } + self.prepare_order_expr(expr, sql, collector); + false + }); + } + + if let Some(limit) = &update.limit { + write!(sql, " LIMIT ").unwrap(); + self.prepare_value(limit, sql, collector); + } + } + + fn prepare_delete_statement(&mut self, delete: &DeleteStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "DELETE ").unwrap(); + + if let Some(table) = &delete.table { + write!(sql, "FROM ").unwrap(); + self.prepare_table_ref(table, sql, collector); + } + + if let Some(wherei) = &delete.wherei { + write!(sql, " WHERE ").unwrap(); + self.prepare_simple_expr(wherei, sql, collector); + } + + if !delete.orders.is_empty() { + write!(sql, " ORDER BY ").unwrap(); + delete.orders.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap(); + } + self.prepare_order_expr(expr, sql, collector); + false + }); + } + + if let Some(limit) = &delete.limit { + write!(sql, " LIMIT ").unwrap(); + self.prepare_value(limit, sql, collector); + } + } + + fn prepare_simple_expr(&mut self, simple_expr: &SimpleExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + match simple_expr { + SimpleExpr::Column(column) => { + column.prepare(sql, '`'); + }, + SimpleExpr::TableColumn(table, column) => { + table.prepare(sql, '`'); + write!(sql, ".").unwrap(); + column.prepare(sql, '`'); + }, + SimpleExpr::Unary(op, expr) => { + self.prepare_un_oper(op, sql, collector); + write!(sql, " ").unwrap(); + self.prepare_simple_expr(expr, sql, collector); + }, + SimpleExpr::FunctionCall(func, exprs) => { + self.prepare_function(func, sql, collector); + write!(sql, "(").unwrap(); + exprs.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap(); + } + self.prepare_simple_expr(expr, sql, collector); + false + }); + write!(sql, ")").unwrap(); + }, + SimpleExpr::Binary(left, op, right) => { + let no_paren = match op { + BinOper::Equal => true, + BinOper::NotEqual => true, + _ => false, + }; + let left_paren = + left.need_parentheses() && + left.is_binary() && *op != left.get_bin_oper().unwrap() && + !no_paren; + if left_paren { + write!(sql, "(").unwrap(); + } + self.prepare_simple_expr(left, sql, collector); + if left_paren { + write!(sql, ")").unwrap(); + } + write!(sql, " ").unwrap(); + self.prepare_bin_oper(op, sql, collector); + write!(sql, " ").unwrap(); + let no_right_paren = match op { + BinOper::Between => true, + BinOper::NotBetween => true, + _ => false, + }; + let right_paren = + (right.need_parentheses() || + right.is_binary() && *op != left.get_bin_oper().unwrap()) && + !no_right_paren && + !no_paren; + if right_paren { + write!(sql, "(").unwrap(); + } + self.prepare_simple_expr(right, sql, collector); + if right_paren { + write!(sql, ")").unwrap(); + } + }, + SimpleExpr::SubQuery(sel) => { + write!(sql, "(").unwrap(); + self.prepare_select_statement(sel, sql, collector); + write!(sql, ")").unwrap(); + }, + SimpleExpr::Value(val) => { + self.prepare_value(val, sql, collector); + }, + SimpleExpr::Values(list) => { + write!(sql, "(").unwrap(); + list.iter().fold(true, |first, val| { + if !first { + write!(sql, ", ").unwrap(); + } + self.prepare_value(val, sql, collector); + false + }); + write!(sql, ")").unwrap(); + }, + SimpleExpr::Custom(s) => { + write!(sql, "{}", s).unwrap(); + }, + } + } + + fn prepare_select_distinct(&mut self, select_distinct: &SelectDistinct, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match select_distinct { + SelectDistinct::All => "ALL", + SelectDistinct::Distinct => "DISTINCT", + SelectDistinct::DistinctRow => "DISTINCTROW", + }).unwrap(); + } + + fn prepare_select_expr(&mut self, select_expr: &SelectExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + self.prepare_simple_expr(&select_expr.expr, sql, collector); + match &select_expr.alias { + Some(alias) => { + write!(sql, " AS ").unwrap(); + alias.prepare(sql, '`'); + }, + None => {}, + } + } + + fn prepare_join_expr(&mut self, join_expr: &JoinExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + self.prepare_join_type(&join_expr.join, sql, collector); + write!(sql, " ").unwrap(); + self.prepare_table_ref(&join_expr.table, sql, collector); + if let Some(on) = &join_expr.on { + write!(sql, " ").unwrap(); + self.prepare_join_on(on, sql, collector); + } + } + + fn prepare_table_ref(&mut self, table_ref: &TableRef, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + match table_ref { + TableRef::Table(iden) => { + iden.prepare(sql, '`'); + }, + TableRef::TableAlias(iden, alias) => { + iden.prepare(sql, '`'); + write!(sql, " AS ").unwrap(); + alias.prepare(sql, '`'); + }, + TableRef::SubQuery(query, alias) => { + write!(sql, "(").unwrap(); + self.prepare_select_statement(query, sql, collector); + write!(sql, ")").unwrap(); + write!(sql, " AS ").unwrap(); + alias.prepare(sql, '`'); + }, + } + } + + fn prepare_un_oper(&mut self, un_oper: &UnOper, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match un_oper { + UnOper::Not => "NOT", + }).unwrap(); + } + + fn prepare_bin_oper(&mut self, bin_oper: &BinOper, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match bin_oper { + BinOper::And => "AND", + BinOper::Or => "OR", + BinOper::Like => "LIKE", + BinOper::NotLike => "NOT LIKE", + BinOper::Is => "IS", + BinOper::IsNot => "IS NOT", + BinOper::In => "IN", + BinOper::NotIn => "NOT IN", + BinOper::Between => "BETWEEN", + BinOper::NotBetween => "NOT BETWEEN", + BinOper::Equal => "=", + BinOper::NotEqual => "<>", + BinOper::SmallerThan => "<", + BinOper::GreaterThan => ">", + BinOper::SmallerThanOrEqual => "<=", + BinOper::GreaterThanOrEqual => ">=", + BinOper::Add => "+", + BinOper::Sub => "-", + BinOper::Mul => "*", + BinOper::Div => "/", + }).unwrap(); + } + + fn prepare_logical_chain_oper(&mut self, log_chain_oper: &LogicalChainOper, i: usize, length: usize, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + let (simple_expr, oper) = match log_chain_oper { + LogicalChainOper::And(simple_expr) => (simple_expr, "AND"), + LogicalChainOper::Or(simple_expr) => (simple_expr, "OR"), + }; + if i > 0 { + write!(sql, " {} ", oper).unwrap(); + } + let both_binary = match simple_expr { + SimpleExpr::Binary(_, _, right) => matches!(right.as_ref(), SimpleExpr::Binary(_, _, _)), + _ => false, + }; + let need_parentheses = length > 1 && both_binary; + if need_parentheses { + write!(sql, "(").unwrap(); + } + self.prepare_simple_expr(simple_expr, sql, collector); + if need_parentheses { + write!(sql, ")").unwrap(); + } + } + + fn prepare_function(&mut self, function: &Function, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match function { + Function::Max => "MAX", + Function::Min => "MIN", + Function::Sum => "SUM", + Function::Count => "COUNT", + Function::IfNull => "IFNULL", + Function::Custom(name) => name.as_ref(), + }).unwrap(); + } + + fn prepare_join_type(&mut self, join_type: &JoinType, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match join_type { + JoinType::Join => "JOIN", + JoinType::InnerJoin => "INNER JOIN", + JoinType::LeftJoin => "LEFT JOIN", + JoinType::RightJoin => "RIGHT JOIN", + }).unwrap() + } + + fn prepare_order_expr(&mut self, order_expr: &OrderExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + self.prepare_simple_expr(&order_expr.expr, sql, collector); + write!(sql, " ").unwrap(); + self.prepare_order(&order_expr.order, sql, collector); + } + + fn prepare_join_on(&mut self, join_on: &JoinOn, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + match join_on { + JoinOn::Condition(c) => { + write!(sql, "ON ").unwrap(); + self.prepare_simple_expr(c, sql, collector); + }, + JoinOn::Columns(_c) => unimplemented!(), + } + } + + fn prepare_order(&mut self, order: &Order, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + match order { + Order::Asc => { + write!(sql, "ASC").unwrap() + }, + Order::Desc => { + write!(sql, "DESC").unwrap() + }, + } + } + + fn prepare_value(&mut self, value: &Value, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "?").unwrap(); + self.prepare_value_param(value, collector); + } + + fn prepare_value_param(&mut self, value: &Value, collector: &mut dyn FnMut(Value)) { + match value { + Value::NULL => { + collector(Value::NULL); + }, + Value::Bytes(v) => { + collector(Value::Bytes(v.to_vec())); + }, + Value::Int(v) => { + collector(Value::Int(*v)); + }, + Value::UInt(v) => { + collector(Value::UInt(*v)); + }, + Value::Float(v) => { + collector(Value::Float(*v)); + }, + Value::Double(v) => { + collector(Value::Double(*v)); + }, + Value::Date(year, month, day, hour, minutes, seconds, micro_seconds) => { + collector(Value::Date(*year, *month, *day, *hour, *minutes, *seconds, *micro_seconds)); + }, + Value::Time(negative, days, hours, minutes, seconds, micro_seconds) => { + collector(Value::Time(*negative, *days, *hours, *minutes, *seconds, *micro_seconds)); + }, + }; + } +} \ No newline at end of file diff --git a/src/backend/mysql/table.rs b/src/backend/mysql/table.rs new file mode 100644 index 000000000..50a9fd356 --- /dev/null +++ b/src/backend/mysql/table.rs @@ -0,0 +1,194 @@ +use super::*; + +impl TableBuilder for MysqlQueryBuilder { + fn prepare_table_create_statement(&mut self, create: &TableCreateStatement, sql: &mut dyn FmtWrite) { + write!(sql, "CREATE TABLE ").unwrap(); + + if create.create_if_not_exists { + write!(sql, "IF NOT EXISTS ").unwrap(); + } + + if let Some(table) = &create.table { + table.prepare(sql, '`'); + } + + write!(sql, " ( ").unwrap(); + let mut count = 0; + + for column_def in create.columns.iter() { + if count > 0 { + write!(sql, ", ").unwrap(); + } + self.prepare_column_def(column_def, sql); + count += 1; + } + + for foreign_key in create.foreign_keys.iter() { + if count > 0 { + write!(sql, ", ").unwrap(); + } + self.prepare_foreign_key_create_statement(foreign_key, sql); + count += 1; + } + + write!(sql, " )").unwrap(); + + for table_opt in create.options.iter() { + write!(sql, " ").unwrap(); + self.prepare_table_opt(table_opt, sql); + } + } + + fn prepare_column_def(&mut self, column_def: &ColumnDef, sql: &mut dyn FmtWrite) { + column_def.name.prepare(sql, '`'); + + if let Some(column_type) = &column_def.types { + write!(sql, " ").unwrap(); + self.prepare_column_type(&column_type, sql); + } + + for column_spec in column_def.spec.iter() { + write!(sql, " ").unwrap(); + self.prepare_column_spec(column_spec, sql); + }; + } + + fn prepare_column_type(&mut self, column_type: &ColumnType, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match column_type { + ColumnType::Char(length) => format!("char({})", length), + ColumnType::CharDefault => "char".into(), + ColumnType::String(length) => format!("varchar({})", length), + ColumnType::StringDefault => "varchar".into(), + ColumnType::Text => "text".into(), + ColumnType::TinyInteger(length) => format!("tinyint({})", length), + ColumnType::TinyIntegerDefault => "tinyint".into(), + ColumnType::SmallInteger(length) => format!("smallint({})", length), + ColumnType::SmallIntegerDefault => "smallint".into(), + ColumnType::Integer(length) => format!("int({})", length), + ColumnType::IntegerDefault => "int".into(), + ColumnType::BigInteger(length) => format!("bigint({})", length), + ColumnType::BigIntegerDefault => "bigint".into(), + ColumnType::Float(precision) => format!("float({})", precision), + ColumnType::FloatDefault => "float".into(), + ColumnType::Double(precision) => format!("double({})", precision), + ColumnType::DoubleDefault => "double".into(), + ColumnType::Decimal(precision, scale) => format!("decimal({}, {})", precision, scale), + ColumnType::DecimalDefault => "decimal".into(), + ColumnType::DateTime(precision) => format!("datetime({})", precision), + ColumnType::DateTimeDefault => "datetime".into(), + ColumnType::Timestamp(precision) => format!("timestamp({})", precision), + ColumnType::TimestampDefault => "timestamp".into(), + ColumnType::Time(precision) => format!("time({})", precision), + ColumnType::TimeDefault => "time".into(), + ColumnType::Date => "date".into(), + ColumnType::Binary(length) => format!("binary({})", length), + ColumnType::BinaryDefault => "binary".into(), + ColumnType::Boolean => "bool".into(), + ColumnType::Money(precision, scale) => format!("money({}, {})", precision, scale), + ColumnType::MoneyDefault => "money".into(), + ColumnType::Json => "json".into(), + }).unwrap() + } + + fn prepare_column_spec(&mut self, column_spec: &ColumnSpec, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match column_spec { + ColumnSpec::Null => "NULL".into(), + ColumnSpec::NotNull => "NOT NULL".into(), + ColumnSpec::Default(value) => format!("DEFAULT {}", value_to_string(value)), + ColumnSpec::AutoIncrement => "AUTO_INCREMENT".into(), + ColumnSpec::UniqueKey => "UNIQUE".into(), + ColumnSpec::PrimaryKey => "PRIMARY KEY".into(), + }).unwrap() + } + + fn prepare_table_opt(&mut self, table_opt: &TableOpt, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match table_opt { + TableOpt::Engine(s) => format!("ENGINE={}", s), + TableOpt::Collate(s) => format!("COLLATE={}", s), + TableOpt::CharacterSet(s) => format!("DEFAULT CHARSET={}", s), + }).unwrap() + } + + fn prepare_table_partition(&mut self, _table_partition: &TablePartition, _sql: &mut dyn FmtWrite) { + + } + + fn prepare_table_drop_statement(&mut self, drop: &TableDropStatement, sql: &mut dyn FmtWrite) { + write!(sql, "DROP TABLE ").unwrap(); + + if drop.if_exist { + write!(sql, "IF EXIST ").unwrap(); + } + + drop.tables.iter().fold(true, |first, table| { + if !first { + write!(sql, ", ").unwrap(); + } + table.prepare(sql, '`'); + false + }); + + for drop_opt in drop.options.iter() { + write!(sql, " ").unwrap(); + self.prepare_table_drop_opt(drop_opt, sql); + } + } + + fn prepare_table_drop_opt(&mut self, drop_opt: &TableDropOpt, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match drop_opt { + TableDropOpt::Restrict => "RESTRICT", + TableDropOpt::Cascade => "CASCADE", + }).unwrap(); + } + + fn prepare_table_truncate_statement(&mut self, truncate: &TableTruncateStatement, sql: &mut dyn FmtWrite) { + write!(sql, "TRUNCATE TABLE ").unwrap(); + + if let Some(table) = &truncate.table { + table.prepare(sql, '`'); + } + } + + fn prepare_table_alter_statement(&mut self, alter: &TableAlterStatement, sql: &mut dyn FmtWrite) { + let alter_option = match &alter.alter_option { + Some(alter_option) => alter_option, + None => panic!("No alter option found"), + }; + write!(sql, "ALTER TABLE ").unwrap(); + if let Some(table) = &alter.table { + table.prepare(sql, '`'); + write!(sql, " ").unwrap(); + } + match alter_option { + TableAlterOption::AddColumn(column_def) => { + write!(sql, "ADD COLUMN ").unwrap(); + self.prepare_column_def(column_def, sql); + }, + TableAlterOption::ModifyColumn(column_def) => { + write!(sql, "MODIFY COLUMN ").unwrap(); + self.prepare_column_def(column_def, sql); + }, + TableAlterOption::RenameColumn(from_name, to_name) => { + write!(sql, "RENAME COLUMN ").unwrap(); + from_name.prepare(sql, '`'); + write!(sql, " TO ").unwrap(); + to_name.prepare(sql, '`'); + }, + TableAlterOption::DropColumn(column_name) => { + write!(sql, "DROP COLUMN ").unwrap(); + column_name.prepare(sql, '`'); + }, + } + } + + fn prepare_table_rename_statement(&mut self, rename: &TableRenameStatement, sql: &mut dyn FmtWrite) { + write!(sql, "RENAME TABLE ").unwrap(); + if let Some(from_name) = &rename.from_name { + from_name.prepare(sql, '`'); + } + write!(sql, " TO ").unwrap(); + if let Some(to_name) = &rename.to_name { + to_name.prepare(sql, '`'); + } + } +} \ No newline at end of file diff --git a/src/backend/postgres/foreign_key.rs b/src/backend/postgres/foreign_key.rs new file mode 100644 index 000000000..1360b049d --- /dev/null +++ b/src/backend/postgres/foreign_key.rs @@ -0,0 +1,76 @@ +use super::*; + +impl ForeignKeyBuilder for PostgresQueryBuilder { + fn prepare_foreign_key_create_statement(&mut self, create: &ForeignKeyCreateStatement, sql: &mut dyn FmtWrite) { + if !create.inside_table_creation { + write!(sql, "ALTER TABLE ").unwrap(); + if let Some(table) = &create.foreign_key.table { + table.prepare(sql, '"'); + } + write!(sql, " ADD ").unwrap(); + } + + write!(sql, "CONSTRAINT ").unwrap(); + if let Some(name) = &create.foreign_key.name { + write!(sql, "\"{}\" ", name).unwrap(); + } + + write!(sql, "FOREIGN KEY (").unwrap(); + create.foreign_key.columns.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap(); + } + col.prepare(sql, '"'); + false + }); + write!(sql, ")").unwrap(); + + write!(sql, " REFERENCES ").unwrap(); + if let Some(ref_table) = &create.foreign_key.ref_table { + ref_table.prepare(sql, '"'); + } + write!(sql, " ").unwrap(); + + write!(sql, "(").unwrap(); + create.foreign_key.ref_columns.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap(); + } + col.prepare(sql, '"'); + false + }); + write!(sql, ")").unwrap(); + + if let Some(foreign_key_action) = &create.foreign_key.on_delete { + write!(sql, " ON DELETE ").unwrap(); + self.prepare_foreign_key_action(&foreign_key_action, sql); + } + + if let Some(foreign_key_action) = &create.foreign_key.on_delete { + write!(sql, " ON UPDATE ").unwrap(); + self.prepare_foreign_key_action(&foreign_key_action, sql); + } + } + + fn prepare_foreign_key_action(&mut self, foreign_key_action: &ForeignKeyAction, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match foreign_key_action { + ForeignKeyAction::Restrict => "RESTRICT", + ForeignKeyAction::Cascade => "CASCADE", + ForeignKeyAction::SetNull => "SET NULL", + ForeignKeyAction::NoAction => "NO ACTION", + ForeignKeyAction::SetDefault => "SET DEFAULT", + }).unwrap() + } + + fn prepare_foreign_key_drop_statement(&mut self, drop: &ForeignKeyDropStatement, sql: &mut dyn FmtWrite) { + write!(sql, "ALTER TABLE ").unwrap(); + if let Some(table) = &drop.table { + table.prepare(sql, '"'); + } + + write!(sql, " DROP CONSTRAINT ").unwrap(); + if let Some(name) = &drop.foreign_key.name { + write!(sql, "\"{}\"", name).unwrap(); + } + } +} \ No newline at end of file diff --git a/src/backend/postgres/index.rs b/src/backend/postgres/index.rs new file mode 100644 index 000000000..627067f04 --- /dev/null +++ b/src/backend/postgres/index.rs @@ -0,0 +1,32 @@ +use super::*; + +impl IndexBuilder for PostgresQueryBuilder { + fn prepare_index_create_statement(&mut self, create: &IndexCreateStatement, sql: &mut dyn FmtWrite) { + write!(sql, "CREATE INDEX ").unwrap(); + if let Some(name) = &create.index.name { + write!(sql, "\"{}\" ", name).unwrap(); + } + + write!(sql, "ON ").unwrap(); + if let Some(table) = &create.table { + table.prepare(sql, '"'); + } + + write!(sql, " (").unwrap(); + create.index.columns.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap(); + } + col.prepare(sql, '"'); + false + }); + write!(sql, ")").unwrap(); + } + + fn prepare_index_drop_statement(&mut self, drop: &IndexDropStatement, sql: &mut dyn FmtWrite) { + write!(sql, "DROP INDEX ").unwrap(); + if let Some(name) = &drop.index.name { + write!(sql, "\"{}\"", name).unwrap(); + } + } +} \ No newline at end of file diff --git a/src/backend/postgres/mod.rs b/src/backend/postgres/mod.rs new file mode 100644 index 000000000..10b04b49b --- /dev/null +++ b/src/backend/postgres/mod.rs @@ -0,0 +1,21 @@ +pub(crate) mod query; +pub(crate) mod table; +pub(crate) mod index; +pub(crate) mod foreign_key; + +use super::*; + +/// Postgres query builder. +pub struct PostgresQueryBuilder; + +impl Default for PostgresQueryBuilder { + fn default() -> Self { + Self::new() + } +} + +impl PostgresQueryBuilder { + pub fn new() -> Self { + Self + } +} \ No newline at end of file diff --git a/src/backend/postgres/query.rs b/src/backend/postgres/query.rs new file mode 100644 index 000000000..772a64909 --- /dev/null +++ b/src/backend/postgres/query.rs @@ -0,0 +1,460 @@ +use super::*; + +impl QueryBuilder for PostgresQueryBuilder { + fn prepare_insert_statement(&mut self, insert: &InsertStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "INSERT").unwrap(); + + if let Some(table) = &insert.table { + write!(sql, " INTO ").unwrap(); + self.prepare_table_ref(table, sql, collector); + write!(sql, " ").unwrap(); + } + + write!(sql, "(").unwrap(); + insert.columns.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap() + } + col.prepare(sql, '"'); + false + }); + write!(sql, ")").unwrap(); + + write!(sql, " VALUES ").unwrap(); + insert.values.iter().fold(true, |first, row| { + if !first { + write!(sql, ", ").unwrap() + } + write!(sql, "(").unwrap(); + row.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap() + } + self.prepare_value(col, sql, collector); + false + }); + write!(sql, ")").unwrap(); + false + }); + } + + fn prepare_select_statement(&mut self, select: &SelectStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "SELECT ").unwrap(); + + if let Some(distinct) = &select.distinct { + write!(sql, " ").unwrap(); + self.prepare_select_distinct(distinct, sql, collector); + write!(sql, " ").unwrap(); + } + + select.selects.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap() + } + self.prepare_select_expr(expr, sql, collector); + false + }); + + if let Some(from) = &select.from { + write!(sql, " FROM ").unwrap(); + self.prepare_table_ref(from, sql, collector); + } + + if !select.join.is_empty() { + for expr in select.join.iter() { + write!(sql, " ").unwrap(); + self.prepare_join_expr(expr, sql, collector); + } + } + + if !select.wherei.is_empty() { + write!(sql, " WHERE ").unwrap(); + } + for (i, log_chain_oper) in select.wherei.iter().enumerate() { + self.prepare_logical_chain_oper(log_chain_oper, i, select.wherei.len(), sql, collector); + } + + if !select.groups.is_empty() { + write!(sql, " GROUP BY ").unwrap(); + select.groups.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap() + } + self.prepare_simple_expr(expr, sql, collector); + false + }); + } + + if !select.having.is_empty() { + write!(sql, " HAVING ").unwrap(); + } + for (i, log_chain_oper) in select.having.iter().enumerate() { + self.prepare_logical_chain_oper(log_chain_oper, i, select.having.len(), sql, collector); + } + + if !select.orders.is_empty() { + write!(sql, " ORDER BY ").unwrap(); + select.orders.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap() + } + self.prepare_order_expr(expr, sql, collector); + false + }); + } + + if let Some(limit) = &select.limit { + write!(sql, " LIMIT ").unwrap(); + self.prepare_value(limit, sql, collector); + } + + if let Some(offset) = &select.offset { + write!(sql, " OFFSET ").unwrap(); + self.prepare_value(offset, sql, collector); + } + } + + fn prepare_update_statement(&mut self, update: &UpdateStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "UPDATE ").unwrap(); + + if let Some(table) = &update.table { + self.prepare_table_ref(table, sql, collector); + } + + write!(sql, " SET ").unwrap(); + + update.values.iter().fold(true, |first, row| { + if !first { + write!(sql, ", ").unwrap() + } + let (k, v) = row; + write!(sql, "\"{}\" = ", k).unwrap(); + self.prepare_simple_expr(v, sql, collector); + false + }); + + if let Some(wherei) = &update.wherei { + write!(sql, " WHERE ").unwrap(); + self.prepare_simple_expr(wherei, sql, collector); + } + + if !update.orders.is_empty() { + write!(sql, " ORDER BY ").unwrap(); + update.orders.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap(); + } + self.prepare_order_expr(expr, sql, collector); + false + }); + } + + if let Some(limit) = &update.limit { + write!(sql, " LIMIT ").unwrap(); + self.prepare_value(limit, sql, collector); + } + } + + fn prepare_delete_statement(&mut self, delete: &DeleteStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "DELETE ").unwrap(); + + if let Some(table) = &delete.table { + write!(sql, "FROM ").unwrap(); + self.prepare_table_ref(table, sql, collector); + } + + if let Some(wherei) = &delete.wherei { + write!(sql, " WHERE ").unwrap(); + self.prepare_simple_expr(wherei, sql, collector); + } + + if !delete.orders.is_empty() { + write!(sql, " ORDER BY ").unwrap(); + delete.orders.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap(); + } + self.prepare_order_expr(expr, sql, collector); + false + }); + } + + if let Some(limit) = &delete.limit { + write!(sql, " LIMIT ").unwrap(); + self.prepare_value(limit, sql, collector); + } + } + + fn prepare_simple_expr(&mut self, simple_expr: &SimpleExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + match simple_expr { + SimpleExpr::Column(column) => { + column.prepare(sql, '"'); + }, + SimpleExpr::TableColumn(table, column) => { + table.prepare(sql, '"'); + write!(sql, ".").unwrap(); + column.prepare(sql, '"'); + }, + SimpleExpr::Unary(op, expr) => { + self.prepare_un_oper(op, sql, collector); + write!(sql, " ").unwrap(); + self.prepare_simple_expr(expr, sql, collector); + }, + SimpleExpr::FunctionCall(func, exprs) => { + self.prepare_function(func, sql, collector); + write!(sql, "(").unwrap(); + exprs.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap(); + } + self.prepare_simple_expr(expr, sql, collector); + false + }); + write!(sql, ")").unwrap(); + }, + SimpleExpr::Binary(left, op, right) => { + let no_paren = match op { + BinOper::Equal => true, + BinOper::NotEqual => true, + _ => false, + }; + let left_paren = + left.need_parentheses() && + left.is_binary() && *op != left.get_bin_oper().unwrap() && + !no_paren; + if left_paren { + write!(sql, "(").unwrap(); + } + self.prepare_simple_expr(left, sql, collector); + if left_paren { + write!(sql, ")").unwrap(); + } + write!(sql, " ").unwrap(); + self.prepare_bin_oper(op, sql, collector); + write!(sql, " ").unwrap(); + let no_right_paren = match op { + BinOper::Between => true, + BinOper::NotBetween => true, + _ => false, + }; + let right_paren = + (right.need_parentheses() || + right.is_binary() && *op != left.get_bin_oper().unwrap()) && + !no_right_paren && + !no_paren; + if right_paren { + write!(sql, "(").unwrap(); + } + self.prepare_simple_expr(right, sql, collector); + if right_paren { + write!(sql, ")").unwrap(); + } + }, + SimpleExpr::SubQuery(sel) => { + write!(sql, "(").unwrap(); + self.prepare_select_statement(sel, sql, collector); + write!(sql, ")").unwrap(); + }, + SimpleExpr::Value(val) => { + self.prepare_value(val, sql, collector); + }, + SimpleExpr::Values(list) => { + write!(sql, "(").unwrap(); + list.iter().fold(true, |first, val| { + if !first { + write!(sql, ", ").unwrap(); + } + self.prepare_value(val, sql, collector); + false + }); + write!(sql, ")").unwrap(); + }, + SimpleExpr::Custom(s) => { + write!(sql, "{}", s).unwrap(); + }, + } + } + + fn prepare_select_distinct(&mut self, select_distinct: &SelectDistinct, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match select_distinct { + SelectDistinct::All => "ALL", + SelectDistinct::Distinct => "DISTINCT", + SelectDistinct::DistinctRow => "DISTINCTROW", + }).unwrap(); + } + + fn prepare_select_expr(&mut self, select_expr: &SelectExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + self.prepare_simple_expr(&select_expr.expr, sql, collector); + match &select_expr.alias { + Some(alias) => { + write!(sql, " AS ").unwrap(); + alias.prepare(sql, '"'); + }, + None => {}, + } + } + + fn prepare_join_expr(&mut self, join_expr: &JoinExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + self.prepare_join_type(&join_expr.join, sql, collector); + write!(sql, " ").unwrap(); + self.prepare_table_ref(&join_expr.table, sql, collector); + if let Some(on) = &join_expr.on { + write!(sql, " ").unwrap(); + self.prepare_join_on(on, sql, collector); + } + } + + fn prepare_table_ref(&mut self, table_ref: &TableRef, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + match table_ref { + TableRef::Table(iden) => { + iden.prepare(sql, '"'); + }, + TableRef::TableAlias(iden, alias) => { + iden.prepare(sql, '"'); + write!(sql, " AS ").unwrap(); + alias.prepare(sql, '"'); + }, + TableRef::SubQuery(query, alias) => { + write!(sql, "(").unwrap(); + self.prepare_select_statement(query, sql, collector); + write!(sql, ")").unwrap(); + write!(sql, " AS ").unwrap(); + alias.prepare(sql, '"'); + }, + } + } + + fn prepare_un_oper(&mut self, un_oper: &UnOper, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match un_oper { + UnOper::Not => "NOT", + }).unwrap(); + } + + fn prepare_bin_oper(&mut self, bin_oper: &BinOper, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match bin_oper { + BinOper::And => "AND", + BinOper::Or => "OR", + BinOper::Like => "LIKE", + BinOper::NotLike => "NOT LIKE", + BinOper::Is => "IS", + BinOper::IsNot => "IS NOT", + BinOper::In => "IN", + BinOper::NotIn => "NOT IN", + BinOper::Between => "BETWEEN", + BinOper::NotBetween => "NOT BETWEEN", + BinOper::Equal => "=", + BinOper::NotEqual => "<>", + BinOper::SmallerThan => "<", + BinOper::GreaterThan => ">", + BinOper::SmallerThanOrEqual => "<=", + BinOper::GreaterThanOrEqual => ">=", + BinOper::Add => "+", + BinOper::Sub => "-", + BinOper::Mul => "*", + BinOper::Div => "/", + }).unwrap(); + } + + fn prepare_logical_chain_oper(&mut self, log_chain_oper: &LogicalChainOper, i: usize, length: usize, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + let (simple_expr, oper) = match log_chain_oper { + LogicalChainOper::And(simple_expr) => (simple_expr, "AND"), + LogicalChainOper::Or(simple_expr) => (simple_expr, "OR"), + }; + if i > 0 { + write!(sql, " {} ", oper).unwrap(); + } + let both_binary = match simple_expr { + SimpleExpr::Binary(_, _, right) => matches!(right.as_ref(), SimpleExpr::Binary(_, _, _)), + _ => false, + }; + let need_parentheses = length > 1 && both_binary; + if need_parentheses { + write!(sql, "(").unwrap(); + } + self.prepare_simple_expr(simple_expr, sql, collector); + if need_parentheses { + write!(sql, ")").unwrap(); + } + } + + fn prepare_function(&mut self, function: &Function, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match function { + Function::Max => "MAX", + Function::Min => "MIN", + Function::Sum => "SUM", + Function::Count => "COUNT", + Function::IfNull => "COALESCE", + Function::Custom(name) => name.as_ref(), + }).unwrap(); + } + + fn prepare_join_type(&mut self, join_type: &JoinType, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match join_type { + JoinType::Join => "JOIN", + JoinType::InnerJoin => "INNER JOIN", + JoinType::LeftJoin => "LEFT JOIN", + JoinType::RightJoin => "RIGHT JOIN", + }).unwrap() + } + + fn prepare_order_expr(&mut self, order_expr: &OrderExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + self.prepare_simple_expr(&order_expr.expr, sql, collector); + write!(sql, " ").unwrap(); + self.prepare_order(&order_expr.order, sql, collector); + } + + fn prepare_join_on(&mut self, join_on: &JoinOn, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + match join_on { + JoinOn::Condition(c) => { + write!(sql, "ON ").unwrap(); + self.prepare_simple_expr(c, sql, collector); + }, + JoinOn::Columns(_c) => unimplemented!(), + } + } + + fn prepare_order(&mut self, order: &Order, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + match order { + Order::Asc => { + write!(sql, "ASC").unwrap() + }, + Order::Desc => { + write!(sql, "DESC").unwrap() + }, + } + } + + fn prepare_value(&mut self, value: &Value, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "?").unwrap(); + self.prepare_value_param(value, collector); + } + + fn prepare_value_param(&mut self, value: &Value, collector: &mut dyn FnMut(Value)) { + match value { + Value::NULL => { + collector(Value::NULL); + }, + Value::Bytes(v) => { + collector(Value::Bytes(v.to_vec())); + }, + Value::Int(v) => { + collector(Value::Int(*v)); + }, + Value::UInt(v) => { + collector(Value::UInt(*v)); + }, + Value::Float(v) => { + collector(Value::Float(*v)); + }, + Value::Double(v) => { + collector(Value::Double(*v)); + }, + Value::Date(year, month, day, hour, minutes, seconds, micro_seconds) => { + collector(Value::Date(*year, *month, *day, *hour, *minutes, *seconds, *micro_seconds)); + }, + Value::Time(negative, days, hours, minutes, seconds, micro_seconds) => { + collector(Value::Time(*negative, *days, *hours, *minutes, *seconds, *micro_seconds)); + }, + }; + } +} \ No newline at end of file diff --git a/src/backend/postgres/table.rs b/src/backend/postgres/table.rs new file mode 100644 index 000000000..d0c06d6da --- /dev/null +++ b/src/backend/postgres/table.rs @@ -0,0 +1,227 @@ +use super::*; + +impl TableBuilder for PostgresQueryBuilder { + fn prepare_table_create_statement(&mut self, create: &TableCreateStatement, sql: &mut dyn FmtWrite) { + write!(sql, "CREATE TABLE ").unwrap(); + + if create.create_if_not_exists { + write!(sql, "IF NOT EXISTS ").unwrap(); + } + + if let Some(table) = &create.table { + table.prepare(sql, '"'); + } + + write!(sql, " ( ").unwrap(); + let mut count = 0; + + for column_def in create.columns.iter() { + if count > 0 { + write!(sql, ", ").unwrap(); + } + self.prepare_column_def(column_def, sql); + count += 1; + } + + for foreign_key in create.foreign_keys.iter() { + if count > 0 { + write!(sql, ", ").unwrap(); + } + self.prepare_foreign_key_create_statement(foreign_key, sql); + count += 1; + } + + write!(sql, " )").unwrap(); + + for table_opt in create.options.iter() { + write!(sql, " ").unwrap(); + self.prepare_table_opt(table_opt, sql); + } + } + + fn prepare_column_def(&mut self, column_def: &ColumnDef, sql: &mut dyn FmtWrite) { + column_def.name.prepare(sql, '"'); + self.prepare_column_type_check_auto_increment(column_def, sql); + + for column_spec in column_def.spec.iter() { + if let ColumnSpec::AutoIncrement = column_spec { + continue; + } + write!(sql, " ").unwrap(); + self.prepare_column_spec(column_spec, sql); + } + } + + fn prepare_column_type(&mut self, column_type: &ColumnType, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match column_type { + ColumnType::Char(length) => format!("char({})", length), + ColumnType::CharDefault => "char".into(), + ColumnType::String(length) => format!("varchar({})", length), + ColumnType::StringDefault => "varchar".into(), + ColumnType::Text => "text".into(), + ColumnType::TinyInteger(length) => format!("tinyint({})", length), + ColumnType::TinyIntegerDefault => "tinyint".into(), + ColumnType::SmallInteger(length) => format!("smallint({})", length), + ColumnType::SmallIntegerDefault => "smallint".into(), + ColumnType::Integer(length) => format!("integer({})", length), + ColumnType::IntegerDefault => "integer".into(), + ColumnType::BigInteger(length) => format!("bigint({})", length), + ColumnType::BigIntegerDefault => "bigint".into(), + ColumnType::Float(precision) => format!("real({})", precision), + ColumnType::FloatDefault => "real".into(), + ColumnType::Double(precision) => format!("double precision({})", precision), + ColumnType::DoubleDefault => "double precision".into(), + ColumnType::Decimal(precision, scale) => format!("decimal({}, {})", precision, scale), + ColumnType::DecimalDefault => "decimal".into(), + ColumnType::DateTime(precision) => format!("datetime({})", precision), + ColumnType::DateTimeDefault => "datetime".into(), + ColumnType::Timestamp(precision) => format!("timestamp({})", precision), + ColumnType::TimestampDefault => "timestamp".into(), + ColumnType::Time(precision) => format!("time({})", precision), + ColumnType::TimeDefault => "time".into(), + ColumnType::Date => "date".into(), + ColumnType::Binary(length) => format!("binary({})", length), + ColumnType::BinaryDefault => "binary".into(), + ColumnType::Boolean => "bool".into(), + ColumnType::Money(precision, scale) => format!("money({}, {})", precision, scale), + ColumnType::MoneyDefault => "money".into(), + ColumnType::Json => "json".into(), + }).unwrap() + } + + fn prepare_column_spec(&mut self, column_spec: &ColumnSpec, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match column_spec { + ColumnSpec::Null => "NULL".into(), + ColumnSpec::NotNull => "NOT NULL".into(), + ColumnSpec::Default(value) => format!("DEFAULT {}", value_to_string(value)), + ColumnSpec::AutoIncrement => "".into(), + ColumnSpec::UniqueKey => "UNIQUE".into(), + ColumnSpec::PrimaryKey => "PRIMARY KEY".into(), + }).unwrap() + } + + fn prepare_table_opt(&mut self, table_opt: &TableOpt, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match table_opt { + TableOpt::Engine(s) => format!("ENGINE={}", s), + TableOpt::Collate(s) => format!("COLLATE={}", s), + TableOpt::CharacterSet(s) => format!("DEFAULT CHARSET={}", s), + }).unwrap() + } + + fn prepare_table_partition(&mut self, _table_partition: &TablePartition, _sql: &mut dyn FmtWrite) { + + } + + fn prepare_table_drop_statement(&mut self, drop: &TableDropStatement, sql: &mut dyn FmtWrite) { + write!(sql, "DROP TABLE ").unwrap(); + + if drop.if_exist { + write!(sql, "IF EXIST ").unwrap(); + } + + drop.tables.iter().fold(true, |first, table| { + if !first { + write!(sql, ", ").unwrap(); + } + table.prepare(sql, '"'); + false + }); + + for drop_opt in drop.options.iter() { + write!(sql, " ").unwrap(); + self.prepare_table_drop_opt(drop_opt, sql); + } + } + + fn prepare_table_drop_opt(&mut self, drop_opt: &TableDropOpt, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match drop_opt { + TableDropOpt::Restrict => "RESTRICT", + TableDropOpt::Cascade => "CASCADE", + }).unwrap(); + } + + fn prepare_table_truncate_statement(&mut self, truncate: &TableTruncateStatement, sql: &mut dyn FmtWrite) { + write!(sql, "TRUNCATE TABLE ").unwrap(); + + if let Some(table) = &truncate.table { + table.prepare(sql, '"'); + } + } + + fn prepare_table_alter_statement(&mut self, alter: &TableAlterStatement, sql: &mut dyn FmtWrite) { + let alter_option = match &alter.alter_option { + Some(alter_option) => alter_option, + None => panic!("No alter option found"), + }; + write!(sql, "ALTER TABLE ").unwrap(); + if let Some(table) = &alter.table { + table.prepare(sql, '"'); + write!(sql, " ").unwrap(); + } + match alter_option { + TableAlterOption::AddColumn(column_def) => { + write!(sql, "ADD COLUMN ").unwrap(); + self.prepare_column_def(column_def, sql); + }, + TableAlterOption::ModifyColumn(column_def) => { + write!(sql, "ALTER COLUMN ").unwrap(); + column_def.name.prepare(sql, '"'); + write!(sql, " TYPE").unwrap(); + self.prepare_column_type_check_auto_increment(column_def, sql); + for column_spec in column_def.spec.iter() { + if let ColumnSpec::AutoIncrement = column_spec { + continue; + } + write!(sql, ", ").unwrap(); + write!(sql, "ALTER COLUMN ").unwrap(); + column_def.name.prepare(sql, '"'); + write!(sql, " SET ").unwrap(); + self.prepare_column_spec(column_spec, sql); + } + }, + TableAlterOption::RenameColumn(from_name, to_name) => { + write!(sql, "RENAME COLUMN ").unwrap(); + from_name.prepare(sql, '"'); + write!(sql, " TO ").unwrap(); + to_name.prepare(sql, '"'); + }, + TableAlterOption::DropColumn(column_name) => { + write!(sql, "DROP COLUMN ").unwrap(); + column_name.prepare(sql, '"'); + }, + } + } + + fn prepare_table_rename_statement(&mut self, rename: &TableRenameStatement, sql: &mut dyn FmtWrite) { + write!(sql, "ALTER TABLE ").unwrap(); + if let Some(from_name) = &rename.from_name { + from_name.prepare(sql, '"'); + } + write!(sql, " RENAME TO ").unwrap(); + if let Some(to_name) = &rename.to_name { + to_name.prepare(sql, '"'); + } + } +} + +impl PostgresQueryBuilder { + fn prepare_column_type_check_auto_increment(&mut self, column_def: &ColumnDef, sql: &mut dyn FmtWrite) { + if let Some(column_type) = &column_def.types { + write!(sql, " ").unwrap(); + let is_auto_increment = column_def.spec.iter().position(|s| matches!(s, ColumnSpec::AutoIncrement)); + if is_auto_increment.is_some() { + match &column_type { + ColumnType::SmallInteger(_) => write!(sql, "smallserial").unwrap(), + ColumnType::SmallIntegerDefault => write!(sql, "smallserial").unwrap(), + ColumnType::Integer(_) => write!(sql, "serial").unwrap(), + ColumnType::IntegerDefault => write!(sql, "serial").unwrap(), + ColumnType::BigInteger(_) => write!(sql, "bigserial").unwrap(), + ColumnType::BigIntegerDefault => write!(sql, "bigserial").unwrap(), + _ => unimplemented!(), + } + } else { + self.prepare_column_type(&column_type, sql); + } + } + } +} \ No newline at end of file diff --git a/src/backend/sqlite/foreign_key.rs b/src/backend/sqlite/foreign_key.rs new file mode 100644 index 000000000..a201b19cf --- /dev/null +++ b/src/backend/sqlite/foreign_key.rs @@ -0,0 +1,65 @@ +use super::*; + +impl ForeignKeyBuilder for SqliteQueryBuilder { + fn prepare_foreign_key_create_statement(&mut self, create: &ForeignKeyCreateStatement, sql: &mut dyn FmtWrite) { + if !create.inside_table_creation { + unimplemented!() + } + + write!(sql, "FOREIGN KEY (").unwrap(); + create.foreign_key.columns.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap(); + } + col.prepare(sql, '`'); + false + }); + write!(sql, ")").unwrap(); + + write!(sql, " REFERENCES ").unwrap(); + if let Some(ref_table) = &create.foreign_key.ref_table { + ref_table.prepare(sql, '`'); + } + write!(sql, " (").unwrap(); + create.foreign_key.ref_columns.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap(); + } + col.prepare(sql, '`'); + false + }); + write!(sql, ")").unwrap(); + + if let Some(foreign_key_action) = &create.foreign_key.on_delete { + write!(sql, " ON DELETE ").unwrap(); + self.prepare_foreign_key_action(&foreign_key_action, sql); + } + + if let Some(foreign_key_action) = &create.foreign_key.on_delete { + write!(sql, " ON UPDATE ").unwrap(); + self.prepare_foreign_key_action(&foreign_key_action, sql); + } + } + + fn prepare_foreign_key_action(&mut self, foreign_key_action: &ForeignKeyAction, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match foreign_key_action { + ForeignKeyAction::Restrict => "RESTRICT", + ForeignKeyAction::Cascade => "CASCADE", + ForeignKeyAction::SetNull => "SET NULL", + ForeignKeyAction::NoAction => "NO ACTION", + ForeignKeyAction::SetDefault => "SET DEFAULT", + }).unwrap() + } + + fn prepare_foreign_key_drop_statement(&mut self, drop: &ForeignKeyDropStatement, sql: &mut dyn FmtWrite) { + write!(sql, "ALTER TABLE ").unwrap(); + if let Some(table) = &drop.table { + table.prepare(sql, '`'); + } + + write!(sql, " DROP FOREIGN KEY ").unwrap(); + if let Some(name) = &drop.foreign_key.name { + write!(sql, "`{}`", name).unwrap(); + } + } +} \ No newline at end of file diff --git a/src/backend/sqlite/index.rs b/src/backend/sqlite/index.rs new file mode 100644 index 000000000..399a8de0a --- /dev/null +++ b/src/backend/sqlite/index.rs @@ -0,0 +1,37 @@ +use super::*; + +impl IndexBuilder for SqliteQueryBuilder { + fn prepare_index_create_statement(&mut self, create: &IndexCreateStatement, sql: &mut dyn FmtWrite) { + write!(sql, "CREATE INDEX ").unwrap(); + if let Some(name) = &create.index.name { + write!(sql, "`{}`", name).unwrap(); + } + + write!(sql, " ON ").unwrap(); + if let Some(table) = &create.table { + table.prepare(sql, '`'); + } + + write!(sql, " (").unwrap(); + create.index.columns.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap(); + } + col.prepare(sql, '`'); + false + }); + write!(sql, ")").unwrap(); + } + + fn prepare_index_drop_statement(&mut self, drop: &IndexDropStatement, sql: &mut dyn FmtWrite) { + write!(sql, "DROP INDEX ").unwrap(); + if let Some(name) = &drop.index.name { + write!(sql, "`{}`", name).unwrap(); + } + + write!(sql, " ON ").unwrap(); + if let Some(table) = &drop.table { + table.prepare(sql, '`'); + } + } +} \ No newline at end of file diff --git a/src/backend/sqlite/mod.rs b/src/backend/sqlite/mod.rs new file mode 100644 index 000000000..e6d710a82 --- /dev/null +++ b/src/backend/sqlite/mod.rs @@ -0,0 +1,21 @@ +pub(crate) mod query; +pub(crate) mod table; +pub(crate) mod index; +pub(crate) mod foreign_key; + +use super::*; + +/// Sqlite query builder. +pub struct SqliteQueryBuilder; + +impl Default for SqliteQueryBuilder { + fn default() -> Self { + Self::new() + } +} + +impl SqliteQueryBuilder { + pub fn new() -> Self { + Self + } +} \ No newline at end of file diff --git a/src/backend/sqlite/query.rs b/src/backend/sqlite/query.rs new file mode 100644 index 000000000..09403b838 --- /dev/null +++ b/src/backend/sqlite/query.rs @@ -0,0 +1,460 @@ +use super::*; + +impl QueryBuilder for SqliteQueryBuilder { + fn prepare_insert_statement(&mut self, insert: &InsertStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "INSERT").unwrap(); + + if let Some(table) = &insert.table { + write!(sql, " INTO ").unwrap(); + self.prepare_table_ref(table, sql, collector); + write!(sql, " ").unwrap(); + } + + write!(sql, "(").unwrap(); + insert.columns.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap() + } + col.prepare(sql, '`'); + false + }); + write!(sql, ")").unwrap(); + + write!(sql, " VALUES ").unwrap(); + insert.values.iter().fold(true, |first, row| { + if !first { + write!(sql, ", ").unwrap() + } + write!(sql, "(").unwrap(); + row.iter().fold(true, |first, col| { + if !first { + write!(sql, ", ").unwrap() + } + self.prepare_value(col, sql, collector); + false + }); + write!(sql, ")").unwrap(); + false + }); + } + + fn prepare_select_statement(&mut self, select: &SelectStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "SELECT ").unwrap(); + + if let Some(distinct) = &select.distinct { + write!(sql, " ").unwrap(); + self.prepare_select_distinct(distinct, sql, collector); + write!(sql, " ").unwrap(); + } + + select.selects.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap() + } + self.prepare_select_expr(expr, sql, collector); + false + }); + + if let Some(from) = &select.from { + write!(sql, " FROM ").unwrap(); + self.prepare_table_ref(from, sql, collector); + } + + if !select.join.is_empty() { + for expr in select.join.iter() { + write!(sql, " ").unwrap(); + self.prepare_join_expr(expr, sql, collector); + } + } + + if !select.wherei.is_empty() { + write!(sql, " WHERE ").unwrap(); + } + for (i, log_chain_oper) in select.wherei.iter().enumerate() { + self.prepare_logical_chain_oper(log_chain_oper, i, select.wherei.len(), sql, collector); + } + + if !select.groups.is_empty() { + write!(sql, " GROUP BY ").unwrap(); + select.groups.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap() + } + self.prepare_simple_expr(expr, sql, collector); + false + }); + } + + if !select.having.is_empty() { + write!(sql, " HAVING ").unwrap(); + } + for (i, log_chain_oper) in select.having.iter().enumerate() { + self.prepare_logical_chain_oper(log_chain_oper, i, select.having.len(), sql, collector); + } + + if !select.orders.is_empty() { + write!(sql, " ORDER BY ").unwrap(); + select.orders.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap() + } + self.prepare_order_expr(expr, sql, collector); + false + }); + } + + if let Some(limit) = &select.limit { + write!(sql, " LIMIT ").unwrap(); + self.prepare_value(limit, sql, collector); + } + + if let Some(offset) = &select.offset { + write!(sql, " OFFSET ").unwrap(); + self.prepare_value(offset, sql, collector); + } + } + + fn prepare_update_statement(&mut self, update: &UpdateStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "UPDATE ").unwrap(); + + if let Some(table) = &update.table { + self.prepare_table_ref(table, sql, collector); + } + + write!(sql, " SET ").unwrap(); + + update.values.iter().fold(true, |first, row| { + if !first { + write!(sql, ", ").unwrap() + } + let (k, v) = row; + write!(sql, "`{}` = ", k).unwrap(); + self.prepare_simple_expr(v, sql, collector); + false + }); + + if let Some(wherei) = &update.wherei { + write!(sql, " WHERE ").unwrap(); + self.prepare_simple_expr(wherei, sql, collector); + } + + if !update.orders.is_empty() { + write!(sql, " ORDER BY ").unwrap(); + update.orders.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap(); + } + self.prepare_order_expr(expr, sql, collector); + false + }); + } + + if let Some(limit) = &update.limit { + write!(sql, " LIMIT ").unwrap(); + self.prepare_value(limit, sql, collector); + } + } + + fn prepare_delete_statement(&mut self, delete: &DeleteStatement, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "DELETE ").unwrap(); + + if let Some(table) = &delete.table { + write!(sql, "FROM ").unwrap(); + self.prepare_table_ref(table, sql, collector); + } + + if let Some(wherei) = &delete.wherei { + write!(sql, " WHERE ").unwrap(); + self.prepare_simple_expr(wherei, sql, collector); + } + + if !delete.orders.is_empty() { + write!(sql, " ORDER BY ").unwrap(); + delete.orders.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap(); + } + self.prepare_order_expr(expr, sql, collector); + false + }); + } + + if let Some(limit) = &delete.limit { + write!(sql, " LIMIT ").unwrap(); + self.prepare_value(limit, sql, collector); + } + } + + fn prepare_simple_expr(&mut self, simple_expr: &SimpleExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + match simple_expr { + SimpleExpr::Column(column) => { + column.prepare(sql, '`'); + }, + SimpleExpr::TableColumn(table, column) => { + table.prepare(sql, '`'); + write!(sql, ".").unwrap(); + column.prepare(sql, '`'); + }, + SimpleExpr::Unary(op, expr) => { + self.prepare_un_oper(op, sql, collector); + write!(sql, " ").unwrap(); + self.prepare_simple_expr(expr, sql, collector); + }, + SimpleExpr::FunctionCall(func, exprs) => { + self.prepare_function(func, sql, collector); + write!(sql, "(").unwrap(); + exprs.iter().fold(true, |first, expr| { + if !first { + write!(sql, ", ").unwrap(); + } + self.prepare_simple_expr(expr, sql, collector); + false + }); + write!(sql, ")").unwrap(); + }, + SimpleExpr::Binary(left, op, right) => { + let no_paren = match op { + BinOper::Equal => true, + BinOper::NotEqual => true, + _ => false, + }; + let left_paren = + left.need_parentheses() && + left.is_binary() && *op != left.get_bin_oper().unwrap() && + !no_paren; + if left_paren { + write!(sql, "(").unwrap(); + } + self.prepare_simple_expr(left, sql, collector); + if left_paren { + write!(sql, ")").unwrap(); + } + write!(sql, " ").unwrap(); + self.prepare_bin_oper(op, sql, collector); + write!(sql, " ").unwrap(); + let no_right_paren = match op { + BinOper::Between => true, + BinOper::NotBetween => true, + _ => false, + }; + let right_paren = + (right.need_parentheses() || + right.is_binary() && *op != left.get_bin_oper().unwrap()) && + !no_right_paren && + !no_paren; + if right_paren { + write!(sql, "(").unwrap(); + } + self.prepare_simple_expr(right, sql, collector); + if right_paren { + write!(sql, ")").unwrap(); + } + }, + SimpleExpr::SubQuery(sel) => { + write!(sql, "(").unwrap(); + self.prepare_select_statement(sel, sql, collector); + write!(sql, ")").unwrap(); + }, + SimpleExpr::Value(val) => { + self.prepare_value(val, sql, collector); + }, + SimpleExpr::Values(list) => { + write!(sql, "(").unwrap(); + list.iter().fold(true, |first, val| { + if !first { + write!(sql, ", ").unwrap(); + } + self.prepare_value(val, sql, collector); + false + }); + write!(sql, ")").unwrap(); + }, + SimpleExpr::Custom(s) => { + write!(sql, "{}", s).unwrap(); + }, + } + } + + fn prepare_select_distinct(&mut self, select_distinct: &SelectDistinct, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match select_distinct { + SelectDistinct::All => "ALL", + SelectDistinct::Distinct => "DISTINCT", + SelectDistinct::DistinctRow => "DISTINCTROW", + }).unwrap(); + } + + fn prepare_select_expr(&mut self, select_expr: &SelectExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + self.prepare_simple_expr(&select_expr.expr, sql, collector); + match &select_expr.alias { + Some(alias) => { + write!(sql, " AS ").unwrap(); + alias.prepare(sql, '`'); + }, + None => {}, + } + } + + fn prepare_join_expr(&mut self, join_expr: &JoinExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + self.prepare_join_type(&join_expr.join, sql, collector); + write!(sql, " ").unwrap(); + self.prepare_table_ref(&join_expr.table, sql, collector); + if let Some(on) = &join_expr.on { + write!(sql, " ").unwrap(); + self.prepare_join_on(on, sql, collector); + } + } + + fn prepare_table_ref(&mut self, table_ref: &TableRef, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + match table_ref { + TableRef::Table(iden) => { + iden.prepare(sql, '`'); + }, + TableRef::TableAlias(iden, alias) => { + iden.prepare(sql, '`'); + write!(sql, " AS ").unwrap(); + alias.prepare(sql, '`'); + }, + TableRef::SubQuery(query, alias) => { + write!(sql, "(").unwrap(); + self.prepare_select_statement(query, sql, collector); + write!(sql, ")").unwrap(); + write!(sql, " AS ").unwrap(); + alias.prepare(sql, '`'); + }, + } + } + + fn prepare_un_oper(&mut self, un_oper: &UnOper, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match un_oper { + UnOper::Not => "NOT", + }).unwrap(); + } + + fn prepare_bin_oper(&mut self, bin_oper: &BinOper, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match bin_oper { + BinOper::And => "AND", + BinOper::Or => "OR", + BinOper::Like => "LIKE", + BinOper::NotLike => "NOT LIKE", + BinOper::Is => "IS", + BinOper::IsNot => "IS NOT", + BinOper::In => "IN", + BinOper::NotIn => "NOT IN", + BinOper::Between => "BETWEEN", + BinOper::NotBetween => "NOT BETWEEN", + BinOper::Equal => "=", + BinOper::NotEqual => "<>", + BinOper::SmallerThan => "<", + BinOper::GreaterThan => ">", + BinOper::SmallerThanOrEqual => "<=", + BinOper::GreaterThanOrEqual => ">=", + BinOper::Add => "+", + BinOper::Sub => "-", + BinOper::Mul => "*", + BinOper::Div => "/", + }).unwrap(); + } + + fn prepare_logical_chain_oper(&mut self, log_chain_oper: &LogicalChainOper, i: usize, length: usize, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + let (simple_expr, oper) = match log_chain_oper { + LogicalChainOper::And(simple_expr) => (simple_expr, "AND"), + LogicalChainOper::Or(simple_expr) => (simple_expr, "OR"), + }; + if i > 0 { + write!(sql, " {} ", oper).unwrap(); + } + let both_binary = match simple_expr { + SimpleExpr::Binary(_, _, right) => matches!(right.as_ref(), SimpleExpr::Binary(_, _, _)), + _ => false, + }; + let need_parentheses = length > 1 && both_binary; + if need_parentheses { + write!(sql, "(").unwrap(); + } + self.prepare_simple_expr(simple_expr, sql, collector); + if need_parentheses { + write!(sql, ")").unwrap(); + } + } + + fn prepare_function(&mut self, function: &Function, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match function { + Function::Max => "MAX", + Function::Min => "MIN", + Function::Sum => "SUM", + Function::Count => "COUNT", + Function::IfNull => "IFNULL", + Function::Custom(name) => name.as_ref(), + }).unwrap(); + } + + fn prepare_join_type(&mut self, join_type: &JoinType, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + write!(sql, "{}", match join_type { + JoinType::Join => "JOIN", + JoinType::InnerJoin => "INNER JOIN", + JoinType::LeftJoin => "LEFT JOIN", + JoinType::RightJoin => "RIGHT JOIN", + }).unwrap() + } + + fn prepare_order_expr(&mut self, order_expr: &OrderExpr, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + self.prepare_simple_expr(&order_expr.expr, sql, collector); + write!(sql, " ").unwrap(); + self.prepare_order(&order_expr.order, sql, collector); + } + + fn prepare_join_on(&mut self, join_on: &JoinOn, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + match join_on { + JoinOn::Condition(c) => { + write!(sql, "ON ").unwrap(); + self.prepare_simple_expr(c, sql, collector); + }, + JoinOn::Columns(_c) => unimplemented!(), + } + } + + fn prepare_order(&mut self, order: &Order, sql: &mut dyn FmtWrite, _collector: &mut dyn FnMut(Value)) { + match order { + Order::Asc => { + write!(sql, "ASC").unwrap() + }, + Order::Desc => { + write!(sql, "DESC").unwrap() + }, + } + } + + fn prepare_value(&mut self, value: &Value, sql: &mut dyn FmtWrite, collector: &mut dyn FnMut(Value)) { + write!(sql, "?").unwrap(); + self.prepare_value_param(value, collector); + } + + fn prepare_value_param(&mut self, value: &Value, collector: &mut dyn FnMut(Value)) { + match value { + Value::NULL => { + collector(Value::NULL); + }, + Value::Bytes(v) => { + collector(Value::Bytes(v.to_vec())); + }, + Value::Int(v) => { + collector(Value::Int(*v)); + }, + Value::UInt(v) => { + collector(Value::UInt(*v)); + }, + Value::Float(v) => { + collector(Value::Float(*v)); + }, + Value::Double(v) => { + collector(Value::Double(*v)); + }, + Value::Date(year, month, day, hour, minutes, seconds, micro_seconds) => { + collector(Value::Date(*year, *month, *day, *hour, *minutes, *seconds, *micro_seconds)); + }, + Value::Time(negative, days, hours, minutes, seconds, micro_seconds) => { + collector(Value::Time(*negative, *days, *hours, *minutes, *seconds, *micro_seconds)); + }, + }; + } +} \ No newline at end of file diff --git a/src/backend/sqlite/table.rs b/src/backend/sqlite/table.rs new file mode 100644 index 000000000..755ab795a --- /dev/null +++ b/src/backend/sqlite/table.rs @@ -0,0 +1,212 @@ +use super::*; + +impl TableBuilder for SqliteQueryBuilder { + fn prepare_table_create_statement(&mut self, create: &TableCreateStatement, sql: &mut dyn FmtWrite) { + write!(sql, "CREATE TABLE ").unwrap(); + + if create.create_if_not_exists { + write!(sql, "IF NOT EXISTS ").unwrap(); + } + + if let Some(table) = &create.table { + table.prepare(sql, '`'); + } + + write!(sql, " ( ").unwrap(); + let mut count = 0; + + for column_def in create.columns.iter() { + if count > 0 { + write!(sql, ", ").unwrap(); + } + self.prepare_column_def(column_def, sql); + count += 1; + } + + for foreign_key in create.foreign_keys.iter() { + if count > 0 { + write!(sql, ", ").unwrap(); + } + self.prepare_foreign_key_create_statement(foreign_key, sql); + count += 1; + } + + write!(sql, " )").unwrap(); + + for table_opt in create.options.iter() { + write!(sql, " ").unwrap(); + self.prepare_table_opt(table_opt, sql); + } + } + + fn prepare_column_def(&mut self, column_def: &ColumnDef, sql: &mut dyn FmtWrite) { + column_def.name.prepare(sql, '`'); + + if let Some(column_type) = &column_def.types { + write!(sql, " ").unwrap(); + self.prepare_column_type(&column_type, sql); + } + + let mut is_primary_key = false; + let mut is_auto_increment = false; + + for column_spec in column_def.spec.iter() { + if let ColumnSpec::PrimaryKey = column_spec { + is_primary_key = true; + continue + } + if let ColumnSpec::AutoIncrement = column_spec { + is_auto_increment = true; + continue + } + write!(sql, " ").unwrap(); + self.prepare_column_spec(column_spec, sql); + }; + + if is_primary_key { + write!(sql, " ").unwrap(); + self.prepare_column_spec(&ColumnSpec::PrimaryKey, sql); + } + if is_auto_increment { + write!(sql, " ").unwrap(); + self.prepare_column_spec(&ColumnSpec::AutoIncrement, sql); + } + } + + fn prepare_column_type(&mut self, column_type: &ColumnType, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match column_type { + ColumnType::Char(length) => format!("text({})", length), + ColumnType::CharDefault => "text".into(), + ColumnType::String(length) => format!("text({})", length), + ColumnType::StringDefault => "text".into(), + ColumnType::Text => "text".into(), + ColumnType::TinyInteger(length) => format!("integer({})", length), + ColumnType::TinyIntegerDefault => "integer".into(), + ColumnType::SmallInteger(length) => format!("integer({})", length), + ColumnType::SmallIntegerDefault => "integer".into(), + ColumnType::Integer(length) => format!("integer({})", length), + ColumnType::IntegerDefault => "integer".into(), + ColumnType::BigInteger(length) => format!("integer({})", length), + ColumnType::BigIntegerDefault => "integer".into(), + ColumnType::Float(precision) => format!("real({})", precision), + ColumnType::FloatDefault => "real".into(), + ColumnType::Double(precision) => format!("real({})", precision), + ColumnType::DoubleDefault => "real".into(), + ColumnType::Decimal(precision, scale) => format!("real({}, {})", precision, scale), + ColumnType::DecimalDefault => "real".into(), + ColumnType::DateTime(precision) => format!("text({})", precision), + ColumnType::DateTimeDefault => "text".into(), + ColumnType::Timestamp(precision) => format!("text({})", precision), + ColumnType::TimestampDefault => "text".into(), + ColumnType::Time(precision) => format!("text({})", precision), + ColumnType::TimeDefault => "text".into(), + ColumnType::Date => "text".into(), + ColumnType::Binary(length) => format!("binary({})", length), + ColumnType::BinaryDefault => "binary".into(), + ColumnType::Boolean => "integer".into(), + ColumnType::Money(precision, scale) => format!("integer({}, {})", precision, scale), + ColumnType::MoneyDefault => "integer".into(), + ColumnType::Json => "text".into(), + }).unwrap() + } + + fn prepare_column_spec(&mut self, column_spec: &ColumnSpec, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match column_spec { + ColumnSpec::Null => "NULL".into(), + ColumnSpec::NotNull => "NOT NULL".into(), + ColumnSpec::Default(value) => format!("DEFAULT {}", value_to_string(value)), + ColumnSpec::AutoIncrement => "AUTOINCREMENT".into(), + ColumnSpec::UniqueKey => "UNIQUE".into(), + ColumnSpec::PrimaryKey => "PRIMARY KEY".into(), + }).unwrap() + } + + fn prepare_table_opt(&mut self, table_opt: &TableOpt, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match table_opt { + TableOpt::Engine(s) => format!("ENGINE={}", s), + TableOpt::Collate(s) => format!("COLLATE={}", s), + TableOpt::CharacterSet(s) => format!("DEFAULT CHARSET={}", s), + }).unwrap() + } + + fn prepare_table_partition(&mut self, _table_partition: &TablePartition, _sql: &mut dyn FmtWrite) { + + } + + fn prepare_table_drop_statement(&mut self, drop: &TableDropStatement, sql: &mut dyn FmtWrite) { + write!(sql, "DROP TABLE ").unwrap(); + + if drop.if_exist { + write!(sql, "IF EXIST ").unwrap(); + } + + drop.tables.iter().fold(true, |first, table| { + if !first { + write!(sql, ", ").unwrap(); + } + table.prepare(sql, '`'); + false + }); + + for drop_opt in drop.options.iter() { + write!(sql, " ").unwrap(); + self.prepare_table_drop_opt(drop_opt, sql); + } + } + + fn prepare_table_drop_opt(&mut self, drop_opt: &TableDropOpt, sql: &mut dyn FmtWrite) { + write!(sql, "{}", match drop_opt { + TableDropOpt::Restrict => "RESTRICT", + TableDropOpt::Cascade => "CASCADE", + }).unwrap(); + } + + fn prepare_table_truncate_statement(&mut self, truncate: &TableTruncateStatement, sql: &mut dyn FmtWrite) { + write!(sql, "TRUNCATE TABLE ").unwrap(); + + if let Some(table) = &truncate.table { + table.prepare(sql, '`'); + } + } + + fn prepare_table_alter_statement(&mut self, alter: &TableAlterStatement, sql: &mut dyn FmtWrite) { + let alter_option = match &alter.alter_option { + Some(alter_option) => alter_option, + None => panic!("No alter option found"), + }; + write!(sql, "ALTER TABLE ").unwrap(); + if let Some(table) = &alter.table { + table.prepare(sql, '`'); + write!(sql, " ").unwrap(); + } + match alter_option { + TableAlterOption::AddColumn(column_def) => { + write!(sql, "ADD COLUMN ").unwrap(); + self.prepare_column_def(column_def, sql); + }, + TableAlterOption::ModifyColumn(_) => { + panic!("Sqlite not support modifying table column") + }, + TableAlterOption::RenameColumn(from_name, to_name) => { + write!(sql, "RENAME COLUMN ").unwrap(); + from_name.prepare(sql, '`'); + write!(sql, " TO ").unwrap(); + to_name.prepare(sql, '`'); + }, + TableAlterOption::DropColumn(_) => { + panic!("Sqlite not support dropping table column") + }, + } + } + + fn prepare_table_rename_statement(&mut self, rename: &TableRenameStatement, sql: &mut dyn FmtWrite) { + write!(sql, "ALTER TABLE ").unwrap(); + if let Some(from_name) = &rename.from_name { + from_name.prepare(sql, '`'); + } + write!(sql, " RENAME TO ").unwrap(); + if let Some(to_name) = &rename.to_name { + to_name.prepare(sql, '`'); + } + } +} \ No newline at end of file diff --git a/src/expr.rs b/src/expr.rs new file mode 100644 index 000000000..9e8327d52 --- /dev/null +++ b/src/expr.rs @@ -0,0 +1,1380 @@ +//! Building blocks for constructing select, join, where and having expression in query. +//! +//! [`Expr`] representing the most fundamental concept in the expression including concept like +//! table column with and without table name prefix and any custom expression in string. +//! Also common operations or functions can be applied to the table column, +//! such as equal, not equal, not_null and many others. Please reference below for more details. +//! +//! [`SimpleExpr`] represent various kinds of expression can be used in query. +//! Two [`SimpleExpr`] can be chain together with method defined below, such as logical AND, +//! logical OR, arithmetic ADD ...etc. Please reference below for more details. + +use std::rc::Rc; +use crate::{query::*, types::*, value::*}; + +/// Building block of a expression. +/// +/// [`Expr`] representing the most fundamental concept in the expression including concept like +/// table column with and without table name prefix and any custom expression in string. +/// Also common operations or functions can be applied to the table column, +/// such as equal, not equal, not_null and many others. Please reference below for more details. +#[derive(Clone)] +pub struct Expr { + pub(crate) left: Option, + pub(crate) right: Option, + pub(crate) uopr: Option, + pub(crate) bopr: Option, + pub(crate) func: Option, + pub(crate) args: Vec, +} + +impl Expr { + fn new_with_left(left: SimpleExpr) -> Self { + Self { + left: Some(left), + right: None, + uopr: None, + bopr: None, + func: None, + args: Vec::new(), + } + } + + /// Express the target column without table prefix. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::col(Char::SizeW).into()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w`"# + /// ); + /// ``` + pub fn col(n: T) -> Self + where T: Iden { + Self::col_dyn(Rc::new(n)) + } + + /// Express the target column without table prefix, a variation of [`Expr::col`] which takes a `Rc`. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// use std::rc::Rc; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::col_dyn(Rc::new(Char::SizeW)).into()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w`"# + /// ); + /// ``` + pub fn col_dyn(n: Rc) -> Self { + Self::new_with_left(SimpleExpr::Column(n)) + } + + /// Express the target column with table prefix. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).into()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w`"# + /// ); + /// ``` + pub fn tbl(t: T, c: C) -> Self + where T: Iden, C: Iden { + Self::tbl_dyn(Rc::new(t), Rc::new(c)) + } + + /// Express the target column with table prefix, a variation of [`Expr::tbl`] which takes two `Rc`. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// use std::rc::Rc; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl_dyn(Rc::new(Char::Table), Rc::new(Char::SizeW)).into()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w`"# + /// ); + /// ``` + pub fn tbl_dyn(t: Rc, c: Rc) -> Self { + Self::new_with_left(SimpleExpr::TableColumn(t, c)) + } + + /// Express a [`Value`], returning a [`Expr`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::val(1).into()) + /// .and_where(Expr::val(2.5).into()) + /// .and_where(Expr::val("3").into()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 AND 2.5 AND '3'"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 AND 2.5 AND '3'"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 AND 2.5 AND '3'"# + /// ); + /// ``` + pub fn val(v: V) -> Self + where V: Into { + Self::new_with_left(SimpleExpr::Value(v.into())) + } + + /// Wrap a [`SimpleExpr`] and perform some operation on it. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::expr(Expr::col(Char::SizeW).if_null(0)).gt(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE IFNULL(`size_w`, 0) > 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE COALESCE("size_w", 0) > 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE IFNULL(`size_w`, 0) > 2"# + /// ); + /// ``` + pub fn expr(expr: SimpleExpr) -> Self { + Self::new_with_left(expr) + } + + /// Express a [`Value`], returning a [`SimpleExpr`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::value(1).into()) + /// .and_where(Expr::value(2.5).into()) + /// .and_where(Expr::value("3").into()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 AND 2.5 AND '3'"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 AND 2.5 AND '3'"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 AND 2.5 AND '3'"# + /// ); + /// ``` + pub fn value(v: V) -> SimpleExpr + where V: Into { + SimpleExpr::Value(v.into()) + } + + /// Express any custom expression in [`&str`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::cust("").into()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE "# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE "# + /// ); + /// ``` + pub fn cust(s: &str) -> SimpleExpr { + SimpleExpr::Custom(s.to_owned()) + } + + /// Express a equal expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::val("What!").eq("Nothing")) + /// .and_where(Expr::col(Char::Id).eq(1)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 'What!' = 'Nothing' AND `id` = 1"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 'What!' = 'Nothing' AND "id" = 1"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 'What!' = 'Nothing' AND `id` = 1"# + /// ); + /// ``` + pub fn eq(self, v: V) -> SimpleExpr + where V: Into { + self.bin_oper(BinOper::Equal, SimpleExpr::Value(v.into())) + } + + /// Express a equal expression between two table columns, + /// you will mainly use this to relate identical value between two table columns. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`font_id` = `font`.`id`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."font_id" = "font"."id""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`font_id` = `font`.`id`"# + /// ); + /// ``` + pub fn equals(self, t: T, c: C) -> SimpleExpr + where T: Iden, C: Iden { + self.equals_dyn(Rc::new(t), Rc::new(c)) + } + + /// Express a equal expression between two table columns, + /// you will mainly use this to relate identical value between two table columns. + /// + /// A variation of [`Expr::equals`] which takes two `Rc`. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// use std::rc::Rc; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::FontId).equals_dyn(Rc::new(Font::Table), Rc::new(Font::Id))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`font_id` = `font`.`id`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."font_id" = "font"."id""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`font_id` = `font`.`id`"# + /// ); + /// ``` + pub fn equals_dyn(self, t: Rc, c: Rc) -> SimpleExpr { + self.bin_oper(BinOper::Equal, SimpleExpr::TableColumn(t, c)) + } + + /// Express a greater than expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).gt(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` > 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" > 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` > 2"# + /// ); + /// ``` + pub fn gt(self, v: V) -> SimpleExpr + where V: Into { + self.bin_oper(BinOper::GreaterThan, SimpleExpr::Value(v.into())) + } + + /// Express a greater than or equal expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).gte(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` >= 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" >= 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` >= 2"# + /// ); + /// ``` + pub fn gte(self, v: V) -> SimpleExpr + where V: Into { + self.bin_oper(BinOper::GreaterThanOrEqual, SimpleExpr::Value(v.into())) + } + + /// Express a less than expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).lt(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` < 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" < 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` < 2"# + /// ); + /// ``` + pub fn lt(self, v: V) -> SimpleExpr + where V: Into { + self.bin_oper(BinOper::SmallerThan, SimpleExpr::Value(v.into())) + } + + /// Express a less than or equal expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).lte(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` <= 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" <= 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` <= 2"# + /// ); + /// ``` + pub fn lte(self, v: V) -> SimpleExpr + where V: Into { + self.bin_oper(BinOper::SmallerThanOrEqual, SimpleExpr::Value(v.into())) + } + + /// Express an arithmetic addition expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::val(1).add(1).equals(Expr::value(2))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 + 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 + 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 + 1 = 2"# + /// ); + /// ``` + #[allow(clippy::should_implement_trait)] + pub fn add(self, v: V) -> SimpleExpr + where V: Into { + self.bin_oper(BinOper::Add, SimpleExpr::Value(v.into())) + } + + /// Express an arithmetic subtraction expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::val(1).sub(1).equals(Expr::value(2))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 - 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 - 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 - 1 = 2"# + /// ); + /// ``` + #[allow(clippy::should_implement_trait)] + pub fn sub(self, v: V) -> SimpleExpr + where V: Into { + self.bin_oper(BinOper::Sub, SimpleExpr::Value(v.into())) + } + + /// Express an arithmetic multiplication expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::val(1).mul(1).equals(Expr::value(2))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 * 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 * 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 * 1 = 2"# + /// ); + /// ``` + #[allow(clippy::should_implement_trait)] + pub fn mul(self, v: V) -> SimpleExpr + where V: Into { + self.bin_oper(BinOper::Mul, SimpleExpr::Value(v.into())) + } + + /// Express an arithmetic division expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::val(1).div(1).equals(Expr::value(2))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 / 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 / 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 / 1 = 2"# + /// ); + /// ``` + #[allow(clippy::should_implement_trait)] + pub fn div(self, v: V) -> SimpleExpr + where V: Into { + self.bin_oper(BinOper::Div, SimpleExpr::Value(v.into())) + } + + /// Express a between expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).between(1, 10)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` BETWEEN 1 AND 10"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" BETWEEN 1 AND 10"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` BETWEEN 1 AND 10"# + /// ); + /// ``` + pub fn between(self, a: V, b: V) -> SimpleExpr + where V: Into { + self.between_or_not_between(BinOper::Between, a, b) + } + + /// Express a not between expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).not_between(1, 10)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` NOT BETWEEN 1 AND 10"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" NOT BETWEEN 1 AND 10"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` NOT BETWEEN 1 AND 10"# + /// ); + /// ``` + pub fn not_between(self, a: V, b: V) -> SimpleExpr + where V: Into { + self.between_or_not_between(BinOper::NotBetween, a, b) + } + + fn between_or_not_between(self, op: BinOper, a: V, b: V) -> SimpleExpr + where V: Into { + self.bin_oper(op, SimpleExpr::Binary( + Box::new(SimpleExpr::Value(a.into())), + BinOper::And, + Box::new(SimpleExpr::Value(b.into())), + )) + } + + /// Express a like expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::Character).like("Char%")) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`character` LIKE 'Char%'"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."character" LIKE 'Char%'"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`character` LIKE 'Char%'"# + /// ); + /// ``` + pub fn like(self, v: &str) -> SimpleExpr { + self.bin_oper(BinOper::Like, SimpleExpr::Value(Value::Bytes(v.into()))) + } + + /// Express a is null expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).is_null()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` IS NULL"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" IS NULL"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` IS NULL"# + /// ); + /// ``` + #[allow(clippy::wrong_self_convention)] + pub fn is_null(self) -> SimpleExpr { + self.bin_oper(BinOper::Is, SimpleExpr::Value(Value::NULL)) + } + + /// Express a is not null expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).is_not_null()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` IS NOT NULL"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" IS NOT NULL"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` IS NOT NULL"# + /// ); + /// ``` + #[allow(clippy::wrong_self_convention)] + pub fn is_not_null(self) -> SimpleExpr { + self.bin_oper(BinOper::IsNot, SimpleExpr::Value(Value::NULL)) + } + + /// Express a is not null expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::expr(Expr::tbl(Char::Table, Char::SizeW).is_not_null()).not()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE NOT `character`.`size_w` IS NOT NULL"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE NOT "character"."size_w" IS NOT NULL"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE NOT `character`.`size_w` IS NOT NULL"# + /// ); + /// ``` + #[allow(clippy::should_implement_trait)] + pub fn not(self) -> SimpleExpr { + self.un_oper(UnOper::Not) + } + + /// Express a max expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).max()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE MAX(`character`.`size_w`)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE MAX("character"."size_w")"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE MAX(`character`.`size_w`)"# + /// ); + /// ``` + pub fn max(mut self) -> SimpleExpr { + let left = self.left.take(); + self.func(Function::Max, vec![left.unwrap()]) + } + + /// Express a min expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).min()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE MIN(`character`.`size_w`)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE MIN("character"."size_w")"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE MIN(`character`.`size_w`)"# + /// ); + /// ``` + pub fn min(mut self) -> SimpleExpr { + let left = self.left.take(); + self.func(Function::Min, vec![left.unwrap()]) + } + + /// Express a sum expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).sum()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE SUM(`character`.`size_w`)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE SUM("character"."size_w")"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE SUM(`character`.`size_w`)"# + /// ); + /// ``` + pub fn sum(mut self) -> SimpleExpr { + let left = self.left.take(); + self.func(Function::Sum, vec![left.unwrap()]) + } + + /// Express a count expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).count()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE COUNT(`character`.`size_w`)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE COUNT("character"."size_w")"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE COUNT(`character`.`size_w`)"# + /// ); + /// ``` + pub fn count(mut self) -> SimpleExpr { + let left = self.left.take(); + self.func(Function::Count, vec![left.unwrap()]) + } + + /// Express a if null expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).if_null(0)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE IFNULL(`character`.`size_w`, 0)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE COALESCE("character"."size_w", 0)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE IFNULL(`character`.`size_w`, 0)"# + /// ); + /// ``` + pub fn if_null(mut self, v: V) -> SimpleExpr + where V: Into { + let left = self.left.take(); + self.func(Function::IfNull, vec![left.unwrap(), SimpleExpr::Value(v.into())]) + } + + /// Express a is in expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).is_in(vec![1, 2, 3])) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` IN (1, 2, 3)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" IN (1, 2, 3)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` IN (1, 2, 3)"# + /// ); + /// ``` + #[allow(clippy::wrong_self_convention)] + pub fn is_in(mut self, v: Vec) -> SimpleExpr + where V: Into { + self.bopr = Some(BinOper::In); + self.right = Some(SimpleExpr::Values(v.into_iter().map(|v| v.into()).collect())); + self.into() + } + + /// Express a is not expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::tbl(Char::Table, Char::SizeW).is_not_in(vec![1, 2, 3])) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` NOT IN (1, 2, 3)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" NOT IN (1, 2, 3)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` NOT IN (1, 2, 3)"# + /// ); + /// ``` + #[allow(clippy::wrong_self_convention)] + pub fn is_not_in(mut self, v: Vec) -> SimpleExpr + where V: Into { + self.bopr = Some(BinOper::NotIn); + self.right = Some(SimpleExpr::Values(v.into_iter().map(|v| v.into()).collect())); + self.into() + } + + /// Express a in sub-query expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::col(Char::SizeW).in_subquery( + /// Query::select() + /// .expr(Expr::cust("3 + 2 * 2")) + /// .take() + /// )) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` IN (SELECT 3 + 2 * 2)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w" IN (SELECT 3 + 2 * 2)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` IN (SELECT 3 + 2 * 2)"# + /// ); + /// ``` + #[allow(clippy::wrong_self_convention)] + pub fn in_subquery(mut self, sel: SelectStatement) -> SimpleExpr { + self.bopr = Some(BinOper::In); + self.right = Some(SimpleExpr::SubQuery(Box::new(sel))); + self.into() + } + + fn func(mut self, func: Function, args: Vec) -> SimpleExpr { + self.func = Some(func); + self.args = args; + self.into() + } + + fn un_oper(mut self, o: UnOper) -> SimpleExpr { + self.uopr = Some(o); + self.into() + } + + fn bin_oper(mut self, o: BinOper, e: SimpleExpr) -> SimpleExpr { + self.bopr = Some(o); + self.right = Some(e); + self.into() + } +} + +impl Into for Expr { + fn into(self) -> SimpleExpr { + if let Some(uopr) = self.uopr { + SimpleExpr::Unary( + uopr, + Box::new(self.left.unwrap()), + ) + } else if let Some(bopr) = self.bopr { + SimpleExpr::Binary( + Box::new(self.left.unwrap()), + bopr, + Box::new(self.right.unwrap()), + ) + } else if let Some(func) = self.func { + SimpleExpr::FunctionCall( + func, + self.args + ) + } else if let Some(left) = self.left { + left + } else { + panic!("incomplete expression") + } + } +} + +/// Expression used in query, including all supported expression variants. +/// +/// [`SimpleExpr`] represent various kinds of expression can be used in query. +/// Two [`SimpleExpr`] can be chain together with method defined below, such as logical AND, +/// logical OR, arithmetic ADD ...etc. Please reference below for more details. +#[derive(Clone)] +pub enum SimpleExpr { + Column(Rc), + TableColumn(Rc, Rc), + Unary(UnOper, Box), + FunctionCall(Function, Vec), + Binary(Box, BinOper, Box), + SubQuery(Box), + Value(Value), + Values(Vec), + Custom(String), +} + +impl SimpleExpr { + /// Express a logical and expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .or_where(Expr::col(Char::SizeW).eq(1).and(Expr::col(Char::SizeH).eq(2))) + /// .or_where(Expr::col(Char::SizeW).eq(3).and(Expr::col(Char::SizeH).eq(4))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE ((`size_w` = 1) AND (`size_h` = 2)) OR ((`size_w` = 3) AND (`size_h` = 4))"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE (("size_w" = 1) AND ("size_h" = 2)) OR (("size_w" = 3) AND ("size_h" = 4))"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE ((`size_w` = 1) AND (`size_h` = 2)) OR ((`size_w` = 3) AND (`size_h` = 4))"# + /// ); + /// ``` + pub fn and(self, right: SimpleExpr) -> Self { + self.binary(BinOper::And, right) + } + + /// Express a logical or expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::col(Char::SizeW).eq(1).or(Expr::col(Char::SizeH).eq(2))) + /// .and_where(Expr::col(Char::SizeW).eq(3).or(Expr::col(Char::SizeH).eq(4))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE ((`size_w` = 1) OR (`size_h` = 2)) AND ((`size_w` = 3) OR (`size_h` = 4))"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE (("size_w" = 1) OR ("size_h" = 2)) AND (("size_w" = 3) OR ("size_h" = 4))"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE ((`size_w` = 1) OR (`size_h` = 2)) AND ((`size_w` = 3) OR (`size_h` = 4))"# + /// ); + /// ``` + pub fn or(self, right: SimpleExpr) -> Self { + self.binary(BinOper::Or, right) + } + + /// Express a logical equal expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::col(Char::SizeW).max().equals(Expr::col(Char::SizeH).max())) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE MAX(`size_w`) = MAX(`size_h`)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE MAX("size_w") = MAX("size_h")"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE MAX(`size_w`) = MAX(`size_h`)"# + /// ); + /// ``` + pub fn equals(self, right: SimpleExpr) -> Self { + self.binary(BinOper::Equal, right) + } + + /// Express a logical not equal expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::col(Char::SizeW).max().not_equals(Expr::col(Char::SizeH).max())) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE MAX(`size_w`) <> MAX(`size_h`)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE MAX("size_w") <> MAX("size_h")"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE MAX(`size_w`) <> MAX(`size_h`)"# + /// ); + /// ``` + pub fn not_equals(self, right: SimpleExpr) -> Self { + self.binary(BinOper::NotEqual, right) + } + + /// Express a arithmetic addition expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::col(Char::SizeW).max().add(Expr::col(Char::SizeH).max())) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE MAX(`size_w`) + MAX(`size_h`)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE MAX("size_w") + MAX("size_h")"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE MAX(`size_w`) + MAX(`size_h`)"# + /// ); + /// ``` + #[allow(clippy::should_implement_trait)] + pub fn add(self, right: SimpleExpr) -> Self { + self.binary(BinOper::Add, right) + } + + /// Express a arithmetic subtraction expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::col(Char::SizeW).max().sub(Expr::col(Char::SizeH).max())) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE MAX(`size_w`) - MAX(`size_h`)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE MAX("size_w") - MAX("size_h")"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE MAX(`size_w`) - MAX(`size_h`)"# + /// ); + /// ``` + #[allow(clippy::should_implement_trait)] + pub fn sub(self, right: SimpleExpr) -> Self { + self.binary(BinOper::Sub, right) + } + + pub(crate) fn binary(self, op: BinOper, right: SimpleExpr) -> Self { + SimpleExpr::Binary(Box::new(self), op, Box::new(right)) + } + + #[allow(dead_code)] + pub(crate) fn static_conditions(self, b: bool, if_true: T, if_false: F) -> Self + where T: FnOnce(Self) -> Self, F: FnOnce(Self) -> Self { + if b { + if_true(self) + } else { + if_false(self) + } + } + + pub(crate) fn need_parentheses(&self) -> bool { + match self { + Self::Binary(left, oper, _) => match (left.as_ref(), oper) { + (Self::Binary(_, BinOper::And, _), BinOper::And) => false, + (Self::Binary(_, BinOper::Or, _), BinOper::Or) => false, + _ => true, + }, + _ => false, + } + } + + pub(crate) fn is_binary(&self) -> bool { + matches!(self, Self::Binary(_, _, _)) + } + + pub(crate) fn get_bin_oper(&self) -> Option { + match self { + Self::Binary(_, oper, _) => Some(*oper), + _ => None, + } + } +} diff --git a/src/foreign_key/common.rs b/src/foreign_key/common.rs new file mode 100644 index 000000000..14c190a21 --- /dev/null +++ b/src/foreign_key/common.rs @@ -0,0 +1,79 @@ +use std::rc::Rc; +use crate::types::*; + +/// Specification of a foreign key +#[derive(Clone)] +pub struct TableForeignKey { + pub(crate) name: Option, + pub(crate) table: Option>, + pub(crate) ref_table: Option>, + pub(crate) columns: Vec>, + pub(crate) ref_columns: Vec>, + pub(crate) on_delete: Option, + pub(crate) on_update: Option, +} + +/// Foreign key on update & on delete actions +#[derive(Clone)] +pub enum ForeignKeyAction { + Restrict, + Cascade, + SetNull, + NoAction, + SetDefault, +} + +impl Default for TableForeignKey { + fn default() -> Self { + Self::new() + } +} + +impl TableForeignKey { + /// Construct a new foreign key + pub fn new() -> Self { + Self { + name: None, + table: None, + ref_table: None, + columns: Vec::new(), + ref_columns: Vec::new(), + on_delete: None, + on_update: None, + } + } + + /// Set foreign key name + pub fn name(&mut self, name: &str) -> &mut Self { + self.name = Some(name.into()); + self + } + + /// Set key table and referencing table + pub fn table(&mut self, table: T, ref_table: R) -> &mut Self + where T: Iden, R: Iden { + self.table = Some(Rc::new(table)); + self.ref_table = Some(Rc::new(ref_table)); + self + } + + /// Set key column and referencing column + pub fn col(&mut self, column: T, ref_column: R) -> &mut Self + where T: Iden, R: Iden { + self.columns.push(Rc::new(column)); + self.ref_columns.push(Rc::new(ref_column)); + self + } + + /// Set on delete action + pub fn on_delete(&mut self, action: ForeignKeyAction) -> &mut Self { + self.on_delete = Some(action); + self + } + + /// Set on update action + pub fn on_update(&mut self, action: ForeignKeyAction) -> &mut Self { + self.on_update = Some(action); + self + } +} \ No newline at end of file diff --git a/src/foreign_key/create.rs b/src/foreign_key/create.rs new file mode 100644 index 000000000..c7009982b --- /dev/null +++ b/src/foreign_key/create.rs @@ -0,0 +1,101 @@ +use crate::{ForeignKeyAction, TableForeignKey, backend::ForeignKeyBuilder, types::*}; + +/// Create a foreign key constraint for an existing table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let foreign_key = ForeignKey::create() +/// .name("FK_character_font") +/// .table(Char::Table, Font::Table) +/// .col(Char::FontId, Font::Id) +/// .on_delete(ForeignKeyAction::Cascade) +/// .on_update(ForeignKeyAction::Cascade) +/// .to_owned(); +/// +/// assert_eq!( +/// foreign_key.to_string(MysqlQueryBuilder), +/// vec![ +/// r#"ALTER TABLE `character`"#, +/// r#"ADD CONSTRAINT `FK_character_font`"#, +/// r#"FOREIGN KEY `FK_character_font` (`font_id`) REFERENCES `font` (`id`)"#, +/// r#"ON DELETE CASCADE ON UPDATE CASCADE"#, +/// ].join(" ") +/// ); +/// assert_eq!( +/// foreign_key.to_string(PostgresQueryBuilder), +/// vec![ +/// r#"ALTER TABLE "character" ADD CONSTRAINT "FK_character_font""#, +/// r#"FOREIGN KEY ("font_id") REFERENCES "font" ("id")"#, +/// r#"ON DELETE CASCADE ON UPDATE CASCADE"#, +/// ].join(" ") +/// ); +/// // Sqlite does not support modification of foreign key constraints to existing tables +/// ``` +#[derive(Clone)] +pub struct ForeignKeyCreateStatement { + pub(crate) foreign_key: TableForeignKey, + pub(crate) inside_table_creation: bool, +} + +impl Default for ForeignKeyCreateStatement { + fn default() -> Self { + Self::new() + } +} + +impl ForeignKeyCreateStatement { + /// Construct a new [`ForeignKeyCreateStatement`] + pub fn new() -> Self { + Self { + foreign_key: Default::default(), + inside_table_creation: false, + } + } + + /// Set foreign key name + pub fn name(mut self, name: &str) -> Self { + self.foreign_key.name(name); + self + } + + /// Set key table and referencing table + pub fn table(mut self, table: T, ref_table: R) -> Self + where T: Iden, R: Iden { + self.foreign_key.table(table, ref_table); + self + } + + /// Set key column and referencing column + pub fn col(mut self, column: T, ref_column: R) -> Self + where T: Iden, R: Iden { + self.foreign_key.col(column, ref_column); + self + } + + /// Set on delete action + pub fn on_delete(mut self, action: ForeignKeyAction) -> Self { + self.foreign_key.on_delete(action); + self + } + + /// Set on update action + pub fn on_update(mut self, action: ForeignKeyAction) -> Self { + self.foreign_key.on_update(action); + self + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn build(&self, mut foreign_key_builder: T) -> String { + let mut sql = String::new(); + foreign_key_builder.prepare_foreign_key_create_statement(self, &mut sql); + sql + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn to_string(&self, foreign_key_builder: T) -> String { + self.build(foreign_key_builder) + } +} \ No newline at end of file diff --git a/src/foreign_key/drop.rs b/src/foreign_key/drop.rs new file mode 100644 index 000000000..3919f35a3 --- /dev/null +++ b/src/foreign_key/drop.rs @@ -0,0 +1,71 @@ +use std::rc::Rc; +use crate::{TableForeignKey, backend::ForeignKeyBuilder, types::*}; + +/// Drop a foreign key constraint for an existing table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let foreign_key = ForeignKey::drop() +/// .name("FK_character_font") +/// .table(Char::Table) +/// .to_owned(); +/// +/// assert_eq!( +/// foreign_key.to_string(MysqlQueryBuilder), +/// r#"ALTER TABLE `character` DROP FOREIGN KEY `FK_character_font`"# +/// ); +/// assert_eq!( +/// foreign_key.to_string(PostgresQueryBuilder), +/// r#"ALTER TABLE "character" DROP CONSTRAINT "FK_character_font""# +/// ); +/// // Sqlite does not support modification of foreign key constraints to existing tables +/// ``` +#[derive(Clone)] +pub struct ForeignKeyDropStatement { + pub(crate) foreign_key: TableForeignKey, + pub(crate) table: Option>, +} + +impl Default for ForeignKeyDropStatement { + fn default() -> Self { + Self::new() + } +} + +impl ForeignKeyDropStatement { + /// Construct a new [`ForeignKeyDropStatement`] + pub fn new() -> Self { + Self { + foreign_key: Default::default(), + table: None, + } + } + + /// Set foreign key name + pub fn name(mut self, name: &str) -> Self { + self.foreign_key.name(name); + self + } + + /// Set key table and referencing table + pub fn table(mut self, table: T) -> Self + where T: Iden { + self.table = Some(Rc::new(table)); + self + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn build(&self, mut foreign_key_builder: T) -> String { + let mut sql = String::new(); + foreign_key_builder.prepare_foreign_key_drop_statement(self, &mut sql); + sql + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn to_string(&self, foreign_key_builder: T) -> String { + self.build(foreign_key_builder) + } +} \ No newline at end of file diff --git a/src/foreign_key/mod.rs b/src/foreign_key/mod.rs new file mode 100644 index 000000000..d2b199ae2 --- /dev/null +++ b/src/foreign_key/mod.rs @@ -0,0 +1,37 @@ +//! Table foreign key (create and drop). +//! +//! # Usage +//! +//! - Table Foreign Key Create, see [`ForeignKeyCreateStatement`] +//! - Table Foreign Key Drop, see [`ForeignKeyDropStatement`] + +mod common; +mod create; +mod drop; + +pub use common::*; +pub use create::*; +pub use drop::*; + +/// Shorthand for constructing any foreign key statement +#[derive(Clone)] +pub struct ForeignKey; + +/// All available types of foreign key statement +#[derive(Clone)] +pub enum ForeignKeyStatement { + Create(ForeignKeyCreateStatement), + Drop(ForeignKeyDropStatement), +} + +impl ForeignKey { + /// Construct foreign key [`ForeignKeyCreateStatement`] + pub fn create() -> ForeignKeyCreateStatement { + ForeignKeyCreateStatement::new() + } + + /// Construct foreign key [`ForeignKeyDropStatement`] + pub fn drop() -> ForeignKeyDropStatement { + ForeignKeyDropStatement::new() + } +} \ No newline at end of file diff --git a/src/index/common.rs b/src/index/common.rs new file mode 100644 index 000000000..e095365b6 --- /dev/null +++ b/src/index/common.rs @@ -0,0 +1,38 @@ +use std::rc::Rc; +use crate::types::*; + +/// Specification of a table index +#[derive(Clone)] +pub struct TableIndex { + pub(crate) name: Option, + pub(crate) columns: Vec>, +} + +impl Default for TableIndex { + fn default() -> Self { + Self::new() + } +} + +impl TableIndex { + /// Construct a new table index + pub fn new() -> Self { + Self { + name: None, + columns: Vec::new(), + } + } + + /// Set index name + pub fn name(&mut self, name: &str) -> &mut Self { + self.name = Some(name.into()); + self + } + + /// Set index column + pub fn col(&mut self, column: T) -> &mut Self + where T: Iden { + self.columns.push(Rc::new(column)); + self + } +} \ No newline at end of file diff --git a/src/index/create.rs b/src/index/create.rs new file mode 100644 index 000000000..6e9ce3edf --- /dev/null +++ b/src/index/create.rs @@ -0,0 +1,82 @@ +use std::rc::Rc; +use crate::{TableIndex, backend::IndexBuilder, types::*}; + +/// Create an index for an existing table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let index = Index::create() +/// .name("idx-glyph-aspect") +/// .table(Glyph::Table) +/// .col(Glyph::Aspect) +/// .to_owned(); +/// +/// assert_eq!( +/// index.to_string(MysqlQueryBuilder), +/// r#"CREATE INDEX `idx-glyph-aspect` ON `glyph` (`aspect`)"# +/// ); +/// assert_eq!( +/// index.to_string(PostgresQueryBuilder), +/// r#"CREATE INDEX "idx-glyph-aspect" ON "glyph" ("aspect")"# +/// ); +/// assert_eq!( +/// index.to_string(SqliteQueryBuilder), +/// r#"CREATE INDEX `idx-glyph-aspect` ON `glyph` (`aspect`)"# +/// ); +/// ``` +#[derive(Clone)] +pub struct IndexCreateStatement { + pub(crate) table: Option>, + pub(crate) index: TableIndex, +} + +impl Default for IndexCreateStatement { + fn default() -> Self { + Self::new() + } +} + +impl IndexCreateStatement { + /// Construct a new [`IndexCreateStatement`] + pub fn new() -> Self { + Self { + table: None, + index: Default::default(), + } + } + + /// Set index name + pub fn name(mut self, name: &str) -> Self { + self.index.name(name); + self + } + + /// Set target table + pub fn table(mut self, table: T) -> Self + where T: Iden { + self.table = Some(Rc::new(table)); + self + } + + /// Set index column + pub fn col(mut self, column: T) -> Self + where T: Iden { + self.index.col(column); + self + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn build(&self, mut index_builder: T) -> String { + let mut sql = String::new(); + index_builder.prepare_index_create_statement(self, &mut sql); + sql + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn to_string(&self, index_builder: T) -> String { + self.build(index_builder) + } +} \ No newline at end of file diff --git a/src/index/drop.rs b/src/index/drop.rs new file mode 100644 index 000000000..e8bb5d325 --- /dev/null +++ b/src/index/drop.rs @@ -0,0 +1,74 @@ +use std::rc::Rc; +use crate::{TableIndex, backend::IndexBuilder, types::*}; + +/// Drop an index for an existing table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let index = Index::drop() +/// .name("idx-glyph-aspect") +/// .table(Glyph::Table) +/// .to_owned(); +/// +/// assert_eq!( +/// index.to_string(MysqlQueryBuilder), +/// r#"DROP INDEX `idx-glyph-aspect` ON `glyph`"# +/// ); +/// assert_eq!( +/// index.to_string(PostgresQueryBuilder), +/// r#"DROP INDEX "idx-glyph-aspect""# +/// ); +/// assert_eq!( +/// index.to_string(SqliteQueryBuilder), +/// r#"DROP INDEX `idx-glyph-aspect` ON `glyph`"# +/// ); +/// ``` +#[derive(Clone)] +pub struct IndexDropStatement { + pub(crate) table: Option>, + pub(crate) index: TableIndex, +} + +impl Default for IndexDropStatement { + fn default() -> Self { + Self::new() + } +} + +impl IndexDropStatement { + /// Construct a new [`IndexDropStatement`] + pub fn new() -> Self { + Self { + table: None, + index: Default::default(), + } + } + + /// Set index name + pub fn name(mut self, name: &str) -> Self { + self.index.name(name); + self + } + + /// Set target table + pub fn table(mut self, table: T) -> Self + where T: Iden { + self.table = Some(Rc::new(table)); + self + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn build(&self, mut index_builder: T) -> String { + let mut sql = String::new(); + index_builder.prepare_index_drop_statement(self, &mut sql); + sql + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn to_string(&self, index_builder: T) -> String { + self.build(index_builder) + } +} \ No newline at end of file diff --git a/src/index/mod.rs b/src/index/mod.rs new file mode 100644 index 000000000..b1b732e87 --- /dev/null +++ b/src/index/mod.rs @@ -0,0 +1,37 @@ +//! Table index (create and drop). +//! +//! # Usage +//! +//! - Table Index Create, see [`IndexCreateStatement`] +//! - Table Index Drop, see [`IndexDropStatement`] + +mod common; +mod create; +mod drop; + +pub use common::*; +pub use create::*; +pub use drop::*; + +/// Shorthand for constructing any index statement +#[derive(Clone)] +pub struct Index; + +/// All available types of index statement +#[derive(Clone)] +pub enum IndexStatement { + Create(IndexCreateStatement), + Drop(IndexDropStatement), +} + +impl Index { + /// Construct index [`IndexCreateStatement`] + pub fn create() -> IndexCreateStatement { + IndexCreateStatement::new() + } + + /// Construct index [`IndexDropStatement`] + pub fn drop() -> IndexDropStatement { + IndexDropStatement::new() + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..0328f7e33 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,513 @@ +//! A database agnostic runtime query builder for Rust. +//! +//! This library is the foundation of upcoming projects on Document ORM (Sea-ORM) and Database Synchor (Sea-Horse). +//! +//! # Usage +//! +//! Construct a SQL statement with the library then execute the statement +//! with a database connector such as [SQLx](https://github.com/launchbadge/sqlx). +//! +//! Later we will release Document ORM (Sea-ORM) which you can load and save document on any supported relational database, +//! without the need of managing database connection on your own. +//! +//! ## Iden +//! +//! A trait for identifiers used in any query statement. +//! +//! Commonly implemented by Enum where each Enum represent a table found in a database, +//! and its variants include table name and column name. +//! +//! [`Iden::unquoted()`] must be implemented to provide a mapping between Enum variant and its corresponding string value. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! // For example Character table with column id, character, font_size... +//! pub enum Character { +//! Table, +//! Id, +//! Character, +//! FontSize, +//! SizeW, +//! SizeH, +//! FontId, +//! } +//! +//! // Mapping between Enum variant and its corresponding string value +//! impl Iden for Character { +//! fn unquoted(&self, s: &mut dyn FmtWrite) { +//! write!(s, "{}", match self { +//! Self::Table => "character", +//! Self::Id => "id", +//! Self::Character => "character", +//! Self::FontSize => "font_size", +//! Self::SizeW => "size_w", +//! Self::SizeH => "size_h", +//! Self::FontId => "font_id", +//! }).unwrap(); +//! } +//! } +//! ``` +//! +//! ## Expression +//! +//! Use [`Expr`] to construct select, join, where and having expression in query. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! assert_eq!( +//! Query::select() +//! .column(Char::Character) +//! .from(Char::Table) +//! .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) +//! .and_where( +//! Expr::expr(Expr::col(Char::SizeW).add(1)).mul(2) +//! .equals(Expr::expr(Expr::col(Char::SizeH).div(2)).sub(1)) +//! ) +//! .and_where(Expr::col(Char::SizeW).in_subquery( +//! Query::select() +//! .expr(Expr::cust("3 + 2 * 2")) +//! .take() +//! )) +//! .or_where(Expr::col(Char::Character).like("D").and(Expr::col(Char::Character).like("E"))) +//! .to_string(MysqlQueryBuilder::new()), +//! vec![ +//! "SELECT `character` FROM `character`", +//! "LEFT JOIN `font` ON `character`.`font_id` = `font`.`id`", +//! "WHERE ((`size_w` + 1) * 2 = (`size_h` / 2) - 1)", +//! "AND `size_w` IN (SELECT 3 + 2 * 2)", +//! "OR ((`character` LIKE 'D') AND (`character` LIKE 'E'))", +//! ].join(" ") +//! ); +//! ``` +//! +//! ## Query Select +//! +//! See [`SelectStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let query = Query::select() +//! .column(Char::Character) +//! .table_column(Font::Table, Font::Name) +//! .from(Char::Table) +//! .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) +//! .and_where(Expr::col(Char::SizeW).is_in(vec![3, 4])) +//! .and_where(Expr::col(Char::Character).like("A%")) +//! .to_owned(); +//! +//! assert_eq!( +//! query.to_string(MysqlQueryBuilder), +//! r#"SELECT `character`, `font`.`name` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id` WHERE `size_w` IN (3, 4) AND `character` LIKE 'A%'"# +//! ); +//! assert_eq!( +//! query.to_string(PostgresQueryBuilder), +//! r#"SELECT "character", "font"."name" FROM "character" LEFT JOIN "font" ON "character"."font_id" = "font"."id" WHERE "size_w" IN (3, 4) AND "character" LIKE 'A%'"# +//! ); +//! assert_eq!( +//! query.to_string(SqliteQueryBuilder), +//! r#"SELECT `character`, `font`.`name` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id` WHERE `size_w` IN (3, 4) AND `character` LIKE 'A%'"# +//! ); +//! ``` +//! +//! ## Query Insert +//! +//! See [`InsertStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let query = Query::insert() +//! .into_table(Glyph::Table) +//! .columns(vec![ +//! Glyph::Aspect, +//! Glyph::Image, +//! ]) +//! .values_panic(vec![ +//! 5.15.into(), +//! "12A".into(), +//! ]) +//! .json(json!({ +//! "aspect": 4.21, +//! "image": "123", +//! })) +//! .to_owned(); +//! +//! assert_eq!( +//! query.to_string(MysqlQueryBuilder), +//! r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (5.15, '12A'), (4.21, '123')"# +//! ); +//! assert_eq!( +//! query.to_string(PostgresQueryBuilder), +//! r#"INSERT INTO "glyph" ("aspect", "image") VALUES (5.15, '12A'), (4.21, '123')"# +//! ); +//! assert_eq!( +//! query.to_string(SqliteQueryBuilder), +//! r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (5.15, '12A'), (4.21, '123')"# +//! ); +//! ``` +//! +//! ## Query Update +//! +//! See [`UpdateStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let query = Query::update() +//! .into_table(Glyph::Table) +//! .values(vec![ +//! (Glyph::Aspect, 1.23.into()), +//! (Glyph::Image, "123".into()), +//! ]) +//! .and_where(Expr::col(Glyph::Id).eq(1)) +//! .to_owned(); +//! +//! assert_eq!( +//! query.to_string(MysqlQueryBuilder), +//! r#"UPDATE `glyph` SET `aspect` = 1.23, `image` = '123' WHERE `id` = 1"# +//! ); +//! assert_eq!( +//! query.to_string(PostgresQueryBuilder), +//! r#"UPDATE "glyph" SET "aspect" = 1.23, "image" = '123' WHERE "id" = 1"# +//! ); +//! assert_eq!( +//! query.to_string(SqliteQueryBuilder), +//! r#"UPDATE `glyph` SET `aspect` = 1.23, `image` = '123' WHERE `id` = 1"# +//! ); +//! ``` +//! +//! ## Query Delete +//! +//! See [`DeleteStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let query = Query::delete() +//! .from_table(Glyph::Table) +//! .or_where(Expr::col(Glyph::Id).lt(1)) +//! .or_where(Expr::col(Glyph::Id).gt(10)) +//! .to_owned(); +//! +//! assert_eq!( +//! query.to_string(MysqlQueryBuilder), +//! r#"DELETE FROM `glyph` WHERE (`id` < 1) OR (`id` > 10)"# +//! ); +//! assert_eq!( +//! query.to_string(PostgresQueryBuilder), +//! r#"DELETE FROM "glyph" WHERE ("id" < 1) OR ("id" > 10)"# +//! ); +//! assert_eq!( +//! query.to_string(SqliteQueryBuilder), +//! r#"DELETE FROM `glyph` WHERE (`id` < 1) OR (`id` > 10)"# +//! ); +//! ``` +//! +//! ## Table Create +//! +//! See [`TableCreateStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let table = Table::create() +//! .table(Char::Table) +//! .create_if_not_exists() +//! .col(ColumnDef::new(Char::Id).integer().not_null().auto_increment().primary_key()) +//! .col(ColumnDef::new(Char::FontSize).integer().not_null()) +//! .col(ColumnDef::new(Char::Character).string().not_null()) +//! .col(ColumnDef::new(Char::SizeW).integer().not_null()) +//! .col(ColumnDef::new(Char::SizeH).integer().not_null()) +//! .col(ColumnDef::new(Char::FontId).integer().default(Value::NULL)) +//! .foreign_key( +//! ForeignKey::create() +//! .name("FK_2e303c3a712662f1fc2a4d0aad6") +//! .table(Char::Table, Font::Table) +//! .col(Char::FontId, Font::Id) +//! .on_delete(ForeignKeyAction::Cascade) +//! .on_update(ForeignKeyAction::Cascade) +//! ) +//! .to_owned(); +//! +//! assert_eq!( +//! table.to_string(MysqlQueryBuilder), +//! vec![ +//! r#"CREATE TABLE IF NOT EXISTS `character` ("#, +//! r#"`id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,"#, +//! r#"`font_size` int NOT NULL,"#, +//! r#"`character` varchar NOT NULL,"#, +//! r#"`size_w` int NOT NULL,"#, +//! r#"`size_h` int NOT NULL,"#, +//! r#"`font_id` int DEFAULT NULL,"#, +//! r#"CONSTRAINT `FK_2e303c3a712662f1fc2a4d0aad6`"#, +//! r#"FOREIGN KEY `FK_2e303c3a712662f1fc2a4d0aad6` (`font_id`) REFERENCES `font` (`id`)"#, +//! r#"ON DELETE CASCADE ON UPDATE CASCADE"#, +//! r#")"#, +//! ].join(" ") +//! ); +//! assert_eq!( +//! table.to_string(PostgresQueryBuilder), +//! vec![ +//! r#"CREATE TABLE IF NOT EXISTS "character" ("#, +//! r#""id" serial NOT NULL PRIMARY KEY,"#, +//! r#""font_size" integer NOT NULL,"#, +//! r#""character" varchar NOT NULL,"#, +//! r#""size_w" integer NOT NULL,"#, +//! r#""size_h" integer NOT NULL,"#, +//! r#""font_id" integer DEFAULT NULL,"#, +//! r#"CONSTRAINT "FK_2e303c3a712662f1fc2a4d0aad6""#, +//! r#"FOREIGN KEY ("font_id") REFERENCES "font" ("id")"#, +//! r#"ON DELETE CASCADE ON UPDATE CASCADE"#, +//! r#")"#, +//! ].join(" ") +//! ); +//! assert_eq!( +//! table.to_string(SqliteQueryBuilder), +//! vec![ +//! r#"CREATE TABLE IF NOT EXISTS `character` ("#, +//! r#"`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,"#, +//! r#"`font_size` integer NOT NULL,"#, +//! r#"`character` text NOT NULL,"#, +//! r#"`size_w` integer NOT NULL,"#, +//! r#"`size_h` integer NOT NULL,"#, +//! r#"`font_id` integer DEFAULT NULL,"#, +//! r#"FOREIGN KEY (`font_id`) REFERENCES `font` (`id`) ON DELETE CASCADE ON UPDATE CASCADE"#, +//! r#")"#, +//! ].join(" ") +//! ); +//! ``` +//! +//! ## Table Alter +//! +//! See [`TableAlterStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let table = Table::alter() +//! .table(Font::Table) +//! .add_column(ColumnDef::new(Alias::new("new_col")).integer().not_null().default(100)) +//! .to_owned(); +//! +//! assert_eq!( +//! table.to_string(MysqlQueryBuilder), +//! r#"ALTER TABLE `font` ADD COLUMN `new_col` int NOT NULL DEFAULT 100"# +//! ); +//! assert_eq!( +//! table.to_string(PostgresQueryBuilder), +//! r#"ALTER TABLE "font" ADD COLUMN "new_col" integer NOT NULL DEFAULT 100"# +//! ); +//! assert_eq!( +//! table.to_string(SqliteQueryBuilder), +//! r#"ALTER TABLE `font` ADD COLUMN `new_col` integer NOT NULL DEFAULT 100"#, +//! ); +//! ``` +//! +//! ## Table Drop +//! +//! See [`TableDropStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let table = Table::drop() +//! .table(Glyph::Table) +//! .table(Char::Table) +//! .to_owned(); +//! +//! assert_eq!( +//! table.to_string(MysqlQueryBuilder), +//! r#"DROP TABLE `glyph`, `character`"# +//! ); +//! assert_eq!( +//! table.to_string(PostgresQueryBuilder), +//! r#"DROP TABLE "glyph", "character""# +//! ); +//! assert_eq!( +//! table.to_string(SqliteQueryBuilder), +//! r#"DROP TABLE `glyph`, `character`"# +//! ); +//! ``` +//! +//! ## Table Rename +//! +//! See [`TableRenameStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let table = Table::rename() +//! .table(Font::Table, Alias::new("font_new")) +//! .to_owned(); +//! +//! assert_eq!( +//! table.to_string(MysqlQueryBuilder), +//! r#"RENAME TABLE `font` TO `font_new`"# +//! ); +//! assert_eq!( +//! table.to_string(PostgresQueryBuilder), +//! r#"ALTER TABLE "font" RENAME TO "font_new""# +//! ); +//! assert_eq!( +//! table.to_string(SqliteQueryBuilder), +//! r#"ALTER TABLE `font` RENAME TO `font_new`"# +//! ); +//! ``` +//! +//! ## Table Truncate +//! +//! See [`TableTruncateStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let table = Table::truncate() +//! .table(Font::Table) +//! .to_owned(); +//! +//! assert_eq!( +//! table.to_string(MysqlQueryBuilder), +//! r#"TRUNCATE TABLE `font`"# +//! ); +//! assert_eq!( +//! table.to_string(PostgresQueryBuilder), +//! r#"TRUNCATE TABLE "font""# +//! ); +//! assert_eq!( +//! table.to_string(SqliteQueryBuilder), +//! r#"TRUNCATE TABLE `font`"# +//! ); +//! ``` +//! +//! ## Foreign Key Create +//! +//! See [`ForeignKeyCreateStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let foreign_key = ForeignKey::create() +//! .name("FK_character_font") +//! .table(Char::Table, Font::Table) +//! .col(Char::FontId, Font::Id) +//! .on_delete(ForeignKeyAction::Cascade) +//! .on_update(ForeignKeyAction::Cascade) +//! .to_owned(); +//! +//! assert_eq!( +//! foreign_key.to_string(MysqlQueryBuilder), +//! vec![ +//! r#"ALTER TABLE `character`"#, +//! r#"ADD CONSTRAINT `FK_character_font`"#, +//! r#"FOREIGN KEY `FK_character_font` (`font_id`) REFERENCES `font` (`id`)"#, +//! r#"ON DELETE CASCADE ON UPDATE CASCADE"#, +//! ].join(" ") +//! ); +//! assert_eq!( +//! foreign_key.to_string(PostgresQueryBuilder), +//! vec![ +//! r#"ALTER TABLE "character" ADD CONSTRAINT "FK_character_font""#, +//! r#"FOREIGN KEY ("font_id") REFERENCES "font" ("id")"#, +//! r#"ON DELETE CASCADE ON UPDATE CASCADE"#, +//! ].join(" ") +//! ); +//! // Sqlite does not support modification of foreign key constraints to existing tables +//! ``` +//! +//! ## Foreign Key Drop +//! +//! See [`ForeignKeyDropStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let foreign_key = ForeignKey::drop() +//! .name("FK_character_font") +//! .table(Char::Table) +//! .to_owned(); +//! +//! assert_eq!( +//! foreign_key.to_string(MysqlQueryBuilder), +//! r#"ALTER TABLE `character` DROP FOREIGN KEY `FK_character_font`"# +//! ); +//! assert_eq!( +//! foreign_key.to_string(PostgresQueryBuilder), +//! r#"ALTER TABLE "character" DROP CONSTRAINT "FK_character_font""# +//! ); +//! // Sqlite does not support modification of foreign key constraints to existing tables +//! ``` +//! +//! ## Index Create +//! +//! See [`IndexCreateStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let index = Index::create() +//! .name("idx-glyph-aspect") +//! .table(Glyph::Table) +//! .col(Glyph::Aspect) +//! .to_owned(); +//! +//! assert_eq!( +//! index.to_string(MysqlQueryBuilder), +//! r#"CREATE INDEX `idx-glyph-aspect` ON `glyph` (`aspect`)"# +//! ); +//! assert_eq!( +//! index.to_string(PostgresQueryBuilder), +//! r#"CREATE INDEX "idx-glyph-aspect" ON "glyph" ("aspect")"# +//! ); +//! assert_eq!( +//! index.to_string(SqliteQueryBuilder), +//! r#"CREATE INDEX `idx-glyph-aspect` ON `glyph` (`aspect`)"# +//! ); +//! ``` +//! +//! ## Index Drop +//! +//! See [`IndexDropStatement`] for the full documentation. +//! +//! ``` +//! use sea_query::{*, tests_cfg::*}; +//! +//! let index = Index::drop() +//! .name("idx-glyph-aspect") +//! .table(Glyph::Table) +//! .to_owned(); +//! +//! assert_eq!( +//! index.to_string(MysqlQueryBuilder), +//! r#"DROP INDEX `idx-glyph-aspect` ON `glyph`"# +//! ); +//! assert_eq!( +//! index.to_string(PostgresQueryBuilder), +//! r#"DROP INDEX "idx-glyph-aspect""# +//! ); +//! assert_eq!( +//! index.to_string(SqliteQueryBuilder), +//! r#"DROP INDEX `idx-glyph-aspect` ON `glyph`"# +//! ); +//! ``` + +pub mod backend; +pub mod expr; +pub mod foreign_key; +pub mod index; +pub mod query; +pub mod table; +pub mod tests_cfg; +pub mod types; +pub mod value; + +pub use backend::*; +pub use expr::*; +pub use foreign_key::*; +pub use index::*; +pub use query::*; +pub use table::*; +pub use types::*; +pub use value::*; \ No newline at end of file diff --git a/src/query/delete.rs b/src/query/delete.rs new file mode 100644 index 000000000..7ca88db79 --- /dev/null +++ b/src/query/delete.rs @@ -0,0 +1,366 @@ +use std::rc::Rc; +use crate::{backend::QueryBuilder, types::*, expr::*, value::*}; + +/// Delete existing rows from the table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let query = Query::delete() +/// .from_table(Glyph::Table) +/// .or_where(Expr::col(Glyph::Id).lt(1)) +/// .or_where(Expr::col(Glyph::Id).gt(10)) +/// .to_owned(); +/// +/// assert_eq!( +/// query.to_string(MysqlQueryBuilder), +/// r#"DELETE FROM `glyph` WHERE (`id` < 1) OR (`id` > 10)"# +/// ); +/// assert_eq!( +/// query.to_string(PostgresQueryBuilder), +/// r#"DELETE FROM "glyph" WHERE ("id" < 1) OR ("id" > 10)"# +/// ); +/// assert_eq!( +/// query.to_string(SqliteQueryBuilder), +/// r#"DELETE FROM `glyph` WHERE (`id` < 1) OR (`id` > 10)"# +/// ); +/// ``` +#[derive(Clone)] +pub struct DeleteStatement { + pub(crate) table: Option>, + pub(crate) wherei: Option>, + pub(crate) orders: Vec, + pub(crate) limit: Option, +} + +impl Default for DeleteStatement { + fn default() -> Self { + Self::new() + } +} + +impl DeleteStatement { + /// Construct a new [`DeleteStatement`] + pub fn new() -> Self { + Self { + table: None, + wherei: None, + orders: Vec::new(), + limit: None, + } + } + + /// Specify which table to delete from. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::delete() + /// .from_table(Glyph::Table) + /// .and_where(Expr::col(Glyph::Id).eq(1)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"DELETE FROM `glyph` WHERE `id` = 1"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"DELETE FROM "glyph" WHERE "id" = 1"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"DELETE FROM `glyph` WHERE `id` = 1"# + /// ); + /// ``` + #[allow(clippy::wrong_self_convention)] + pub fn from_table(&mut self, table: T) -> &mut Self + where T: Iden { + self.from_table_dyn(Rc::new(table)) + } + + /// Specify which table to delete from, variation of [`DeleteStatement::from_table`]. + #[allow(clippy::wrong_self_convention)] + pub fn from_table_dyn(&mut self, table: Rc) -> &mut Self { + self.table = Some(Box::new(TableRef::Table(table))); + self + } + + /// And where condition. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::delete() + /// .from_table(Glyph::Table) + /// .and_where(Expr::col(Glyph::Id).gt(1)) + /// .and_where(Expr::col(Glyph::Id).lt(10)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"DELETE FROM `glyph` WHERE (`id` > 1) AND (`id` < 10)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"DELETE FROM "glyph" WHERE ("id" > 1) AND ("id" < 10)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"DELETE FROM `glyph` WHERE (`id` > 1) AND (`id` < 10)"# + /// ); + /// ``` + pub fn and_where(&mut self, other: SimpleExpr) -> &mut Self { + self.and_or_where(BinOper::And, other) + } + + /// And where condition. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::delete() + /// .from_table(Glyph::Table) + /// .or_where(Expr::col(Glyph::Id).lt(1)) + /// .or_where(Expr::col(Glyph::Id).gt(10)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"DELETE FROM `glyph` WHERE (`id` < 1) OR (`id` > 10)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"DELETE FROM "glyph" WHERE ("id" < 1) OR ("id" > 10)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"DELETE FROM `glyph` WHERE (`id` < 1) OR (`id` > 10)"# + /// ); + /// ``` + pub fn or_where(&mut self, other: SimpleExpr) -> &mut Self { + self.and_or_where(BinOper::Or, other) + } + + fn and_or_where(&mut self, bopr: BinOper, right: SimpleExpr) -> &mut Self { + self.wherei = Self::merge_expr( + self.wherei.take(), + match bopr { + BinOper::And => BinOper::And, + BinOper::Or => BinOper::Or, + _ => panic!("not allow"), + }, + right + ); + self + } + + fn merge_expr(left: Option>, bopr: BinOper, right: SimpleExpr) -> Option> { + Some(Box::new(match left { + Some(left) => SimpleExpr::Binary( + left, + bopr, + Box::new(right) + ), + None => right, + })) + } + + /// Order by column. + pub fn order_by(&mut self, col: T, order: Order) -> &mut Self + where T: Iden { + self.order_by_dyn(Rc::new(col), order) + } + + /// Order by column, variation of [`DeleteStatement::order_by`]. + pub fn order_by_dyn(&mut self, col: Rc, order: Order) -> &mut Self { + self.orders.push(OrderExpr { + expr: SimpleExpr::Column(col), + order, + }); + self + } + + /// Order by column with table name prefix. + pub fn order_by_tbl + (&mut self, table: T, col: C, order: Order) -> &mut Self + where T: Iden, C: Iden { + self.order_by_tbl_dyn(Rc::new(table), Rc::new(col), order) + } + + /// Order by column with table name prefix, variation of [`DeleteStatement::order_by_tbl`]. + pub fn order_by_tbl_dyn(&mut self, table: Rc, col: Rc, order: Order) -> &mut Self { + self.orders.push(OrderExpr { + expr: SimpleExpr::TableColumn(table, col), + order, + }); + self + } + + /// Order by [`SimpleExpr`]. + pub fn order_by_expr(&mut self, expr: SimpleExpr, order: Order) -> &mut Self { + self.orders.push(OrderExpr { + expr, + order, + }); + self + } + + /// Order by custom string. + pub fn order_by_customs(&mut self, cols: Vec<(T, Order)>) -> &mut Self + where T: ToString { + let mut orders = cols.into_iter().map( + |(c, order)| OrderExpr { + expr: SimpleExpr::Custom(c.to_string()), + order, + }).collect(); + self.orders.append(&mut orders); + self + } + + /// Order by columns. + pub fn order_by_columns(&mut self, cols: Vec<(T, Order)>) -> &mut Self + where T: Iden { + self.order_by_columns_dyn(cols.into_iter().map( + |(c, order)| (Rc::new(c) as Rc, order) + ).collect()) + } + + /// Order by columns, variation of [`DeleteStatement::order_by_columns`]. + pub fn order_by_columns_dyn(&mut self, cols: Vec<(Rc, Order)>) -> &mut Self { + let mut orders = cols.into_iter().map( + |(c, order)| OrderExpr { + expr: SimpleExpr::Column(c), + order, + }).collect(); + self.orders.append(&mut orders); + self + } + + /// Order by columns with table prefix. + pub fn order_by_table_columns + (&mut self, cols: Vec<(T, C, Order)>) -> &mut Self + where T: Iden, C: Iden { + self.order_by_table_columns_dyn(cols.into_iter().map( + |(t, c, order)| (Rc::new(t) as Rc, Rc::new(c) as Rc, order) + ).collect()) + } + + /// Order by columns with table prefix, variation of [`DeleteStatement::order_by_columns`]. + #[allow(clippy::type_complexity)] + pub fn order_by_table_columns_dyn(&mut self, cols: Vec<(Rc, Rc, Order)>) -> &mut Self { + let mut orders = cols.into_iter().map( + |(t, c, order)| OrderExpr { + expr: SimpleExpr::TableColumn(t, c), + order, + }).collect(); + self.orders.append(&mut orders); + self + } + + /// Limit number of updated rows. + pub fn limit(&mut self, limit: u64) -> &mut Self { + self.limit = Some(Value::UInt(limit)); + self + } + + /// Build corresponding SQL statement for certain database backend and collect query parameters + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::delete() + /// .from_table(Glyph::Table) + /// .and_where(Expr::col(Glyph::Id).eq(1)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"DELETE FROM `glyph` WHERE `id` = 1"# + /// ); + /// + /// let mut params = Vec::new(); + /// let mut collector = |v| params.push(v); + /// + /// assert_eq!( + /// query.build_collect(MysqlQueryBuilder, &mut collector), + /// r#"DELETE FROM `glyph` WHERE `id` = ?"# + /// ); + /// assert_eq!( + /// params, + /// vec![ + /// Value::Int(1), + /// ] + /// ); + /// ``` + pub fn build_collect(&self, mut query_builder: T, collector: &mut dyn FnMut(Value)) -> String { + let mut sql = String::new(); + query_builder.prepare_delete_statement(self, &mut sql, collector); + sql + } + + /// Build corresponding SQL statement for certain database backend and collect query parameters into a vector + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let (query, params) = Query::delete() + /// .from_table(Glyph::Table) + /// .and_where(Expr::col(Glyph::Id).eq(1)) + /// .build(MysqlQueryBuilder); + /// + /// assert_eq!( + /// query, + /// r#"DELETE FROM `glyph` WHERE `id` = ?"# + /// ); + /// assert_eq!( + /// params, + /// vec![ + /// Value::Int(1), + /// ] + /// ); + /// ``` + pub fn build(&self, query_builder: T) -> (String, Vec) { + let mut params = Vec::new(); + let mut collector = |v| params.push(v); + let sql = self.build_collect(query_builder, &mut collector); + (sql, params) + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::delete() + /// .from_table(Glyph::Table) + /// .and_where(Expr::col(Glyph::Id).eq(1)) + /// .to_string(MysqlQueryBuilder); + /// + /// assert_eq!( + /// query, + /// r#"DELETE FROM `glyph` WHERE `id` = 1"# + /// ); + /// ``` + pub fn to_string(&self, query_builder: T) -> String { + let (mut string, values) = self.build(query_builder); + for v in values.iter() { + string = string.replacen("?", value_to_string(v).as_ref(), 1); + } + string + } +} \ No newline at end of file diff --git a/src/query/insert.rs b/src/query/insert.rs new file mode 100644 index 000000000..28c46918c --- /dev/null +++ b/src/query/insert.rs @@ -0,0 +1,333 @@ +use std::rc::Rc; +use serde_json::Value as JsonValue; +use crate::{backend::QueryBuilder, types::*, value::*}; + +/// Insert any new rows into an existing table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let query = Query::insert() +/// .into_table(Glyph::Table) +/// .columns(vec![ +/// Glyph::Aspect, +/// Glyph::Image, +/// ]) +/// .values_panic(vec![ +/// 5.15.into(), +/// "12A".into(), +/// ]) +/// .json(json!({ +/// "aspect": 4.21, +/// "image": "123", +/// })) +/// .to_owned(); +/// +/// assert_eq!( +/// query.to_string(MysqlQueryBuilder), +/// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (5.15, '12A'), (4.21, '123')"# +/// ); +/// assert_eq!( +/// query.to_string(PostgresQueryBuilder), +/// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (5.15, '12A'), (4.21, '123')"# +/// ); +/// assert_eq!( +/// query.to_string(SqliteQueryBuilder), +/// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (5.15, '12A'), (4.21, '123')"# +/// ); +/// ``` +#[derive(Clone)] +pub struct InsertStatement { + pub(crate) table: Option>, + pub(crate) columns: Vec>, + pub(crate) values: Vec>, +} + +impl Default for InsertStatement { + fn default() -> Self { + Self::new() + } +} + +impl InsertStatement { + /// Construct a new [`InsertStatement`] + pub fn new() -> Self { + Self { + table: None, + columns: Vec::new(), + values: Vec::new(), + } + } + + /// Specify which table to insert into. + /// + /// # Examples + /// + /// See [`InsertStatement::values`] + #[allow(clippy::wrong_self_convention)] + pub fn into_table(&mut self, table: T) -> &mut Self + where T: Iden { + self.into_table_dyn(Rc::new(table)) + } + + /// Specify which table to insert into, variation of [`InsertStatement::into_table`]. + /// + /// # Examples + /// + /// See [`InsertStatement::values`] + #[allow(clippy::wrong_self_convention)] + pub fn into_table_dyn(&mut self, table: Rc) -> &mut Self { + self.table = Some(Box::new(TableRef::Table(table))); + self + } + + /// Specify what columns to insert. + /// + /// # Examples + /// + /// See [`InsertStatement::values`] + pub fn columns(&mut self, columns: Vec) -> &mut Self + where C: Iden { + self.columns_dyn(columns.into_iter().map(|c| Rc::new(c) as Rc).collect()) + } + + /// Specify what columns to insert, variation of [`InsertStatement::columns`]. + /// + /// # Examples + /// + /// See [`InsertStatement::values`] + pub fn columns_dyn(&mut self, columns: Vec>) -> &mut Self { + self.columns = columns; + self + } + + /// Specify a row of values to be inserted. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::insert() + /// .into_table(Glyph::Table) + /// .columns(vec![ + /// Glyph::Aspect, + /// Glyph::Image, + /// ]) + /// .values(vec![ + /// 2.1345.into(), + /// "24B".into(), + /// ]) + /// .unwrap() + /// .values_panic(vec![ + /// 5.15.into(), + /// "12A".into(), + /// ]) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (2.1345, '24B'), (5.15, '12A')"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2.1345, '24B'), (5.15, '12A')"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (2.1345, '24B'), (5.15, '12A')"# + /// ); + /// ``` + pub fn values(&mut self, values: Vec) -> Result<&mut Self, String> { + if self.columns.len() != values.len() { + return Err(format!("columns and values length mismatch: {} != {}", self.columns.len(), values.len())); + } + self.values.push(values); + Ok(self) + } + + /// Specify a row of values to be inserted, variation of [`InsertStatement::values`]. + pub fn values_panic(&mut self, values: Vec) -> &mut Self { + self.values(values).unwrap() + } + + /// Specify a row of values to be inserted, taking input of json values. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::insert() + /// .into_table(Glyph::Table) + /// .columns(vec![ + /// Glyph::Aspect, + /// Glyph::Image, + /// ]) + /// .json(json!({ + /// "aspect": 2.1345, + /// "image": "24B", + /// })) + /// .json(json!({ + /// "aspect": 4.21, + /// "image": "123", + /// })) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (2.1345, '24B'), (4.21, '123')"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2.1345, '24B'), (4.21, '123')"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (2.1345, '24B'), (4.21, '123')"# + /// ); + /// ``` + pub fn json(&mut self, object: JsonValue) -> &mut Self { + match object { + JsonValue::Object(_) => (), + _ => panic!("object must be JsonValue::Object"), + } + let mut values = Vec::new(); + if self.columns.is_empty() { + let map = object.as_object().unwrap(); + let mut keys: Vec = map.keys().cloned().collect(); + keys.sort(); + for k in keys.iter() { + self.columns.push(Rc::new(Alias::new(k))); + } + } + for col in self.columns.iter() { + values.push( + match object.get(col.to_string()) { + Some(value) => json_value_to_mysql_value(value), + None => Value::NULL, + } + ); + } + self.values.push(values); + self + } + + /// Build corresponding SQL statement for certain database backend and collect query parameters + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::insert() + /// .into_table(Glyph::Table) + /// .columns(vec![ + /// Glyph::Aspect, + /// Glyph::Image, + /// ]) + /// .values_panic(vec![ + /// 3.1415.into(), + /// "041080".into(), + /// ]) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (3.1415, '041080')"# + /// ); + /// + /// let mut params = Vec::new(); + /// let mut collector = |v| params.push(v); + /// + /// assert_eq!( + /// query.build_collect(MysqlQueryBuilder, &mut collector), + /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (?, ?)"# + /// ); + /// assert_eq!( + /// params, + /// vec![ + /// Value::Double(3.1415), + /// Value::Bytes(String::from("041080").into_bytes()), + /// ] + /// ); + /// ``` + pub fn build_collect(&self, mut query_builder: T, collector: &mut dyn FnMut(Value)) -> String { + let mut sql = String::new(); + query_builder.prepare_insert_statement(self, &mut sql, collector); + sql + } + + /// Build corresponding SQL statement for certain database backend and collect query parameters into a vector + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let (query, params) = Query::insert() + /// .into_table(Glyph::Table) + /// .columns(vec![ + /// Glyph::Aspect, + /// Glyph::Image, + /// ]) + /// .values_panic(vec![ + /// 3.1415.into(), + /// "04108048005887010020060000204E0180400400".into(), + /// ]) + /// .build(MysqlQueryBuilder); + /// + /// assert_eq!( + /// query, + /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (?, ?)"# + /// ); + /// assert_eq!( + /// params, + /// vec![ + /// Value::Double(3.1415), + /// Value::Bytes(String::from("04108048005887010020060000204E0180400400").into_bytes()), + /// ] + /// ); + /// ``` + pub fn build(&self, query_builder: T) -> (String, Vec) { + let mut params = Vec::new(); + let mut collector = |v| params.push(v); + let sql = self.build_collect(query_builder, &mut collector); + (sql, params) + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::insert() + /// .into_table(Glyph::Table) + /// .columns(vec![ + /// Glyph::Aspect, + /// Glyph::Image, + /// ]) + /// .values_panic(vec![ + /// 3.1415.into(), + /// "041".into(), + /// ]) + /// .to_string(MysqlQueryBuilder); + /// + /// assert_eq!( + /// query, + /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (3.1415, '041')"# + /// ); + /// ``` + pub fn to_string(&self, query_builder: T) -> String { + let (mut string, values) = self.build(query_builder); + for v in values.iter() { + string = string.replacen("?", value_to_string(v).as_ref(), 1); + } + string + + } +} \ No newline at end of file diff --git a/src/query/mod.rs b/src/query/mod.rs new file mode 100644 index 000000000..4b8dfb2fb --- /dev/null +++ b/src/query/mod.rs @@ -0,0 +1,53 @@ +//! Table query (select, insert, update & delete). +//! +//! # Usage +//! +//! - Query Select, see [`SelectStatement`] +//! - Query Insert, see [`InsertStatement`] +//! - Query Update, see [`UpdateStatement`] +//! - Query Delete, see [`DeleteStatement`] + +mod select; +mod insert; +mod update; +mod delete; + +pub use select::*; +pub use insert::*; +pub use update::*; +pub use delete::*; + +/// Shorthand for constructing any table query +#[derive(Clone)] +pub struct Query; + +/// All available types of table query +#[derive(Clone)] +pub enum QueryStatement { + Select(SelectStatement), + Insert(InsertStatement), + Update(UpdateStatement), + Delete(DeleteStatement), +} + +impl Query { + /// Construct table [`SelectStatement`] + pub fn select() -> SelectStatement { + SelectStatement::new() + } + + /// Construct table [`InsertStatement`] + pub fn insert() -> InsertStatement { + InsertStatement::new() + } + + /// Construct table [`UpdateStatement`] + pub fn update() -> UpdateStatement { + UpdateStatement::new() + } + + /// Construct table [`DeleteStatement`] + pub fn delete() -> DeleteStatement { + DeleteStatement::new() + } +} diff --git a/src/query/select.rs b/src/query/select.rs new file mode 100644 index 000000000..9e2f68c9f --- /dev/null +++ b/src/query/select.rs @@ -0,0 +1,1319 @@ +use std::rc::Rc; +use crate::{backend::QueryBuilder, types::*, expr::*, value::*}; + +/// Select rows from an existing table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let query = Query::select() +/// .column(Char::Character) +/// .table_column(Font::Table, Font::Name) +/// .from(Char::Table) +/// .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) +/// .and_where(Expr::col(Char::SizeW).is_in(vec![3, 4])) +/// .and_where(Expr::col(Char::Character).like("A%")) +/// .to_owned(); +/// +/// assert_eq!( +/// query.to_string(MysqlQueryBuilder), +/// r#"SELECT `character`, `font`.`name` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id` WHERE `size_w` IN (3, 4) AND `character` LIKE 'A%'"# +/// ); +/// assert_eq!( +/// query.to_string(PostgresQueryBuilder), +/// r#"SELECT "character", "font"."name" FROM "character" LEFT JOIN "font" ON "character"."font_id" = "font"."id" WHERE "size_w" IN (3, 4) AND "character" LIKE 'A%'"# +/// ); +/// assert_eq!( +/// query.to_string(SqliteQueryBuilder), +/// r#"SELECT `character`, `font`.`name` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id` WHERE `size_w` IN (3, 4) AND `character` LIKE 'A%'"# +/// ); +/// ``` +#[derive(Clone)] +pub struct SelectStatement { + pub(crate) distinct: Option, + pub(crate) selects: Vec, + pub(crate) from: Option>, + pub(crate) join: Vec, + pub(crate) wherei: Vec, + pub(crate) groups: Vec, + pub(crate) having: Vec, + pub(crate) orders: Vec, + pub(crate) limit: Option, + pub(crate) offset: Option, +} + +/// List of distinct keywords that can be used in select statement +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum SelectDistinct { + All, + Distinct, + DistinctRow, +} + +/// Select expression used in select statement +#[derive(Clone)] +pub struct SelectExpr { + pub expr: SimpleExpr, + pub alias: Option>, +} + +/// Join expression used in select statement +#[derive(Clone)] +pub struct JoinExpr { + pub join: JoinType, + pub table: Box, + pub on: Option, +} + +impl Into for SimpleExpr { + fn into(self) -> SelectExpr { + SelectExpr { + expr: self, + alias: None, + } + } +} + +impl Default for SelectStatement { + fn default() -> Self { + Self::new() + } +} + +impl SelectStatement { + /// Construct a new [`SelectStatement`] + pub fn new() -> Self { + Self { + distinct: None, + selects: Vec::new(), + from: None, + join: Vec::new(), + wherei: Vec::new(), + groups: Vec::new(), + having: Vec::new(), + orders: Vec::new(), + limit: None, + offset: None, + } + } + + /// Take the ownership of data in the current [`SelectStatement`] + pub fn take(&mut self) -> Self { + Self { + distinct: self.distinct.take(), + selects: std::mem::replace(&mut self.selects, Vec::new()), + from: self.from.take(), + join: std::mem::replace(&mut self.join, Vec::new()), + wherei: std::mem::replace(&mut self.wherei, Vec::new()), + groups: std::mem::replace(&mut self.groups, Vec::new()), + having: std::mem::replace(&mut self.having, Vec::new()), + orders: std::mem::replace(&mut self.orders, Vec::new()), + limit: self.limit.take(), + offset: self.offset.take(), + } + } + + /// A shorthand to express if ... else ... when constructing the select statement. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Char::Character) + /// .from(Char::Table) + /// .conditions( + /// true, + /// |x| { x.and_where(Expr::col(Char::FontId).eq(5)); }, + /// |x| { x.and_where(Expr::col(Char::FontId).eq(10)); } + /// ) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character` FROM `character` WHERE `font_id` = 5"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character" FROM "character" WHERE "font_id" = 5"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character` FROM `character` WHERE `font_id` = 5"# + /// ); + /// ``` + pub fn conditions(&mut self, b: bool, if_true: T, if_false: F) -> &mut Self + where T: FnOnce(&mut Self), F: FnOnce(&mut Self) { + if b { + if_true(self) + } else { + if_false(self) + } + self + } + + /// Add a new select expression from [`SelectExpr`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .expr(Expr::col(Char::Id).max()) + /// .expr((1..10_i32).fold(Expr::value(0), |expr, i| { + /// expr.add(Expr::value(i)) + /// })) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT MAX(`id`), 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT MAX("id"), 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT MAX(`id`), 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 FROM `character`"# + /// ); + /// ``` + pub fn expr(&mut self, expr: T) -> &mut Self + where T: Into { + self.selects.push(expr.into()); + self + } + + /// Add select expressions from vector of [`SelectExpr`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .exprs(vec![ + /// Expr::col(Char::Id).max(), + /// (1..10_i32).fold(Expr::value(0), |expr, i| { + /// expr.add(Expr::value(i)) + /// }), + /// ]) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT MAX(`id`), 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT MAX("id"), 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT MAX(`id`), 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 FROM `character`"# + /// ); + /// ``` + pub fn exprs(&mut self, exprs: Vec) -> &mut Self + where T: Into { + self.selects.append(&mut exprs.into_iter().map(|c| c.into()).collect()); + self + } + + /// Select distinct + pub fn distinct(&mut self) -> &mut Self { + self.distinct = Some(SelectDistinct::Distinct); + self + } + + /// Select column. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .column(Char::Character) + /// .column(Char::SizeW) + /// .column(Char::SizeH) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character`"# + /// ); + /// ``` + pub fn column(&mut self, col: C) -> &mut Self + where C: Iden { + self.column_dyn(Rc::new(col)) + } + + /// Select column, variation of [`SelectStatement::column`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// use std::rc::Rc; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .column_dyn(Rc::new(Char::Character)) + /// .column_dyn(Rc::new(Char::SizeW)) + /// .column_dyn(Rc::new(Char::SizeH)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character`"# + /// ); + /// ``` + pub fn column_dyn(&mut self, col: Rc) -> &mut Self { + self.expr(SimpleExpr::Column(col)); + self + } + + /// Select columns. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .columns(vec![ + /// Char::Character, + /// Char::SizeW, + /// Char::SizeH, + /// ]) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character`"# + /// ); + /// ``` + pub fn columns(&mut self, cols: Vec) -> &mut Self + where T: Iden { + self.columns_dyn(cols.into_iter().map(|c| Rc::new(c) as Rc).collect()) + } + + /// Select column, variation of [`SelectStatement::column`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// use std::rc::Rc; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .columns_dyn(vec![ + /// Rc::new(Char::Character), + /// Rc::new(Char::SizeW), + /// Rc::new(Char::SizeH), + /// ]) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character`"# + /// ); + /// ``` + pub fn columns_dyn(&mut self, cols: Vec>) -> &mut Self { + self.exprs(cols.into_iter().map(|c| SimpleExpr::Column(c)).collect()); + self + } + + /// Select column with table prefix. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .table_column(Char::Table, Char::Character) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`.`character` FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character"."character" FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`.`character` FROM `character`"# + /// ); + /// ``` + pub fn table_column(&mut self, t: T, c: C) -> &mut Self + where T: Iden, C: Iden { + self.table_column_dyn(Rc::new(t), Rc::new(c)) + } + + /// Select column with table prefix, variation of [`SelectStatement::table_column`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// use std::rc::Rc; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .table_column_dyn(Rc::new(Char::Table), Rc::new(Char::Character)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`.`character` FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character"."character" FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`.`character` FROM `character`"# + /// ); + /// ``` + pub fn table_column_dyn(&mut self, t: Rc, c: Rc) -> &mut Self { + self.expr(SimpleExpr::TableColumn(t, c)); + self + } + + /// Select columns with table prefix. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .table_columns(vec![ + /// (Char::Table, Char::Character), + /// (Char::Table, Char::SizeW), + /// (Char::Table, Char::SizeH), + /// ]) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`.`character`, `character`.`size_w`, `character`.`size_h` FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character"."character", "character"."size_w", "character"."size_h" FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`.`character`, `character`.`size_w`, `character`.`size_h` FROM `character`"# + /// ); + /// ``` + pub fn table_columns(&mut self, cols: Vec<(T, C)>) -> &mut Self + where T: Iden, C: Iden { + self.table_columns_dyn(cols.into_iter().map( + |(t, c)| (Rc::new(t) as Rc, Rc::new(c) as Rc) + ).collect()) + } + + /// Select columns with table prefix, variation of [`SelectStatement::table_columns_dyn`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// use std::rc::Rc; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .table_columns_dyn(vec![ + /// (Rc::new(Char::Table), Rc::new(Char::Character)), + /// (Rc::new(Char::Table), Rc::new(Char::SizeW)), + /// (Rc::new(Char::Table), Rc::new(Char::SizeH)), + /// ]) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`.`character`, `character`.`size_w`, `character`.`size_h` FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character"."character", "character"."size_w", "character"."size_h" FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`.`character`, `character`.`size_w`, `character`.`size_h` FROM `character`"# + /// ); + /// ``` + pub fn table_columns_dyn(&mut self, cols: Vec<(Rc, Rc)>) -> &mut Self { + self.exprs(cols.into_iter().map(|(t, c)| SimpleExpr::TableColumn(t, c)).collect()); + self + } + + /// Select column. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .expr_alias(Expr::col(Char::Character), Alias::new("C")) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character` AS `C` FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character" AS "C" FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character` AS `C` FROM `character`"# + /// ); + /// ``` + pub fn expr_alias(&mut self, expr: T, alias: A) -> &mut Self + where T: Into, A: Iden { + self.expr(SelectExpr { + expr: expr.into(), + alias: Some(Rc::new(alias)) + }); + self + } + + /// From table. + pub fn from(&mut self, table: T) -> &mut Self + where T: Iden { + self.from_dyn(Rc::new(table)) + } + + /// From table, variation of [`SelectStatement::from`]. + pub fn from_dyn(&mut self, table: Rc) -> &mut Self { + self.from_from(TableRef::Table(table)); + self + } + + /// From table with alias. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let table_alias = Alias::new("char"); + /// + /// let query = Query::select() + /// .from_alias(Char::Table, table_alias.clone()) + /// .table_column(table_alias, Char::Character) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `char`.`character` FROM `character` AS `char`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "char"."character" FROM "character" AS "char""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `char`.`character` FROM `character` AS `char`"# + /// ); + /// ``` + pub fn from_alias(&mut self, table: T, alias: Y) -> &mut Self + where T: Iden, Y: Iden { + self.from_alias_dyn(Rc::new(table), Rc::new(alias)) + } + + /// From table with alias, variation of [`SelectStatement::from_alias`]. + pub fn from_alias_dyn(&mut self, table: Rc, alias: Rc) -> &mut Self { + self.from_from(TableRef::TableAlias(table, alias)); + self + } + + /// From sub-query. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns(vec![ + /// Glyph::Image + /// ]) + /// .from_subquery( + /// Query::select() + /// .columns(vec![ + /// Glyph::Image, Glyph::Aspect + /// ]) + /// .from(Glyph::Table) + /// .take(), + /// Alias::new("subglyph") + /// ) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `image` FROM (SELECT `image`, `aspect` FROM `glyph`) AS `subglyph`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "image" FROM (SELECT "image", "aspect" FROM "glyph") AS "subglyph""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `image` FROM (SELECT `image`, `aspect` FROM `glyph`) AS `subglyph`"# + /// ); + /// ``` + pub fn from_subquery(&mut self, query: SelectStatement, alias: T) -> &mut Self + where T: Iden { + self.from_subquery_dyn(query, Rc::new(alias)) + } + + /// From sub-query, variation of [`SelectStatement::from_subquery`]. + pub fn from_subquery_dyn(&mut self, query: SelectStatement, alias: Rc) -> &mut Self { + self.from_from(TableRef::SubQuery(query, alias)); + self + } + + fn from_from(&mut self, select: TableRef) -> &mut Self { + self.from = Some(Box::new(select)); + self + } + + /// Left join. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Char::Character) + /// .table_column(Font::Table, Font::Name) + /// .from(Char::Table) + /// .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `font`.`name` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "font"."name" FROM "character" LEFT JOIN "font" ON "character"."font_id" = "font"."id""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `font`.`name` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id`"# + /// ); + /// ``` + pub fn left_join(&mut self, table: T, condition: SimpleExpr) -> &mut Self + where T: Iden { + self.left_join_dyn(Rc::new(table), condition) + } + + /// Left join, variation of [`SelectStatement::left_join`]. + pub fn left_join_dyn(&mut self, table: Rc, condition: SimpleExpr) -> &mut Self { + self.join_join(JoinType::LeftJoin, TableRef::Table(table), JoinOn::Condition(Box::new(condition))); + self + } + + /// Inner join. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Char::Character) + /// .table_column(Font::Table, Font::Name) + /// .from(Char::Table) + /// .inner_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `font`.`name` FROM `character` INNER JOIN `font` ON `character`.`font_id` = `font`.`id`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "font"."name" FROM "character" INNER JOIN "font" ON "character"."font_id" = "font"."id""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `font`.`name` FROM `character` INNER JOIN `font` ON `character`.`font_id` = `font`.`id`"# + /// ); + /// ``` + pub fn inner_join(&mut self, table: T, condition: SimpleExpr) -> &mut Self + where T: Iden { + self.inner_join_dyn(Rc::new(table), condition) + } + + /// Inner join, variation of [`SelectStatement::inner_join`]. + pub fn inner_join_dyn(&mut self, table: Rc, condition: SimpleExpr) -> &mut Self { + self.join_join(JoinType::InnerJoin, TableRef::Table(table), JoinOn::Condition(Box::new(condition))); + self + } + + /// Join with other table by [`JoinType`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Char::Character) + /// .table_column(Font::Table, Font::Name) + /// .from(Char::Table) + /// .join(JoinType::RightJoin, Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `font`.`name` FROM `character` RIGHT JOIN `font` ON `character`.`font_id` = `font`.`id`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "font"."name" FROM "character" RIGHT JOIN "font" ON "character"."font_id" = "font"."id""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `font`.`name` FROM `character` RIGHT JOIN `font` ON `character`.`font_id` = `font`.`id`"# + /// ); + /// ``` + pub fn join(&mut self, join: JoinType, table: T, condition: SimpleExpr) -> &mut Self + where T: Iden { + self.join_dyn(join, Rc::new(table), condition) + } + + /// Join with other table by [`JoinType`], variation of [`SelectStatement::join`]. + pub fn join_dyn(&mut self, join: JoinType, table: Rc, condition: SimpleExpr) -> &mut Self { + self.join_join(join, TableRef::Table(table), JoinOn::Condition(Box::new(condition))); + self + } + + /// Join with sub-query. + /// + /// # Examples + /// + /// ... + /// + pub fn join_subquery(&mut self, join: JoinType, query: SelectStatement, alias: T, condition: SimpleExpr) -> &mut Self + where T: Iden { + self.join_subquery_dyn(join, query, Rc::new(alias), condition) + } + + /// Join with sub-query, variation of [`SelectStatement::join_subquery`]. + pub fn join_subquery_dyn(&mut self, join: JoinType, query: SelectStatement, alias: Rc, condition: SimpleExpr) -> &mut Self { + self.join_join(join, TableRef::SubQuery(query, alias), JoinOn::Condition(Box::new(condition))); + self + } + + fn join_join(&mut self, join: JoinType, table: TableRef, on: JoinOn) -> &mut Self { + self.join.push(JoinExpr { + join, + table: Box::new(table), + on: Some(on), + }); + self + } + + /// Group by columns. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Char::Character) + /// .table_column(Font::Table, Font::Name) + /// .from(Char::Table) + /// .join(JoinType::RightJoin, Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + /// .group_by_columns(vec![ + /// Char::Character, + /// ]) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `font`.`name` FROM `character` RIGHT JOIN `font` ON `character`.`font_id` = `font`.`id` GROUP BY `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "font"."name" FROM "character" RIGHT JOIN "font" ON "character"."font_id" = "font"."id" GROUP BY "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `font`.`name` FROM `character` RIGHT JOIN `font` ON `character`.`font_id` = `font`.`id` GROUP BY `character`"# + /// ); + /// ``` + pub fn group_by_columns(&mut self, cols: Vec) -> &mut Self + where T: Iden { + self.group_by_columns_dyn(cols.into_iter().map(|c| Rc::new(c) as Rc).collect()) + } + + /// Group by columns, variation of [`SelectStatement::group_by_columns`]. + pub fn group_by_columns_dyn(&mut self, cols: Vec>) -> &mut Self { + self.add_group_by(cols.into_iter().map(|c| SimpleExpr::Column(c)).collect()); + self + } + + /// Group by columns with table prefix. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Char::Character) + /// .table_column(Font::Table, Font::Name) + /// .from(Char::Table) + /// .join(JoinType::RightJoin, Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + /// .group_by_table_columns(vec![ + /// (Char::Table, Char::Character), + /// ]) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `font`.`name` FROM `character` RIGHT JOIN `font` ON `character`.`font_id` = `font`.`id` GROUP BY `character`.`character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "font"."name" FROM "character" RIGHT JOIN "font" ON "character"."font_id" = "font"."id" GROUP BY "character"."character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character`, `font`.`name` FROM `character` RIGHT JOIN `font` ON `character`.`font_id` = `font`.`id` GROUP BY `character`.`character`"# + /// ); + /// ``` + pub fn group_by_table_columns(&mut self, cols: Vec<(T, T)>) -> &mut Self + where T: Iden { + self.group_by_table_columns_dyn(cols.into_iter().map( + |(t, c)| (Rc::new(t) as Rc, Rc::new(c) as Rc) + ).collect()) + } + + /// Group by columns with table prefix, variation of [`SelectStatement::group_by_table_columns`]. + pub fn group_by_table_columns_dyn(&mut self, cols: Vec<(Rc, Rc)>) -> &mut Self { + self.add_group_by(cols.into_iter().map(|(t, c)| SimpleExpr::TableColumn(t, c)).collect()); + self + } + + /// And where condition. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .table_column(Glyph::Table, Glyph::Image) + /// .from(Glyph::Table) + /// .and_where(Expr::tbl(Glyph::Table, Glyph::Aspect).is_in(vec![3, 4])) + /// .and_where(Expr::tbl(Glyph::Table, Glyph::Image).like("A%")) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `glyph`.`image` FROM `glyph` WHERE `glyph`.`aspect` IN (3, 4) AND `glyph`.`image` LIKE 'A%'"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "glyph"."image" FROM "glyph" WHERE "glyph"."aspect" IN (3, 4) AND "glyph"."image" LIKE 'A%'"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `glyph`.`image` FROM `glyph` WHERE `glyph`.`aspect` IN (3, 4) AND `glyph`.`image` LIKE 'A%'"# + /// ); + /// ``` + pub fn and_where(&mut self, other: SimpleExpr) -> &mut Self { + self.wherei.push(LogicalChainOper::And(other)); + self + } + + /// And where condition, variation of [`SelectStatement::and_where`]. + pub fn and_where_option(&mut self, other: Option) -> &mut Self { + if let Some(other) = other { + self.wherei.push(LogicalChainOper::And(other)); + } + self + } + + /// Or where condition. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .table_column(Glyph::Table, Glyph::Image) + /// .from(Glyph::Table) + /// .or_where(Expr::tbl(Glyph::Table, Glyph::Aspect).is_in(vec![3, 4])) + /// .or_where(Expr::tbl(Glyph::Table, Glyph::Image).like("A%")) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `glyph`.`image` FROM `glyph` WHERE `glyph`.`aspect` IN (3, 4) OR `glyph`.`image` LIKE 'A%'"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "glyph"."image" FROM "glyph" WHERE "glyph"."aspect" IN (3, 4) OR "glyph"."image" LIKE 'A%'"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `glyph`.`image` FROM `glyph` WHERE `glyph`.`aspect` IN (3, 4) OR `glyph`.`image` LIKE 'A%'"# + /// ); + /// ``` + pub fn or_where(&mut self, other: SimpleExpr) -> &mut Self { + self.wherei.push(LogicalChainOper::Or(other)); + self + } + + /// Add group by expressions from vector of [`SelectExpr`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .column(Char::Character) + /// .add_group_by(vec![ + /// Expr::col(Char::SizeW).into(), + /// Expr::col(Char::SizeH).into(), + /// ]) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character` FROM `character` GROUP BY `size_w`, `size_h`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character" FROM "character" GROUP BY "size_w", "size_h""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `character` FROM `character` GROUP BY `size_w`, `size_h`"# + /// ); + /// ``` + pub fn add_group_by(&mut self, mut expr: Vec) -> &mut Self { + self.groups.append(&mut expr); + self + } + + /// And having condition. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Glyph::Aspect) + /// .expr(Expr::col(Glyph::Image).max()) + /// .from(Glyph::Table) + /// .group_by_columns(vec![ + /// Glyph::Aspect, + /// ]) + /// .and_having(Expr::col(Glyph::Aspect).gt(2)) + /// .and_having(Expr::col(Glyph::Aspect).lt(8)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `aspect`, MAX(`image`) FROM `glyph` GROUP BY `aspect` HAVING `aspect` > 2 AND `aspect` < 8"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "aspect", MAX("image") FROM "glyph" GROUP BY "aspect" HAVING "aspect" > 2 AND "aspect" < 8"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `aspect`, MAX(`image`) FROM `glyph` GROUP BY `aspect` HAVING `aspect` > 2 AND `aspect` < 8"# + /// ); + /// ``` + pub fn and_having(&mut self, other: SimpleExpr) -> &mut Self { + self.having.push(LogicalChainOper::And(other)); + self + } + + /// Or having condition. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Glyph::Aspect) + /// .expr(Expr::col(Glyph::Image).max()) + /// .from(Glyph::Table) + /// .group_by_columns(vec![ + /// Glyph::Aspect, + /// ]) + /// .or_having(Expr::col(Glyph::Aspect).lt(1)) + /// .or_having(Expr::col(Glyph::Aspect).gt(10)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `aspect`, MAX(`image`) FROM `glyph` GROUP BY `aspect` HAVING `aspect` < 1 OR `aspect` > 10"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "aspect", MAX("image") FROM "glyph" GROUP BY "aspect" HAVING "aspect" < 1 OR "aspect" > 10"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `aspect`, MAX(`image`) FROM `glyph` GROUP BY `aspect` HAVING `aspect` < 1 OR `aspect` > 10"# + /// ); + /// ``` + pub fn or_having(&mut self, other: SimpleExpr) -> &mut Self { + self.having.push(LogicalChainOper::Or(other)); + self + } + + /// Order by column. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Glyph::Aspect) + /// .from(Glyph::Table) + /// .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + /// .order_by(Glyph::Image, Order::Desc) + /// .order_by_tbl(Glyph::Table, Glyph::Aspect, Order::Asc) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, 0) > 2 ORDER BY `image` DESC, `glyph`.`aspect` ASC"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "aspect" FROM "glyph" WHERE COALESCE("aspect", 0) > 2 ORDER BY "image" DESC, "glyph"."aspect" ASC"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, 0) > 2 ORDER BY `image` DESC, `glyph`.`aspect` ASC"# + /// ); + /// ``` + pub fn order_by(&mut self, col: T, order: Order) -> &mut Self + where T: Iden { + self.order_by_dyn(Rc::new(col), order) + } + + /// Order by column, variation of [`SelectStatement::order_by`] + pub fn order_by_dyn(&mut self, col: Rc, order: Order) -> &mut Self { + self.orders.push(OrderExpr { + expr: SimpleExpr::Column(col), + order, + }); + self + } + + /// Order by column with table prefix. + /// + /// # Examples + /// + /// See [`SelectStatement::order_by`]. + pub fn order_by_tbl + (&mut self, table: T, col: C, order: Order) -> &mut Self + where T: Iden, C: Iden { + self.order_by_tbl_dyn(Rc::new(table), Rc::new(col), order) + } + + /// Order by column with table prefix, variation of [`SelectStatement::order_by_tbl`]. + pub fn order_by_tbl_dyn(&mut self, table: Rc, col: Rc, order: Order) -> &mut Self { + self.orders.push(OrderExpr { + expr: SimpleExpr::TableColumn(table, col), + order, + }); + self + } + + /// Order by [`SimpleExpr`]. + pub fn order_by_expr(&mut self, expr: SimpleExpr, order: Order) -> &mut Self { + self.orders.push(OrderExpr { + expr, + order, + }); + self + } + + /// Order by custom string expression. + pub fn order_by_customs(&mut self, cols: Vec<(T, Order)>) -> &mut Self + where T: ToString { + let mut orders = cols.into_iter().map( + |(c, order)| OrderExpr { + expr: SimpleExpr::Custom(c.to_string()), + order, + }).collect(); + self.orders.append(&mut orders); + self + } + + /// Order by vector of columns. + pub fn order_by_columns(&mut self, cols: Vec<(T, Order)>) -> &mut Self + where T: Iden { + self.order_by_columns_dyn(cols.into_iter().map( + |(c, order)| (Rc::new(c) as Rc, order) + ).collect()) + } + + /// Order by vector of columns, variation of [`SelectStatement::order_by_columns`]. + pub fn order_by_columns_dyn(&mut self, cols: Vec<(Rc, Order)>) -> &mut Self { + let mut orders = cols.into_iter().map( + |(c, order)| OrderExpr { + expr: SimpleExpr::Column(c), + order, + }).collect(); + self.orders.append(&mut orders); + self + } + + /// Order by vector of columns with table prefix. + pub fn order_by_table_columns + (&mut self, cols: Vec<(T, C, Order)>) -> &mut Self + where T: Iden, C: Iden { + self.order_by_table_columns_dyn(cols.into_iter().map( + |(t, c, order)| (Rc::new(t) as Rc, Rc::new(c) as Rc, order) + ).collect()) + } + + /// Order by vector of columns with table prefix, variation of [`SelectStatement::order_by_table_columns`]. + #[allow(clippy::type_complexity)] + pub fn order_by_table_columns_dyn(&mut self, cols: Vec<(Rc, Rc, Order)>) -> &mut Self { + let mut orders = cols.into_iter().map( + |(t, c, order)| OrderExpr { + expr: SimpleExpr::TableColumn(t, c), + order, + }).collect(); + self.orders.append(&mut orders); + self + } + + /// Limit the number of returned rows. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Glyph::Aspect) + /// .from(Glyph::Table) + /// .limit(10) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `aspect` FROM `glyph` LIMIT 10"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "aspect" FROM "glyph" LIMIT 10"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `aspect` FROM `glyph` LIMIT 10"# + /// ); + /// ``` + pub fn limit(&mut self, limit: u64) -> &mut Self { + self.limit = Some(Value::UInt(limit)); + self + } + + /// Offset number of returned rows. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Glyph::Aspect) + /// .from(Glyph::Table) + /// .limit(10) + /// .offset(10) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `aspect` FROM `glyph` LIMIT 10 OFFSET 10"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "aspect" FROM "glyph" LIMIT 10 OFFSET 10"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT `aspect` FROM `glyph` LIMIT 10 OFFSET 10"# + /// ); + /// ``` + pub fn offset(&mut self, offset: u64) -> &mut Self { + self.offset = Some(Value::UInt(offset)); + self + } + + /// Build corresponding SQL statement for certain database backend and collect query parameters + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Glyph::Aspect) + /// .from(Glyph::Table) + /// .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + /// .order_by(Glyph::Image, Order::Desc) + /// .order_by_tbl(Glyph::Table, Glyph::Aspect, Order::Asc) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, 0) > 2 ORDER BY `image` DESC, `glyph`.`aspect` ASC"# + /// ); + /// + /// let mut params = Vec::new(); + /// let mut collector = |v| params.push(v); + /// + /// assert_eq!( + /// query.build_collect(MysqlQueryBuilder, &mut collector), + /// r#"SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, ?) > ? ORDER BY `image` DESC, `glyph`.`aspect` ASC"# + /// ); + /// assert_eq!( + /// params, + /// vec![Value::Int(0), Value::Int(2)] + /// ); + /// ``` + pub fn build_collect(&self, mut query_builder: T, collector: &mut dyn FnMut(Value)) -> String { + let mut sql = String::new(); + query_builder.prepare_select_statement(self, &mut sql, collector); + sql + } + + /// Build corresponding SQL statement for certain database backend and collect query parameters into a vector + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let (query, params) = Query::select() + /// .column(Glyph::Aspect) + /// .from(Glyph::Table) + /// .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + /// .order_by(Glyph::Image, Order::Desc) + /// .order_by_tbl(Glyph::Table, Glyph::Aspect, Order::Asc) + /// .build(MysqlQueryBuilder); + /// + /// assert_eq!( + /// query, + /// r#"SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, ?) > ? ORDER BY `image` DESC, `glyph`.`aspect` ASC"# + /// ); + /// assert_eq!( + /// params, + /// vec![Value::Int(0), Value::Int(2)] + /// ); + /// ``` + pub fn build(&self, query_builder: T) -> (String, Vec) { + let mut params = Vec::new(); + let mut collector = |v| params.push(v); + let sql = self.build_collect(query_builder, &mut collector); + (sql, params) + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .column(Glyph::Aspect) + /// .from(Glyph::Table) + /// .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + /// .order_by(Glyph::Image, Order::Desc) + /// .order_by_tbl(Glyph::Table, Glyph::Aspect, Order::Asc) + /// .to_string(MysqlQueryBuilder); + /// + /// assert_eq!( + /// query, + /// r#"SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, 0) > 2 ORDER BY `image` DESC, `glyph`.`aspect` ASC"# + /// ); + /// ``` + pub fn to_string(&self, query_builder: T) -> String { + let (mut string, values) = self.build(query_builder); + for v in values.iter() { + string = string.replacen("?", value_to_string(v).as_ref(), 1); + } + string + } +} \ No newline at end of file diff --git a/src/query/update.rs b/src/query/update.rs new file mode 100644 index 000000000..f934a913e --- /dev/null +++ b/src/query/update.rs @@ -0,0 +1,508 @@ +use std::rc::Rc; +use serde_json::Value as JsonValue; +use crate::{backend::QueryBuilder, types::*, expr::*, value::*}; + +/// Update existing rows in the table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let query = Query::update() +/// .into_table(Glyph::Table) +/// .values(vec![ +/// (Glyph::Aspect, 1.23.into()), +/// (Glyph::Image, "123".into()), +/// ]) +/// .and_where(Expr::col(Glyph::Id).eq(1)) +/// .to_owned(); +/// +/// assert_eq!( +/// query.to_string(MysqlQueryBuilder), +/// r#"UPDATE `glyph` SET `aspect` = 1.23, `image` = '123' WHERE `id` = 1"# +/// ); +/// assert_eq!( +/// query.to_string(PostgresQueryBuilder), +/// r#"UPDATE "glyph" SET "aspect" = 1.23, "image" = '123' WHERE "id" = 1"# +/// ); +/// assert_eq!( +/// query.to_string(SqliteQueryBuilder), +/// r#"UPDATE `glyph` SET `aspect` = 1.23, `image` = '123' WHERE `id` = 1"# +/// ); +/// ``` +#[derive(Clone)] +pub struct UpdateStatement { + pub(crate) table: Option>, + pub(crate) values: Vec<(String, Box)>, + pub(crate) wherei: Option>, + pub(crate) orders: Vec, + pub(crate) limit: Option, +} + +impl Default for UpdateStatement { + fn default() -> Self { + Self::new() + } +} + +impl UpdateStatement { + /// Construct a new [`UpdateStatement`] + pub fn new() -> Self { + Self { + table: None, + values: Vec::new(), + wherei: None, + orders: Vec::new(), + limit: None, + } + } + + /// Specify which table to update. + /// + /// # Examples + /// + /// See [`UpdateStatement::values`] + #[allow(clippy::wrong_self_convention)] + pub fn into_table(&mut self, table: T) -> &mut Self + where T: Iden { + self.into_table_dyn(Rc::new(table)) + } + + /// Specify which table to update, variation of [`UpdateStatement::into_table`]. + /// + /// # Examples + /// + /// See [`UpdateStatement::values`] + #[allow(clippy::wrong_self_convention)] + pub fn into_table_dyn(&mut self, table: Rc) -> &mut Self { + self.table = Some(Box::new(TableRef::Table(table))); + self + } + + /// Update column value by [`SimpleExpr`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::update() + /// .into_table(Glyph::Table) + /// .value_expr(Glyph::Aspect, Expr::cust("60 * 24 * 24")) + /// .values(vec![ + /// (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), + /// ]) + /// .and_where(Expr::col(Glyph::Id).eq(1)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 60 * 24 * 24, `image` = '24B0E11951B03B07F8300FD003983F03F0780060' WHERE `id` = 1"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"UPDATE "glyph" SET "aspect" = 60 * 24 * 24, "image" = '24B0E11951B03B07F8300FD003983F03F0780060' WHERE "id" = 1"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 60 * 24 * 24, `image` = '24B0E11951B03B07F8300FD003983F03F0780060' WHERE `id` = 1"# + /// ); + /// ``` + pub fn value_expr(&mut self, col: T, exp: SimpleExpr) -> &mut Self + where T: Iden { + self.value_expr_dyn(Rc::new(col), exp) + } + + /// Update column value by [`SimpleExpr`], variation of [`UpdateStatement::value_expr`]. + pub fn value_expr_dyn(&mut self, col: Rc, exp: SimpleExpr) -> &mut Self { + self.push_boxed_value(col.to_string(), exp); + self + } + + /// Update column values by [`SimpleExpr`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::update() + /// .into_table(Glyph::Table) + /// .json(json!({ + /// "aspect": 2.1345, + /// "image": "235m", + /// })) + /// .and_where(Expr::col(Glyph::Id).eq(1)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 2.1345, `image` = '235m' WHERE `id` = 1"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"UPDATE "glyph" SET "aspect" = 2.1345, "image" = '235m' WHERE "id" = 1"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 2.1345, `image` = '235m' WHERE `id` = 1"# + /// ); + /// ``` + pub fn json(&mut self, values: JsonValue) -> &mut Self { + match values { + JsonValue::Object(_) => (), + _ => unimplemented!(), + } + for (k, v) in values.as_object().unwrap() { + let v = json_value_to_mysql_value(v); + self.push_boxed_value(k.into(), SimpleExpr::Value(v)); + } + self + } + + /// Update column values. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::update() + /// .into_table(Glyph::Table) + /// .values(vec![ + /// (Glyph::Aspect, 2.1345.into()), + /// (Glyph::Image, "235m".into()), + /// ]) + /// .and_where(Expr::col(Glyph::Id).eq(1)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 2.1345, `image` = '235m' WHERE `id` = 1"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"UPDATE "glyph" SET "aspect" = 2.1345, "image" = '235m' WHERE "id" = 1"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 2.1345, `image` = '235m' WHERE `id` = 1"# + /// ); + /// ``` + pub fn values(&mut self, values: Vec<(T, Value)>) -> &mut Self + where T: Iden { + self.values_dyn(values.into_iter().map(|(k, v)| (Rc::new(k) as Rc, v)).collect()) + } + + /// Update column values, variation of [`UpdateStatement::values`]. + pub fn values_dyn(&mut self, values: Vec<(Rc, Value)>) -> &mut Self { + for (k, v) in values.into_iter() { + self.push_boxed_value(k.to_string(), SimpleExpr::Value(v)); + } + self + } + + fn push_boxed_value(&mut self, k: String, v: SimpleExpr) -> &mut Self { + self.values.push((k, Box::new(v))); + self + } + + /// And where condition. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::update() + /// .into_table(Glyph::Table) + /// .values(vec![ + /// (Glyph::Aspect, 2.1345.into()), + /// (Glyph::Image, "235m".into()), + /// ]) + /// .and_where(Expr::col(Glyph::Id).gt(1)) + /// .and_where(Expr::col(Glyph::Id).lt(3)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 2.1345, `image` = '235m' WHERE (`id` > 1) AND (`id` < 3)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"UPDATE "glyph" SET "aspect" = 2.1345, "image" = '235m' WHERE ("id" > 1) AND ("id" < 3)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 2.1345, `image` = '235m' WHERE (`id` > 1) AND (`id` < 3)"# + /// ); + /// ``` + pub fn and_where(&mut self, other: SimpleExpr) -> &mut Self { + self.and_or_where(BinOper::And, other) + } + + /// Or where condition. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::update() + /// .into_table(Glyph::Table) + /// .values(vec![ + /// (Glyph::Aspect, 2.1345.into()), + /// (Glyph::Image, "235m".into()), + /// ]) + /// .or_where(Expr::col(Glyph::Aspect).lt(1)) + /// .or_where(Expr::col(Glyph::Aspect).gt(3)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 2.1345, `image` = '235m' WHERE (`aspect` < 1) OR (`aspect` > 3)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"UPDATE "glyph" SET "aspect" = 2.1345, "image" = '235m' WHERE ("aspect" < 1) OR ("aspect" > 3)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 2.1345, `image` = '235m' WHERE (`aspect` < 1) OR (`aspect` > 3)"# + /// ); + /// ``` + pub fn or_where(&mut self, other: SimpleExpr) -> &mut Self { + self.and_or_where(BinOper::Or, other) + } + + fn and_or_where(&mut self, bopr: BinOper, right: SimpleExpr) -> &mut Self { + self.wherei = Self::merge_expr( + self.wherei.take(), + match bopr { + BinOper::And => BinOper::And, + BinOper::Or => BinOper::Or, + _ => panic!("not allow"), + }, + right + ); + self + } + + fn merge_expr(left: Option>, bopr: BinOper, right: SimpleExpr) -> Option> { + Some(Box::new(match left { + Some(left) => SimpleExpr::Binary( + left, + bopr, + Box::new(right) + ), + None => right, + })) + } + + /// Order by column. + pub fn order_by(&mut self, col: T, order: Order) -> &mut Self + where T: Iden { + self.order_by_dyn(Rc::new(col), order) + } + + /// Order by column, variation of [`UpdateStatement::order_by`]. + pub fn order_by_dyn(&mut self, col: Rc, order: Order) -> &mut Self { + self.orders.push(OrderExpr { + expr: SimpleExpr::Column(col), + order, + }); + self + } + + /// Order by column with table name prefix. + pub fn order_by_tbl + (&mut self, table: T, col: C, order: Order) -> &mut Self + where T: Iden, C: Iden { + self.order_by_tbl_dyn(Rc::new(table), Rc::new(col), order) + } + + /// Order by column with table name prefix, variation of [`UpdateStatement::order_by_tbl`]. + pub fn order_by_tbl_dyn(&mut self, table: Rc, col: Rc, order: Order) -> &mut Self { + self.orders.push(OrderExpr { + expr: SimpleExpr::TableColumn(table, col), + order, + }); + self + } + + /// Order by [`SimpleExpr`]. + pub fn order_by_expr(&mut self, expr: SimpleExpr, order: Order) -> &mut Self { + self.orders.push(OrderExpr { + expr, + order, + }); + self + } + + /// Order by custom string. + pub fn order_by_customs(&mut self, cols: Vec<(T, Order)>) -> &mut Self + where T: ToString { + let mut orders = cols.into_iter().map( + |(c, order)| OrderExpr { + expr: SimpleExpr::Custom(c.to_string()), + order, + }).collect(); + self.orders.append(&mut orders); + self + } + + /// Order by columns. + pub fn order_by_columns(&mut self, cols: Vec<(T, Order)>) -> &mut Self + where T: Iden { + self.order_by_columns_dyn(cols.into_iter().map( + |(c, order)| (Rc::new(c) as Rc, order) + ).collect()) + } + + /// Order by columns, variation of [`UpdateStatement::order_by_columns`]. + pub fn order_by_columns_dyn(&mut self, cols: Vec<(Rc, Order)>) -> &mut Self { + let mut orders = cols.into_iter().map( + |(c, order)| OrderExpr { + expr: SimpleExpr::Column(c), + order, + }).collect(); + self.orders.append(&mut orders); + self + } + + /// Order by columns with table prefix. + pub fn order_by_table_columns + (&mut self, cols: Vec<(T, C, Order)>) -> &mut Self + where T: Iden, C: Iden { + self.order_by_table_columns_dyn(cols.into_iter().map( + |(t, c, order)| (Rc::new(t) as Rc, Rc::new(c) as Rc, order) + ).collect()) + } + + /// Order by columns with table prefix, variation of [`UpdateStatement::order_by_columns`]. + #[allow(clippy::type_complexity)] + pub fn order_by_table_columns_dyn(&mut self, cols: Vec<(Rc, Rc, Order)>) -> &mut Self { + let mut orders = cols.into_iter().map( + |(t, c, order)| OrderExpr { + expr: SimpleExpr::TableColumn(t, c), + order, + }).collect(); + self.orders.append(&mut orders); + self + } + + /// Limit number of updated rows. + pub fn limit(&mut self, limit: u64) -> &mut Self { + self.limit = Some(Value::UInt(limit)); + self + } + + /// Build corresponding SQL statement for certain database backend and collect query parameters + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::update() + /// .into_table(Glyph::Table) + /// .values(vec![ + /// (Glyph::Aspect, 2.1345.into()), + /// (Glyph::Image, "235m".into()), + /// ]) + /// .and_where(Expr::col(Glyph::Id).eq(1)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 2.1345, `image` = '235m' WHERE `id` = 1"# + /// ); + /// + /// let mut params = Vec::new(); + /// let mut collector = |v| params.push(v); + /// + /// assert_eq!( + /// query.build_collect(MysqlQueryBuilder, &mut collector), + /// r#"UPDATE `glyph` SET `aspect` = ?, `image` = ? WHERE `id` = ?"# + /// ); + /// assert_eq!( + /// params, + /// vec![ + /// Value::Double(2.1345), + /// Value::Bytes(String::from("235m").into_bytes()), + /// Value::Int(1), + /// ] + /// ); + /// ``` + pub fn build_collect(&self, mut query_builder: T, collector: &mut dyn FnMut(Value)) -> String { + let mut sql = String::new(); + query_builder.prepare_update_statement(self, &mut sql, collector); + sql + } + + /// Build corresponding SQL statement for certain database backend and collect query parameters into a vector + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let (query, params) = Query::update() + /// .into_table(Glyph::Table) + /// .values(vec![ + /// (Glyph::Aspect, 2.1345.into()), + /// (Glyph::Image, "235m".into()), + /// ]) + /// .and_where(Expr::col(Glyph::Id).eq(1)) + /// .build(MysqlQueryBuilder); + /// + /// assert_eq!( + /// query, + /// r#"UPDATE `glyph` SET `aspect` = ?, `image` = ? WHERE `id` = ?"# + /// ); + /// assert_eq!( + /// params, + /// vec![ + /// Value::Double(2.1345), + /// Value::Bytes(String::from("235m").into_bytes()), + /// Value::Int(1), + /// ] + /// ); + /// ``` + pub fn build(&self, query_builder: T) -> (String, Vec) { + let mut params = Vec::new(); + let mut collector = |v| params.push(v); + let sql = self.build_collect(query_builder, &mut collector); + (sql, params) + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::update() + /// .into_table(Glyph::Table) + /// .values(vec![ + /// (Glyph::Aspect, 2.1345.into()), + /// (Glyph::Image, "235m".into()), + /// ]) + /// .and_where(Expr::col(Glyph::Id).eq(1)) + /// .to_string(MysqlQueryBuilder); + /// + /// assert_eq!( + /// query, + /// r#"UPDATE `glyph` SET `aspect` = 2.1345, `image` = '235m' WHERE `id` = 1"# + /// ); + /// ``` + pub fn to_string(&self, query_builder: T) -> String { + let (mut string, values) = self.build(query_builder); + for v in values.iter() { + string = string.replacen("?", value_to_string(v).as_ref(), 1); + } + string + } +} \ No newline at end of file diff --git a/src/table/alter.rs b/src/table/alter.rs new file mode 100644 index 000000000..f5c752394 --- /dev/null +++ b/src/table/alter.rs @@ -0,0 +1,206 @@ +use std::rc::Rc; +use crate::{ColumnDef, backend::TableBuilder, types::*}; + +/// Alter a table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let table = Table::alter() +/// .table(Font::Table) +/// .add_column(ColumnDef::new(Alias::new("new_col")).integer().not_null().default(100)) +/// .to_owned(); +/// +/// assert_eq!( +/// table.to_string(MysqlQueryBuilder), +/// r#"ALTER TABLE `font` ADD COLUMN `new_col` int NOT NULL DEFAULT 100"# +/// ); +/// assert_eq!( +/// table.to_string(PostgresQueryBuilder), +/// r#"ALTER TABLE "font" ADD COLUMN "new_col" integer NOT NULL DEFAULT 100"# +/// ); +/// assert_eq!( +/// table.to_string(SqliteQueryBuilder), +/// r#"ALTER TABLE `font` ADD COLUMN `new_col` integer NOT NULL DEFAULT 100"#, +/// ); +/// ``` +#[derive(Clone)] +pub struct TableAlterStatement { + pub(crate) table: Option>, + pub(crate) alter_option: Option, + pub(crate) partition_option: Option, +} + +/// All available table alter options +#[derive(Clone)] +pub enum TableAlterOption { + AddColumn(ColumnDef), + ModifyColumn(ColumnDef), + RenameColumn(Rc, Rc), + DropColumn(Rc), +} + +/// All available table partition options +#[derive(Clone)] +pub enum TablePartitionOption { + +} + +impl Default for TableAlterStatement { + fn default() -> Self { + Self::new() + } +} + +impl TableAlterStatement { + /// Construct alter table statement + pub fn new() -> Self { + Self { + table: None, + alter_option: None, + partition_option: None, + } + } + + /// Set table name + pub fn table(mut self, table: T) -> Self + where T: Iden { + self.table = Some(Rc::new(table)); + self + } + + /// Add a column to an existing table + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let table = Table::alter() + /// .table(Font::Table) + /// .add_column(ColumnDef::new(Alias::new("new_col")).integer().not_null().default(100)) + /// .to_owned(); + /// + /// assert_eq!( + /// table.to_string(MysqlQueryBuilder), + /// r#"ALTER TABLE `font` ADD COLUMN `new_col` int NOT NULL DEFAULT 100"# + /// ); + /// assert_eq!( + /// table.to_string(PostgresQueryBuilder), + /// r#"ALTER TABLE "font" ADD COLUMN "new_col" integer NOT NULL DEFAULT 100"# + /// ); + /// assert_eq!( + /// table.to_string(SqliteQueryBuilder), + /// r#"ALTER TABLE `font` ADD COLUMN `new_col` integer NOT NULL DEFAULT 100"#, + /// ); + /// ``` + pub fn add_column(self, column_def: ColumnDef) -> Self { + self.alter_option(TableAlterOption::AddColumn(column_def)) + } + + /// Modify a column in an existing table + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let table = Table::alter() + /// .table(Font::Table) + /// .modify_column(ColumnDef::new(Alias::new("new_col")).big_integer().default(999)) + /// .to_owned(); + /// + /// assert_eq!( + /// table.to_string(MysqlQueryBuilder), + /// r#"ALTER TABLE `font` MODIFY COLUMN `new_col` bigint DEFAULT 999"# + /// ); + /// assert_eq!( + /// table.to_string(PostgresQueryBuilder), + /// vec![ + /// r#"ALTER TABLE "font""#, + /// r#"ALTER COLUMN "new_col" TYPE bigint,"#, + /// r#"ALTER COLUMN "new_col" SET DEFAULT 999"#, + /// ].join(" ") + /// ); + /// // Sqlite not support modifying table column + /// ``` + pub fn modify_column(self, column_def: ColumnDef) -> Self { + self.alter_option(TableAlterOption::ModifyColumn(column_def)) + } + + /// Rename a column in an existing table + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let table = Table::alter() + /// .table(Font::Table) + /// .rename_column(Alias::new("new_col"), Alias::new("new_column")) + /// .to_owned(); + /// + /// assert_eq!( + /// table.to_string(MysqlQueryBuilder), + /// r#"ALTER TABLE `font` RENAME COLUMN `new_col` TO `new_column`"# + /// ); + /// assert_eq!( + /// table.to_string(PostgresQueryBuilder), + /// r#"ALTER TABLE "font" RENAME COLUMN "new_col" TO "new_column""# + /// ); + /// assert_eq!( + /// table.to_string(SqliteQueryBuilder), + /// r#"ALTER TABLE `font` RENAME COLUMN `new_col` TO `new_column`"# + /// ); + /// ``` + pub fn rename_column(self, from_name: T, to_name: R) -> Self + where T: Iden, R: Iden { + self.alter_option(TableAlterOption::RenameColumn(Rc::new(from_name), Rc::new(to_name))) + } + + /// Add a column to existing table + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let table = Table::alter() + /// .table(Font::Table) + /// .drop_column(Alias::new("new_column")) + /// .to_owned(); + /// + /// assert_eq!( + /// table.to_string(MysqlQueryBuilder), + /// r#"ALTER TABLE `font` DROP COLUMN `new_column`"# + /// ); + /// assert_eq!( + /// table.to_string(PostgresQueryBuilder), + /// r#"ALTER TABLE "font" DROP COLUMN "new_column""# + /// ); + /// // Sqlite not support modifying table column + /// ``` + pub fn drop_column(self, col_name: T) -> Self + where T: Iden { + self.alter_option(TableAlterOption::DropColumn(Rc::new(col_name))) + } + + fn alter_option(mut self, alter_option: TableAlterOption) -> Self { + self.alter_option = Some(alter_option); + self + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn build(&self, mut table_builder: T) -> String { + let mut sql = String::new(); + table_builder.prepare_table_alter_statement(self, &mut sql); + sql + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn to_string(&self, table_builder: T) -> String { + self.build(table_builder) + } +} \ No newline at end of file diff --git a/src/table/common.rs b/src/table/common.rs new file mode 100644 index 000000000..484f19eda --- /dev/null +++ b/src/table/common.rs @@ -0,0 +1,295 @@ +use std::rc::Rc; +use crate::{types::*, value::*}; + +/// Specification of a table column +#[derive(Clone)] +pub struct ColumnDef { + pub(crate) table: Option>, + pub(crate) name: Rc, + pub(crate) types: Option, + pub(crate) spec: Vec, +} + +impl ColumnDef { + /// Construct a table column + pub fn new(name: T) -> Self + where T: Iden{ + Self { + table: None, + name: Rc::new(name), + types: None, + spec: Vec::new(), + } + } + + /// Set column not null + pub fn not_null(mut self) -> Self { + self.spec.push(ColumnSpec::NotNull); + self + } + + /// Set default value of a column + pub fn default(mut self, value: T) -> Self + where T: Into { + self.spec.push(ColumnSpec::Default(value.into())); + self + } + + /// Set column auto increment + pub fn auto_increment(mut self) -> Self { + self.spec.push(ColumnSpec::AutoIncrement); + self + } + + /// Set column unique constraint + pub fn unique_key(mut self) -> Self { + self.spec.push(ColumnSpec::UniqueKey); + self + } + + /// Set column as primary key + pub fn primary_key(mut self) -> Self { + self.spec.push(ColumnSpec::PrimaryKey); + self + } + + /// Set column type as char with custom length + pub fn char_len(mut self, length: u32) -> Self { + self.types = Some(ColumnType::Char(length)); + self + } + + /// Set column type as char + pub fn char(mut self) -> Self { + self.types = Some(ColumnType::CharDefault); + self + } + + /// Set column type as string with custom length + pub fn string_len(mut self, length: u32) -> Self { + self.types = Some(ColumnType::String(length)); + self + } + + /// Set column type as string + pub fn string(mut self) -> Self { + self.types = Some(ColumnType::StringDefault); + self + } + + /// Set column type as text + pub fn text(mut self) -> Self { + self.types = Some(ColumnType::Text); + self + } + + /// Set column type as tiny_integer with custom length + pub fn tiny_integer_len(mut self, length: u32) -> Self { + self.types = Some(ColumnType::TinyInteger(length)); + self + } + + /// Set column type as tiny_integer + pub fn tiny_integer(mut self) -> Self { + self.types = Some(ColumnType::TinyIntegerDefault); + self + } + + /// Set column type as small_integer with custom length + pub fn small_integer_len(mut self, length: u32) -> Self { + self.types = Some(ColumnType::SmallInteger(length)); + self + } + + /// Set column type as small_integer + pub fn small_integer(mut self) -> Self { + self.types = Some(ColumnType::SmallIntegerDefault); + self + } + + /// Set column type as integer with custom length + pub fn integer_len(mut self, length: u32) -> Self { + self.types = Some(ColumnType::Integer(length)); + self + } + + /// Set column type as integer + pub fn integer(mut self) -> Self { + self.types = Some(ColumnType::IntegerDefault); + self + } + + /// Set column type as big_integer with custom length + pub fn big_integer_len(mut self, length: u32) -> Self { + self.types = Some(ColumnType::BigInteger(length)); + self + } + + /// Set column type as big_integer + pub fn big_integer(mut self) -> Self { + self.types = Some(ColumnType::BigIntegerDefault); + self + } + + /// Set column type as float with custom precision + pub fn float_len(mut self, precision: u32) -> Self { + self.types = Some(ColumnType::Float(precision)); + self + } + + /// Set column type as float + pub fn float(mut self) -> Self { + self.types = Some(ColumnType::FloatDefault); + self + } + + /// Set column type as double with custom precision + pub fn double_len(mut self, precision: u32) -> Self { + self.types = Some(ColumnType::Double(precision)); + self + } + + /// Set column type as double + pub fn double(mut self) -> Self { + self.types = Some(ColumnType::DoubleDefault); + self + } + + /// Set column type as decimal with custom precision and scale + pub fn decimal_len(mut self, precision: u32, scale: u32) -> Self { + self.types = Some(ColumnType::Decimal(precision, scale)); + self + } + + /// Set column type as decimal + pub fn decimal(mut self) -> Self { + self.types = Some(ColumnType::DecimalDefault); + self + } + + /// Set column type as date_time with custom precision + pub fn date_time_len(mut self, precision: u32) -> Self { + self.types = Some(ColumnType::DateTime(precision)); + self + } + + /// Set column type as date_time + pub fn date_time(mut self) -> Self { + self.types = Some(ColumnType::DateTimeDefault); + self + } + + /// Set column type as timestamp with custom precision + pub fn timestamp_len(mut self, precision: u32) -> Self { + self.types = Some(ColumnType::Timestamp(precision)); + self + } + + /// Set column type as timestamp + pub fn timestamp(mut self) -> Self { + self.types = Some(ColumnType::TimestampDefault); + self + } + + /// Set column type as time with custom precision + pub fn time_len(mut self, precision: u32) -> Self { + self.types = Some(ColumnType::Time(precision)); + self + } + + /// Set column type as time + pub fn time(mut self) -> Self { + self.types = Some(ColumnType::TimeDefault); + self + } + + /// Set column type as date + pub fn date(mut self) -> Self { + self.types = Some(ColumnType::Date); + self + } + + /// Set column type as binary with custom length + pub fn binary_len(mut self, length: u32) -> Self { + self.types = Some(ColumnType::Binary(length)); + self + } + + /// Set column type as binary + pub fn binary(mut self) -> Self { + self.types = Some(ColumnType::BinaryDefault); + self + } + + /// Set column type as boolean + pub fn boolean(mut self) -> Self { + self.types = Some(ColumnType::Boolean); + self + } + + /// Set column type as money with custom precision ans scale + pub fn money_len(mut self, precision: u32, scale: u32) -> Self { + self.types = Some(ColumnType::Money(precision, scale)); + self + } + + /// Set column type as money + pub fn money(mut self) -> Self { + self.types = Some(ColumnType::MoneyDefault); + self + } + + /// Set column type as json + pub fn json(mut self) -> Self { + self.types = Some(ColumnType::Json); + self + } +} + +/// All available column types +#[derive(Clone)] +pub enum ColumnType { + Char(u32), + CharDefault, + String(u32), + StringDefault, + Text, + TinyInteger(u32), + TinyIntegerDefault, + SmallInteger(u32), + SmallIntegerDefault, + Integer(u32), + IntegerDefault, + BigInteger(u32), + BigIntegerDefault, + Float(u32), + FloatDefault, + Double(u32), + DoubleDefault, + Decimal(u32, u32), + DecimalDefault, + DateTime(u32), + DateTimeDefault, + Timestamp(u32), + TimestampDefault, + Time(u32), + TimeDefault, + Date, + Binary(u32), + BinaryDefault, + Boolean, + Money(u32, u32), + MoneyDefault, + Json, +} + +/// All available column specification keywords +#[derive(Clone)] +pub enum ColumnSpec { + Null, + NotNull, + Default(Value), + AutoIncrement, + UniqueKey, + PrimaryKey, +} \ No newline at end of file diff --git a/src/table/create.rs b/src/table/create.rs new file mode 100644 index 000000000..38c8dc019 --- /dev/null +++ b/src/table/create.rs @@ -0,0 +1,189 @@ +use std::rc::Rc; +use crate::{ColumnDef, backend::TableBuilder, foreign_key::*, types::*}; + +/// Create a table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let table = Table::create() +/// .table(Char::Table) +/// .create_if_not_exists() +/// .col(ColumnDef::new(Char::Id).integer().not_null().auto_increment().primary_key()) +/// .col(ColumnDef::new(Char::FontSize).integer().not_null()) +/// .col(ColumnDef::new(Char::Character).string().not_null()) +/// .col(ColumnDef::new(Char::SizeW).integer().not_null()) +/// .col(ColumnDef::new(Char::SizeH).integer().not_null()) +/// .col(ColumnDef::new(Char::FontId).integer().default(Value::NULL)) +/// .foreign_key( +/// ForeignKey::create() +/// .name("FK_2e303c3a712662f1fc2a4d0aad6") +/// .table(Char::Table, Font::Table) +/// .col(Char::FontId, Font::Id) +/// .on_delete(ForeignKeyAction::Cascade) +/// .on_update(ForeignKeyAction::Cascade) +/// ) +/// .to_owned(); +/// +/// assert_eq!( +/// table.to_string(MysqlQueryBuilder), +/// vec![ +/// r#"CREATE TABLE IF NOT EXISTS `character` ("#, +/// r#"`id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,"#, +/// r#"`font_size` int NOT NULL,"#, +/// r#"`character` varchar NOT NULL,"#, +/// r#"`size_w` int NOT NULL,"#, +/// r#"`size_h` int NOT NULL,"#, +/// r#"`font_id` int DEFAULT NULL,"#, +/// r#"CONSTRAINT `FK_2e303c3a712662f1fc2a4d0aad6`"#, +/// r#"FOREIGN KEY `FK_2e303c3a712662f1fc2a4d0aad6` (`font_id`) REFERENCES `font` (`id`)"#, +/// r#"ON DELETE CASCADE ON UPDATE CASCADE"#, +/// r#")"#, +/// ].join(" ") +/// ); +/// assert_eq!( +/// table.to_string(PostgresQueryBuilder), +/// vec![ +/// r#"CREATE TABLE IF NOT EXISTS "character" ("#, +/// r#""id" serial NOT NULL PRIMARY KEY,"#, +/// r#""font_size" integer NOT NULL,"#, +/// r#""character" varchar NOT NULL,"#, +/// r#""size_w" integer NOT NULL,"#, +/// r#""size_h" integer NOT NULL,"#, +/// r#""font_id" integer DEFAULT NULL,"#, +/// r#"CONSTRAINT "FK_2e303c3a712662f1fc2a4d0aad6""#, +/// r#"FOREIGN KEY ("font_id") REFERENCES "font" ("id")"#, +/// r#"ON DELETE CASCADE ON UPDATE CASCADE"#, +/// r#")"#, +/// ].join(" ") +/// ); +/// assert_eq!( +/// table.to_string(SqliteQueryBuilder), +/// vec![ +/// r#"CREATE TABLE IF NOT EXISTS `character` ("#, +/// r#"`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,"#, +/// r#"`font_size` integer NOT NULL,"#, +/// r#"`character` text NOT NULL,"#, +/// r#"`size_w` integer NOT NULL,"#, +/// r#"`size_h` integer NOT NULL,"#, +/// r#"`font_id` integer DEFAULT NULL,"#, +/// r#"FOREIGN KEY (`font_id`) REFERENCES `font` (`id`) ON DELETE CASCADE ON UPDATE CASCADE"#, +/// r#")"#, +/// ].join(" ") +/// ); +/// ``` +#[derive(Clone)] +pub struct TableCreateStatement { + pub(crate) table: Option>, + pub(crate) columns: Vec, + pub(crate) options: Vec, + pub(crate) partitions: Vec, + pub(crate) foreign_keys: Vec, + pub(crate) create_if_not_exists: bool, +} + +/// All available table options +#[derive(Clone)] +pub enum TableOpt { + Engine(String), + Collate(String), + CharacterSet(String), +} + +/// All available table partition options +#[derive(Clone)] +pub enum TablePartition { + +} + +impl Default for TableCreateStatement { + fn default() -> Self { + Self::new() + } +} + +impl TableCreateStatement { + /// Construct create table statement + pub fn new() -> Self { + Self { + table: None, + columns: Vec::new(), + options: Vec::new(), + partitions: Vec::new(), + foreign_keys: Vec::new(), + create_if_not_exists: false, + } + } + + /// Create table if table not exists + pub fn create_if_not_exists(&mut self) -> &mut Self { + self.create_if_not_exists = true; + self + } + + /// Set table name + pub fn table(&mut self, table: T) -> &mut Self + where T: Iden { + self.table = Some(Rc::new(table)); + self + } + + /// Add a new table column + pub fn col(&mut self, column: ColumnDef) -> &mut Self { + let mut column = column; + column.table = self.table.clone(); + self.columns.push(column); + self + } + + /// Add a foreign key + pub fn foreign_key(&mut self, foreign_key: ForeignKeyCreateStatement) -> &mut Self { + let mut foreign_key = foreign_key; + foreign_key.inside_table_creation = true; + self.foreign_keys.push(foreign_key); + self + } + + /// Set database engine (for MySQL only) + pub fn engine(&mut self, string: &str) -> &mut Self { + self.opt(TableOpt::Engine(string.into())); + self + } + + /// Set database collate (for MySQL only) + pub fn collate(&mut self, string: &str) -> &mut Self { + self.opt(TableOpt::Collate(string.into())); + self + } + + /// Set database character set (for MySQL only) + pub fn character_set(&mut self, string: &str) -> &mut Self { + self.opt(TableOpt::CharacterSet(string.into())); + self + } + + fn opt(&mut self, option: TableOpt) -> &mut Self { + self.options.push(option); + self + } + + #[allow(dead_code)] + fn partition(&mut self, partition: TablePartition) -> &mut Self { + self.partitions.push(partition); + self + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn build(&self, mut table_builder: T) -> String { + let mut sql = String::new(); + table_builder.prepare_table_create_statement(self, &mut sql); + sql + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn to_string(&self, table_builder: T) -> String { + self.build(table_builder) + } +} \ No newline at end of file diff --git a/src/table/drop.rs b/src/table/drop.rs new file mode 100644 index 000000000..036a47804 --- /dev/null +++ b/src/table/drop.rs @@ -0,0 +1,95 @@ +use std::rc::Rc; +use crate::{backend::TableBuilder, types::*}; + +/// Drop a table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let table = Table::drop() +/// .table(Glyph::Table) +/// .table(Char::Table) +/// .to_owned(); +/// +/// assert_eq!( +/// table.to_string(MysqlQueryBuilder), +/// r#"DROP TABLE `glyph`, `character`"# +/// ); +/// assert_eq!( +/// table.to_string(PostgresQueryBuilder), +/// r#"DROP TABLE "glyph", "character""# +/// ); +/// assert_eq!( +/// table.to_string(SqliteQueryBuilder), +/// r#"DROP TABLE `glyph`, `character`"# +/// ); +/// ``` +#[derive(Clone)] +pub struct TableDropStatement { + pub(crate) tables: Vec>, + pub(crate) options: Vec, + pub(crate) if_exist: bool, +} + +/// All available table drop options +#[derive(Clone)] +pub enum TableDropOpt { + Restrict, + Cascade, +} + +impl Default for TableDropStatement { + fn default() -> Self { + Self::new() + } +} + +impl TableDropStatement { + /// Construct drop table statement + pub fn new() -> Self { + Self { + tables: Vec::new(), + options: Vec::new(), + if_exist: false, + } + } + + /// Set table name + pub fn table(mut self, table: T) -> Self + where T: Iden { + self.tables.push(Rc::new(table)); + self + } + + /// Drop table if exists + pub fn if_exist(mut self) -> Self { + self.if_exist = true; + self + } + + /// Drop option restrict + pub fn restrict(mut self) -> Self { + self.options.push(TableDropOpt::Restrict); + self + } + + /// Drop option cacade + pub fn cascade(mut self) -> Self { + self.options.push(TableDropOpt::Cascade); + self + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn build(&self, mut table_builder: T) -> String { + let mut sql = String::new(); + table_builder.prepare_table_drop_statement(self, &mut sql); + sql + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn to_string(&self, table_builder: T) -> String { + self.build(table_builder) + } +} \ No newline at end of file diff --git a/src/table/mod.rs b/src/table/mod.rs new file mode 100644 index 000000000..7d347f37e --- /dev/null +++ b/src/table/mod.rs @@ -0,0 +1,64 @@ +//! Database table (create, alter, drop, rename & truncate). +//! +//! # Usage +//! +//! - Table Create, see [`TableCreateStatement`] +//! - Table Alter, see [`TableAlterStatement`] +//! - Table Drop, see [`TableDropStatement`] +//! - Table Rename, see [`TableRenameStatement`] +//! - Table Truncate, see [`TableTruncateStatement`] + +mod alter; +mod common; +mod create; +mod drop; +mod rename; +mod truncate; + +pub use alter::*; +pub use common::*; +pub use create::*; +pub use drop::*; +pub use rename::*; +pub use truncate::*; + +/// Shorthand for constructing any table statement +#[derive(Clone)] +pub struct Table; + +/// All available types of table statement +#[derive(Clone)] +pub enum TableStatement { + Create(TableCreateStatement), + Alter(TableAlterStatement), + Drop(TableDropStatement), + Rename(TableRenameStatement), + Truncate(TableTruncateStatement), +} + +impl Table { + /// Construct table [`TableCreateStatement`] + pub fn create() -> TableCreateStatement { + TableCreateStatement::new() + } + + /// Construct table [`TableAlterStatement`] + pub fn alter() -> TableAlterStatement { + TableAlterStatement::new() + } + + /// Construct table [`TableDropStatement`] + pub fn drop() -> TableDropStatement { + TableDropStatement::new() + } + + /// Construct table [`TableRenameStatement`] + pub fn rename() -> TableRenameStatement { + TableRenameStatement::new() + } + + /// Construct table [`TableTruncateStatement`] + pub fn truncate() -> TableTruncateStatement { + TableTruncateStatement::new() + } +} \ No newline at end of file diff --git a/src/table/rename.rs b/src/table/rename.rs new file mode 100644 index 000000000..0d105a60a --- /dev/null +++ b/src/table/rename.rs @@ -0,0 +1,68 @@ +use std::rc::Rc; +use crate::{backend::TableBuilder, types::*}; + +/// Rename a table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let table = Table::rename() +/// .table(Font::Table, Alias::new("font_new")) +/// .to_owned(); +/// +/// assert_eq!( +/// table.to_string(MysqlQueryBuilder), +/// r#"RENAME TABLE `font` TO `font_new`"# +/// ); +/// assert_eq!( +/// table.to_string(PostgresQueryBuilder), +/// r#"ALTER TABLE "font" RENAME TO "font_new""# +/// ); +/// assert_eq!( +/// table.to_string(SqliteQueryBuilder), +/// r#"ALTER TABLE `font` RENAME TO `font_new`"# +/// ); +/// ``` +#[derive(Clone)] +pub struct TableRenameStatement { + pub(crate) from_name: Option>, + pub(crate) to_name: Option>, +} + +impl Default for TableRenameStatement { + fn default() -> Self { + Self::new() + } +} + +impl TableRenameStatement { + /// Construct rename table statement + pub fn new() -> Self { + Self { + from_name: None, + to_name: None, + } + } + + /// Set old and new table name + pub fn table(mut self, from_name: T, to_name: R) -> Self + where T: Iden, R: Iden { + self.from_name = Some(Rc::new(from_name)); + self.to_name = Some(Rc::new(to_name)); + self + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn build(&self, mut table_builder: T) -> String { + let mut sql = String::new(); + table_builder.prepare_table_rename_statement(self, &mut sql); + sql + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn to_string(&self, table_builder: T) -> String { + self.build(table_builder) + } +} \ No newline at end of file diff --git a/src/table/truncate.rs b/src/table/truncate.rs new file mode 100644 index 000000000..b9b3e6625 --- /dev/null +++ b/src/table/truncate.rs @@ -0,0 +1,65 @@ +use std::rc::Rc; +use crate::{backend::TableBuilder, types::*}; + +/// Drop a table +/// +/// # Examples +/// +/// ``` +/// use sea_query::{*, tests_cfg::*}; +/// +/// let table = Table::truncate() +/// .table(Font::Table) +/// .to_owned(); +/// +/// assert_eq!( +/// table.to_string(MysqlQueryBuilder), +/// r#"TRUNCATE TABLE `font`"# +/// ); +/// assert_eq!( +/// table.to_string(PostgresQueryBuilder), +/// r#"TRUNCATE TABLE "font""# +/// ); +/// assert_eq!( +/// table.to_string(SqliteQueryBuilder), +/// r#"TRUNCATE TABLE `font`"# +/// ); +/// ``` +#[derive(Clone)] +pub struct TableTruncateStatement { + pub(crate) table: Option>, +} + +impl Default for TableTruncateStatement { + fn default() -> Self { + Self::new() + } +} + +impl TableTruncateStatement { + /// Construct truncate table statement + pub fn new() -> Self { + Self { + table: None, + } + } + + /// Set table name + pub fn table(mut self, table: T) -> Self + where T: Iden { + self.table = Some(Rc::new(table)); + self + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn build(&self, mut table_builder: T) -> String { + let mut sql = String::new(); + table_builder.prepare_table_truncate_statement(self, &mut sql); + sql + } + + /// Build corresponding SQL statement for certain database backend and return SQL string + pub fn to_string(&self, table_builder: T) -> String { + self.build(table_builder) + } +} \ No newline at end of file diff --git a/src/tests_cfg.rs b/src/tests_cfg.rs new file mode 100644 index 000000000..f65b03dd1 --- /dev/null +++ b/src/tests_cfg.rs @@ -0,0 +1,87 @@ +//! Helper module setting up rustdoc and test environment. + +pub use std::fmt::Write as FmtWrite; + +pub use serde_json::json; + +use crate::Iden; + +/// Representation of a database table named `Character`. +/// +/// A `Enum` implemented [`Iden`] used in rustdoc and test to demonstrate the library usage. +/// +/// [`Iden`]: crate::types::Iden +pub enum Character { + Table, + Id, + Character, + FontSize, + SizeW, + SizeH, + FontId, +} + +/// A shorthand for [`Character`] +pub type Char = Character; + +impl Iden for Character { + fn unquoted(&self, s: &mut dyn FmtWrite) { + write!(s, "{}", match self { + Self::Table => "character", + Self::Id => "id", + Self::Character => "character", + Self::FontSize => "font_size", + Self::SizeW => "size_w", + Self::SizeH => "size_h", + Self::FontId => "font_id", + }).unwrap(); + } +} + +/// Representation of a database table named `Font`. +/// +/// A `Enum` implemented [`Iden`] used in rustdoc and test to demonstrate the library usage. +/// +/// [`Iden`]: crate::types::Iden +pub enum Font { + Table, + Id, + Name, + Variant, + Language, +} + +impl Iden for Font { + fn unquoted(&self, s: &mut dyn FmtWrite) { + write!(s, "{}", match self { + Self::Table => "font", + Self::Id => "id", + Self::Name => "name", + Self::Variant => "variant", + Self::Language => "language", + }).unwrap(); + } +} + +/// Representation of a database table named `Glyph`. +/// +/// A `Enum` implemented [`Iden`] used in rustdoc and test to demonstrate the library usage. +/// +/// [`Iden`]: crate::types::Iden +pub enum Glyph { + Table, + Id, + Image, + Aspect, +} + +impl Iden for Glyph { + fn unquoted(&self, s: &mut dyn FmtWrite) { + write!(s, "{}", match self { + Self::Table => "glyph", + Self::Id => "id", + Self::Image => "image", + Self::Aspect => "aspect", + }).unwrap(); + } +} \ No newline at end of file diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 000000000..b321d3ef1 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,210 @@ +//! Common types used in the library. + +use std::{rc::Rc, str::from_utf8}; +use std::fmt::Write as FmtWrite; +use serde_json::Value as JsonValue; +use crate::{query::*, expr::*, value::*}; + +/// Identifier in query +pub trait Iden { + fn prepare(&self, s: &mut dyn FmtWrite, q: char) { + write!(s, "{}", q).unwrap(); + self.unquoted(s); + write!(s, "{}", q).unwrap(); + } + + fn to_string(&self) -> String { + let s = &mut String::new(); + self.unquoted(s); + s.to_owned() + } + + fn unquoted(&self, s: &mut dyn FmtWrite); +} + +/// All table references +#[derive(Clone)] +pub enum TableRef { + Table(Rc), + TableAlias(Rc, Rc), + SubQuery(SelectStatement, Rc), +} + +/// Unary operator +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum UnOper { + Not, +} + +/// Binary operator +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum BinOper { + And, + Or, + Like, + NotLike, + Is, + IsNot, + In, + NotIn, + Between, + NotBetween, + Equal, + NotEqual, + SmallerThan, + GreaterThan, + SmallerThanOrEqual, + GreaterThanOrEqual, + Add, + Sub, + Mul, + Div, +} + +/// Logical chain operator +#[derive(Clone)] +pub enum LogicalChainOper { + And(SimpleExpr), + Or(SimpleExpr), +} + +/// Query functions +#[derive(Clone, PartialEq, Eq)] +pub enum Function { + Max, + Min, + Sum, + Count, + IfNull, + Custom(String), +} + +/// Join types +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum JoinType { + Join, + InnerJoin, + LeftJoin, + RightJoin, +} + +/// Order expression +#[derive(Clone)] +pub struct OrderExpr { + pub(crate) expr: SimpleExpr, + pub(crate) order: Order, +} + +/// Join on types +#[derive(Clone)] +pub enum JoinOn { + Condition(Box), + Columns(Vec), +} + +/// Ordering options +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Order { + Asc, + Desc, +} + +/// Shorthand to create name alias +#[derive(Clone)] +pub struct Alias(String); + +impl Alias { + pub fn new(n: &str) -> Self { + Self(n.to_owned()) + } +} + +impl Iden for Alias { + fn unquoted(&self, s: &mut dyn FmtWrite) { + write!(s, "{}", self.0).unwrap(); + } +} + +/// Convert value to string +pub fn value_to_string(v: &Value) -> String { + let mut s = String::new(); + match v { + Value::NULL => write!(s, "NULL").unwrap(), + Value::Bytes(v) => write!(s, "\'{}\'", std::str::from_utf8(v).unwrap()).unwrap(), + Value::Int(v) => write!(s, "{}", v).unwrap(), + Value::UInt(v) => write!(s, "{}", v).unwrap(), + Value::Float(v) => write!(s, "{}", v).unwrap(), + Value::Double(v) => write!(s, "{}", v).unwrap(), + Value::Date(year, month, day, hour, minutes, seconds, _micro_seconds) => + write!( + s, "{:04}{:02}{:02} {:02}{:02}{:02}", + year, month, day, hour, minutes, seconds + ).unwrap(), + Value::Time(negative, days, hours, minutes, seconds, _micro_seconds) => + write!( + s, "{}{:02}{:02} {:02}{:02}.{:03}", + if *negative { "-" } else { "" }, days, hours, minutes, seconds, _micro_seconds / 1000 + ).unwrap(), + }; + s +} + +/// Convert json value to value +pub fn json_value_to_mysql_value(v: &JsonValue) -> Value { + match v { + JsonValue::Null => Value::NULL, + JsonValue::Bool(v) => Value::Int(v.to_owned().into()), + JsonValue::Number(v) => + if v.is_f64() { + Value::Double(v.as_f64().unwrap()) + } else if v.is_u64() { + Value::UInt(v.as_u64().unwrap()) + } else if v.is_i64() { + Value::Int(v.as_i64().unwrap()) + } else { + unimplemented!() + }, + JsonValue::String(v) => Value::Bytes(v.as_bytes().to_vec()), + JsonValue::Array(_) => unimplemented!(), + JsonValue::Object(_) => unimplemented!(), + } +} + +/// Convert value to json value +#[allow(clippy::many_single_char_names)] +pub fn mysql_value_to_json_value(v: &Value) -> JsonValue { + match v { + Value::NULL => JsonValue::Null, + Value::Bytes(v) => JsonValue::String(from_utf8(v).unwrap().to_string()), + Value::Int(v) => (*v).into(), + Value::UInt(v) => (*v).into(), + Value::Float(v) => (*v).into(), + Value::Double(v) => (*v).into(), + Value::Date(y, m, d, 0, 0, 0, 0) => { + JsonValue::String(format!("'{:04}-{:02}-{:02}'", y, m, d)) + }, + Value::Date(y, m, d, h, i, s, 0) => { + JsonValue::String(format!("'{:04}-{:02}-{:02} {:02}:{:02}:{:02}'", y, m, d, h, i, s)) + }, + Value::Date(y, m, d, h, i, s, u) => { + JsonValue::String(format!( + "'{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:06}'", + y, m, d, h, i, s, u + )) + }, + Value::Time(neg, d, h, i, s, 0) => { + if *neg { + JsonValue::String(format!("'-{:03}:{:02}:{:02}'", d * 24 + u32::from(*h), i, s)) + } else { + JsonValue::String(format!("'{:03}:{:02}:{:02}'", d * 24 + u32::from(*h), i, s)) + } + }, + Value::Time(neg, d, h, i, s, u) => { + if *neg { + JsonValue::String(format!("'-{:03}:{:02}:{:02}.{:06}'", d * 24 + u32::from(*h), i, s, u)) + } else { + JsonValue::String(format!("'{:03}:{:02}:{:02}.{:06}'", d * 24 + u32::from(*h), i, s, u)) + } + }, + } +} diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 000000000..9ba691d69 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,129 @@ +//! Universal value variants used in the library. + +/// Value variants +#[derive(Clone, PartialEq, PartialOrd, Debug)] +pub enum Value { + NULL, + Bytes(Vec), + Int(i64), + UInt(u64), + Float(f32), + Double(f64), + Date(u16, u8, u8, u8, u8, u8, u32), + Time(bool, u32, u8, u8, u8, u32), +} + +macro_rules! into_value_impl ( + (signed $t:ty) => ( + impl From<$t> for Value { + fn from(x: $t) -> Value { + Value::Int(x as i64) + } + } + ); + (unsigned $t:ty) => ( + impl From<$t> for Value { + fn from(x: $t) -> Value { + Value::UInt(x as u64) + } + } + ); +); + +into_value_impl!(signed i8); +into_value_impl!(signed i16); +into_value_impl!(signed i32); +into_value_impl!(signed i64); +into_value_impl!(signed isize); +into_value_impl!(unsigned u8); +into_value_impl!(unsigned u16); +into_value_impl!(unsigned u32); +into_value_impl!(unsigned u64); +into_value_impl!(unsigned usize); + +impl From for Value { + fn from(x: f32) -> Value { + Value::Float(x) + } +} + +impl From for Value { + fn from(x: f64) -> Value { + Value::Double(x) + } +} + +impl From for Value { + fn from(x: bool) -> Value { + Value::Int(if x { 1 } else { 0 }) + } +} + +impl<'a> From<&'a [u8]> for Value { + fn from(x: &'a [u8]) -> Value { + Value::Bytes(x.into()) + } +} + +impl From> for Value { + fn from(x: Vec) -> Value { + Value::Bytes(x) + } +} + +impl<'a> From<&'a str> for Value { + fn from(x: &'a str) -> Value { + let string: String = x.into(); + Value::Bytes(string.into_bytes()) + } +} + +impl From for Value { + fn from(x: String) -> Value { + Value::Bytes(x.into_bytes()) + } +} + +macro_rules! from_array_impl { + ($n:expr) => { + impl From<[u8; $n]> for Value { + fn from(x: [u8; $n]) -> Value { + Value::from(&x[..]) + } + } + }; +} + +from_array_impl!(0); +from_array_impl!(1); +from_array_impl!(2); +from_array_impl!(3); +from_array_impl!(4); +from_array_impl!(5); +from_array_impl!(6); +from_array_impl!(7); +from_array_impl!(8); +from_array_impl!(9); +from_array_impl!(10); +from_array_impl!(11); +from_array_impl!(12); +from_array_impl!(13); +from_array_impl!(14); +from_array_impl!(15); +from_array_impl!(16); +from_array_impl!(17); +from_array_impl!(18); +from_array_impl!(19); +from_array_impl!(20); +from_array_impl!(21); +from_array_impl!(22); +from_array_impl!(23); +from_array_impl!(24); +from_array_impl!(25); +from_array_impl!(26); +from_array_impl!(27); +from_array_impl!(28); +from_array_impl!(29); +from_array_impl!(30); +from_array_impl!(31); +from_array_impl!(32); \ No newline at end of file diff --git a/tests/mod.rs b/tests/mod.rs new file mode 100644 index 000000000..31a884087 --- /dev/null +++ b/tests/mod.rs @@ -0,0 +1,59 @@ +mod mysql; +mod postgres; +mod sqlite; + +use sea_query::{*, tests_cfg::*}; + +use sqlx::{Any, Pool, AnyPool}; +use async_std::task; + +struct TestEnv { + connection: Pool, +} + +impl TestEnv { + #[allow(dead_code)] + fn new(db_url: &str) -> Self { + let db_url = String::from(db_url); + let mut parts: Vec<&str> = db_url.split('/').collect(); + let db = parts.pop().unwrap(); + let database_root_url = &parts.join("/"); + + let connection = task::block_on(async { + AnyPool::connect(database_root_url).await.unwrap() + }); + let mut pool = connection.try_acquire().unwrap(); + task::block_on(async { + let lines = vec![ + vec!["DROP SCHEMA IF EXISTS ", db].join(""), + vec!["CREATE SCHEMA IF NOT EXISTS ", db].join(""), + ]; + for line in lines.into_iter() { + println!("{}", line); + task::block_on(async { + sqlx::query(&line) + .execute(&mut pool) + .await + .unwrap(); + }); + } + }); + + Self { + connection: task::block_on(async { + AnyPool::connect(&db_url).await.unwrap() + }), + } + } + + fn exec(&mut self, sql: &str) { + let mut pool = self.connection.try_acquire().unwrap(); + println!("\n{}\n", sql); + task::block_on(async { + sqlx::query(sql) + .execute(&mut pool) + .await + .unwrap(); + }); + } +} \ No newline at end of file diff --git a/tests/mysql/foreign_key.rs b/tests/mysql/foreign_key.rs new file mode 100644 index 000000000..9b44580be --- /dev/null +++ b/tests/mysql/foreign_key.rs @@ -0,0 +1,31 @@ +use super::*; + +#[test] +fn create_1() { + assert_eq!( + ForeignKey::create() + .name("FK_2e303c3a712662f1fc2a4d0aad6") + .table(Char::Table, Font::Table) + .col(Char::FontId, Font::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) + .to_string(MysqlQueryBuilder::new()), + vec![ + "ALTER TABLE `character`", + "ADD CONSTRAINT `FK_2e303c3a712662f1fc2a4d0aad6`", + "FOREIGN KEY `FK_2e303c3a712662f1fc2a4d0aad6` (`font_id`) REFERENCES `font` (`id`)", + "ON DELETE CASCADE ON UPDATE CASCADE", + ].join(" ") + ); +} + +#[test] +fn drop_1() { + assert_eq!( + ForeignKey::drop() + .name("FK_2e303c3a712662f1fc2a4d0aad6") + .table(Char::Table) + .to_string(MysqlQueryBuilder::new()), + "ALTER TABLE `character` DROP FOREIGN KEY `FK_2e303c3a712662f1fc2a4d0aad6`" + ); +} \ No newline at end of file diff --git a/tests/mysql/index.rs b/tests/mysql/index.rs new file mode 100644 index 000000000..76ce72996 --- /dev/null +++ b/tests/mysql/index.rs @@ -0,0 +1,37 @@ +use super::*; + +#[test] +fn create_1() { + assert_eq!( + Index::create() + .name("idx-glyph-aspect") + .table(Glyph::Table) + .col(Glyph::Aspect) + .to_string(MysqlQueryBuilder::new()), + "CREATE INDEX `idx-glyph-aspect` ON `glyph` (`aspect`)" + ); +} + +#[test] +fn create_2() { + assert_eq!( + Index::create() + .name("idx-glyph-aspect-image") + .table(Glyph::Table) + .col(Glyph::Aspect) + .col(Glyph::Image) + .to_string(MysqlQueryBuilder::new()), + "CREATE INDEX `idx-glyph-aspect-image` ON `glyph` (`aspect`, `image`)" + ); +} + +#[test] +fn drop_1() { + assert_eq!( + Index::drop() + .name("idx-glyph-aspect") + .table(Glyph::Table) + .to_string(MysqlQueryBuilder::new()), + "DROP INDEX `idx-glyph-aspect` ON `glyph`" + ); +} \ No newline at end of file diff --git a/tests/mysql/mod.rs b/tests/mysql/mod.rs new file mode 100644 index 000000000..a34ab3cde --- /dev/null +++ b/tests/mysql/mod.rs @@ -0,0 +1,7 @@ +mod query; +mod table; +mod online; +mod index; +mod foreign_key; + +use super::*; \ No newline at end of file diff --git a/tests/mysql/online.rs b/tests/mysql/online.rs new file mode 100644 index 000000000..fa6aebd05 --- /dev/null +++ b/tests/mysql/online.rs @@ -0,0 +1,204 @@ +use super::*; + +#[test] +#[ignore] +#[allow(clippy::approx_constant)] +fn online_1() { + let mut env = TestEnv::new("mysql://query:query@127.0.0.1/query_test"); + + let sql = Table::create() + .table(Font::Table) + .col(ColumnDef::new(Font::Id).integer_len(11).not_null().auto_increment().primary_key()) + .col(ColumnDef::new(Font::Name).string_len(255).not_null()) + .col(ColumnDef::new(Font::Variant).string_len(255).not_null()) + .col(ColumnDef::new(Font::Language).string_len(255).not_null()) + .engine("InnoDB") + .character_set("utf8mb4") + .collate("utf8mb4_unicode_ci") + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + vec![ + "CREATE TABLE `font` (", + "`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,", + "`name` varchar(255) NOT NULL,", + "`variant` varchar(255) NOT NULL,", + "`language` varchar(255) NOT NULL", + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + ].join(" ") + ); + env.exec(&sql); + + let sql = Index::create() + .name("idx-font-name") + .table(Font::Table) + .col(Font::Name) + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + "CREATE INDEX `idx-font-name` ON `font` (`name`)" + ); + env.exec(&sql); + + let sql = Index::drop() + .name("idx-font-name") + .table(Font::Table) + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + "DROP INDEX `idx-font-name` ON `font`" + ); + env.exec(&sql); + + let sql = Table::create() + .table(Char::Table) + .create_if_not_exists() + .col(ColumnDef::new(Char::Id).integer_len(11).not_null().auto_increment().primary_key()) + .col(ColumnDef::new(Char::FontSize).integer_len(11).not_null()) + .col(ColumnDef::new(Char::Character).string_len(255).not_null()) + .col(ColumnDef::new(Char::SizeW).integer_len(11).not_null()) + .col(ColumnDef::new(Char::SizeH).integer_len(11).not_null()) + .col(ColumnDef::new(Char::FontId).integer_len(11).default(Value::NULL)) + .engine("InnoDB") + .character_set("utf8mb4") + .collate("utf8mb4_unicode_ci") + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + vec![ + "CREATE TABLE IF NOT EXISTS `character` (", + "`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,", + "`font_size` int(11) NOT NULL,", + "`character` varchar(255) NOT NULL,", + "`size_w` int(11) NOT NULL,", + "`size_h` int(11) NOT NULL,", + "`font_id` int(11) DEFAULT NULL", + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + ].join(" ") + ); + env.exec(&sql); + + let sql = Index::create() + .name("idx-character-font_size") + .table(Char::Table) + .col(Char::FontSize) + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + "CREATE INDEX `idx-character-font_size` ON `character` (`font_size`)" + ); + env.exec(&sql); + + let sql = Index::drop() + .name("idx-character-font_size") + .table(Char::Table) + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + "DROP INDEX `idx-character-font_size` ON `character`" + ); + env.exec(&sql); + + let sql = ForeignKey::create() + .name("FK_2e303c3a712662f1fc2a4d0aad6") + .table(Char::Table, Font::Table) + .col(Char::FontId, Font::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + vec![ + "ALTER TABLE `character`", + "ADD CONSTRAINT `FK_2e303c3a712662f1fc2a4d0aad6`", + "FOREIGN KEY `FK_2e303c3a712662f1fc2a4d0aad6` (`font_id`) REFERENCES `font` (`id`)", + "ON DELETE CASCADE ON UPDATE CASCADE", + ].join(" ") + ); + env.exec(&sql); + + let sql = ForeignKey::drop() + .name("FK_2e303c3a712662f1fc2a4d0aad6") + .table(Char::Table) + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + "ALTER TABLE `character` DROP FOREIGN KEY `FK_2e303c3a712662f1fc2a4d0aad6`" + ); + env.exec(&sql); + + let sql = Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .limit(10) + .offset(100) + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + "SELECT `character`, `size_w`, `size_h` FROM `character` LIMIT 10 OFFSET 100" + ); + env.exec(&sql); + + let sql = Query::insert() + .into_table(Char::Table) + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH, Char::FontSize, Char::FontId + ]) + .values_panic(vec![ + "Character".into(), + 123.into(), + 456.into(), + 3.into(), + Value::NULL, + ]) + .json(json!({ + "character": "S", + "size_w": 12, + "size_h": 34, + "font_size": 2, + })) + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + "INSERT INTO `character` (`character`, `size_w`, `size_h`, `font_size`, `font_id`) VALUES ('Character', 123, 456, 3, NULL), ('S', 12, 34, 2, NULL)" + ); + env.exec(&sql); + + let sql = Query::update() + .into_table(Char::Table) + .values(vec![ + (Char::Character, "S".into()), + (Char::SizeW, 1233.into()), + (Char::SizeH, 12.into()), + ]) + .and_where(Expr::col(Char::Id).eq(1)) + .order_by(Char::Id, Order::Asc) + .limit(1) + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + "UPDATE `character` SET `character` = 'S', `size_w` = 1233, `size_h` = 12 WHERE `id` = 1 ORDER BY `id` ASC LIMIT 1" + ); + env.exec(&sql); + + let sql = Table::truncate() + .table(Char::Table) + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + "TRUNCATE TABLE `character`" + ); + env.exec(&sql); + + let sql = Table::drop() + .table(Char::Table) + .cascade() + .to_string(MysqlQueryBuilder::new()); + assert_eq!( + sql, + "DROP TABLE `character` CASCADE" + ); + env.exec(&sql); +} \ No newline at end of file diff --git a/tests/mysql/query.rs b/tests/mysql/query.rs new file mode 100644 index 000000000..ab385b06b --- /dev/null +++ b/tests/mysql/query.rs @@ -0,0 +1,650 @@ +use super::*; + +#[test] +fn select_1() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .limit(10) + .offset(100) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` LIMIT 10 OFFSET 100" + ); +} + +#[test] +fn select_2() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where(Expr::col(Char::SizeW).eq(3)) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` = 3" + ); +} + +#[test] +fn select_3() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where(Expr::col(Char::SizeW).eq(3)) + .and_where(Expr::col(Char::SizeH).eq(4)) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` = 3 AND `size_h` = 4" + ); +} + +#[test] +fn select_4() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Image + ]) + .from_subquery( + Query::select() + .columns(vec![ + Glyph::Image, Glyph::Aspect + ]) + .from(Glyph::Table) + .take(), + Alias::new("subglyph") + ) + .to_string(MysqlQueryBuilder::new()), + "SELECT `image` FROM (SELECT `image`, `aspect` FROM `glyph`) AS `subglyph`" + ); +} + +#[test] +fn select_5() { + assert_eq!( + Query::select() + .table_column(Glyph::Table, Glyph::Image) + .from(Glyph::Table) + .and_where(Expr::tbl(Glyph::Table, Glyph::Aspect).is_in(vec![3, 4])) + .to_string(MysqlQueryBuilder::new()), + "SELECT `glyph`.`image` FROM `glyph` WHERE `glyph`.`aspect` IN (3, 4)" + ); +} + +#[test] +fn select_6() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .exprs(vec![ + Expr::col(Glyph::Image).max(), + ]) + .from(Glyph::Table) + .group_by_columns(vec![ + Glyph::Aspect, + ]) + .and_having(Expr::col(Glyph::Aspect).gt(2)) + .to_string(MysqlQueryBuilder::new()), + "SELECT `aspect`, MAX(`image`) FROM `glyph` GROUP BY `aspect` HAVING `aspect` > 2" + ); +} + +#[test] +fn select_7() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + .to_string(MysqlQueryBuilder::new()), + "SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, 0) > 2" + ); +} + +#[test] +fn select_8() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, + ]) + .from(Char::Table) + .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id`" + ); +} + +#[test] +fn select_9() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, + ]) + .from(Char::Table) + .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + .inner_join(Glyph::Table, Expr::tbl(Char::Table, Char::Character).equals(Glyph::Table, Glyph::Image)) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id` INNER JOIN `glyph` ON `character`.`character` = `glyph`.`image`" + ); +} + +#[test] +fn select_10() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, + ]) + .from(Char::Table) + .left_join(Font::Table, + Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id) + .and(Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + ) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character` LEFT JOIN `font` ON (`character`.`font_id` = `font`.`id`) AND (`character`.`font_id` = `font`.`id`)" + ); +} + +#[test] +fn select_11() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + .order_by(Glyph::Image, Order::Desc) + .order_by_tbl(Glyph::Table, Glyph::Aspect, Order::Asc) + .to_string(MysqlQueryBuilder::new()), + "SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, 0) > 2 ORDER BY `image` DESC, `glyph`.`aspect` ASC" + ); +} + +#[test] +fn select_12() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + .order_by_columns(vec![ + (Glyph::Id, Order::Asc), + (Glyph::Aspect, Order::Desc), + ]) + .to_string(MysqlQueryBuilder::new()), + "SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, 0) > 2 ORDER BY `id` ASC, `aspect` DESC" + ); +} + +#[test] +fn select_13() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + .order_by_table_columns(vec![ + (Glyph::Table, Glyph::Id, Order::Asc), + (Glyph::Table, Glyph::Aspect, Order::Desc), + ]) + .to_string(MysqlQueryBuilder::new()), + "SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, 0) > 2 ORDER BY `glyph`.`id` ASC, `glyph`.`aspect` DESC" + ); +} + +#[test] +fn select_14() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Id, + Glyph::Aspect, + ]) + .expr(Expr::col(Glyph::Image).max()) + .from(Glyph::Table) + .group_by_table_columns(vec![ + (Glyph::Table, Glyph::Id), + (Glyph::Table, Glyph::Aspect), + ]) + .and_having(Expr::col(Glyph::Aspect).gt(2)) + .to_string(MysqlQueryBuilder::new()), + "SELECT `id`, `aspect`, MAX(`image`) FROM `glyph` GROUP BY `glyph`.`id`, `glyph`.`aspect` HAVING `aspect` > 2" + ); +} + +#[test] +fn select_15() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character + ]) + .from(Char::Table) + .and_where(Expr::col(Char::FontId).is_null()) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `font_id` IS NULL" + ); +} + +#[test] +fn select_16() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character + ]) + .from(Char::Table) + .and_where(Expr::col(Char::FontId).is_null()) + .and_where(Expr::col(Char::Character).is_not_null()) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `font_id` IS NULL AND `character` IS NOT NULL" + ); +} + +#[test] +fn select_17() { + assert_eq!( + Query::select() + .table_columns(vec![ + (Glyph::Table, Glyph::Image), + ]) + .from(Glyph::Table) + .and_where(Expr::tbl(Glyph::Table, Glyph::Aspect).between(3, 5)) + .to_string(MysqlQueryBuilder::new()), + "SELECT `glyph`.`image` FROM `glyph` WHERE `glyph`.`aspect` BETWEEN 3 AND 5" + ); +} + +#[test] +fn select_18() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::col(Glyph::Aspect).between(3, 5)) + .and_where(Expr::col(Glyph::Aspect).not_between(8, 10)) + .to_string(MysqlQueryBuilder::new()), + "SELECT `aspect` FROM `glyph` WHERE (`aspect` BETWEEN 3 AND 5) AND (`aspect` NOT BETWEEN 8 AND 10)" + ); +} + +#[test] +fn select_19() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character + ]) + .from(Char::Table) + .and_where(Expr::col(Char::Character).eq("A")) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `character` = 'A'" + ); +} + +#[test] +fn select_20() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .and_where(Expr::col(Char::Character).like("A")) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `character` LIKE 'A'" + ); +} + +#[test] +fn select_21() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character + ]) + .from(Char::Table) + .or_where(Expr::col(Char::Character).like("A%")) + .or_where(Expr::col(Char::Character).like("%B")) + .or_where(Expr::col(Char::Character).like("%C%")) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `character` LIKE 'A%' OR `character` LIKE '%B' OR `character` LIKE '%C%'" + ); +} + +#[test] +fn select_22() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .or_where(Expr::col(Char::Character).like("C")) + .or_where(Expr::col(Char::Character).like("D").and(Expr::col(Char::Character).like("E"))) + .and_where(Expr::col(Char::Character).like("F").or(Expr::col(Char::Character).like("G"))) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `character` LIKE 'C' OR ((`character` LIKE 'D') AND (`character` LIKE 'E')) AND ((`character` LIKE 'F') OR (`character` LIKE 'G'))" + ); +} + +#[test] +fn select_23() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .and_where_option(None) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character`" + ); +} + +#[test] +fn select_24() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .conditions(true, |x| { + x.and_where(Expr::col(Char::FontId).eq(5)); + }, |_|()) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `font_id` = 5" + ); +} + +#[test] +fn select_25() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .and_where( + Expr::col(Char::SizeW).mul(2) + .equals(Expr::col(Char::SizeH).div(2)) + ) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `size_w` * 2 = `size_h` / 2" + ); +} + +#[test] +fn select_26() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .and_where( + Expr::expr(Expr::col(Char::SizeW).add(1)).mul(2) + .equals(Expr::expr(Expr::col(Char::SizeH).div(2)).sub(1)) + ) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE (`size_w` + 1) * 2 = (`size_h` / 2) - 1" + ); +} + +#[test] +fn select_27() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where(Expr::col(Char::SizeW).eq(3)) + .and_where(Expr::col(Char::SizeH).eq(4)) + .and_where(Expr::col(Char::SizeH).eq(5)) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` = 3 AND `size_h` = 4 AND `size_h` = 5" + ); +} + +#[test] +fn select_28() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .or_where(Expr::col(Char::SizeW).eq(3)) + .or_where(Expr::col(Char::SizeH).eq(4)) + .or_where(Expr::col(Char::SizeH).eq(5)) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` = 3 OR `size_h` = 4 OR `size_h` = 5" + ); +} + +#[test] +fn select_29() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where(Expr::col(Char::SizeW).eq(3)) + .or_where(Expr::col(Char::SizeH).eq(4)) + .and_where(Expr::col(Char::SizeH).eq(5)) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` = 3 OR `size_h` = 4 AND `size_h` = 5" + ); +} + +#[test] +fn select_30() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where( + Expr::col(Char::SizeW).mul(2) + .add(Expr::col(Char::SizeH).div(3)) + .equals(Expr::value(4)) + ) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` WHERE (`size_w` * 2) + (`size_h` / 3) = 4" + ); +} + +#[test] +fn select_31() { + assert_eq!( + Query::select() + .expr((1..10_i32).fold(Expr::value(0), |expr, i| { + expr.add(Expr::value(i)) + })) + .to_string(MysqlQueryBuilder::new()), + "SELECT 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9" + ); +} + +#[test] +fn select_32() { + assert_eq!( + Query::select() + .expr_alias(Expr::col(Char::Character), Alias::new("C")) + .from(Char::Table) + .to_string(MysqlQueryBuilder::new()), + "SELECT `character` AS `C` FROM `character`" + ); +} + +#[test] +fn select_33() { + assert_eq!( + Query::select() + .column(Glyph::Image) + .from(Glyph::Table) + .and_where(Expr::col(Glyph::Aspect).in_subquery( + Query::select() + .expr(Expr::cust("3 + 2 * 2")) + .take() + )) + .to_string(MysqlQueryBuilder::new()), + "SELECT `image` FROM `glyph` WHERE `aspect` IN (SELECT 3 + 2 * 2)" + ); +} + +#[test] +fn select_34() { + assert_eq!( + Query::select() + .column(Glyph::Aspect) + .expr(Expr::col(Glyph::Image).max()) + .from(Glyph::Table) + .group_by_columns(vec![ + Glyph::Aspect, + ]) + .or_having(Expr::col(Glyph::Aspect).gt(2).or(Expr::col(Glyph::Aspect).lt(8))) + .or_having(Expr::col(Glyph::Aspect).gt(12).and(Expr::col(Glyph::Aspect).lt(18))) + .and_having(Expr::col(Glyph::Aspect).gt(22).or(Expr::col(Glyph::Aspect).lt(28))) + .or_having(Expr::col(Glyph::Aspect).gt(32)) + .to_string(MysqlQueryBuilder), + vec![ + "SELECT `aspect`, MAX(`image`) FROM `glyph` GROUP BY `aspect`", + "HAVING ((`aspect` > 2) OR (`aspect` < 8))", + "OR ((`aspect` > 12) AND (`aspect` < 18))", + "AND ((`aspect` > 22) OR (`aspect` < 28))", + "OR `aspect` > 32", + ].join(" ") + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_1() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .json(json!({ + "image": "24B0E11951B03B07F8300FD003983F03F0780060", + "aspect": 2.1345, + })) + .to_string(MysqlQueryBuilder::new()), + "INSERT INTO `glyph` (`aspect`, `image`) VALUES (2.1345, '24B0E11951B03B07F8300FD003983F03F0780060')" + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_2() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns(vec![ + Glyph::Image, + Glyph::Aspect, + ]) + .values_panic(vec![ + "04108048005887010020060000204E0180400400".into(), + 3.1415.into(), + ]) + .to_string(MysqlQueryBuilder::new()), + "INSERT INTO `glyph` (`image`, `aspect`) VALUES ('04108048005887010020060000204E0180400400', 3.1415)" + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_3() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns(vec![ + Glyph::Image, + Glyph::Aspect, + ]) + .values_panic(vec![ + "04108048005887010020060000204E0180400400".into(), + 3.1415.into(), + ]) + .json(json!({ + "aspect": 2.1345, + })) + .to_string(MysqlQueryBuilder::new()), + "INSERT INTO `glyph` (`image`, `aspect`) VALUES ('04108048005887010020060000204E0180400400', 3.1415), (NULL, 2.1345)" + ); +} + +#[test] +fn update_1() { + assert_eq!( + Query::update() + .into_table(Glyph::Table) + .values(vec![ + (Glyph::Aspect, 2.1345.into()), + (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), + ]) + .and_where(Expr::col(Glyph::Id).eq(1)) + .order_by(Glyph::Id, Order::Asc) + .limit(1) + .to_string(MysqlQueryBuilder::new()), + "UPDATE `glyph` SET `aspect` = 2.1345, `image` = '24B0E11951B03B07F8300FD003983F03F0780060' WHERE `id` = 1 ORDER BY `id` ASC LIMIT 1" + ); +} + +#[test] +fn update_2() { + assert_eq!( + Query::update() + .into_table(Glyph::Table) + .json(json!({ + "aspect": 2.1345, + "image": "24B0E11951B03B07F8300FD003983F03F0780060", + })) + .and_where(Expr::col(Glyph::Id).eq(1)) + .order_by(Glyph::Id, Order::Asc) + .limit(1) + .to_string(MysqlQueryBuilder::new()), + "UPDATE `glyph` SET `aspect` = 2.1345, `image` = '24B0E11951B03B07F8300FD003983F03F0780060' WHERE `id` = 1 ORDER BY `id` ASC LIMIT 1" + ); +} + +#[test] +fn update_3() { + assert_eq!( + Query::update() + .into_table(Glyph::Table) + .value_expr(Glyph::Aspect, Expr::cust("60 * 24 * 24")) + .values(vec![ + (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), + ]) + .and_where(Expr::col(Glyph::Id).eq(1)) + .order_by(Glyph::Id, Order::Asc) + .limit(1) + .to_string(MysqlQueryBuilder::new()), + "UPDATE `glyph` SET `aspect` = 60 * 24 * 24, `image` = '24B0E11951B03B07F8300FD003983F03F0780060' WHERE `id` = 1 ORDER BY `id` ASC LIMIT 1" + ); +} + +#[test] +fn delete_1() { + assert_eq!( + Query::delete() + .from_table(Glyph::Table) + .and_where(Expr::col(Glyph::Id).eq(1)) + .order_by(Glyph::Id, Order::Asc) + .limit(1) + .to_string(MysqlQueryBuilder::new()), + "DELETE FROM `glyph` WHERE `id` = 1 ORDER BY `id` ASC LIMIT 1" + ); +} \ No newline at end of file diff --git a/tests/mysql/table.rs b/tests/mysql/table.rs new file mode 100644 index 000000000..20c1e09e5 --- /dev/null +++ b/tests/mysql/table.rs @@ -0,0 +1,169 @@ +use super::*; + +#[test] +fn create_1() { + assert_eq!( + Table::create() + .table(Glyph::Table) + .col(ColumnDef::new(Glyph::Id).integer_len(11).not_null().auto_increment().primary_key()) + .col(ColumnDef::new(Glyph::Aspect).double().not_null()) + .col(ColumnDef::new(Glyph::Image).text()) + .engine("InnoDB") + .character_set("utf8mb4") + .collate("utf8mb4_unicode_ci") + .to_string(MysqlQueryBuilder::new()), + vec![ + "CREATE TABLE `glyph` (", + "`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,", + "`aspect` double NOT NULL,", + "`image` text", + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + ].join(" ") + ); +} + +#[test] +fn create_2() { + assert_eq!( + Table::create() + .table(Font::Table) + .col(ColumnDef::new(Font::Id).integer_len(11).not_null().auto_increment().primary_key()) + .col(ColumnDef::new(Font::Name).string_len(255).not_null()) + .col(ColumnDef::new(Font::Variant).string_len(255).not_null()) + .col(ColumnDef::new(Font::Language).string_len(255).not_null()) + .engine("InnoDB") + .character_set("utf8mb4") + .collate("utf8mb4_unicode_ci") + .to_string(MysqlQueryBuilder::new()), + vec![ + "CREATE TABLE `font` (", + "`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,", + "`name` varchar(255) NOT NULL,", + "`variant` varchar(255) NOT NULL,", + "`language` varchar(255) NOT NULL", + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + ].join(" ") + ); +} + +#[test] +fn create_3() { + assert_eq!( + Table::create() + .table(Char::Table) + .create_if_not_exists() + .col(ColumnDef::new(Char::Id).integer_len(11).not_null().auto_increment().primary_key()) + .col(ColumnDef::new(Char::FontSize).integer_len(11).not_null()) + .col(ColumnDef::new(Char::Character).string_len(255).not_null()) + .col(ColumnDef::new(Char::SizeW).integer_len(11).not_null()) + .col(ColumnDef::new(Char::SizeH).integer_len(11).not_null()) + .col(ColumnDef::new(Char::FontId).integer_len(11).default(Value::NULL)) + .foreign_key( + ForeignKey::create() + .name("FK_2e303c3a712662f1fc2a4d0aad6") + .table(Char::Table, Font::Table) + .col(Char::FontId, Font::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) + ) + .engine("InnoDB") + .character_set("utf8mb4") + .collate("utf8mb4_unicode_ci") + .to_string(MysqlQueryBuilder::new()), + vec![ + "CREATE TABLE IF NOT EXISTS `character` (", + "`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,", + "`font_size` int(11) NOT NULL,", + "`character` varchar(255) NOT NULL,", + "`size_w` int(11) NOT NULL,", + "`size_h` int(11) NOT NULL,", + "`font_id` int(11) DEFAULT NULL,", + "CONSTRAINT `FK_2e303c3a712662f1fc2a4d0aad6`", + "FOREIGN KEY `FK_2e303c3a712662f1fc2a4d0aad6` (`font_id`) REFERENCES `font` (`id`)", + "ON DELETE CASCADE ON UPDATE CASCADE", + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + ].join(" ") + ); +} + +#[test] +fn drop_1() { + assert_eq!( + Table::drop() + .table(Glyph::Table) + .table(Char::Table) + .cascade() + .to_string(MysqlQueryBuilder::new()), + "DROP TABLE `glyph`, `character` CASCADE" + ); +} + +#[test] +fn truncate_1() { + assert_eq!( + Table::truncate() + .table(Font::Table) + .to_string(MysqlQueryBuilder::new()), + "TRUNCATE TABLE `font`" + ); +} + +#[test] +fn alter_1() { + assert_eq!( + Table::alter() + .table(Font::Table) + .add_column(ColumnDef::new(Alias::new("new_col")).integer().not_null().default(100)) + .to_string(MysqlQueryBuilder::new()), + "ALTER TABLE `font` ADD COLUMN `new_col` int NOT NULL DEFAULT 100" + ); +} + +#[test] +fn alter_2() { + assert_eq!( + Table::alter() + .table(Font::Table) + .modify_column(ColumnDef::new(Alias::new("new_col")).big_integer().default(999)) + .to_string(MysqlQueryBuilder::new()), + "ALTER TABLE `font` MODIFY COLUMN `new_col` bigint DEFAULT 999" + ); +} + +#[test] +fn alter_3() { + assert_eq!( + Table::alter() + .table(Font::Table) + .rename_column(Alias::new("new_col"), Alias::new("new_column")) + .to_string(MysqlQueryBuilder::new()), + "ALTER TABLE `font` RENAME COLUMN `new_col` TO `new_column`" + ); +} + +#[test] +fn alter_4() { + assert_eq!( + Table::alter() + .table(Font::Table) + .drop_column(Alias::new("new_column")) + .to_string(MysqlQueryBuilder::new()), + "ALTER TABLE `font` DROP COLUMN `new_column`" + ); +} + +#[test] +fn alter_5() { + assert_eq!( + Table::rename() + .table(Font::Table, Alias::new("font_new")) + .to_string(MysqlQueryBuilder::new()), + "RENAME TABLE `font` TO `font_new`" + ); +} + +#[test] +#[should_panic(expected = "No alter option found")] +fn alter_6() { + Table::alter().to_string(MysqlQueryBuilder::new()); +} \ No newline at end of file diff --git a/tests/postgres/foreign_key.rs b/tests/postgres/foreign_key.rs new file mode 100644 index 000000000..4c2a17f38 --- /dev/null +++ b/tests/postgres/foreign_key.rs @@ -0,0 +1,30 @@ +use super::*; + +#[test] +fn create_1() { + assert_eq!( + ForeignKey::create() + .name("FK_2e303c3a712662f1fc2a4d0aad6") + .table(Char::Table, Font::Table) + .col(Char::FontId, Font::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) + .to_string(PostgresQueryBuilder::new()), + vec![ + r#"ALTER TABLE "character" ADD CONSTRAINT "FK_2e303c3a712662f1fc2a4d0aad6""#, + r#"FOREIGN KEY ("font_id") REFERENCES "font" ("id")"#, + r#"ON DELETE CASCADE ON UPDATE CASCADE"#, + ].join(" ") + ); +} + +#[test] +fn drop_1() { + assert_eq!( + ForeignKey::drop() + .name("FK_2e303c3a712662f1fc2a4d0aad6") + .table(Char::Table) + .to_string(PostgresQueryBuilder::new()), + r#"ALTER TABLE "character" DROP CONSTRAINT "FK_2e303c3a712662f1fc2a4d0aad6""# + ); +} \ No newline at end of file diff --git a/tests/postgres/index.rs b/tests/postgres/index.rs new file mode 100644 index 000000000..b9919de16 --- /dev/null +++ b/tests/postgres/index.rs @@ -0,0 +1,36 @@ +use super::*; + +#[test] +fn create_1() { + assert_eq!( + Index::create() + .name("idx-glyph-aspect") + .table(Glyph::Table) + .col(Glyph::Aspect) + .to_string(PostgresQueryBuilder::new()), + r#"CREATE INDEX "idx-glyph-aspect" ON "glyph" ("aspect")"# + ); +} + +#[test] +fn create_2() { + assert_eq!( + Index::create() + .name("idx-glyph-aspect-image") + .table(Glyph::Table) + .col(Glyph::Aspect) + .col(Glyph::Image) + .to_string(PostgresQueryBuilder::new()), + r#"CREATE INDEX "idx-glyph-aspect-image" ON "glyph" ("aspect", "image")"# + ); +} + +#[test] +fn drop_1() { + assert_eq!( + Index::drop() + .name("idx-glyph-aspect") + .to_string(PostgresQueryBuilder::new()), + r#"DROP INDEX "idx-glyph-aspect""# + ); +} \ No newline at end of file diff --git a/tests/postgres/mod.rs b/tests/postgres/mod.rs new file mode 100644 index 000000000..a34ab3cde --- /dev/null +++ b/tests/postgres/mod.rs @@ -0,0 +1,7 @@ +mod query; +mod table; +mod online; +mod index; +mod foreign_key; + +use super::*; \ No newline at end of file diff --git a/tests/postgres/online.rs b/tests/postgres/online.rs new file mode 100644 index 000000000..31cfa1007 --- /dev/null +++ b/tests/postgres/online.rs @@ -0,0 +1,195 @@ +use super::*; + +#[test] +#[ignore] +#[allow(clippy::approx_constant)] +fn online_1() { + let mut env = TestEnv::new("postgresql://query:query@127.0.0.1/query_test"); + + let sql = Table::create() + .table(Font::Table) + .col(ColumnDef::new(Font::Id).integer().not_null().primary_key().auto_increment()) + .col(ColumnDef::new(Font::Name).string_len(255).not_null()) + .col(ColumnDef::new(Font::Variant).string_len(255).not_null()) + .col(ColumnDef::new(Font::Language).string_len(255).not_null()) + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + vec![ + r#"CREATE TABLE "font" ("#, + r#""id" serial NOT NULL PRIMARY KEY,"#, + r#""name" varchar(255) NOT NULL,"#, + r#""variant" varchar(255) NOT NULL,"#, + r#""language" varchar(255) NOT NULL"#, + r#")"#, + ].join(" ") + ); + env.exec(&sql); + + let sql = Index::create() + .name("idx-font-name") + .table(Font::Table) + .col(Font::Name) + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + r#"CREATE INDEX "idx-font-name" ON "font" ("name")"# + ); + env.exec(&sql); + + let sql = Index::drop() + .name("idx-font-name") + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + r#"DROP INDEX "idx-font-name""# + ); + env.exec(&sql); + + let sql = Table::create() + .table(Char::Table) + .create_if_not_exists() + .col(ColumnDef::new(Char::Id).integer().not_null().primary_key().auto_increment()) + .col(ColumnDef::new(Char::FontSize).integer().not_null()) + .col(ColumnDef::new(Char::Character).string_len(255).not_null()) + .col(ColumnDef::new(Char::SizeW).integer().not_null()) + .col(ColumnDef::new(Char::SizeH).integer().not_null()) + .col(ColumnDef::new(Char::FontId).integer().default(Value::NULL)) + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + vec![ + r#"CREATE TABLE IF NOT EXISTS "character" ("#, + r#""id" serial NOT NULL PRIMARY KEY,"#, + r#""font_size" integer NOT NULL,"#, + r#""character" varchar(255) NOT NULL,"#, + r#""size_w" integer NOT NULL,"#, + r#""size_h" integer NOT NULL,"#, + r#""font_id" integer DEFAULT NULL"#, + r#")"#, + ].join(" ") + ); + env.exec(&sql); + + let sql = Index::create() + .name("idx-character-font_size") + .table(Char::Table) + .col(Char::FontSize) + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + r#"CREATE INDEX "idx-character-font_size" ON "character" ("font_size")"# + ); + env.exec(&sql); + + let sql = Index::drop() + .name("idx-character-font_size") + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + r#"DROP INDEX "idx-character-font_size""# + ); + env.exec(&sql); + + let sql = ForeignKey::create() + .name("FK_2e303c3a712662f1fc2a4d0aad6") + .table(Char::Table, Font::Table) + .col(Char::FontId, Font::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + vec![ + r#"ALTER TABLE "character""#, + r#"ADD CONSTRAINT "FK_2e303c3a712662f1fc2a4d0aad6""#, + r#"FOREIGN KEY ("font_id") REFERENCES "font" ("id")"#, + r#"ON DELETE CASCADE ON UPDATE CASCADE"#, + ].join(" ") + ); + env.exec(&sql); + + let sql = ForeignKey::drop() + .name("FK_2e303c3a712662f1fc2a4d0aad6") + .table(Char::Table) + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + r#"ALTER TABLE "character" DROP CONSTRAINT "FK_2e303c3a712662f1fc2a4d0aad6""# + ); + env.exec(&sql); + + let sql = Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .limit(10) + .offset(100) + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + r#"SELECT "character", "size_w", "size_h" FROM "character" LIMIT 10 OFFSET 100"# + ); + env.exec(&sql); + + let sql = Query::insert() + .into_table(Char::Table) + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH, Char::FontSize, Char::FontId + ]) + .values_panic(vec![ + "Character".into(), + 123.into(), + 456.into(), + 3.into(), + Value::NULL, + ]) + .json(json!({ + "character": "S", + "size_w": 12, + "size_h": 34, + "font_size": 2, + })) + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + r#"INSERT INTO "character" ("character", "size_w", "size_h", "font_size", "font_id") VALUES ('Character', 123, 456, 3, NULL), ('S', 12, 34, 2, NULL)"# + ); + env.exec(&sql); + + let sql = Query::update() + .into_table(Char::Table) + .values(vec![ + (Char::Character, "S".into()), + (Char::SizeW, 1233.into()), + (Char::SizeH, 12.into()), + ]) + .and_where(Expr::col(Char::Id).eq(1)) + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + r#"UPDATE "character" SET "character" = 'S', "size_w" = 1233, "size_h" = 12 WHERE "id" = 1"# + ); + env.exec(&sql); + + let sql = Table::truncate() + .table(Char::Table) + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + r#"TRUNCATE TABLE "character""# + ); + env.exec(&sql); + + let sql = Table::drop() + .table(Char::Table) + .table(Font::Table) + .cascade() + .to_string(PostgresQueryBuilder::new()); + assert_eq!( + sql, + r#"DROP TABLE "character", "font" CASCADE"# + ); + env.exec(&sql); +} \ No newline at end of file diff --git a/tests/postgres/query.rs b/tests/postgres/query.rs new file mode 100644 index 000000000..b2e3f3c09 --- /dev/null +++ b/tests/postgres/query.rs @@ -0,0 +1,642 @@ +use super::*; + +#[test] +fn select_1() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .limit(10) + .offset(100) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character", "size_w", "size_h" FROM "character" LIMIT 10 OFFSET 100"# + ); +} + +#[test] +fn select_2() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where(Expr::col(Char::SizeW).eq(3)) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w" = 3"# + ); +} + +#[test] +fn select_3() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where(Expr::col(Char::SizeW).eq(3)) + .and_where(Expr::col(Char::SizeH).eq(4)) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w" = 3 AND "size_h" = 4"# + ); +} + +#[test] +fn select_4() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect + ]) + .from_subquery( + Query::select() + .columns(vec![ + Glyph::Image, Glyph::Aspect + ]) + .from(Glyph::Table) + .take(), + Alias::new("subglyph") + ) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "aspect" FROM (SELECT "image", "aspect" FROM "glyph") AS "subglyph""# + ); +} + +#[test] +fn select_5() { + assert_eq!( + Query::select() + .table_column(Glyph::Table, Glyph::Image) + .from(Glyph::Table) + .and_where(Expr::tbl(Glyph::Table, Glyph::Aspect).is_in(vec![3, 4])) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "glyph"."image" FROM "glyph" WHERE "glyph"."aspect" IN (3, 4)"# + ); +} + +#[test] +fn select_6() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .exprs(vec![ + Expr::col(Glyph::Image).max(), + ]) + .from(Glyph::Table) + .group_by_columns(vec![ + Glyph::Aspect, + ]) + .and_having(Expr::col(Glyph::Aspect).gt(2)) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "aspect", MAX("image") FROM "glyph" GROUP BY "aspect" HAVING "aspect" > 2"# + ); +} + +#[test] +fn select_7() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "aspect" FROM "glyph" WHERE COALESCE("aspect", 0) > 2"# + ); +} + +#[test] +fn select_8() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, + ]) + .from(Char::Table) + .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character" LEFT JOIN "font" ON "character"."font_id" = "font"."id""# + ); +} + +#[test] +fn select_9() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, + ]) + .from(Char::Table) + .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + .inner_join(Glyph::Table, Expr::tbl(Char::Table, Char::Character).equals(Glyph::Table, Glyph::Image)) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character" LEFT JOIN "font" ON "character"."font_id" = "font"."id" INNER JOIN "glyph" ON "character"."character" = "glyph"."image""# + ); +} + +#[test] +fn select_10() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, + ]) + .from(Char::Table) + .left_join(Font::Table, + Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id) + .and(Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + ) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character" LEFT JOIN "font" ON ("character"."font_id" = "font"."id") AND ("character"."font_id" = "font"."id")"# + ); +} + +#[test] +fn select_11() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + .order_by(Glyph::Image, Order::Desc) + .order_by_tbl(Glyph::Table, Glyph::Aspect, Order::Asc) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "aspect" FROM "glyph" WHERE COALESCE("aspect", 0) > 2 ORDER BY "image" DESC, "glyph"."aspect" ASC"# + ); +} + +#[test] +fn select_12() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + .order_by_columns(vec![ + (Glyph::Id, Order::Asc), + (Glyph::Aspect, Order::Desc), + ]) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "aspect" FROM "glyph" WHERE COALESCE("aspect", 0) > 2 ORDER BY "id" ASC, "aspect" DESC"# + ); +} + +#[test] +fn select_13() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + .order_by_table_columns(vec![ + (Glyph::Table, Glyph::Id, Order::Asc), + (Glyph::Table, Glyph::Aspect, Order::Desc), + ]) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "aspect" FROM "glyph" WHERE COALESCE("aspect", 0) > 2 ORDER BY "glyph"."id" ASC, "glyph"."aspect" DESC"# + ); +} + +#[test] +fn select_14() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Id, + Glyph::Aspect, + ]) + .expr(Expr::col(Glyph::Image).max()) + .from(Glyph::Table) + .group_by_table_columns(vec![ + (Glyph::Table, Glyph::Id), + (Glyph::Table, Glyph::Aspect), + ]) + .and_having(Expr::col(Glyph::Aspect).gt(2)) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "id", "aspect", MAX("image") FROM "glyph" GROUP BY "glyph"."id", "glyph"."aspect" HAVING "aspect" > 2"# + ); +} + +#[test] +fn select_15() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character + ]) + .from(Char::Table) + .and_where(Expr::col(Char::FontId).is_null()) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character" WHERE "font_id" IS NULL"# + ); +} + +#[test] +fn select_16() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character + ]) + .from(Char::Table) + .and_where(Expr::col(Char::FontId).is_null()) + .and_where(Expr::col(Char::Character).is_not_null()) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character" WHERE "font_id" IS NULL AND "character" IS NOT NULL"# + ); +} + +#[test] +fn select_17() { + assert_eq!( + Query::select() + .table_columns(vec![ + (Glyph::Table, Glyph::Image), + ]) + .from(Glyph::Table) + .and_where(Expr::tbl(Glyph::Table, Glyph::Aspect).between(3, 5)) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "glyph"."image" FROM "glyph" WHERE "glyph"."aspect" BETWEEN 3 AND 5"# + ); +} + +#[test] +fn select_18() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::col(Glyph::Aspect).between(3, 5)) + .and_where(Expr::col(Glyph::Aspect).not_between(8, 10)) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "aspect" FROM "glyph" WHERE ("aspect" BETWEEN 3 AND 5) AND ("aspect" NOT BETWEEN 8 AND 10)"# + ); +} + +#[test] +fn select_19() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character + ]) + .from(Char::Table) + .and_where(Expr::col(Char::Character).eq("A")) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character" WHERE "character" = 'A'"# + ); +} + +#[test] +fn select_20() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .and_where(Expr::col(Char::Character).like("A")) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character" WHERE "character" LIKE 'A'"# + ); +} + +#[test] +fn select_21() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character + ]) + .from(Char::Table) + .or_where(Expr::col(Char::Character).like("A%")) + .or_where(Expr::col(Char::Character).like("%B")) + .or_where(Expr::col(Char::Character).like("%C%")) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character" WHERE "character" LIKE 'A%' OR "character" LIKE '%B' OR "character" LIKE '%C%'"# + ); +} + +#[test] +fn select_22() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .or_where(Expr::col(Char::Character).like("C")) + .or_where(Expr::col(Char::Character).like("D").and(Expr::col(Char::Character).like("E"))) + .and_where(Expr::col(Char::Character).like("F").or(Expr::col(Char::Character).like("G"))) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character" WHERE "character" LIKE 'C' OR (("character" LIKE 'D') AND ("character" LIKE 'E')) AND (("character" LIKE 'F') OR ("character" LIKE 'G'))"# + ); +} + +#[test] +fn select_23() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .and_where_option(None) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character""# + ); +} + +#[test] +fn select_24() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .conditions(true, |x| { + x.and_where(Expr::col(Char::FontId).eq(5)); + }, |_|()) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character" WHERE "font_id" = 5"# + ); +} + +#[test] +fn select_25() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .and_where( + Expr::col(Char::SizeW).mul(2) + .equals(Expr::col(Char::SizeH).div(2)) + ) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character" WHERE "size_w" * 2 = "size_h" / 2"# + ); +} + +#[test] +fn select_26() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .and_where( + Expr::expr(Expr::col(Char::SizeW).add(1)).mul(2) + .equals(Expr::expr(Expr::col(Char::SizeH).div(2)).sub(1)) + ) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" FROM "character" WHERE ("size_w" + 1) * 2 = ("size_h" / 2) - 1"# + ); +} + +#[test] +fn select_27() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where(Expr::col(Char::SizeW).eq(3)) + .and_where(Expr::col(Char::SizeH).eq(4)) + .and_where(Expr::col(Char::SizeH).eq(5)) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w" = 3 AND "size_h" = 4 AND "size_h" = 5"# + ); +} + +#[test] +fn select_28() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .or_where(Expr::col(Char::SizeW).eq(3)) + .or_where(Expr::col(Char::SizeH).eq(4)) + .or_where(Expr::col(Char::SizeH).eq(5)) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w" = 3 OR "size_h" = 4 OR "size_h" = 5"# + ); +} + +#[test] +fn select_29() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where(Expr::col(Char::SizeW).eq(3)) + .or_where(Expr::col(Char::SizeH).eq(4)) + .and_where(Expr::col(Char::SizeH).eq(5)) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w" = 3 OR "size_h" = 4 AND "size_h" = 5"# + ); +} + +#[test] +fn select_30() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where( + Expr::col(Char::SizeW).mul(2) + .add(Expr::col(Char::SizeH).div(3)) + .equals(Expr::value(4)) + ) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE ("size_w" * 2) + ("size_h" / 3) = 4"# + ); +} + +#[test] +fn select_31() { + assert_eq!( + Query::select() + .expr((1..10_i32).fold(Expr::value(0), |expr, i| { + expr.add(Expr::value(i)) + })) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9"# + ); +} + +#[test] +fn select_32() { + assert_eq!( + Query::select() + .expr_alias(Expr::col(Char::Character), Alias::new("C")) + .from(Char::Table) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "character" AS "C" FROM "character""# + ); +} + +#[test] +fn select_33() { + assert_eq!( + Query::select() + .column(Glyph::Image) + .from(Glyph::Table) + .and_where(Expr::col(Glyph::Aspect).in_subquery( + Query::select() + .expr(Expr::cust("3 + 2 * 2")) + .take() + )) + .to_string(PostgresQueryBuilder::new()), + r#"SELECT "image" FROM "glyph" WHERE "aspect" IN (SELECT 3 + 2 * 2)"# + ); +} + +#[test] +fn select_34() { + assert_eq!( + Query::select() + .column(Glyph::Aspect) + .expr(Expr::col(Glyph::Image).max()) + .from(Glyph::Table) + .group_by_columns(vec![ + Glyph::Aspect, + ]) + .or_having(Expr::col(Glyph::Aspect).gt(2).or(Expr::col(Glyph::Aspect).lt(8))) + .or_having(Expr::col(Glyph::Aspect).gt(12).and(Expr::col(Glyph::Aspect).lt(18))) + .and_having(Expr::col(Glyph::Aspect).gt(22).or(Expr::col(Glyph::Aspect).lt(28))) + .or_having(Expr::col(Glyph::Aspect).gt(32)) + .to_string(PostgresQueryBuilder), + vec![ + r#"SELECT "aspect", MAX("image") FROM "glyph" GROUP BY "aspect""#, + r#"HAVING (("aspect" > 2) OR ("aspect" < 8))"#, + r#"OR (("aspect" > 12) AND ("aspect" < 18))"#, + r#"AND (("aspect" > 22) OR ("aspect" < 28))"#, + r#"OR "aspect" > 32"#, + ].join(" ") + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_1() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .json(json!({ + "image": "24B0E11951B03B07F8300FD003983F03F0780060", + "aspect": 2.1345, + })) + .to_string(PostgresQueryBuilder::new()), + r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2.1345, '24B0E11951B03B07F8300FD003983F03F0780060')"# + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_2() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns(vec![ + Glyph::Image, + Glyph::Aspect, + ]) + .values_panic(vec![ + "04108048005887010020060000204E0180400400".into(), + 3.1415.into(), + ]) + .to_string(PostgresQueryBuilder::new()), + r#"INSERT INTO "glyph" ("image", "aspect") VALUES ('04108048005887010020060000204E0180400400', 3.1415)"# + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_3() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns(vec![ + Glyph::Image, + Glyph::Aspect, + ]) + .values_panic(vec![ + "04108048005887010020060000204E0180400400".into(), + 3.1415.into(), + ]) + .json(json!({ + "aspect": 2.1345, + })) + .to_string(PostgresQueryBuilder::new()), + r#"INSERT INTO "glyph" ("image", "aspect") VALUES ('04108048005887010020060000204E0180400400', 3.1415), (NULL, 2.1345)"# + ); +} + +#[test] +fn update_1() { + assert_eq!( + Query::update() + .into_table(Glyph::Table) + .values(vec![ + (Glyph::Aspect, 2.1345.into()), + (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), + ]) + .and_where(Expr::col(Glyph::Id).eq(1)) + .to_string(PostgresQueryBuilder::new()), + r#"UPDATE "glyph" SET "aspect" = 2.1345, "image" = '24B0E11951B03B07F8300FD003983F03F0780060' WHERE "id" = 1"# + ); +} + +#[test] +fn update_2() { + assert_eq!( + Query::update() + .into_table(Glyph::Table) + .json(json!({ + "aspect": 2.1345, + "image": "24B0E11951B03B07F8300FD003983F03F0780060", + })) + .and_where(Expr::col(Glyph::Id).eq(1)) + .to_string(PostgresQueryBuilder::new()), + r#"UPDATE "glyph" SET "aspect" = 2.1345, "image" = '24B0E11951B03B07F8300FD003983F03F0780060' WHERE "id" = 1"# + ); +} + +#[test] +fn update_3() { + assert_eq!( + Query::update() + .into_table(Glyph::Table) + .value_expr(Glyph::Aspect, Expr::cust("60 * 24 * 24")) + .values(vec![ + (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), + ]) + .and_where(Expr::col(Glyph::Id).eq(1)) + .to_string(PostgresQueryBuilder::new()), + r#"UPDATE "glyph" SET "aspect" = 60 * 24 * 24, "image" = '24B0E11951B03B07F8300FD003983F03F0780060' WHERE "id" = 1"# + ); +} + +#[test] +fn delete_1() { + assert_eq!( + Query::delete() + .from_table(Glyph::Table) + .and_where(Expr::col(Glyph::Id).eq(1)) + .to_string(PostgresQueryBuilder::new()), + r#"DELETE FROM "glyph" WHERE "id" = 1"# + ); +} \ No newline at end of file diff --git a/tests/postgres/table.rs b/tests/postgres/table.rs new file mode 100644 index 000000000..ff120ed92 --- /dev/null +++ b/tests/postgres/table.rs @@ -0,0 +1,164 @@ +use super::*; + +#[test] +fn create_1() { + assert_eq!( + Table::create() + .table(Glyph::Table) + .col(ColumnDef::new(Glyph::Id).integer().not_null().auto_increment().primary_key()) + .col(ColumnDef::new(Glyph::Aspect).double().not_null()) + .col(ColumnDef::new(Glyph::Image).text()) + .to_string(PostgresQueryBuilder::new()), + vec![ + r#"CREATE TABLE "glyph" ("#, + r#""id" serial NOT NULL PRIMARY KEY,"#, + r#""aspect" double precision NOT NULL,"#, + r#""image" text"#, + r#")"#, + ].join(" ") + ); +} + +#[test] +fn create_2() { + assert_eq!( + Table::create() + .table(Font::Table) + .col(ColumnDef::new(Font::Id).integer().not_null().primary_key().auto_increment()) + .col(ColumnDef::new(Font::Name).string_len(255).not_null()) + .col(ColumnDef::new(Font::Variant).string_len(255).not_null()) + .col(ColumnDef::new(Font::Language).string_len(255).not_null()) + .to_string(PostgresQueryBuilder::new()), + vec![ + r#"CREATE TABLE "font" ("#, + r#""id" serial NOT NULL PRIMARY KEY,"#, + r#""name" varchar(255) NOT NULL,"#, + r#""variant" varchar(255) NOT NULL,"#, + r#""language" varchar(255) NOT NULL"#, + r#")"#, + ].join(" ") + ); +} + +#[test] +fn create_3() { +assert_eq!( + Table::create() + .table(Char::Table) + .create_if_not_exists() + .col(ColumnDef::new(Char::Id).integer().not_null().primary_key().auto_increment()) + .col(ColumnDef::new(Char::FontSize).integer().not_null()) + .col(ColumnDef::new(Char::Character).string_len(255).not_null()) + .col(ColumnDef::new(Char::SizeW).integer().not_null()) + .col(ColumnDef::new(Char::SizeH).integer().not_null()) + .col(ColumnDef::new(Char::FontId).integer().default(Value::NULL)) + .foreign_key( + ForeignKey::create() + .name("FK_2e303c3a712662f1fc2a4d0aad6") + .table(Char::Table, Font::Table) + .col(Char::FontId, Font::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) + ) + .to_string(PostgresQueryBuilder::new()), + vec![ + r#"CREATE TABLE IF NOT EXISTS "character" ("#, + r#""id" serial NOT NULL PRIMARY KEY,"#, + r#""font_size" integer NOT NULL,"#, + r#""character" varchar(255) NOT NULL,"#, + r#""size_w" integer NOT NULL,"#, + r#""size_h" integer NOT NULL,"#, + r#""font_id" integer DEFAULT NULL,"#, + r#"CONSTRAINT "FK_2e303c3a712662f1fc2a4d0aad6""#, + r#"FOREIGN KEY ("font_id") REFERENCES "font" ("id")"#, + r#"ON DELETE CASCADE ON UPDATE CASCADE"#, + r#")"#, + ].join(" ") +); +} + +#[test] +fn drop_1() { + assert_eq!( + Table::drop() + .table(Glyph::Table) + .table(Char::Table) + .cascade() + .to_string(PostgresQueryBuilder::new()), + r#"DROP TABLE "glyph", "character" CASCADE"# + ); +} + +#[test] +fn truncate_1() { + assert_eq!( + Table::truncate() + .table(Font::Table) + .to_string(PostgresQueryBuilder::new()), + r#"TRUNCATE TABLE "font""# + ); +} + +#[test] +fn alter_1() { + assert_eq!( + Table::alter() + .table(Font::Table) + .add_column(ColumnDef::new(Alias::new("new_col")).integer().not_null().default(100)) + .to_string(PostgresQueryBuilder::new()), + r#"ALTER TABLE "font" ADD COLUMN "new_col" integer NOT NULL DEFAULT 100"# + ); +} + +#[test] +fn alter_2() { + assert_eq!( + Table::alter() + .table(Font::Table) + .modify_column(ColumnDef::new(Alias::new("new_col")).big_integer().default(999)) + .to_string(PostgresQueryBuilder::new()), + vec![ + r#"ALTER TABLE "font""#, + r#"ALTER COLUMN "new_col" TYPE bigint,"#, + r#"ALTER COLUMN "new_col" SET DEFAULT 999"#, + ].join(" ") + ); +} + +#[test] +fn alter_3() { + assert_eq!( + Table::alter() + .table(Font::Table) + .rename_column(Alias::new("new_col"), Alias::new("new_column")) + .to_string(PostgresQueryBuilder::new()), + r#"ALTER TABLE "font" RENAME COLUMN "new_col" TO "new_column""# + ); +} + +#[test] +fn alter_4() { + assert_eq!( + Table::alter() + .table(Font::Table) + .drop_column(Alias::new("new_column")) + .to_string(PostgresQueryBuilder::new()), + r#"ALTER TABLE "font" DROP COLUMN "new_column""# + ); +} + +#[test] +fn alter_5() { + assert_eq!( + Table::rename() + .table(Font::Table, Alias::new("font_new")) + .to_string(PostgresQueryBuilder::new()), + r#"ALTER TABLE "font" RENAME TO "font_new""# + ); +} + +#[test] +#[should_panic(expected = "No alter option found")] +fn alter_6() { + Table::alter().to_string(PostgresQueryBuilder::new()); +} \ No newline at end of file diff --git a/tests/sqlite/foreign_key.rs b/tests/sqlite/foreign_key.rs new file mode 100644 index 000000000..70a372a30 --- /dev/null +++ b/tests/sqlite/foreign_key.rs @@ -0,0 +1 @@ +// Sqlite does not support modification of foreign key constraints to existing tables \ No newline at end of file diff --git a/tests/sqlite/index.rs b/tests/sqlite/index.rs new file mode 100644 index 000000000..765a3de2a --- /dev/null +++ b/tests/sqlite/index.rs @@ -0,0 +1,37 @@ +use super::*; + +#[test] +fn create_1() { + assert_eq!( + Index::create() + .name("idx-glyph-aspect") + .table(Glyph::Table) + .col(Glyph::Aspect) + .to_string(SqliteQueryBuilder::new()), + "CREATE INDEX `idx-glyph-aspect` ON `glyph` (`aspect`)" + ); +} + +#[test] +fn create_2() { + assert_eq!( + Index::create() + .name("idx-glyph-aspect-image") + .table(Glyph::Table) + .col(Glyph::Aspect) + .col(Glyph::Image) + .to_string(SqliteQueryBuilder::new()), + "CREATE INDEX `idx-glyph-aspect-image` ON `glyph` (`aspect`, `image`)" + ); +} + +#[test] +fn drop_1() { + assert_eq!( + Index::drop() + .name("idx-glyph-aspect") + .table(Glyph::Table) + .to_string(SqliteQueryBuilder::new()), + "DROP INDEX `idx-glyph-aspect` ON `glyph`" + ); +} \ No newline at end of file diff --git a/tests/sqlite/mod.rs b/tests/sqlite/mod.rs new file mode 100644 index 000000000..a34ab3cde --- /dev/null +++ b/tests/sqlite/mod.rs @@ -0,0 +1,7 @@ +mod query; +mod table; +mod online; +mod index; +mod foreign_key; + +use super::*; \ No newline at end of file diff --git a/tests/sqlite/online.rs b/tests/sqlite/online.rs new file mode 100644 index 000000000..e69de29bb diff --git a/tests/sqlite/query.rs b/tests/sqlite/query.rs new file mode 100644 index 000000000..9a5f95fa4 --- /dev/null +++ b/tests/sqlite/query.rs @@ -0,0 +1,642 @@ +use super::*; + +#[test] +fn select_1() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .limit(10) + .offset(100) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` LIMIT 10 OFFSET 100" + ); +} + +#[test] +fn select_2() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where(Expr::col(Char::SizeW).eq(3)) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` = 3" + ); +} + +#[test] +fn select_3() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where(Expr::col(Char::SizeW).eq(3)) + .and_where(Expr::col(Char::SizeH).eq(4)) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` = 3 AND `size_h` = 4" + ); +} + +#[test] +fn select_4() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Image + ]) + .from_subquery( + Query::select() + .columns(vec![ + Glyph::Image, Glyph::Aspect + ]) + .from(Glyph::Table) + .take(), + Alias::new("subglyph") + ) + .to_string(SqliteQueryBuilder::new()), + "SELECT `image` FROM (SELECT `image`, `aspect` FROM `glyph`) AS `subglyph`" + ); +} + +#[test] +fn select_5() { + assert_eq!( + Query::select() + .table_column(Glyph::Table, Glyph::Image) + .from(Glyph::Table) + .and_where(Expr::tbl(Glyph::Table, Glyph::Aspect).is_in(vec![3, 4])) + .to_string(SqliteQueryBuilder::new()), + "SELECT `glyph`.`image` FROM `glyph` WHERE `glyph`.`aspect` IN (3, 4)" + ); +} + +#[test] +fn select_6() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .exprs(vec![ + Expr::col(Glyph::Image).max(), + ]) + .from(Glyph::Table) + .group_by_columns(vec![ + Glyph::Aspect, + ]) + .and_having(Expr::col(Glyph::Aspect).gt(2)) + .to_string(SqliteQueryBuilder::new()), + "SELECT `aspect`, MAX(`image`) FROM `glyph` GROUP BY `aspect` HAVING `aspect` > 2" + ); +} + +#[test] +fn select_7() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + .to_string(SqliteQueryBuilder::new()), + "SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, 0) > 2" + ); +} + +#[test] +fn select_8() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, + ]) + .from(Char::Table) + .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id`" + ); +} + +#[test] +fn select_9() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, + ]) + .from(Char::Table) + .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + .inner_join(Glyph::Table, Expr::tbl(Char::Table, Char::Character).equals(Glyph::Table, Glyph::Image)) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id` INNER JOIN `glyph` ON `character`.`character` = `glyph`.`image`" + ); +} + +#[test] +fn select_10() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, + ]) + .from(Char::Table) + .left_join(Font::Table, + Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id) + .and(Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id)) + ) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character` LEFT JOIN `font` ON (`character`.`font_id` = `font`.`id`) AND (`character`.`font_id` = `font`.`id`)" + ); +} + +#[test] +fn select_11() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + .order_by(Glyph::Image, Order::Desc) + .order_by_tbl(Glyph::Table, Glyph::Aspect, Order::Asc) + .to_string(SqliteQueryBuilder::new()), + "SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, 0) > 2 ORDER BY `image` DESC, `glyph`.`aspect` ASC" + ); +} + +#[test] +fn select_12() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + .order_by_columns(vec![ + (Glyph::Id, Order::Asc), + (Glyph::Aspect, Order::Desc), + ]) + .to_string(SqliteQueryBuilder::new()), + "SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, 0) > 2 ORDER BY `id` ASC, `aspect` DESC" + ); +} + +#[test] +fn select_13() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::expr(Expr::col(Glyph::Aspect).if_null(0)).gt(2)) + .order_by_table_columns(vec![ + (Glyph::Table, Glyph::Id, Order::Asc), + (Glyph::Table, Glyph::Aspect, Order::Desc), + ]) + .to_string(SqliteQueryBuilder::new()), + "SELECT `aspect` FROM `glyph` WHERE IFNULL(`aspect`, 0) > 2 ORDER BY `glyph`.`id` ASC, `glyph`.`aspect` DESC" + ); +} + +#[test] +fn select_14() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Id, + Glyph::Aspect, + ]) + .expr(Expr::col(Glyph::Image).max()) + .from(Glyph::Table) + .group_by_table_columns(vec![ + (Glyph::Table, Glyph::Id), + (Glyph::Table, Glyph::Aspect), + ]) + .and_having(Expr::col(Glyph::Aspect).gt(2)) + .to_string(SqliteQueryBuilder::new()), + "SELECT `id`, `aspect`, MAX(`image`) FROM `glyph` GROUP BY `glyph`.`id`, `glyph`.`aspect` HAVING `aspect` > 2" + ); +} + +#[test] +fn select_15() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character + ]) + .from(Char::Table) + .and_where(Expr::col(Char::FontId).is_null()) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `font_id` IS NULL" + ); +} + +#[test] +fn select_16() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character + ]) + .from(Char::Table) + .and_where(Expr::col(Char::FontId).is_null()) + .and_where(Expr::col(Char::Character).is_not_null()) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `font_id` IS NULL AND `character` IS NOT NULL" + ); +} + +#[test] +fn select_17() { + assert_eq!( + Query::select() + .table_columns(vec![ + (Glyph::Table, Glyph::Image), + ]) + .from(Glyph::Table) + .and_where(Expr::tbl(Glyph::Table, Glyph::Aspect).between(3, 5)) + .to_string(SqliteQueryBuilder::new()), + "SELECT `glyph`.`image` FROM `glyph` WHERE `glyph`.`aspect` BETWEEN 3 AND 5" + ); +} + +#[test] +fn select_18() { + assert_eq!( + Query::select() + .columns(vec![ + Glyph::Aspect, + ]) + .from(Glyph::Table) + .and_where(Expr::col(Glyph::Aspect).between(3, 5)) + .and_where(Expr::col(Glyph::Aspect).not_between(8, 10)) + .to_string(SqliteQueryBuilder::new()), + "SELECT `aspect` FROM `glyph` WHERE (`aspect` BETWEEN 3 AND 5) AND (`aspect` NOT BETWEEN 8 AND 10)" + ); +} + +#[test] +fn select_19() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character + ]) + .from(Char::Table) + .and_where(Expr::col(Char::Character).eq("A")) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `character` = 'A'" + ); +} + +#[test] +fn select_20() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .and_where(Expr::col(Char::Character).like("A")) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `character` LIKE 'A'" + ); +} + +#[test] +fn select_21() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character + ]) + .from(Char::Table) + .or_where(Expr::col(Char::Character).like("A%")) + .or_where(Expr::col(Char::Character).like("%B")) + .or_where(Expr::col(Char::Character).like("%C%")) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `character` LIKE 'A%' OR `character` LIKE '%B' OR `character` LIKE '%C%'" + ); +} + +#[test] +fn select_22() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .or_where(Expr::col(Char::Character).like("C")) + .or_where(Expr::col(Char::Character).like("D").and(Expr::col(Char::Character).like("E"))) + .and_where(Expr::col(Char::Character).like("F").or(Expr::col(Char::Character).like("G"))) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `character` LIKE 'C' OR ((`character` LIKE 'D') AND (`character` LIKE 'E')) AND ((`character` LIKE 'F') OR (`character` LIKE 'G'))" + ); +} + +#[test] +fn select_23() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .and_where_option(None) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character`" + ); +} + +#[test] +fn select_24() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .conditions(true, |x| { + x.and_where(Expr::col(Char::FontId).eq(5)); + }, |_|()) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `font_id` = 5" + ); +} + +#[test] +fn select_25() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .and_where( + Expr::col(Char::SizeW).mul(2) + .equals(Expr::col(Char::SizeH).div(2)) + ) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE `size_w` * 2 = `size_h` / 2" + ); +} + +#[test] +fn select_26() { + assert_eq!( + Query::select() + .column(Char::Character) + .from(Char::Table) + .and_where( + Expr::expr(Expr::col(Char::SizeW).add(1)).mul(2) + .equals(Expr::expr(Expr::col(Char::SizeH).div(2)).sub(1)) + ) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` FROM `character` WHERE (`size_w` + 1) * 2 = (`size_h` / 2) - 1" + ); +} + +#[test] +fn select_27() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where(Expr::col(Char::SizeW).eq(3)) + .and_where(Expr::col(Char::SizeH).eq(4)) + .and_where(Expr::col(Char::SizeH).eq(5)) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` = 3 AND `size_h` = 4 AND `size_h` = 5" + ); +} + +#[test] +fn select_28() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .or_where(Expr::col(Char::SizeW).eq(3)) + .or_where(Expr::col(Char::SizeH).eq(4)) + .or_where(Expr::col(Char::SizeH).eq(5)) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` = 3 OR `size_h` = 4 OR `size_h` = 5" + ); +} + +#[test] +fn select_29() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where(Expr::col(Char::SizeW).eq(3)) + .or_where(Expr::col(Char::SizeH).eq(4)) + .and_where(Expr::col(Char::SizeH).eq(5)) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` = 3 OR `size_h` = 4 AND `size_h` = 5" + ); +} + +#[test] +fn select_30() { + assert_eq!( + Query::select() + .columns(vec![ + Char::Character, Char::SizeW, Char::SizeH + ]) + .from(Char::Table) + .and_where( + Expr::col(Char::SizeW).mul(2) + .add(Expr::col(Char::SizeH).div(3)) + .equals(Expr::value(4)) + ) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character`, `size_w`, `size_h` FROM `character` WHERE (`size_w` * 2) + (`size_h` / 3) = 4" + ); +} + +#[test] +fn select_31() { + assert_eq!( + Query::select() + .expr((1..10_i32).fold(Expr::value(0), |expr, i| { + expr.add(Expr::value(i)) + })) + .to_string(SqliteQueryBuilder::new()), + "SELECT 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9" + ); +} + +#[test] +fn select_32() { + assert_eq!( + Query::select() + .expr_alias(Expr::col(Char::Character), Alias::new("C")) + .from(Char::Table) + .to_string(SqliteQueryBuilder::new()), + "SELECT `character` AS `C` FROM `character`" + ); +} + +#[test] +fn select_33() { + assert_eq!( + Query::select() + .column(Glyph::Image) + .from(Glyph::Table) + .and_where(Expr::col(Glyph::Aspect).in_subquery( + Query::select() + .expr(Expr::cust("3 + 2 * 2")) + .take() + )) + .to_string(SqliteQueryBuilder::new()), + "SELECT `image` FROM `glyph` WHERE `aspect` IN (SELECT 3 + 2 * 2)" + ); +} + +#[test] +fn select_34() { + assert_eq!( + Query::select() + .column(Glyph::Aspect) + .expr(Expr::col(Glyph::Image).max()) + .from(Glyph::Table) + .group_by_columns(vec![ + Glyph::Aspect, + ]) + .or_having(Expr::col(Glyph::Aspect).gt(2).or(Expr::col(Glyph::Aspect).lt(8))) + .or_having(Expr::col(Glyph::Aspect).gt(12).and(Expr::col(Glyph::Aspect).lt(18))) + .and_having(Expr::col(Glyph::Aspect).gt(22).or(Expr::col(Glyph::Aspect).lt(28))) + .or_having(Expr::col(Glyph::Aspect).gt(32)) + .to_string(SqliteQueryBuilder), + vec![ + "SELECT `aspect`, MAX(`image`) FROM `glyph` GROUP BY `aspect`", + "HAVING ((`aspect` > 2) OR (`aspect` < 8))", + "OR ((`aspect` > 12) AND (`aspect` < 18))", + "AND ((`aspect` > 22) OR (`aspect` < 28))", + "OR `aspect` > 32", + ].join(" ") + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_1() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .json(json!({ + "image": "24B0E11951B03B07F8300FD003983F03F0780060", + "aspect": 2.1345, + })) + .to_string(SqliteQueryBuilder::new()), + "INSERT INTO `glyph` (`aspect`, `image`) VALUES (2.1345, '24B0E11951B03B07F8300FD003983F03F0780060')" + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_2() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns(vec![ + Glyph::Image, + Glyph::Aspect, + ]) + .values_panic(vec![ + "04108048005887010020060000204E0180400400".into(), + 3.1415.into(), + ]) + .to_string(SqliteQueryBuilder::new()), + "INSERT INTO `glyph` (`image`, `aspect`) VALUES ('04108048005887010020060000204E0180400400', 3.1415)" + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_3() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns(vec![ + Glyph::Image, + Glyph::Aspect, + ]) + .values_panic(vec![ + "04108048005887010020060000204E0180400400".into(), + 3.1415.into(), + ]) + .json(json!({ + "aspect": 2.1345, + })) + .to_string(SqliteQueryBuilder::new()), + "INSERT INTO `glyph` (`image`, `aspect`) VALUES ('04108048005887010020060000204E0180400400', 3.1415), (NULL, 2.1345)" + ); +} + +#[test] +fn update_1() { + assert_eq!( + Query::update() + .into_table(Glyph::Table) + .values(vec![ + (Glyph::Aspect, 2.1345.into()), + (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), + ]) + .and_where(Expr::col(Glyph::Id).eq(1)) + .to_string(SqliteQueryBuilder::new()), + "UPDATE `glyph` SET `aspect` = 2.1345, `image` = '24B0E11951B03B07F8300FD003983F03F0780060' WHERE `id` = 1" + ); +} + +#[test] +fn update_2() { + assert_eq!( + Query::update() + .into_table(Glyph::Table) + .json(json!({ + "aspect": 2.1345, + "image": "24B0E11951B03B07F8300FD003983F03F0780060", + })) + .and_where(Expr::col(Glyph::Id).eq(1)) + .to_string(SqliteQueryBuilder::new()), + "UPDATE `glyph` SET `aspect` = 2.1345, `image` = '24B0E11951B03B07F8300FD003983F03F0780060' WHERE `id` = 1" + ); +} + +#[test] +fn update_3() { + assert_eq!( + Query::update() + .into_table(Glyph::Table) + .value_expr(Glyph::Aspect, Expr::cust("60 * 24 * 24")) + .values(vec![ + (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), + ]) + .and_where(Expr::col(Glyph::Id).eq(1)) + .to_string(SqliteQueryBuilder::new()), + "UPDATE `glyph` SET `aspect` = 60 * 24 * 24, `image` = '24B0E11951B03B07F8300FD003983F03F0780060' WHERE `id` = 1" + ); +} + +#[test] +fn delete_1() { + assert_eq!( + Query::delete() + .from_table(Glyph::Table) + .and_where(Expr::col(Glyph::Id).eq(1)) + .to_string(SqliteQueryBuilder::new()), + "DELETE FROM `glyph` WHERE `id` = 1" + ); +} \ No newline at end of file diff --git a/tests/sqlite/table.rs b/tests/sqlite/table.rs new file mode 100644 index 000000000..79708e73f --- /dev/null +++ b/tests/sqlite/table.rs @@ -0,0 +1,153 @@ +use super::*; + +#[test] +fn create_1() { + assert_eq!( + Table::create() + .table(Glyph::Table) + .col(ColumnDef::new(Glyph::Id).integer().not_null().auto_increment().primary_key()) + .col(ColumnDef::new(Glyph::Aspect).double().not_null()) + .col(ColumnDef::new(Glyph::Image).text()) + .to_string(SqliteQueryBuilder::new()), + vec![ + "CREATE TABLE `glyph` (", + "`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,", + "`aspect` real NOT NULL,", + "`image` text", + ")", + ].join(" ") + ); +} + +#[test] +fn create_2() { + assert_eq!( + Table::create() + .table(Font::Table) + .col(ColumnDef::new(Font::Id).integer().not_null().primary_key().auto_increment()) + .col(ColumnDef::new(Font::Name).string().not_null()) + .col(ColumnDef::new(Font::Variant).string().not_null()) + .col(ColumnDef::new(Font::Language).string().not_null()) + .to_string(SqliteQueryBuilder::new()), + vec![ + "CREATE TABLE `font` (", + "`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,", + "`name` text NOT NULL,", + "`variant` text NOT NULL,", + "`language` text NOT NULL", + ")", + ].join(" ") + ); +} + +#[test] +fn create_3() { + assert_eq!( + Table::create() + .table(Char::Table) + .create_if_not_exists() + .col(ColumnDef::new(Char::Id).integer().not_null().auto_increment().primary_key()) + .col(ColumnDef::new(Char::FontSize).integer().not_null()) + .col(ColumnDef::new(Char::Character).string().not_null()) + .col(ColumnDef::new(Char::SizeW).integer().not_null()) + .col(ColumnDef::new(Char::SizeH).integer().not_null()) + .col(ColumnDef::new(Char::FontId).integer().default(Value::NULL)) + .foreign_key( + ForeignKey::create() + .table(Char::Table, Font::Table) + .col(Char::FontId, Font::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) + ) + .to_string(SqliteQueryBuilder::new()), + vec![ + "CREATE TABLE IF NOT EXISTS `character` (", + "`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,", + "`font_size` integer NOT NULL,", + "`character` text NOT NULL,", + "`size_w` integer NOT NULL,", + "`size_h` integer NOT NULL,", + "`font_id` integer DEFAULT NULL,", + "FOREIGN KEY (`font_id`) REFERENCES `font` (`id`) ON DELETE CASCADE ON UPDATE CASCADE", + ")", + ].join(" ") + ); +} + +#[test] +fn drop_1() { + assert_eq!( + Table::drop() + .table(Glyph::Table) + .table(Char::Table) + .cascade() + .to_string(SqliteQueryBuilder::new()), + "DROP TABLE `glyph`, `character` CASCADE" + ); +} + +#[test] +fn truncate_1() { + assert_eq!( + Table::truncate() + .table(Font::Table) + .to_string(SqliteQueryBuilder::new()), + "TRUNCATE TABLE `font`" + ); +} + +#[test] +fn alter_1() { + assert_eq!( + Table::alter() + .table(Font::Table) + .add_column(ColumnDef::new(Alias::new("new_col")).integer().not_null().default(99)) + .to_string(SqliteQueryBuilder::new()), + "ALTER TABLE `font` ADD COLUMN `new_col` integer NOT NULL DEFAULT 99" + ); +} + +#[test] +#[should_panic(expected = "Sqlite not support modifying table column")] +fn alter_2() { + Table::alter() + .table(Font::Table) + .modify_column(ColumnDef::new(Alias::new("new_col")).double()) + .to_string(SqliteQueryBuilder::new()); +} + +#[test] +fn alter_3() { + assert_eq!( + Table::alter() + .table(Font::Table) + .rename_column(Alias::new("new_col"), Alias::new("new_column")) + .to_string(SqliteQueryBuilder::new()), + "ALTER TABLE `font` RENAME COLUMN `new_col` TO `new_column`" + ); +} + +#[test] +#[should_panic(expected = "Sqlite not support dropping table column")] +fn alter_4() { + Table::alter() + .table(Font::Table) + .drop_column(Alias::new("new_column")) + .to_string(SqliteQueryBuilder::new()); +} + +#[test] +fn alter_5() { + assert_eq!( + Table::rename() + .table(Font::Table, Alias::new("font_new")) + .to_string(SqliteQueryBuilder::new()), + "ALTER TABLE `font` RENAME TO `font_new`" + ); +} + +#[test] +#[should_panic(expected = "No alter option found")] +fn alter_6() { + Table::alter().to_string(SqliteQueryBuilder::new()); +} \ No newline at end of file