From e80094ebe213333a29e76881b71cfaa21337af95 Mon Sep 17 00:00:00 2001 From: Alex Vergara <68871459+Pyzyryab@users.noreply.github.com> Date: Tue, 21 Mar 2023 11:35:22 +0100 Subject: [PATCH 01/82] v0.1.1 (#35) * Fixed nullability on migrations process (#34) * Fixed nullability on migrations process * Fixed format * Adding more types to the supported ones in the row mapper for tiberius * v0.1.1 * Refreshing and updating the GitHub actions for the project * Continuous integration runs only on main/develop. Upgrading the release action to its v2, and setting up it's content to directly publish the workspace * Adding a 15000ms of waiting between publishing every package in the release action --------- Co-authored-by: Gonzalo Busto Musi <35508741+gbm25@users.noreply.github.com> Co-authored-by: Alex Vergara --- .github/workflows/code-coverage.yml | 3 +- .github/workflows/continuous-integration.yml | 7 ++-- .github/workflows/macos-tests.yml | 27 ------------- .github/workflows/release.yml | 24 +---------- .github/workflows/windows-tests.yml | 27 ------------- CHANGELOG.md | 6 +++ README.md | 6 +-- canyon_connection/Cargo.toml | 2 +- canyon_crud/Cargo.toml | 4 +- canyon_macros/Cargo.toml | 8 ++-- canyon_macros/src/lib.rs | 15 +++++-- canyon_observer/Cargo.toml | 6 +-- canyon_observer/src/migrations/processor.rs | 40 ++++++++++++++++--- .../src/migrations/register_types.rs | 5 +++ canyon_sql/Cargo.toml | 10 ++--- tests/Cargo.toml | 2 +- 16 files changed, 84 insertions(+), 108 deletions(-) delete mode 100644 .github/workflows/macos-tests.yml delete mode 100644 .github/workflows/windows-tests.yml diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 0f019f69..a0cbdbb0 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -1,8 +1,7 @@ -name: Linux CI +name: Code Coverage on: push: - branches: 'development' tags: - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+rc[0-9]+' diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 83c1861b..add5e3e3 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -2,9 +2,9 @@ name: Continuous Integration on: push: - branches: '*' + branches: ['main', 'development'] pull_request: - branches: '*' + branches: ['main', 'development'] env: CARGO_TERM_COLOR: always @@ -49,5 +49,6 @@ jobs: if: ${{ matrix.os == 'ubuntu-latest' }} run: cargo test --verbose --workspace --all-features --no-fail-fast -- --show-output --test-threads=1 - - name: Run UNIT tests with no external connections for the rest of the defined targets + - name: Run only UNIT tests for the rest of the defined targets + if: ${{ matrix.os != 'ubuntu-latest' }} run: cargo test --verbose --workspace --exclude tests --all-features --no-fail-fast -- --show-output diff --git a/.github/workflows/macos-tests.yml b/.github/workflows/macos-tests.yml deleted file mode 100644 index 21ca2e01..00000000 --- a/.github/workflows/macos-tests.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: macOS CI - -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - - 'v[0-9]+.[0-9]+.[0-9]+rc[0-9]+' - -env: - CARGO_TERM_COLOR: always - -jobs: - linux-tests: - runs-on: macos-latest - name: Tests for macOS - env: - CARGO_TERM_COLOR: always - steps: - - uses: actions/checkout@v3 - - - name: Caching cargo deps - id: ci-cache - uses: Swatinem/rust-cache@v2 - - - name: Running tests for macOS targets - run: | - cargo test --all-features --workspace --exclude tests \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 357bee0e..f909195d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,30 +21,10 @@ jobs: toolchain: stable override: true - - uses: katyo/publish-crates@v1 + - uses: katyo/publish-crates@v2 with: registry-token: ${{ secrets.CRATES_IO_TOKEN }} - path: './canyon_connection' - - - uses: katyo/publish-crates@v1 - with: - registry-token: ${{ secrets.CRATES_IO_TOKEN }} - path: './canyon_crud' - - - uses: katyo/publish-crates@v1 - with: - registry-token: ${{ secrets.CRATES_IO_TOKEN }} - path: './canyon_observer' - - - uses: katyo/publish-crates@v1 - with: - registry-token: ${{ secrets.CRATES_IO_TOKEN }} - path: './canyon_macros' - - - uses: katyo/publish-crates@v1 - with: - registry-token: ${{ secrets.CRATES_IO_TOKEN }} - path: './canyon_sql' + publish-delay: 15000 release-publisher: needs: 'publish' diff --git a/.github/workflows/windows-tests.yml b/.github/workflows/windows-tests.yml deleted file mode 100644 index a6ace765..00000000 --- a/.github/workflows/windows-tests.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Windows CI - -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - - 'v[0-9]+.[0-9]+.[0-9]+rc[0-9]+' - -env: - CARGO_TERM_COLOR: always - -jobs: - windows-tests: - runs-on: windows-latest - name: Tests for Windows - env: - CARGO_TERM_COLOR: always - steps: - - uses: actions/checkout@v3 - - - name: Caching cargo deps - id: ci-cache - uses: Swatinem/rust-cache@v2 - - - name: Running tests for Windows OS targets - run: | - cargo test --all-features --workspace --exclude tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e1d3570..d0200423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ Year format is defined as: `YYYY-m-d` ## [Unreleased] +## [0.1.1] - 2023 - 03 - 20 + +### Fix + +- Adding more types to the supported ones for Tiberius in the row mapper + ## [0.1.0] - 2022 - 12 - 25 ### Added diff --git a/README.md b/README.md index c62a762d..9617bf8c 100755 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ **A full written in `Rust` ORM for multiple databases.** - ![crates.io](https://img.shields.io/crates/v/canyon_sql.svg) +- [![Continuous Integration](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/continuous-integration.yml) +- [![Code Quality](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-quality.yml/badge.svg)](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-quality.yml) - [![Code Coverage Measure](https://zerodaycode.github.io/Canyon-SQL/badges/flat.svg)](https://zerodaycode.github.io/Canyon-SQL) -- [![Linux CI](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-coverage.yml/badge.svg)](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-coverage.yml) -- [![Tests on macOS](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/macos-tests.yml/badge.svg)](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/macos-tests.yml) -- [![Tests on Windows](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/windows-tests.yml/badge.svg)](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/windows-tests.yml) +- [![Code Coverage Status](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-coverage.yml/badge.svg)](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-coverage.yml) `Canyon-SQL` is a high level abstraction for working with multiple databases concurrently. Is build on top of the `async` language features to provide a high speed, high performant library to handling data access for consumers. diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index d62b3fc3..8f7e97ac 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_connection" -version = "0.1.0" +version = "0.1.1" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index 406da6fc..b5b43fee 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_crud" -version = "0.1.0" +version = "0.1.1" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -12,4 +12,4 @@ description = "A Rust ORM and QueryBuilder" chrono = { version = "0.4", features = ["serde"] } async-trait = { version = "0.1.50" } -canyon_connection = { version = "0.1.0", path = "../canyon_connection" } +canyon_connection = { version = "0.1.1", path = "../canyon_connection" } diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index 37b322c5..83c59f3d 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_macros" -version = "0.1.0" +version = "0.1.1" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -18,6 +18,6 @@ proc-macro2 = "1.0.27" futures = "0.3.21" tokio = { version = "1.9.0", features = ["full"] } -canyon_observer = { version = "0.1.0", path = "../canyon_observer" } -canyon_crud = { version = "0.1.0", path = "../canyon_crud" } -canyon_connection = { version = "0.1.0", path = "../canyon_connection" } +canyon_observer = { version = "0.1.1", path = "../canyon_observer" } +canyon_crud = { version = "0.1.1", path = "../canyon_crud" } +canyon_connection = { version = "0.1.1", path = "../canyon_connection" } diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index 9257fd38..cbf6ab92 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -143,7 +143,7 @@ pub fn canyon_tokio_test( } /// Generates the enums that contains the `TypeFields` and `TypeFieldsValues` -/// that the querybuilder requires for construct its queries +/// that the query-builder requires for construct its queries #[proc_macro_derive(Fields)] pub fn querybuilder_fields(input: CompilerTokenStream) -> CompilerTokenStream { let entity_res = syn::parse::(input); @@ -472,7 +472,7 @@ pub fn implement_foreignkeyable_for_type( let ast: DeriveInput = syn::parse(input).unwrap(); let ty = ast.ident; - // Recovers the identifiers of the struct's members + // Recovers the identifiers of the structs members let fields = filter_fields(match ast.data { syn::Data::Struct(ref s) => &s.fields, _ => { @@ -519,7 +519,7 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac // Gets the data from the AST let ast: DeriveInput = syn::parse(input).unwrap(); - // Recovers the identifiers of the struct's members + // Recovers the identifiers of the structs members let fields = fields_with_types(match ast.data { syn::Data::Struct(ref s) => &s.fields, _ => { @@ -539,6 +539,7 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac } }); + // TODO rework this ugly piece of code in the upcoming versions let init_field_values_sqlserver = fields.iter().map(|(_vis, ident, ty)| { let ident_name = ident.to_string(); @@ -552,6 +553,14 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac quote! { #ident: row.get::(#ident_name) } + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { + #ident: row.get::(#ident_name) + } + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { + #ident: row.get::(#ident_name) + } } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { quote! { #ident: row.get::(#ident_name) diff --git a/canyon_observer/Cargo.toml b/canyon_observer/Cargo.toml index c3bfbdf7..5cea2d56 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_observer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_observer" -version = "0.1.0" +version = "0.1.1" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -23,5 +23,5 @@ quote = "1.0.9" partialdebug = "0.2.0" # Internal dependencies -canyon_crud = { version = "0.1.0", path = "../canyon_crud" } -canyon_connection = { version = "0.1.0", path = "../canyon_connection" } +canyon_crud = { version = "0.1.1", path = "../canyon_crud" } +canyon_connection = { version = "0.1.1", path = "../canyon_connection" } diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index fb991717..8f9a67bb 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -200,7 +200,7 @@ impl MigrationsProcessor { } } - // Creates or modify (currently only datatype) a column for a given canyon register entity field + // Creates or modify (currently only datatype and nullability) a column for a given canyon register entity field fn create_or_modify_field( &mut self, entity_name: &str, @@ -211,13 +211,29 @@ impl MigrationsProcessor { // If we do not retrieve data for this database column, it does not exist yet // and therefore it has to be created if current_column_metadata.is_none() { - self.create_column(entity_name.to_string(), canyon_register_entity_field) + self.create_column( + entity_name.to_string(), + canyon_register_entity_field.clone(), + ) } else if !MigrationsHelper::is_same_datatype( db_type, &canyon_register_entity_field, current_column_metadata.unwrap(), ) { - self.change_column_datatype(entity_name.to_string(), canyon_register_entity_field) + self.change_column_datatype( + entity_name.to_string(), + canyon_register_entity_field.clone(), + ) + } + + if let Some(column_metadata) = current_column_metadata { + if canyon_register_entity_field.is_nullable() != column_metadata.is_nullable { + if column_metadata.is_nullable { + self.set_not_null(entity_name.to_string(), canyon_register_entity_field) + } else { + self.drop_not_null(entity_name.to_string(), canyon_register_entity_field) + } + } } } @@ -254,6 +270,20 @@ impl MigrationsProcessor { ))); } + fn set_not_null(&mut self, table_name: String, field: CanyonRegisterEntityField) { + self.operations + .push(Box::new(ColumnOperation::AlterColumnSetNotNull( + table_name, field, + ))); + } + + fn drop_not_null(&mut self, table_name: String, field: CanyonRegisterEntityField) { + self.operations + .push(Box::new(ColumnOperation::AlterColumnDropNotNull( + table_name, field, + ))); + } + fn add_constraints( &mut self, entity_name: &str, @@ -555,7 +585,7 @@ impl MigrationsHelper { .iter() .any(|v| v.table_name.to_lowercase() == entity_name.to_lowercase()) } - // Get the table metadata for a given entity name or his old entity name if the table was renamed. + /// Get the table metadata for a given entity name or his old entity name if the table was renamed. fn get_current_table_metadata<'a>( canyon_memory: &'_ CanyonMemory, entity_name: &'a str, @@ -575,7 +605,7 @@ impl MigrationsHelper { .map(|e| e.to_owned()) } - // Get the column metadata for a given column name + /// Get the column metadata for a given column name fn get_current_column_metadata( column_name: String, current_table_metadata: Option<&TableMetadata>, diff --git a/canyon_observer/src/migrations/register_types.rs b/canyon_observer/src/migrations/register_types.rs index b101cb77..c89d89be 100644 --- a/canyon_observer/src/migrations/register_types.rs +++ b/canyon_observer/src/migrations/register_types.rs @@ -262,4 +262,9 @@ impl CanyonRegisterEntityField { numeric.contains(&self.field_type.as_str()) && pk_is_autoincremental } + + /// Return the nullability of a the field + pub fn is_nullable(&self) -> bool { + self.field_type.to_uppercase().starts_with("OPTION") + } } diff --git a/canyon_sql/Cargo.toml b/canyon_sql/Cargo.toml index b0376f61..97b32aa9 100755 --- a/canyon_sql/Cargo.toml +++ b/canyon_sql/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_sql" -version = "0.1.0" +version = "0.1.1" edition = "2021" authors = ["Alex Vergara, Gonzalo Busto"] documentation = "https://zerodaycode.github.io/canyon-book/" @@ -13,7 +13,7 @@ description = "A Rust ORM and QueryBuilder" async-trait = { version = "0.1.50" } # Project crates -canyon_macros = { version = "0.1.0", path = "../canyon_macros" } -canyon_observer = { version = "0.1.0", path = "../canyon_observer" } -canyon_crud = { version = "0.1.0", path = "../canyon_crud" } -canyon_connection = { version = "0.1.0", path = "../canyon_connection" } +canyon_macros = { version = "0.1.1", path = "../canyon_macros" } +canyon_observer = { version = "0.1.1", path = "../canyon_observer" } +canyon_crud = { version = "0.1.1", path = "../canyon_crud" } +canyon_connection = { version = "0.1.1", path = "../canyon_connection" } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index a6aacb83..7fe54756 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tests" -version = "0.1.0" +version = "0.1.1" edition = "2021" publish = false From 6d779e13138f285e1ab0dbb955a8fda61d717e17 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 21 Mar 2023 12:50:15 +0100 Subject: [PATCH 02/82] #update - Removing the .xml code coverage report from the GH-pages, since it's exceeding the capacity --- .github/workflows/code-coverage.yml | 2 -- .github/workflows/release.yml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index a0cbdbb0..43fce450 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -55,14 +55,12 @@ jobs: - name: Generate code coverage report run: | grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing -o ./target/debug/coverage - grcov . -s . --binary-path ./target/debug/ -t cobertura --branch --ignore-not-existing -o ./target/debug/coverage/code_cov.xml - name: Publish Test Results uses: actions/upload-artifact@v3 with: name: Unit Test Results path: | - ./target/debug/coverage/code_cov.xml ./target/debug/coverage/index.html - name: Publish coverage report to GitHub Pages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f909195d..2efaf9ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} - name: "Update the CHANGELOG.md for the release" - uses: mikepenz/release-changelog-builder-action@{latest-release} + uses: mikepenz/release-changelog-builder-action@v3.7.0 with: configuration: "./.github/changelog_configuration.json" env: From 2e484714aa195fc39a889bd221afd013372ebdfa Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Fri, 24 Mar 2023 12:27:14 +0100 Subject: [PATCH 03/82] #update Refactored and upgraded the Canyon Memory process. Also, queries are now full parameterized. --- CHANGELOG.md | 12 +- canyon_macros/src/lib.rs | 13 +- canyon_macros/src/utils/helpers.rs | 6 +- canyon_observer/src/constants.rs | 32 +-- canyon_observer/src/lib.rs | 24 ++- canyon_observer/src/migrations/handler.rs | 25 ++- canyon_observer/src/migrations/memory.rs | 187 +++++++----------- canyon_observer/src/migrations/processor.rs | 62 +++--- .../src/migrations/register_types.rs | 19 +- 9 files changed, 191 insertions(+), 189 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0200423..58967652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,20 @@ Year format is defined as: `YYYY-m-d` ## [Unreleased] -## [0.1.1] - 2023 - 03 - 20 +## [0.1.2] - 2023 - 03 - 23 + +### Update + +- Removed from `postgresql` migrations the auto conversion to lowercase of the table and column names ### Fix +- Solved a bug in the canyon_entity proc macro that was wiring the incorrect user table name in the migrations + +## [0.1.1] - 2023 - 03 - 20 + +### Update + - Adding more types to the supported ones for Tiberius in the row mapper ## [0.1.0] - 2022 - 12 - 25 diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index cbf6ab92..51433b5e 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -196,11 +196,14 @@ pub fn canyon_entity( .expect("Something went wrong parsing the `table_name` argument") .to_string(); - if attr_arg_ident == "table_name" || attr_arg_ident == "schema" { - table_name = Some(Box::leak(attr_arg_ident.into_boxed_str())); + if &attr_arg_ident == "table_name" || &attr_arg_ident == "schema" { match nv.lit { syn::Lit::Str(ref l) => { - schema_name = Some(Box::leak(l.value().into_boxed_str())) + if &attr_arg_ident == "table_name" { + table_name = Some(Box::leak(l.value().into_boxed_str())) + } else { + schema_name = Some(Box::leak(l.value().into_boxed_str())) + } } _ => { parsing_attribute_error = Some(syn::Error::new( @@ -257,7 +260,9 @@ pub fn canyon_entity( let mut new_entity = CanyonRegisterEntity::default(); let e = Box::leak(entity.struct_name.to_string().into_boxed_str()); new_entity.entity_name = e; - new_entity.user_table_name = table_name; + new_entity.entity_db_table_name = table_name.unwrap_or( + Box::leak(helpers::default_database_table_name_from_entity_name(e).into_boxed_str()) + ); new_entity.user_schema_name = schema_name; // The entity fields diff --git a/canyon_macros/src/utils/helpers.rs b/canyon_macros/src/utils/helpers.rs index 9ad14792..85d6a88f 100644 --- a/canyon_macros/src/utils/helpers.rs +++ b/canyon_macros/src/utils/helpers.rs @@ -81,7 +81,6 @@ pub fn table_schema_parser(macro_data: &MacroTokens<'_>) -> Result String { let struct_name: String = ty.to_string(); let mut table_name: String = String::new(); @@ -105,9 +104,8 @@ pub fn _database_table_name_from_struct(ty: &Ident) -> String { table_name } -/// Parses a syn::Identifier to get a snake case database name from the type identifier -/// TODO: #[macro(table_name = 'user_defined_db_table_name)]' -pub fn _database_table_name_from_entity_name(ty: &str) -> String { +/// Parses a syn::Identifier to create a defaulted snake case database table name +pub fn default_database_table_name_from_entity_name(ty: &str) -> String { let struct_name: String = ty.to_string(); let mut table_name: String = String::new(); diff --git a/canyon_observer/src/constants.rs b/canyon_observer/src/constants.rs index 5383a0f2..ee2e0bfe 100644 --- a/canyon_observer/src/constants.rs +++ b/canyon_observer/src/constants.rs @@ -1,10 +1,22 @@ -pub mod queries {} +pub const NUMERIC_PK_DATATYPE: [&str; 6] = ["i16", "u16", "i32", "u32", "i64", "u64"]; + +pub mod queries { + pub const INSERT_INTO_CANYON_MEMORY: &str = + "INSERT INTO canyon_memory (filepath, struct_name, declared_table_name) \ + VALUES ($1, $2, $3)"; + pub const UPDATE_CANYON_MEMORY: &str = + "UPDATE canyon_memory SET filepath = $1, struct_name = $2, \ + declared_table_name = $3 WHERE id = $4"; + pub const DELETE_FROM_CANYON_MEMORY: &str = + "DELETE FROM canyon_memory WHERE struct_name = $1"; +} pub mod postgresql_queries { pub static CANYON_MEMORY_TABLE: &str = "CREATE TABLE IF NOT EXISTS canyon_memory ( id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, filepath VARCHAR NOT NULL, - struct_name VARCHAR NOT NULL + struct_name VARCHAR NOT NULL, + declared_table_name VARCHAR NOT NULL )"; pub static FETCH_PUBLIC_SCHEMA: &str = @@ -38,9 +50,10 @@ pub mod mssql_queries { pub static CANYON_MEMORY_TABLE: &str = "IF OBJECT_ID(N'[dbo].[canyon_memory]', N'U') IS NULL BEGIN CREATE TABLE dbo.canyon_memory ( - id INT PRIMARY KEY IDENTITY, - filepath NVARCHAR(250) NOT NULL, - struct_name NVARCHAR(100) NOT NULL + id INT PRIMARY KEY IDENTITY, + filepath NVARCHAR(250) NOT NULL, + struct_name NVARCHAR(100) NOT NULL, + declared_table_name NVARCHAR(100) NOT NULL ); END"; @@ -166,17 +179,8 @@ pub mod sqlserver_type { pub const DATETIME: &str = "DATETIME2"; } -/// Contains fragments queries to be invoked as const items and to be concatenated -/// with dynamic data -/// -/// Ex: ` format!("{} PRIMARY KEY GENERATED ALWAYS AS IDENTITY", postgres_datatype_syntax)` -pub mod query_chunk { - // TODO @gbm25 -} - pub mod mocked_data { use canyon_connection::lazy_static::lazy_static; - use crate::migrations::information_schema::{ColumnMetadata, TableMetadata}; lazy_static! { diff --git a/canyon_observer/src/lib.rs b/canyon_observer/src/lib.rs index 2af5a36f..95dff51b 100644 --- a/canyon_observer/src/lib.rs +++ b/canyon_observer/src/lib.rs @@ -13,7 +13,6 @@ pub mod migrations; extern crate canyon_crud; -// The migrator tool mod constants; pub mod manager; @@ -26,4 +25,27 @@ pub static CANYON_REGISTER_ENTITIES: Mutex>> = lazy_static! { pub static ref QUERIES_TO_EXECUTE: Mutex>> = Mutex::new(HashMap::new()); + pub static ref CM_QUERIES_TO_EXECUTE: Mutex)>>> = // TODO Provisional until we parameterize as well the migration queries + Mutex::new(HashMap::new()); } + +// TODO replace the unwraps for the operator ? when the appropiated crate will be added +pub fn add_cm_query_to_execute(stmt: &'static str, datasource_name: &'static str, params: Vec) { + if CM_QUERIES_TO_EXECUTE + .lock() + .unwrap() + .contains_key(datasource_name) + { + CM_QUERIES_TO_EXECUTE + .lock() + .unwrap() + .get_mut(datasource_name) + .unwrap() + .push((stmt, params)); + } else { + CM_QUERIES_TO_EXECUTE + .lock() + .unwrap() + .insert(datasource_name, vec![(stmt, params)]); + } +} \ No newline at end of file diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index dfa84ef4..67a98b23 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -47,34 +47,41 @@ impl Migrations { let mut migrations_processor = MigrationsProcessor::default(); - let canyon_memory = CanyonMemory::remember(datasource).await; - let canyon_tables = CANYON_REGISTER_ENTITIES.lock().unwrap().to_vec(); + let canyon_entities = CANYON_REGISTER_ENTITIES.lock().unwrap().to_vec(); + let canyon_memory = CanyonMemory::remember(datasource, &canyon_entities).await; + // println!("Canyon memory: {:?}", &canyon_memory); + // println!("Canyon tables: {:?}", &canyon_entities); // Tracked entities that must be migrated whenever Canyon starts let schema_status = Self::fetch_database(datasource.name, datasource.properties.db_type).await; let database_tables_schema_info = Self::map_rows(schema_status); + // println!("DB tables: {:?}", &database_tables_schema_info); + // We filter the tables from the schema that aren't Canyon entities let mut user_database_tables = vec![]; for parsed_table in database_tables_schema_info.iter() { if canyon_memory .memory - .values() - .any(|f| f.to_lowercase() == parsed_table.table_name) - || canyon_memory - .renamed_entities - .values() - .any(|f| *f == parsed_table.table_name.to_lowercase()) + .iter() + .any(|f| + f.declared_table_name.eq(&parsed_table.table_name) + ) || canyon_memory + .renamed_entities + .values() + .any(|f| *f == parsed_table.table_name) { user_database_tables.append(&mut vec![parsed_table]); } } + println!("Tables to process: {:?}", user_database_tables.iter().map(|t| &t.table_name).collect::>()); + migrations_processor .process( canyon_memory, - canyon_tables, + canyon_entities, user_database_tables, datasource, ) diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index f5047baa..781094e6 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -1,9 +1,12 @@ +use canyon_crud::bounds::QueryParameter; use canyon_crud::{bounds::RowOperations, crud::Transaction, DatabaseType, DatasourceConfig}; +use regex::Regex; use std::collections::HashMap; use std::fs; use walkdir::WalkDir; +use crate::constants; -use crate::{constants, QUERIES_TO_EXECUTE}; +use super::register_types::CanyonRegisterEntity; /// Convenient struct that contains the necessary data and operations to implement /// the `Canyon Memory`. @@ -45,7 +48,7 @@ use crate::{constants, QUERIES_TO_EXECUTE}; /// The `memory field` HashMap is made by the filepath as a key, and the struct's name as value #[derive(Debug)] pub struct CanyonMemory { - pub memory: HashMap, + pub memory: Vec, pub renamed_entities: HashMap, } @@ -55,10 +58,8 @@ impl Transaction for CanyonMemory {} impl CanyonMemory { /// Queries the database to retrieve internal data about the structures /// tracked by `CanyonSQL` - /// - /// TODO fetch schemas if structures have not default ones #[allow(clippy::nonminimal_bool)] - pub async fn remember(datasource: &DatasourceConfig<'static>) -> Self { + pub async fn remember(datasource: &DatasourceConfig<'static>, canyon_entities: &Vec>) -> Self { // Creates the memory table if not exists Self::create_memory(datasource.name, &datasource.properties.db_type).await; @@ -76,142 +77,97 @@ impl CanyonMemory { id: row.get::("id"), filepath: row.get::<&str>("filepath"), struct_name: row.get::<&str>("struct_name"), + declared_table_name: row.get::<&str>("declared_table_name"), }; db_rows.push(db_row); } + println!("Data in the canyon_memory table: {db_rows:?}"); // Parses the source code files looking for the #[canyon_entity] annotated classes let mut mem = Self { - memory: HashMap::new(), + memory: Vec::new(), renamed_entities: HashMap::new(), }; - Self::find_canyon_entity_annotated_structs(&mut mem).await; + Self::find_canyon_entity_annotated_structs(&mut mem, canyon_entities).await; // Insert into the memory table the new discovered entities // Care, insert the new ones, delete the olds // Also, updates the registry when the fields changes - let mut values_to_insert = String::new(); let mut updates = Vec::new(); - for (filepath, struct_name) in &mem.memory { + for _struct in &mem.memory { // When the filepath and the struct hasn't been modified and are already on db let already_in_db = db_rows.iter().any(|el| { - (el.filepath == *filepath && el.struct_name == *struct_name) - || ((el.filepath != *filepath && el.struct_name == *struct_name) - || (el.filepath == *filepath && el.struct_name != *struct_name)) + (el.filepath == _struct.filepath && el.struct_name == _struct.struct_name) + || ((el.filepath != _struct.filepath && el.struct_name == _struct.struct_name) + || (el.filepath == _struct.filepath && el.struct_name != _struct.struct_name)) }); if !already_in_db { - values_to_insert.push_str(format!("('{filepath}', '{struct_name}'),").as_str()); + match CanyonMemory::query( + constants::queries::INSERT_INTO_CANYON_MEMORY, + &[ + &_struct.filepath as &dyn QueryParameter, + &_struct.struct_name, + &_struct.declared_table_name + ], + datasource.name + ).await { + Ok(v) => println!("Query insert CM OK: {v:?}"), + Err(e) => println!("Error update CM: {e:?}") + } } + + // When the struct or the filepath it's already on db but one of the two has been modified let need_to_update = db_rows.iter().find(|el| { - (el.filepath == *filepath || el.struct_name == *struct_name) - && !(el.filepath == *filepath && el.struct_name == *struct_name) + (el.filepath == _struct.filepath || el.struct_name == _struct.struct_name) + && !(el.filepath == _struct.filepath && el.struct_name == _struct.struct_name) }); // updated means: the old one. The value to update if let Some(old) = need_to_update { updates.push(old.struct_name); - let stmt = format!( - "UPDATE canyon_memory SET filepath = '{}', struct_name = '{}' \ - WHERE id = {}", - filepath, struct_name, old.id - ); - if QUERIES_TO_EXECUTE - .lock() - .unwrap() - .contains_key(datasource.name) - { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .get_mut(datasource.name) - .unwrap() - .push(stmt); - } else { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .insert(datasource.name, vec![stmt]); + match CanyonMemory::query( + constants::queries::UPDATE_CANYON_MEMORY, + &[ + &_struct.filepath as &dyn QueryParameter, + &_struct.struct_name, + &_struct.declared_table_name, + &old.id + ], + datasource.name + ).await { + Ok(v) => println!("Query update CM OK: {v:?}"), + Err(e) => println!("Error update CM: {e:?}") } // if the updated element is the struct name, we add it to the table_rename Hashmap - let rename_table = old.struct_name != struct_name; + let rename_table = old.struct_name != _struct.struct_name; if rename_table { mem.renamed_entities.insert( - struct_name.to_lowercase(), // The new one - old.struct_name.to_lowercase(), // The old one + _struct.struct_name.to_string(), // The new one + old.struct_name.to_string(), // The old one ); } } } - if !values_to_insert.is_empty() { - values_to_insert.pop(); - values_to_insert.push(';'); - - let stmt = format!( - "INSERT INTO canyon_memory (filepath, struct_name) VALUES {values_to_insert}" - ); - - if QUERIES_TO_EXECUTE - .lock() - .unwrap() - .contains_key(datasource.name) - { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .get_mut(datasource.name) - .unwrap() - .push(stmt); - } else { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .insert(datasource.name, vec![stmt]); - } - } - // Deletes the records when a table is dropped on the previous Canyon run - let in_memory = mem.memory.values().collect::>(); db_rows.into_iter().for_each(|db_row| { - if !in_memory.contains(&&db_row.struct_name.to_string()) + if !mem.memory.iter().any(|entity| entity.struct_name == db_row.struct_name) && !updates.contains(&db_row.struct_name) { - let stmt = format!( - "DELETE FROM canyon_memory WHERE struct_name = '{}'", - db_row.struct_name - ); - - if QUERIES_TO_EXECUTE - .lock() - .unwrap() - .contains_key(datasource.name) - { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .get_mut(datasource.name) - .unwrap() - .push(stmt); - } else { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .insert(datasource.name, vec![stmt]); - } + // crate::add_cm_query_to_execute(stmt, datasource.name, &[&db_row.struct_name]); } }); - mem } /// Parses the Rust source code files to find the one who contains Canyon entities - /// ie -> annotated with `#{canyon_entity}` - async fn find_canyon_entity_annotated_structs(&mut self) { + /// ie -> annotated with `#[canyon_entity]` + async fn find_canyon_entity_annotated_structs(&mut self, canyon_entities: &Vec>) { for file in WalkDir::new("./src") .into_iter() .filter_map(|file| file.ok()) @@ -226,19 +182,14 @@ impl CanyonMemory { let mut canyon_entity_macro_counter = 0; let mut struct_name = String::new(); for line in contents.split('\n') { - if !line.starts_with("//") && line.contains("struct") { - struct_name.push_str( - line.split_whitespace() - .collect::>() - .get(2) - .unwrap_or(&"FAILED"), - ) - } if line.contains("#[") // separated checks for possible different paths && line.contains("canyon_entity") && !line.starts_with("//") - { - canyon_entity_macro_counter += 1; + { canyon_entity_macro_counter += 1; } + + let re = Regex::new(r#"\bstruct\s+(\w+)"#).unwrap(); + if let Some(captures) = re.captures(line) { + struct_name.push_str(captures.get(1).unwrap().as_str()); } } @@ -247,15 +198,20 @@ impl CanyonMemory { match canyon_entity_macro_counter { 0 => (), 1 => { - self.memory.insert( - file.path().display().to_string().replace('\\', "/"), - struct_name, - ); + let canyon_entity = canyon_entities.iter() + .find(|ce| ce.entity_name == struct_name); + if let Some(c_entity) = canyon_entity { + self.memory.push( + CanyonMemoryAnalyzer { + filepath: file.path().display().to_string().replace('\\', "/"), + struct_name: struct_name.clone(), + declared_table_name: c_entity.entity_db_table_name.to_string() } + ) + } } _ => panic!( - "Canyon does not support having multiple structs annotated - with `#[canyon::entity]` on the same file when the `#[canyon]` - macro it's present on the program" + "Canyon-SQL does not support having multiple structs annotated + with `#[canyon::entity]` on the same file when the migrations are enabled" ), } } @@ -268,7 +224,7 @@ impl CanyonMemory { constants::postgresql_queries::CANYON_MEMORY_TABLE } else { constants::mssql_queries::CANYON_MEMORY_TABLE - }; + }; Self::query(query, [], datasource_name) .await @@ -282,4 +238,13 @@ struct CanyonMemoryRow<'a> { id: i32, filepath: &'a str, struct_name: &'a str, + declared_table_name: &'a str +} + +/// Represents the data that will be serialized in the `canyon_memory` table +#[derive(Debug)] +pub struct CanyonMemoryAnalyzer { + pub filepath: String, + pub struct_name: String, + pub declared_table_name: String } diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index 8f9a67bb..6bd19290 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -38,25 +38,25 @@ impl MigrationsProcessor { let db_type = datasource.properties.db_type; // For each entity (table) on the register (Rust structs) for canyon_register_entity in canyon_entities { - // TODO Check if its disabled for the current datasource - let entity_name = canyon_register_entity.entity_name.to_lowercase(); + let entity_name = canyon_register_entity.entity_db_table_name; + println!("Processing migrations for entity: {entity_name}"); // 1st operation -> self.create_or_rename_tables( &canyon_memory, - entity_name.as_str(), + entity_name, canyon_register_entity.entity_fields.clone(), &database_tables, ); let current_table_metadata = MigrationsHelper::get_current_table_metadata( &canyon_memory, - entity_name.as_str(), + entity_name, &database_tables, ); self.delete_fields( - entity_name.as_str(), + entity_name, canyon_register_entity.entity_fields.clone(), current_table_metadata, db_type, @@ -74,7 +74,7 @@ impl MigrationsProcessor { // if not, the columns are already create in the previous operation (create table) if current_table_metadata.is_some() { self.create_or_modify_field( - entity_name.as_str(), + entity_name, db_type, canyon_register_field.clone(), current_column_metadata, @@ -87,13 +87,13 @@ impl MigrationsProcessor { && !canyon_register_field.annotations.is_empty()) || (current_table_metadata.is_some() && current_column_metadata.is_none()) { - self.add_constraints(entity_name.as_str(), canyon_register_field.clone()) + self.add_constraints(entity_name, canyon_register_field.clone()) } // Case when we need to compare the entity with the database contain if current_table_metadata.is_some() && current_column_metadata.is_some() { self.add_modify_or_remove_constraints( - entity_name.as_str(), + entity_name, canyon_register_field, current_column_metadata.unwrap(), ) @@ -127,7 +127,7 @@ impl MigrationsProcessor { database_tables: &'a [&'a TableMetadata], ) { // 1st operation -> Check if the current entity is already on the target database. - // If isn't present (this if case), we + println!("Checking create or rename table for: {entity_name}"); if !MigrationsHelper::entity_already_on_database(entity_name, database_tables) { // [`CanyonMemory`] holds a HashMap with the tables who changed their name in // the Rust side. If this table name is present, we don't create a new table, @@ -583,7 +583,10 @@ impl MigrationsHelper { ) -> bool { database_tables .iter() - .any(|v| v.table_name.to_lowercase() == entity_name.to_lowercase()) + .any(|db_table_data| { + println!("Matching db entity name: {} vs db table name: {entity_name}", db_table_data.table_name); + db_table_data.table_name == entity_name + }) } /// Get the table metadata for a given entity name or his old entity name if the table was renamed. fn get_current_table_metadata<'a>( @@ -755,19 +758,17 @@ impl DatabaseOperation for TableOperation { TableOperation::CreateTable(table_name, table_fields) => { if db_type == DatabaseType::PostgreSql { format!( - "CREATE TABLE {:?} ({:?});", - table_name, + "CREATE TABLE \"{table_name}\" ({});", table_fields .iter() .map(|entity_field| format!( - "{} {}", + "\"{}\" {}", entity_field.field_name, entity_field.to_postgres_syntax() )) .collect::>() .join(", ") ) - .replace('"', "") } else if db_type == DatabaseType::SqlServer { format!( "CREATE TABLE {:?} ({:?});", @@ -784,7 +785,7 @@ impl DatabaseOperation for TableOperation { ) .replace('"', "") } else { - todo!() + todo!("There's no other databases supported in Canyon-SQL right now") } } @@ -846,7 +847,7 @@ impl DatabaseOperation for TableOperation { TableOperation::AddTablePrimaryKey(table_name, entity_field) => { if db_type == DatabaseType::PostgreSql { format!( - "ALTER TABLE {table_name} ADD PRIMARY KEY (\"{}\");", + "ALTER TABLE \"{table_name}\" ADD PRIMARY KEY (\"{}\");", entity_field.field_name ) } else if db_type == DatabaseType::SqlServer { @@ -913,7 +914,7 @@ impl DatabaseOperation for ColumnOperation { ColumnOperation::CreateColumn(table_name, entity_field) => if db_type == DatabaseType::PostgreSql { format!( - "ALTER TABLE {} ADD COLUMN \"{}\" {};", + "ALTER TABLE \"{}\" ADD COLUMN \"{}\" {};", table_name, entity_field.field_name, entity_field.to_postgres_syntax()) @@ -929,14 +930,14 @@ impl DatabaseOperation for ColumnOperation { }, ColumnOperation::DeleteColumn(table_name, column_name) => { // TODO Check if operation for SQL server is different - format!("ALTER TABLE {table_name} DROP COLUMN {column_name};") + format!("ALTER TABLE \"{table_name}\" DROP COLUMN \"{column_name}\";") }, ColumnOperation::AlterColumnType(table_name, entity_field) => if db_type == DatabaseType::PostgreSql { format!( - "ALTER TABLE {table_name} ALTER COLUMN \"{}\" TYPE {};", - entity_field.field_name, - entity_field.to_postgres_alter_syntax()) + "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" TYPE {};", + entity_field.field_name, entity_field.to_postgres_alter_syntax() + ) } else if db_type == DatabaseType::SqlServer { todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } else { @@ -945,14 +946,11 @@ impl DatabaseOperation for ColumnOperation { , ColumnOperation::AlterColumnDropNotNull(table_name, entity_field) => if db_type == DatabaseType::PostgreSql { - format!( - "ALTER TABLE {:?} ALTER COLUMN \"{}\" DROP NOT NULL;", - table_name, entity_field.field_name - ) + format!("ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" DROP NOT NULL;", entity_field.field_name) } else if db_type == DatabaseType::SqlServer { format!( - "ALTER TABLE {} ALTER COLUMN {} {} NULL", - table_name, entity_field.field_name, entity_field.to_sqlserver_alter_syntax() + "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NULL", + entity_field.field_name, entity_field.to_sqlserver_alter_syntax() ) } else { todo!() @@ -974,15 +972,15 @@ impl DatabaseOperation for ColumnOperation { ), ColumnOperation::AlterColumnSetNotNull(table_name, entity_field) => format!( - "ALTER TABLE {table_name} ALTER COLUMN \"{}\" SET NOT NULL;", entity_field.field_name + "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" SET NOT NULL;", entity_field.field_name ), ColumnOperation::AlterColumnAddIdentity(table_name, entity_field) => format!( - "ALTER TABLE {table_name} ALTER COLUMN \"{}\" ADD GENERATED ALWAYS AS IDENTITY;", entity_field.field_name + "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" ADD GENERATED ALWAYS AS IDENTITY;", entity_field.field_name ), ColumnOperation::AlterColumnDropIdentity(table_name, entity_field) => format!( - "ALTER TABLE {table_name} ALTER COLUMN \"{}\" DROP IDENTITY;", entity_field.field_name + "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" DROP IDENTITY;", entity_field.field_name ), }; @@ -1024,8 +1022,8 @@ impl DatabaseOperation for SequenceOperation { SequenceOperation::ModifySequence(table_name, entity_field) => { if db_type == DatabaseType::PostgreSql { format!( - "SELECT setval(pg_get_serial_sequence('{:?}', '{}'), max(\"{}\")) from {:?};", - table_name, entity_field.field_name, entity_field.field_name, table_name + "SELECT setval(pg_get_serial_sequence('\"{table_name}\"', '{}'), max(\"{}\")) from \"{table_name}\";", + entity_field.field_name, entity_field.field_name ) } else if db_type == DatabaseType::SqlServer { todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") diff --git a/canyon_observer/src/migrations/register_types.rs b/canyon_observer/src/migrations/register_types.rs index c89d89be..a06fe21b 100644 --- a/canyon_observer/src/migrations/register_types.rs +++ b/canyon_observer/src/migrations/register_types.rs @@ -1,6 +1,6 @@ use regex::Regex; -use crate::constants::{postgresql_type, regex_patterns, rust_type, sqlserver_type}; +use crate::constants::{postgresql_type, regex_patterns, rust_type, sqlserver_type, NUMERIC_PK_DATATYPE}; /// This file contains `Rust` types that represents an entry on the `CanyonRegister` /// where `Canyon` tracks the user types that has to manage @@ -10,7 +10,7 @@ use crate::constants::{postgresql_type, regex_patterns, rust_type, sqlserver_typ #[derive(Debug, Clone, Default)] pub struct CanyonRegisterEntity<'a> { pub entity_name: &'a str, - pub user_table_name: Option<&'a str>, + pub entity_db_table_name: &'a str, pub user_schema_name: Option<&'a str>, pub entity_fields: Vec, } @@ -24,8 +24,7 @@ pub struct CanyonRegisterEntityField { pub annotations: Vec, } -impl CanyonRegisterEntityField { - /// Return the postgres datatype and parameters to create a column for a given rust type +impl CanyonRegisterEntityField {/// Return the postgres datatype and parameters to create a column for a given rust type pub fn to_postgres_syntax(&self) -> String { let rust_type_clean = self.field_type.replace(' ', ""); @@ -211,11 +210,9 @@ impl CanyonRegisterEntityField { None => false, }; - let numeric = vec!["i16", "i32", "i64"]; - let postgres_datatype_syntax = Self::to_postgres_syntax(self); - if numeric.contains(&self.field_type.as_str()) && pk_is_autoincremental { + if NUMERIC_PK_DATATYPE.contains(&self.field_type.as_str()) && pk_is_autoincremental { format!("{postgres_datatype_syntax} PRIMARY KEY GENERATED ALWAYS AS IDENTITY") } else { format!("{postgres_datatype_syntax} PRIMARY KEY") @@ -235,11 +232,9 @@ impl CanyonRegisterEntityField { None => false, }; - let numeric = vec!["i16", "i32", "i64"]; - let sqlserver_datatype_syntax = Self::to_sqlserver_syntax(self); - if numeric.contains(&self.field_type.as_str()) && pk_is_autoincremental { + if NUMERIC_PK_DATATYPE.contains(&self.field_type.as_str()) && pk_is_autoincremental { format!("{sqlserver_datatype_syntax} IDENTITY PRIMARY") } else { format!("{sqlserver_datatype_syntax} PRIMARY KEY") @@ -258,9 +253,7 @@ impl CanyonRegisterEntityField { None => false, }; - let numeric = vec!["i16", "i32", "i64"]; - - numeric.contains(&self.field_type.as_str()) && pk_is_autoincremental + NUMERIC_PK_DATATYPE.contains(&self.field_type.as_str()) && pk_is_autoincremental } /// Return the nullability of a the field From 51c81bb33cc7ff8529a9129f620a00152479d9c4 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Fri, 24 Mar 2023 15:12:34 +0100 Subject: [PATCH 04/82] Minimal intermediate cleanup --- canyon_macros/src/lib.rs | 6 +- canyon_macros/src/utils/helpers.rs | 59 ++++++------ canyon_observer/src/constants.rs | 9 +- canyon_observer/src/lib.rs | 23 ----- canyon_observer/src/migrations/handler.rs | 20 +++-- canyon_observer/src/migrations/memory.rs | 89 +++++++++++-------- canyon_observer/src/migrations/processor.rs | 8 +- .../src/migrations/register_types.rs | 7 +- 8 files changed, 106 insertions(+), 115 deletions(-) diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index 51433b5e..fd9c4cf8 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -260,9 +260,9 @@ pub fn canyon_entity( let mut new_entity = CanyonRegisterEntity::default(); let e = Box::leak(entity.struct_name.to_string().into_boxed_str()); new_entity.entity_name = e; - new_entity.entity_db_table_name = table_name.unwrap_or( - Box::leak(helpers::default_database_table_name_from_entity_name(e).into_boxed_str()) - ); + new_entity.entity_db_table_name = table_name.unwrap_or(Box::leak( + helpers::default_database_table_name_from_entity_name(e).into_boxed_str(), + )); new_entity.user_schema_name = schema_name; // The entity fields diff --git a/canyon_macros/src/utils/helpers.rs b/canyon_macros/src/utils/helpers.rs index 85d6a88f..fb9aa08c 100644 --- a/canyon_macros/src/utils/helpers.rs +++ b/canyon_macros/src/utils/helpers.rs @@ -21,45 +21,40 @@ pub fn table_schema_parser(macro_data: &MacroTokens<'_>) -> Result, syn::Error> = attr.parse_args_with(Punctuated::parse_terminated); - match name_values_result { - Ok(meta_name_values) => { - for nv in meta_name_values { - let ident = nv.path.get_ident(); - if let Some(i) = ident { - let identifier = i.to_string(); - match &nv.lit { - syn::Lit::Str(s) => { - if identifier == "table_name" { - table_name = Some(s.value()) - } else if identifier == "schema" { - schema = Some(s.value()) - } else { - return Err( - syn::Error::new_spanned( - Ident::new(&identifier, i.span()), - "Only string literals are valid values for the attribute arguments" - ).into_compile_error() - ); - } - }, - _ => + if let Ok(meta_name_values) = name_values_result { + for nv in meta_name_values { + let ident = nv.path.get_ident(); + if let Some(i) = ident { + let identifier = i; + match &nv.lit { + syn::Lit::Str(s) => { + if identifier == "table_name" { + table_name = Some(s.value()) + } else if identifier == "schema" { + schema = Some(s.value()) + } else { return Err( syn::Error::new_spanned( - Ident::new(&identifier, i.span()), - "Only string literals are valid values for the attribute arguments" + Ident::new(&identifier.to_string(), i.span()), + "Only string literals are valid values for the attribute arguments" ).into_compile_error() - ), + ); + } } - } else { - return Err(syn::Error::new( - Span::call_site(), + _ => return Err(syn::Error::new_spanned( + Ident::new(&identifier.to_string(), i.span()), "Only string literals are valid values for the attribute arguments", ) - .into_compile_error()); + .into_compile_error()), } + } else { + return Err(syn::Error::new( + Span::call_site(), + "Only string literals are valid values for the attribute arguments", + ) + .into_compile_error()); } } - Err(_) => return Ok(macro_data.ty.to_string()), } let mut final_table_name = String::new(); @@ -70,7 +65,9 @@ pub fn table_schema_parser(macro_data: &MacroTokens<'_>) -> Result>> = lazy_static! { pub static ref QUERIES_TO_EXECUTE: Mutex>> = Mutex::new(HashMap::new()); - pub static ref CM_QUERIES_TO_EXECUTE: Mutex)>>> = // TODO Provisional until we parameterize as well the migration queries - Mutex::new(HashMap::new()); } - -// TODO replace the unwraps for the operator ? when the appropiated crate will be added -pub fn add_cm_query_to_execute(stmt: &'static str, datasource_name: &'static str, params: Vec) { - if CM_QUERIES_TO_EXECUTE - .lock() - .unwrap() - .contains_key(datasource_name) - { - CM_QUERIES_TO_EXECUTE - .lock() - .unwrap() - .get_mut(datasource_name) - .unwrap() - .push((stmt, params)); - } else { - CM_QUERIES_TO_EXECUTE - .lock() - .unwrap() - .insert(datasource_name, vec![(stmt, params)]); - } -} \ No newline at end of file diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index 67a98b23..8f86c862 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -58,25 +58,29 @@ impl Migrations { let database_tables_schema_info = Self::map_rows(schema_status); // println!("DB tables: {:?}", &database_tables_schema_info); - // We filter the tables from the schema that aren't Canyon entities let mut user_database_tables = vec![]; for parsed_table in database_tables_schema_info.iter() { if canyon_memory .memory .iter() - .any(|f| - f.declared_table_name.eq(&parsed_table.table_name) - ) || canyon_memory - .renamed_entities - .values() - .any(|f| *f == parsed_table.table_name) + .any(|f| f.declared_table_name.eq(&parsed_table.table_name)) + || canyon_memory + .renamed_entities + .values() + .any(|f| *f == parsed_table.table_name) { user_database_tables.append(&mut vec![parsed_table]); } } - println!("Tables to process: {:?}", user_database_tables.iter().map(|t| &t.table_name).collect::>()); + println!( + "Tables to process: {:?}", + user_database_tables + .iter() + .map(|t| &t.table_name) + .collect::>() + ); migrations_processor .process( diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index 781094e6..792e4f6e 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -1,10 +1,10 @@ +use crate::constants; use canyon_crud::bounds::QueryParameter; use canyon_crud::{bounds::RowOperations, crud::Transaction, DatabaseType, DatasourceConfig}; use regex::Regex; use std::collections::HashMap; use std::fs; use walkdir::WalkDir; -use crate::constants; use super::register_types::CanyonRegisterEntity; @@ -59,7 +59,10 @@ impl CanyonMemory { /// Queries the database to retrieve internal data about the structures /// tracked by `CanyonSQL` #[allow(clippy::nonminimal_bool)] - pub async fn remember(datasource: &DatasourceConfig<'static>, canyon_entities: &Vec>) -> Self { + pub async fn remember( + datasource: &DatasourceConfig<'static>, + canyon_entities: &Vec>, + ) -> Self { // Creates the memory table if not exists Self::create_memory(datasource.name, &datasource.properties.db_type).await; @@ -81,7 +84,7 @@ impl CanyonMemory { }; db_rows.push(db_row); } - println!("Data in the canyon_memory table: {db_rows:?}"); + println!("Data in the canyon_memory table: {db_rows:?}"); // Parses the source code files looking for the #[canyon_entity] annotated classes let mut mem = Self { @@ -100,23 +103,25 @@ impl CanyonMemory { let already_in_db = db_rows.iter().any(|el| { (el.filepath == _struct.filepath && el.struct_name == _struct.struct_name) || ((el.filepath != _struct.filepath && el.struct_name == _struct.struct_name) - || (el.filepath == _struct.filepath && el.struct_name != _struct.struct_name)) + || (el.filepath == _struct.filepath + && el.struct_name != _struct.struct_name)) }); if !already_in_db { - match CanyonMemory::query( - constants::queries::INSERT_INTO_CANYON_MEMORY, - &[ - &_struct.filepath as &dyn QueryParameter, - &_struct.struct_name, - &_struct.declared_table_name - ], - datasource.name - ).await { - Ok(v) => println!("Query insert CM OK: {v:?}"), - Err(e) => println!("Error update CM: {e:?}") - } + match CanyonMemory::query( + constants::queries::INSERT_INTO_CANYON_MEMORY, + [ + &_struct.filepath as &dyn QueryParameter, + &_struct.struct_name, + &_struct.declared_table_name, + ], + datasource.name, + ) + .await + { + Ok(v) => println!("Query insert CM OK: {v:?}"), + Err(e) => println!("Error update CM: {e:?}"), + } } - // When the struct or the filepath it's already on db but one of the two has been modified let need_to_update = db_rows.iter().find(|el| { @@ -130,16 +135,18 @@ impl CanyonMemory { match CanyonMemory::query( constants::queries::UPDATE_CANYON_MEMORY, - &[ + [ &_struct.filepath as &dyn QueryParameter, &_struct.struct_name, &_struct.declared_table_name, - &old.id + &old.id, ], - datasource.name - ).await { + datasource.name, + ) + .await + { Ok(v) => println!("Query update CM OK: {v:?}"), - Err(e) => println!("Error update CM: {e:?}") + Err(e) => println!("Error update CM: {e:?}"), } // if the updated element is the struct name, we add it to the table_rename Hashmap @@ -147,8 +154,8 @@ impl CanyonMemory { if rename_table { mem.renamed_entities.insert( - _struct.struct_name.to_string(), // The new one - old.struct_name.to_string(), // The old one + _struct.struct_name.to_string(), // The new one + old.struct_name.to_string(), // The old one ); } } @@ -156,7 +163,10 @@ impl CanyonMemory { // Deletes the records when a table is dropped on the previous Canyon run db_rows.into_iter().for_each(|db_row| { - if !mem.memory.iter().any(|entity| entity.struct_name == db_row.struct_name) + if !mem + .memory + .iter() + .any(|entity| entity.struct_name == db_row.struct_name) && !updates.contains(&db_row.struct_name) { // crate::add_cm_query_to_execute(stmt, datasource.name, &[&db_row.struct_name]); @@ -167,7 +177,10 @@ impl CanyonMemory { /// Parses the Rust source code files to find the one who contains Canyon entities /// ie -> annotated with `#[canyon_entity]` - async fn find_canyon_entity_annotated_structs(&mut self, canyon_entities: &Vec>) { + async fn find_canyon_entity_annotated_structs( + &mut self, + canyon_entities: &[CanyonRegisterEntity<'_>], + ) { for file in WalkDir::new("./src") .into_iter() .filter_map(|file| file.ok()) @@ -185,7 +198,9 @@ impl CanyonMemory { if line.contains("#[") // separated checks for possible different paths && line.contains("canyon_entity") && !line.starts_with("//") - { canyon_entity_macro_counter += 1; } + { + canyon_entity_macro_counter += 1; + } let re = Regex::new(r#"\bstruct\s+(\w+)"#).unwrap(); if let Some(captures) = re.captures(line) { @@ -198,15 +213,15 @@ impl CanyonMemory { match canyon_entity_macro_counter { 0 => (), 1 => { - let canyon_entity = canyon_entities.iter() + let canyon_entity = canyon_entities + .iter() .find(|ce| ce.entity_name == struct_name); if let Some(c_entity) = canyon_entity { - self.memory.push( - CanyonMemoryAnalyzer { - filepath: file.path().display().to_string().replace('\\', "/"), - struct_name: struct_name.clone(), - declared_table_name: c_entity.entity_db_table_name.to_string() } - ) + self.memory.push(CanyonMemoryAnalyzer { + filepath: file.path().display().to_string().replace('\\', "/"), + struct_name: struct_name.clone(), + declared_table_name: c_entity.entity_db_table_name.to_string(), + }) } } _ => panic!( @@ -224,7 +239,7 @@ impl CanyonMemory { constants::postgresql_queries::CANYON_MEMORY_TABLE } else { constants::mssql_queries::CANYON_MEMORY_TABLE - }; + }; Self::query(query, [], datasource_name) .await @@ -238,7 +253,7 @@ struct CanyonMemoryRow<'a> { id: i32, filepath: &'a str, struct_name: &'a str, - declared_table_name: &'a str + declared_table_name: &'a str, } /// Represents the data that will be serialized in the `canyon_memory` table @@ -246,5 +261,5 @@ struct CanyonMemoryRow<'a> { pub struct CanyonMemoryAnalyzer { pub filepath: String, pub struct_name: String, - pub declared_table_name: String + pub declared_table_name: String, } diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index 6bd19290..7b906c12 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -127,7 +127,6 @@ impl MigrationsProcessor { database_tables: &'a [&'a TableMetadata], ) { // 1st operation -> Check if the current entity is already on the target database. - println!("Checking create or rename table for: {entity_name}"); if !MigrationsHelper::entity_already_on_database(entity_name, database_tables) { // [`CanyonMemory`] holds a HashMap with the tables who changed their name in // the Rust side. If this table name is present, we don't create a new table, @@ -583,10 +582,7 @@ impl MigrationsHelper { ) -> bool { database_tables .iter() - .any(|db_table_data| { - println!("Matching db entity name: {} vs db table name: {entity_name}", db_table_data.table_name); - db_table_data.table_name == entity_name - }) + .any(|db_table_data| db_table_data.table_name == entity_name) } /// Get the table metadata for a given entity name or his old entity name if the table was renamed. fn get_current_table_metadata<'a>( @@ -762,7 +758,7 @@ impl DatabaseOperation for TableOperation { table_fields .iter() .map(|entity_field| format!( - "\"{}\" {}", + "\"{}\" {}", entity_field.field_name, entity_field.to_postgres_syntax() )) diff --git a/canyon_observer/src/migrations/register_types.rs b/canyon_observer/src/migrations/register_types.rs index a06fe21b..470944db 100644 --- a/canyon_observer/src/migrations/register_types.rs +++ b/canyon_observer/src/migrations/register_types.rs @@ -1,6 +1,8 @@ use regex::Regex; -use crate::constants::{postgresql_type, regex_patterns, rust_type, sqlserver_type, NUMERIC_PK_DATATYPE}; +use crate::constants::{ + postgresql_type, regex_patterns, rust_type, sqlserver_type, NUMERIC_PK_DATATYPE, +}; /// This file contains `Rust` types that represents an entry on the `CanyonRegister` /// where `Canyon` tracks the user types that has to manage @@ -24,7 +26,8 @@ pub struct CanyonRegisterEntityField { pub annotations: Vec, } -impl CanyonRegisterEntityField {/// Return the postgres datatype and parameters to create a column for a given rust type +impl CanyonRegisterEntityField { + /// Return the postgres datatype and parameters to create a column for a given rust type pub fn to_postgres_syntax(&self) -> String { let rust_type_clean = self.field_type.replace(' ', ""); From 65babf09f4b4305b1d1d277a6f2a7950da8918c0 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Mon, 27 Mar 2023 17:12:40 +0200 Subject: [PATCH 05/82] Latest updates to the canyon memory process --- canyon_macros/src/canyon_macro.rs | 12 +- canyon_observer/src/constants.rs | 10 -- canyon_observer/src/lib.rs | 3 + canyon_observer/src/migrations/handler.rs | 11 -- canyon_observer/src/migrations/memory.rs | 127 +++++++++++----------- 5 files changed, 75 insertions(+), 88 deletions(-) diff --git a/canyon_macros/src/canyon_macro.rs b/canyon_macros/src/canyon_macro.rs index ebc02629..dc6d6e2c 100644 --- a/canyon_macros/src/canyon_macro.rs +++ b/canyon_macros/src/canyon_macro.rs @@ -5,7 +5,7 @@ use proc_macro2::{Ident, TokenStream}; use quote::quote; -use canyon_observer::QUERIES_TO_EXECUTE; +use canyon_observer::{QUERIES_TO_EXECUTE, CM_QUERIES_TO_EXECUTE}; use syn::{Lit, NestedMeta}; #[derive(Debug)] @@ -107,7 +107,12 @@ fn report_literals_not_allowed(ident: &str, s: &Lit) -> TokenStream1 { /// Creates a TokenScream that is used to load the data generated at compile-time /// by the `CanyonManaged` macros again on the queries register pub fn wire_queries_to_execute(canyon_manager_tokens: &mut Vec) { + let cm_data = CM_QUERIES_TO_EXECUTE.lock().unwrap(); let data = QUERIES_TO_EXECUTE.lock().unwrap(); + + let cm_data_to_wire = cm_data.iter().map(|(key, value)| { + quote! { cm_hm.insert(#key, vec![#(#value),*]); } + }); let data_to_wire = data.iter().map(|(key, value)| { quote! { hm.insert(#key, vec![#(#value),*]); } }); @@ -116,8 +121,13 @@ pub fn wire_queries_to_execute(canyon_manager_tokens: &mut Vec) { use std::collections::HashMap; use canyon_sql::migrations::processor::MigrationsProcessor; + let mut cm_hm: HashMap<&str, Vec<&str>> = HashMap::new(); let mut hm: HashMap<&str, Vec<&str>> = HashMap::new(); + + #(#cm_data_to_wire)*; #(#data_to_wire)*; + + MigrationsProcessor::from_query_register(&cm_hm).await; MigrationsProcessor::from_query_register(&hm).await; }; diff --git a/canyon_observer/src/constants.rs b/canyon_observer/src/constants.rs index dedbeb57..c9db74e8 100644 --- a/canyon_observer/src/constants.rs +++ b/canyon_observer/src/constants.rs @@ -1,15 +1,5 @@ pub const NUMERIC_PK_DATATYPE: [&str; 6] = ["i16", "u16", "i32", "u32", "i64", "u64"]; -pub mod queries { - pub const INSERT_INTO_CANYON_MEMORY: &str = - "INSERT INTO canyon_memory (filepath, struct_name, declared_table_name) \ - VALUES ($1, $2, $3)"; - pub const UPDATE_CANYON_MEMORY: &str = - "UPDATE canyon_memory SET filepath = $1, struct_name = $2, \ - declared_table_name = $3 WHERE id = $4"; - pub const DELETE_FROM_CANYON_MEMORY: &str = "DELETE FROM canyon_memory WHERE struct_name = $1"; -} - pub mod postgresql_queries { pub static CANYON_MEMORY_TABLE: &str = "CREATE TABLE IF NOT EXISTS canyon_memory ( id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, diff --git a/canyon_observer/src/lib.rs b/canyon_observer/src/lib.rs index ae1c7523..2c813365 100644 --- a/canyon_observer/src/lib.rs +++ b/canyon_observer/src/lib.rs @@ -25,4 +25,7 @@ pub static CANYON_REGISTER_ENTITIES: Mutex>> = lazy_static! { pub static ref QUERIES_TO_EXECUTE: Mutex>> = Mutex::new(HashMap::new()); + + pub static ref CM_QUERIES_TO_EXECUTE: Mutex>> = + Mutex::new(HashMap::new()); } diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index 8f86c862..eae26ef8 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -49,14 +49,11 @@ impl Migrations { let canyon_entities = CANYON_REGISTER_ENTITIES.lock().unwrap().to_vec(); let canyon_memory = CanyonMemory::remember(datasource, &canyon_entities).await; - // println!("Canyon memory: {:?}", &canyon_memory); - // println!("Canyon tables: {:?}", &canyon_entities); // Tracked entities that must be migrated whenever Canyon starts let schema_status = Self::fetch_database(datasource.name, datasource.properties.db_type).await; let database_tables_schema_info = Self::map_rows(schema_status); - // println!("DB tables: {:?}", &database_tables_schema_info); // We filter the tables from the schema that aren't Canyon entities let mut user_database_tables = vec![]; @@ -74,14 +71,6 @@ impl Migrations { } } - println!( - "Tables to process: {:?}", - user_database_tables - .iter() - .map(|t| &t.table_name) - .collect::>() - ); - migrations_processor .process( canyon_memory, diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index 792e4f6e..7887afd7 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -1,5 +1,4 @@ use crate::constants; -use canyon_crud::bounds::QueryParameter; use canyon_crud::{bounds::RowOperations, crud::Transaction, DatabaseType, DatasourceConfig}; use regex::Regex; use std::collections::HashMap; @@ -58,6 +57,7 @@ impl Transaction for CanyonMemory {} impl CanyonMemory { /// Queries the database to retrieve internal data about the structures /// tracked by `CanyonSQL` + #[cfg(not(cargo_check))] #[allow(clippy::nonminimal_bool)] pub async fn remember( datasource: &DatasourceConfig<'static>, @@ -67,7 +67,6 @@ impl CanyonMemory { Self::create_memory(datasource.name, &datasource.properties.db_type).await; // Retrieve the last status data from the `canyon_memory` table - // TODO still pending on the target schema, for now they are created on the default one let res = Self::query("SELECT * FROM canyon_memory", [], datasource.name) .await .expect("Error querying Canyon Memory"); @@ -84,7 +83,6 @@ impl CanyonMemory { }; db_rows.push(db_row); } - println!("Data in the canyon_memory table: {db_rows:?}"); // Parses the source code files looking for the #[canyon_entity] annotated classes let mut mem = Self { @@ -92,91 +90,64 @@ impl CanyonMemory { renamed_entities: HashMap::new(), }; Self::find_canyon_entity_annotated_structs(&mut mem, canyon_entities).await; - - // Insert into the memory table the new discovered entities - // Care, insert the new ones, delete the olds - // Also, updates the registry when the fields changes + let mut updates = Vec::new(); - for _struct in &mem.memory { - // When the filepath and the struct hasn't been modified and are already on db - let already_in_db = db_rows.iter().any(|el| { - (el.filepath == _struct.filepath && el.struct_name == _struct.struct_name) - || ((el.filepath != _struct.filepath && el.struct_name == _struct.struct_name) - || (el.filepath == _struct.filepath - && el.struct_name != _struct.struct_name)) - }); - if !already_in_db { - match CanyonMemory::query( - constants::queries::INSERT_INTO_CANYON_MEMORY, - [ - &_struct.filepath as &dyn QueryParameter, - &_struct.struct_name, - &_struct.declared_table_name, - ], - datasource.name, - ) - .await - { - Ok(v) => println!("Query insert CM OK: {v:?}"), - Err(e) => println!("Error update CM: {e:?}"), - } - } + for _struct in &mem.memory { // For every program entity detected + let already_in_db = db_rows.iter().find(|el| + el.filepath == _struct.filepath || el.struct_name == _struct.struct_name || el.declared_table_name == _struct.declared_table_name + ); - // When the struct or the filepath it's already on db but one of the two has been modified - let need_to_update = db_rows.iter().find(|el| { - (el.filepath == _struct.filepath || el.struct_name == _struct.struct_name) - && !(el.filepath == _struct.filepath && el.struct_name == _struct.struct_name) - }); + if let Some(old) = already_in_db { + if !(old.filepath == _struct.filepath && old.struct_name == _struct.struct_name && old.declared_table_name == _struct.declared_table_name) { + updates.push(old.struct_name); + let stmt = format!( + "UPDATE canyon_memory SET filepath = '{}', struct_name = '{}', declared_table_name = '{}' \ + WHERE id = {}", + _struct.filepath, _struct.struct_name, _struct.declared_table_name, old.id + ); + save_canyon_memory_query(stmt, datasource.name); - // updated means: the old one. The value to update - if let Some(old) = need_to_update { - updates.push(old.struct_name); + // if the updated element is the struct name, we add it to the table_rename Hashmap + let rename_table = old.declared_table_name != _struct.declared_table_name; - match CanyonMemory::query( - constants::queries::UPDATE_CANYON_MEMORY, - [ - &_struct.filepath as &dyn QueryParameter, - &_struct.struct_name, - &_struct.declared_table_name, - &old.id, - ], - datasource.name, - ) - .await - { - Ok(v) => println!("Query update CM OK: {v:?}"), - Err(e) => println!("Error update CM: {e:?}"), + if rename_table { + mem.renamed_entities.insert( + _struct.declared_table_name.to_string(), // The new one + old.declared_table_name.to_string(), // The old one + ); + } } + } - // if the updated element is the struct name, we add it to the table_rename Hashmap - let rename_table = old.struct_name != _struct.struct_name; - - if rename_table { - mem.renamed_entities.insert( - _struct.struct_name.to_string(), // The new one - old.struct_name.to_string(), // The old one - ); - } + if already_in_db.is_none() { + println!("\tInsert action for: {_struct:?}"); + let stmt = format!( + "INSERT INTO canyon_memory (filepath, struct_name, declared_table_name) \ + VALUES ('{}', '{}', '{}')", + _struct.filepath, _struct.struct_name, _struct.declared_table_name + ); + save_canyon_memory_query(stmt, datasource.name) } } - // Deletes the records when a table is dropped on the previous Canyon run - db_rows.into_iter().for_each(|db_row| { + // Deletes the records from canyon_memory, because they stopped to be tracked by Canyon + for db_row in db_rows.into_iter() { if !mem .memory .iter() .any(|entity| entity.struct_name == db_row.struct_name) && !updates.contains(&db_row.struct_name) { - // crate::add_cm_query_to_execute(stmt, datasource.name, &[&db_row.struct_name]); + save_canyon_memory_query(format!("DELETE FROM canyon_memory WHERE struct_name = '{}'", db_row.struct_name), datasource.name); } - }); + } mem } /// Parses the Rust source code files to find the one who contains Canyon entities /// ie -> annotated with `#[canyon_entity]` + #[cfg(not(cargo_check))] async fn find_canyon_entity_annotated_structs( &mut self, canyon_entities: &[CanyonRegisterEntity<'_>], @@ -234,6 +205,7 @@ impl CanyonMemory { } /// Generates, if not exists the `canyon_memory` table + #[cfg(not(cargo_check))] async fn create_memory(datasource_name: &str, database_type: &DatabaseType) { let query = if database_type == &DatabaseType::PostgreSql { constants::postgresql_queries::CANYON_MEMORY_TABLE @@ -247,6 +219,29 @@ impl CanyonMemory { } } + +fn save_canyon_memory_query<'a>(stmt: String, ds_name: &'static str) { + use crate::CM_QUERIES_TO_EXECUTE; + + if CM_QUERIES_TO_EXECUTE + .lock() + .unwrap() + .contains_key(ds_name) + { + CM_QUERIES_TO_EXECUTE + .lock() + .unwrap() + .get_mut(ds_name) + .unwrap() + .push(stmt); + } else { + CM_QUERIES_TO_EXECUTE + .lock() + .unwrap() + .insert(ds_name, vec![stmt]); + } +} + /// Represents a single row from the `canyon_memory` table #[derive(Debug)] struct CanyonMemoryRow<'a> { From dda3e889b8d48cbea66b2e11d352532f22a734b6 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 28 Mar 2023 11:03:43 +0200 Subject: [PATCH 06/82] #feature Implemented bool types for QueryParameter<'_>. Upgraded to v0.1.2 --- CHANGELOG.md | 8 ----- canyon_connection/Cargo.toml | 2 +- canyon_crud/Cargo.toml | 4 +-- canyon_crud/src/bounds.rs | 9 +++++ canyon_macros/Cargo.toml | 8 ++--- canyon_macros/src/canyon_macro.rs | 8 ++--- canyon_observer/Cargo.toml | 6 ++-- canyon_observer/src/lib.rs | 1 - canyon_observer/src/migrations/memory.rs | 42 ++++++++++++++---------- canyon_sql/Cargo.toml | 10 +++--- tests/Cargo.toml | 2 +- 11 files changed, 53 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58967652..d4ef370f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,14 +9,6 @@ Year format is defined as: `YYYY-m-d` ## [Unreleased] -## [0.1.2] - 2023 - 03 - 23 - -### Update - -- Removed from `postgresql` migrations the auto conversion to lowercase of the table and column names - -### Fix - - Solved a bug in the canyon_entity proc macro that was wiring the incorrect user table name in the migrations ## [0.1.1] - 2023 - 03 - 20 diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 8f7e97ac..63c0a869 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_connection" -version = "0.1.1" +version = "0.1.2" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index b5b43fee..6b25867d 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_crud" -version = "0.1.1" +version = "0.1.2" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -12,4 +12,4 @@ description = "A Rust ORM and QueryBuilder" chrono = { version = "0.4", features = ["serde"] } async-trait = { version = "0.1.50" } -canyon_connection = { version = "0.1.1", path = "../canyon_connection" } +canyon_connection = { version = "0.1.2", path = "../canyon_connection" } diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs index 9a00b12c..e484fe8c 100644 --- a/canyon_crud/src/bounds.rs +++ b/canyon_crud/src/bounds.rs @@ -237,6 +237,15 @@ impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { } } +impl<'a> QueryParameter<'a> for bool { + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::Bit(Some(*self)) + } +} impl<'a> QueryParameter<'a> for i16 { fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index 83c59f3d..11e0c341 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_macros" -version = "0.1.1" +version = "0.1.2" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -18,6 +18,6 @@ proc-macro2 = "1.0.27" futures = "0.3.21" tokio = { version = "1.9.0", features = ["full"] } -canyon_observer = { version = "0.1.1", path = "../canyon_observer" } -canyon_crud = { version = "0.1.1", path = "../canyon_crud" } -canyon_connection = { version = "0.1.1", path = "../canyon_connection" } +canyon_observer = { version = "0.1.2", path = "../canyon_observer" } +canyon_crud = { version = "0.1.2", path = "../canyon_crud" } +canyon_connection = { version = "0.1.2", path = "../canyon_connection" } diff --git a/canyon_macros/src/canyon_macro.rs b/canyon_macros/src/canyon_macro.rs index dc6d6e2c..1424de92 100644 --- a/canyon_macros/src/canyon_macro.rs +++ b/canyon_macros/src/canyon_macro.rs @@ -5,7 +5,7 @@ use proc_macro2::{Ident, TokenStream}; use quote::quote; -use canyon_observer::{QUERIES_TO_EXECUTE, CM_QUERIES_TO_EXECUTE}; +use canyon_observer::{CM_QUERIES_TO_EXECUTE, QUERIES_TO_EXECUTE}; use syn::{Lit, NestedMeta}; #[derive(Debug)] @@ -109,7 +109,7 @@ fn report_literals_not_allowed(ident: &str, s: &Lit) -> TokenStream1 { pub fn wire_queries_to_execute(canyon_manager_tokens: &mut Vec) { let cm_data = CM_QUERIES_TO_EXECUTE.lock().unwrap(); let data = QUERIES_TO_EXECUTE.lock().unwrap(); - + let cm_data_to_wire = cm_data.iter().map(|(key, value)| { quote! { cm_hm.insert(#key, vec![#(#value),*]); } }); @@ -123,10 +123,10 @@ pub fn wire_queries_to_execute(canyon_manager_tokens: &mut Vec) { let mut cm_hm: HashMap<&str, Vec<&str>> = HashMap::new(); let mut hm: HashMap<&str, Vec<&str>> = HashMap::new(); - + #(#cm_data_to_wire)*; #(#data_to_wire)*; - + MigrationsProcessor::from_query_register(&cm_hm).await; MigrationsProcessor::from_query_register(&hm).await; }; diff --git a/canyon_observer/Cargo.toml b/canyon_observer/Cargo.toml index 5cea2d56..d6424714 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_observer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_observer" -version = "0.1.1" +version = "0.1.2" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -23,5 +23,5 @@ quote = "1.0.9" partialdebug = "0.2.0" # Internal dependencies -canyon_crud = { version = "0.1.1", path = "../canyon_crud" } -canyon_connection = { version = "0.1.1", path = "../canyon_connection" } +canyon_crud = { version = "0.1.2", path = "../canyon_crud" } +canyon_connection = { version = "0.1.2", path = "../canyon_connection" } diff --git a/canyon_observer/src/lib.rs b/canyon_observer/src/lib.rs index 2c813365..3c9b9fa7 100644 --- a/canyon_observer/src/lib.rs +++ b/canyon_observer/src/lib.rs @@ -25,7 +25,6 @@ pub static CANYON_REGISTER_ENTITIES: Mutex>> = lazy_static! { pub static ref QUERIES_TO_EXECUTE: Mutex>> = Mutex::new(HashMap::new()); - pub static ref CM_QUERIES_TO_EXECUTE: Mutex>> = Mutex::new(HashMap::new()); } diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index 7887afd7..79a590a7 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -61,7 +61,7 @@ impl CanyonMemory { #[allow(clippy::nonminimal_bool)] pub async fn remember( datasource: &DatasourceConfig<'static>, - canyon_entities: &Vec>, + canyon_entities: &[CanyonRegisterEntity<'_>], ) -> Self { // Creates the memory table if not exists Self::create_memory(datasource.name, &datasource.properties.db_type).await; @@ -90,18 +90,24 @@ impl CanyonMemory { renamed_entities: HashMap::new(), }; Self::find_canyon_entity_annotated_structs(&mut mem, canyon_entities).await; - + let mut updates = Vec::new(); - for _struct in &mem.memory { // For every program entity detected - let already_in_db = db_rows.iter().find(|el| - el.filepath == _struct.filepath || el.struct_name == _struct.struct_name || el.declared_table_name == _struct.declared_table_name - ); + for _struct in &mem.memory { + // For every program entity detected + let already_in_db = db_rows.iter().find(|el| { + el.filepath == _struct.filepath + || el.struct_name == _struct.struct_name + || el.declared_table_name == _struct.declared_table_name + }); if let Some(old) = already_in_db { - if !(old.filepath == _struct.filepath && old.struct_name == _struct.struct_name && old.declared_table_name == _struct.declared_table_name) { + if !(old.filepath == _struct.filepath + && old.struct_name == _struct.struct_name + && old.declared_table_name == _struct.declared_table_name) + { updates.push(old.struct_name); - let stmt = format!( + let stmt = format!( "UPDATE canyon_memory SET filepath = '{}', struct_name = '{}', declared_table_name = '{}' \ WHERE id = {}", _struct.filepath, _struct.struct_name, _struct.declared_table_name, old.id @@ -121,7 +127,6 @@ impl CanyonMemory { } if already_in_db.is_none() { - println!("\tInsert action for: {_struct:?}"); let stmt = format!( "INSERT INTO canyon_memory (filepath, struct_name, declared_table_name) \ VALUES ('{}', '{}', '{}')", @@ -139,7 +144,13 @@ impl CanyonMemory { .any(|entity| entity.struct_name == db_row.struct_name) && !updates.contains(&db_row.struct_name) { - save_canyon_memory_query(format!("DELETE FROM canyon_memory WHERE struct_name = '{}'", db_row.struct_name), datasource.name); + save_canyon_memory_query( + format!( + "DELETE FROM canyon_memory WHERE struct_name = '{}'", + db_row.struct_name + ), + datasource.name, + ); } } mem @@ -219,15 +230,10 @@ impl CanyonMemory { } } - -fn save_canyon_memory_query<'a>(stmt: String, ds_name: &'static str) { +fn save_canyon_memory_query(stmt: String, ds_name: &'static str) { use crate::CM_QUERIES_TO_EXECUTE; - if CM_QUERIES_TO_EXECUTE - .lock() - .unwrap() - .contains_key(ds_name) - { + if CM_QUERIES_TO_EXECUTE.lock().unwrap().contains_key(ds_name) { CM_QUERIES_TO_EXECUTE .lock() .unwrap() @@ -240,7 +246,7 @@ fn save_canyon_memory_query<'a>(stmt: String, ds_name: &'static str) { .unwrap() .insert(ds_name, vec![stmt]); } -} +} /// Represents a single row from the `canyon_memory` table #[derive(Debug)] diff --git a/canyon_sql/Cargo.toml b/canyon_sql/Cargo.toml index 97b32aa9..e2ae054f 100755 --- a/canyon_sql/Cargo.toml +++ b/canyon_sql/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_sql" -version = "0.1.1" +version = "0.1.2" edition = "2021" authors = ["Alex Vergara, Gonzalo Busto"] documentation = "https://zerodaycode.github.io/canyon-book/" @@ -13,7 +13,7 @@ description = "A Rust ORM and QueryBuilder" async-trait = { version = "0.1.50" } # Project crates -canyon_macros = { version = "0.1.1", path = "../canyon_macros" } -canyon_observer = { version = "0.1.1", path = "../canyon_observer" } -canyon_crud = { version = "0.1.1", path = "../canyon_crud" } -canyon_connection = { version = "0.1.1", path = "../canyon_connection" } +canyon_macros = { version = "0.1.2", path = "../canyon_macros" } +canyon_observer = { version = "0.1.2", path = "../canyon_observer" } +canyon_crud = { version = "0.1.2", path = "../canyon_crud" } +canyon_connection = { version = "0.1.2", path = "../canyon_connection" } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 7fe54756..bdb58930 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tests" -version = "0.1.1" +version = "0.1.2" edition = "2021" publish = false From b919392c28ae29f7fb808ef00ce8b1cf30422f85 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 28 Mar 2023 12:02:15 +0200 Subject: [PATCH 07/82] Unit tests for the standalone function that defaults by convention a entity name (an struct identifier) to a database table name --- canyon_macros/src/utils/helpers.rs | 10 ++++++++++ canyon_observer/src/migrations/processor.rs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/canyon_macros/src/utils/helpers.rs b/canyon_macros/src/utils/helpers.rs index fb9aa08c..39d699ef 100644 --- a/canyon_macros/src/utils/helpers.rs +++ b/canyon_macros/src/utils/helpers.rs @@ -102,6 +102,16 @@ pub fn _database_table_name_from_struct(ty: &Ident) -> String { } /// Parses a syn::Identifier to create a defaulted snake case database table name +#[test] +fn test_entity_database_name_defaulter() { + assert_eq!(default_database_table_name_from_entity_name("League"), "league".to_owned()); + assert_eq!(default_database_table_name_from_entity_name("MajorLeague"), "major_league".to_owned()); + assert_eq!(default_database_table_name_from_entity_name("MajorLeagueTournament"), "major_league_tournament".to_owned()); + + assert_ne!(default_database_table_name_from_entity_name("MajorLeague"), "majorleague".to_owned()); + assert_ne!(default_database_table_name_from_entity_name("MajorLeague"), "MajorLeague".to_owned()); +} +/// pub fn default_database_table_name_from_entity_name(ty: &str) -> String { let struct_name: String = ty.to_string(); let mut table_name: String = String::new(); diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index 7b906c12..9a1e0294 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -695,7 +695,7 @@ mod migrations_helper_tests { use super::*; use crate::constants; - const MOCKED_ENTITY_NAME: &str = "League"; + const MOCKED_ENTITY_NAME: &str = "league"; #[test] fn test_entity_already_on_database() { From c82a8af6db468b9c193097dd39b1f3bd0be59e53 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 28 Mar 2023 12:20:13 +0200 Subject: [PATCH 08/82] Ignoring tests on nightly due to linker issues in Unix machines --- .github/workflows/continuous-integration.yml | 2 +- canyon_macros/src/utils/helpers.rs | 29 +++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index add5e3e3..1201895c 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -18,7 +18,7 @@ jobs: matrix: include: - { rust: stable, os: ubuntu-latest } - - { rust: nightly, os: ubuntu-latest } + # - { rust: nightly, os: ubuntu-latest } - { rust: stable, os: macos-latest } - { rust: stable, os: windows-latest } diff --git a/canyon_macros/src/utils/helpers.rs b/canyon_macros/src/utils/helpers.rs index 39d699ef..32e8fef3 100644 --- a/canyon_macros/src/utils/helpers.rs +++ b/canyon_macros/src/utils/helpers.rs @@ -104,14 +104,29 @@ pub fn _database_table_name_from_struct(ty: &Ident) -> String { /// Parses a syn::Identifier to create a defaulted snake case database table name #[test] fn test_entity_database_name_defaulter() { - assert_eq!(default_database_table_name_from_entity_name("League"), "league".to_owned()); - assert_eq!(default_database_table_name_from_entity_name("MajorLeague"), "major_league".to_owned()); - assert_eq!(default_database_table_name_from_entity_name("MajorLeagueTournament"), "major_league_tournament".to_owned()); - - assert_ne!(default_database_table_name_from_entity_name("MajorLeague"), "majorleague".to_owned()); - assert_ne!(default_database_table_name_from_entity_name("MajorLeague"), "MajorLeague".to_owned()); + assert_eq!( + default_database_table_name_from_entity_name("League"), + "league".to_owned() + ); + assert_eq!( + default_database_table_name_from_entity_name("MajorLeague"), + "major_league".to_owned() + ); + assert_eq!( + default_database_table_name_from_entity_name("MajorLeagueTournament"), + "major_league_tournament".to_owned() + ); + + assert_ne!( + default_database_table_name_from_entity_name("MajorLeague"), + "majorleague".to_owned() + ); + assert_ne!( + default_database_table_name_from_entity_name("MajorLeague"), + "MajorLeague".to_owned() + ); } -/// +/// pub fn default_database_table_name_from_entity_name(ty: &str) -> String { let struct_name: String = ty.to_string(); let mut table_name: String = String::new(); From 5409cd1352bb0b016161c7d59fdcc96d953d047e Mon Sep 17 00:00:00 2001 From: Alex Vergara <68871459+Pyzyryab@users.noreply.github.com> Date: Mon, 10 Apr 2023 11:39:00 +0200 Subject: [PATCH 09/82] Refactors-and-upgrades (#36) * Cleaning the Result error handling by applying the operator ? whenever is possible * Refactored the parsing of the canyon entity macro --- canyon_crud/src/crud.rs | 25 +-- .../src/query_elements/query_builder.rs | 16 +- canyon_macros/src/canyon_entity_macro.rs | 75 +++++++++ canyon_macros/src/lib.rs | 68 +------- canyon_macros/src/query_operations/delete.rs | 24 +-- canyon_macros/src/query_operations/insert.rs | 10 +- canyon_macros/src/query_operations/select.rs | 155 +++++++----------- canyon_macros/src/query_operations/update.rs | 16 +- 8 files changed, 169 insertions(+), 220 deletions(-) create mode 100644 canyon_macros/src/canyon_entity_macro.rs diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index aed59307..134c41cc 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -164,7 +164,6 @@ mod postgres_query_launcher { pub async fn launch<'a, T>( db_conn: &DatabaseConnection, - // datasource_name: &str, stmt: String, params: &'a [&'_ dyn QueryParameter<'_>], ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> { @@ -173,21 +172,15 @@ mod postgres_query_launcher { m_params.push(param.as_postgres_param()); } - let query_result = db_conn - .postgres_connection - .as_ref() - .unwrap() - .client - .query(&stmt, m_params.as_slice()) - .await; - - if let Err(error) = query_result { - Err(Box::new(error)) - } else { - Ok(DatabaseResult::new_postgresql( - query_result.expect("A really bad error happened querying PostgreSQL"), - )) - } + Ok(DatabaseResult::new_postgresql( + db_conn + .postgres_connection + .as_ref() + .unwrap() + .client + .query(&stmt, m_params.as_slice()) + .await?, + )) } } diff --git a/canyon_crud/src/query_elements/query_builder.rs b/canyon_crud/src/query_elements/query_builder.rs index 3676d93c..f0e68223 100644 --- a/canyon_crud/src/query_elements/query_builder.rs +++ b/canyon_crud/src/query_elements/query_builder.rs @@ -163,25 +163,18 @@ where /// Launches the generated query against the database targeted /// by the selected datasource - #[allow(clippy::question_mark)] pub async fn query( &'a mut self, ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> { - // Close the query, we are ready to go self.query.sql.push(';'); - let result = T::query( + Ok(T::query( self.query.sql.clone(), self.query.params.to_vec(), self.datasource_name, ) - .await; - - if let Err(error) = result { - Err(error) - } else { - Ok(result.ok().unwrap().get_entities::()) - } + .await? + .get_entities::()) } pub fn r#where>(&mut self, r#where: Z, op: impl Operator) { @@ -511,8 +504,7 @@ where ) } - let cap = columns.len() * 50; // Reserving an enough initial capacity per set clause - let mut set_clause = String::with_capacity(cap); + let mut set_clause = String::new(); set_clause.push_str(" SET "); for (idx, column) in columns.iter().enumerate() { diff --git a/canyon_macros/src/canyon_entity_macro.rs b/canyon_macros/src/canyon_entity_macro.rs new file mode 100644 index 00000000..483f8f8e --- /dev/null +++ b/canyon_macros/src/canyon_entity_macro.rs @@ -0,0 +1,75 @@ +use proc_macro2::{Span, TokenStream}; +use syn::NestedMeta; + +pub(crate) fn parse_canyon_entity_proc_macro_attr( + attrs: Vec, +) -> ( + Option<&'static str>, + Option<&'static str>, + Option, +) { + let mut table_name: Option<&str> = None; + let mut schema_name: Option<&str> = None; + + let mut parsing_attribute_error: Option = None; + + // The parse of the available options to configure the Canyon Entity + for element in attrs { + match element { + syn::NestedMeta::Meta(m) => { + match m { + syn::Meta::NameValue(nv) => { + let attr_arg_ident = nv + .path + .get_ident() + .expect("Something went wrong parsing the `table_name` argument") + .to_string(); + + if &attr_arg_ident == "table_name" || &attr_arg_ident == "schema" { + match nv.lit { + syn::Lit::Str(ref l) => { + if &attr_arg_ident == "table_name" { + table_name = Some(Box::leak(l.value().into_boxed_str())) + } else { + schema_name = Some(Box::leak(l.value().into_boxed_str())) + } + } + _ => { + parsing_attribute_error = Some(syn::Error::new( + Span::call_site(), + "Only string literals are valid values for the attributes" + ).into_compile_error()); + } + } + } else { + parsing_attribute_error = Some( + syn::Error::new( + Span::call_site(), + format!( + "Argument: `{:?}` are not allowed in the canyon_macro attr", + &attr_arg_ident + ), + ) + .into_compile_error(), + ); + } + } + _ => { + parsing_attribute_error = Some(syn::Error::new( + Span::call_site(), + "Only argument identifiers with a value after an `=` sign are allowed on the `canyon_macros::canyon_entity` proc macro" + ).into_compile_error()); + } + } + } + syn::NestedMeta::Lit(_) => { + parsing_attribute_error = Some(syn::Error::new( + Span::call_site(), + "No literal values allowed on the `canyon_macros::canyon_entity` proc macro" + ).into_compile_error()); + } + } + } + + (table_name, schema_name, parsing_attribute_error) +} diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index fd9c4cf8..34a166e8 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -1,11 +1,13 @@ extern crate proc_macro; +mod canyon_entity_macro; mod canyon_macro; mod query_operations; mod utils; use canyon_connection::CANYON_TOKIO_RUNTIME; -use proc_macro::{Span, TokenStream as CompilerTokenStream}; +use canyon_entity_macro::parse_canyon_entity_proc_macro_attr; +use proc_macro::TokenStream as CompilerTokenStream; use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use syn::{DeriveInput, Fields, Type, Visibility}; @@ -179,68 +181,8 @@ pub fn canyon_entity( ) -> CompilerTokenStream { let attrs = syn::parse_macro_input!(_meta as syn::AttributeArgs); - let mut table_name: Option<&str> = None; - let mut schema_name: Option<&str> = None; - - let mut parsing_attribute_error: Option = None; - - // The parse of the available options to configure the Canyon Entity - for element in &attrs { - match element { - syn::NestedMeta::Meta(m) => { - match m { - syn::Meta::NameValue(nv) => { - let attr_arg_ident = nv - .path - .get_ident() - .expect("Something went wrong parsing the `table_name` argument") - .to_string(); - - if &attr_arg_ident == "table_name" || &attr_arg_ident == "schema" { - match nv.lit { - syn::Lit::Str(ref l) => { - if &attr_arg_ident == "table_name" { - table_name = Some(Box::leak(l.value().into_boxed_str())) - } else { - schema_name = Some(Box::leak(l.value().into_boxed_str())) - } - } - _ => { - parsing_attribute_error = Some(syn::Error::new( - Span::call_site().into(), - "Only string literals are valid values for the attributes" - ).into_compile_error()); - } - } - } else { - parsing_attribute_error = Some( - syn::Error::new( - Span::call_site().into(), - format!( - "Argument: `{:?}` are not allowed in the canyon_macro attr", - &attr_arg_ident - ), - ) - .into_compile_error(), - ); - } - } - _ => { - parsing_attribute_error = Some(syn::Error::new( - Span::call_site().into(), - "Only argument identifiers with a value after an `=` sign are allowed on the `canyon_macros::canyon_entity` proc macro" - ).into_compile_error()); - } - } - } - syn::NestedMeta::Lit(_) => { - parsing_attribute_error = Some(syn::Error::new( - Span::call_site().into(), - "No literal values allowed on the `canyon_macros::canyon_entity` proc macro" - ).into_compile_error()); - } - } - } + let (table_name, schema_name, parsing_attribute_error) = + parse_canyon_entity_proc_macro_attr(attrs); let entity_res = syn::parse::(input); diff --git a/canyon_macros/src/query_operations/delete.rs b/canyon_macros/src/query_operations/delete.rs index 4d5f3fce..cabfa37f 100644 --- a/canyon_macros/src/query_operations/delete.rs +++ b/canyon_macros/src/query_operations/delete.rs @@ -26,17 +26,13 @@ pub fn generate_delete_tokens(macro_data: &MacroTokens, table_schema_data: &Stri /// the current instance of a T type, returning a result /// indicating a possible failure querying the database. async fn delete(&self) -> Result<(), Box<(dyn std::error::Error + Send + Sync + 'static)>> { - let stmt = format!("DELETE FROM {} WHERE {:?} = $1", #table_schema_data, #primary_key); - - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, + <#ty as canyon_sql::crud::Transaction<#ty>>::query( + format!("DELETE FROM {} WHERE {:?} = $1", #table_schema_data, #primary_key), &[#pk_field_value], "" - ).await; + ).await?; - if let Err(error) = result { - Err(error) - } else { Ok(()) } + Ok(()) } /// Deletes from a database entity the row that matches @@ -45,17 +41,13 @@ pub fn generate_delete_tokens(macro_data: &MacroTokens, table_schema_data: &Stri async fn delete_datasource<'a>(&self, datasource_name: &'a str) -> Result<(), Box<(dyn std::error::Error + Send + Sync + 'static)>> { - let stmt = format!("DELETE FROM {} WHERE {:?} = $1", #table_schema_data, #primary_key); - - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, + <#ty as canyon_sql::crud::Transaction<#ty>>::query( + format!("DELETE FROM {} WHERE {:?} = $1", #table_schema_data, #primary_key), &[#pk_field_value], datasource_name - ).await; + ).await?; - if let Err(error) = result { - Err(error) - } else { Ok(()) } + Ok(()) } } } else { diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index e5b8fc12..11890b31 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -91,17 +91,13 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri #primary_key ); - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( + <#ty as canyon_sql::crud::Transaction<#ty>>::query( stmt, values, datasource_name - ).await; + ).await?; - if let Err(error) = result { - Err(error) - } else { - Ok(()) - } + Ok(()) } }; diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index c54a2a09..8c616034 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -26,9 +26,8 @@ pub fn generate_find_all_unchecked_tokens( &[], "" ).await - .ok() - .unwrap() - .get_entities::<#ty>() + .unwrap() + .get_entities::<#ty>() } /// Performns a `SELECT * FROM table_name`, where `table_name` it's @@ -45,9 +44,8 @@ pub fn generate_find_all_unchecked_tokens( &[], datasource_name ).await - .ok() - .unwrap() - .get_entities::<#ty>() + .unwrap() + .get_entities::<#ty>() } } } @@ -69,17 +67,14 @@ pub fn generate_find_all_tokens( async fn find_all<'a>() -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> { - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - #stmt, - &[], - "" - ).await; - - if let Err(error) = result { - Err(error) - } else { - Ok(result.ok().unwrap().get_entities::<#ty>()) - } + Ok( + <#ty as canyon_sql::crud::Transaction<#ty>>::query( + #stmt, + &[], + "" + ).await? + .get_entities::<#ty>() + ) } /// Performns a `SELECT * FROM table_name`, where `table_name` it's @@ -97,17 +92,14 @@ pub fn generate_find_all_tokens( async fn find_all_datasource<'a>(datasource_name: &'a str) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> { - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - #stmt, - &[], - datasource_name - ).await; - - if let Err(error) = result { - Err(error) - } else { - Ok(result.ok().unwrap().get_entities::<#ty>()) - } + Ok( + <#ty as canyon_sql::crud::Transaction<#ty>>::query( + #stmt, + &[], + datasource_name + ).await? + .get_entities::<#ty>() + ) } } } @@ -159,28 +151,23 @@ pub fn generate_count_tokens( let stmt = format!("SELECT COUNT (*) FROM {table_schema_data}"); let result_handling = quote! { - if let Err(error) = count { - Err(error) - } else { - let c = count.ok().unwrap(); - match c.get_active_ds() { - canyon_sql::crud::DatabaseType::PostgreSql => { - Ok( - c.postgres.get(0) - .expect(&format!("Count operation failed for {:?}", #ty_str)) - .get::<&str, i64>("count") - .to_owned() - ) - }, - canyon_sql::crud::DatabaseType::SqlServer => { - Ok( - c.sqlserver.get(0) - .expect(&format!("Count operation failed for {:?}", #ty_str)) - .get::(0) - .expect(&format!("SQL Server failed to return the count values for {:?}", #ty_str)) - .into() - ) - } + match count.get_active_ds() { + canyon_sql::crud::DatabaseType::PostgreSql => { + Ok( + count.postgres.get(0) + .expect(&format!("Count operation failed for {:?}", #ty_str)) + .get::<&str, i64>("count") + .to_owned() + ) + }, + canyon_sql::crud::DatabaseType::SqlServer => { + Ok( + count.sqlserver.get(0) + .expect(&format!("Count operation failed for {:?}", #ty_str)) + .get::(0) + .expect(&format!("SQL Server failed to return the count values for {:?}", #ty_str)) + .into() + ) } } }; @@ -193,7 +180,7 @@ pub fn generate_count_tokens( #stmt, &[], "" - ).await; + ).await?; #result_handling } @@ -205,7 +192,7 @@ pub fn generate_count_tokens( #stmt, &[], datasource_name - ).await; + ).await?; #result_handling } @@ -254,19 +241,11 @@ pub fn generate_find_by_pk_tokens( } let result_handling = quote! { - if let Err(error) = result { - Err(error) - } else { - match result.as_ref().ok().unwrap() { - n if n.number_of_results() == 0 => Ok(None), - _ => Ok( - Some( - result.unwrap() - .get_entities::<#ty>() - .remove(0) - ) - ) - } + match result { + n if n.number_of_results() == 0 => Ok(None), + _ => Ok( + Some(result.get_entities::<#ty>().remove(0)) + ) } }; @@ -290,7 +269,7 @@ pub fn generate_find_by_pk_tokens( #stmt, vec![value], "" - ).await; + ).await?; #result_handling } @@ -320,7 +299,7 @@ pub fn generate_find_by_pk_tokens( #stmt, vec![value], datasource_name - ).await; + ).await?; #result_handling } @@ -367,18 +346,11 @@ pub fn generate_find_by_foreign_key_tokens( format!("\"{column}\"").as_str(), ); let result_handler = quote! { - if let Err(error) = result { - Err(error) - } else { - match result.as_ref().ok().unwrap() { - n if n.number_of_results() == 0 => Ok(None), - _ => Ok(Some( - result - .unwrap() - .get_entities::<#fk_ty>() - .remove(0) - )) - } + match result { + n if n.number_of_results() == 0 => Ok(None), + _ => Ok(Some( + result.get_entities::<#fk_ty>().remove(0) + )) } }; @@ -391,7 +363,7 @@ pub fn generate_find_by_foreign_key_tokens( #stmt, &[&self.#field_ident as &dyn canyon_sql::crud::bounds::QueryParameter<'_>], "" - ).await; + ).await?; #result_handler } @@ -407,7 +379,7 @@ pub fn generate_find_by_foreign_key_tokens( #stmt, &[&self.#field_ident as &dyn canyon_sql::crud::bounds::QueryParameter<'_>], datasource_name - ).await; + ).await?; #result_handler } @@ -452,13 +424,6 @@ pub fn generate_find_by_reverse_foreign_key_tokens( Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> }; - let result_handler = quote! { - if let Err(error) = result { - Err(error) - } else { - Ok(result.ok().unwrap().get_entities::<#ty>()) - } - }; let f_ident = field_ident.to_string(); rev_fk_quotes.push(( @@ -479,13 +444,12 @@ pub fn generate_find_by_reverse_foreign_key_tokens( format!("\"{}\"", #f_ident).as_str() ); - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( + Ok(<#ty as canyon_sql::crud::Transaction<#ty>>::query( stmt, &[lookage_value], "" - ).await; - - #result_handler + ).await? + .get_entities::<#ty>()) } }, )); @@ -509,13 +473,12 @@ pub fn generate_find_by_reverse_foreign_key_tokens( format!("\"{}\"", #f_ident).as_str() ); - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( + Ok(<#ty as canyon_sql::crud::Transaction<#ty>>::query( stmt, &[lookage_value], datasource_name - ).await; - - #result_handler + ).await? + .get_entities::<#ty>()) } }, )); diff --git a/canyon_macros/src/query_operations/update.rs b/canyon_macros/src/query_operations/update.rs index 94a9abf3..5837325a 100644 --- a/canyon_macros/src/query_operations/update.rs +++ b/canyon_macros/src/query_operations/update.rs @@ -41,13 +41,11 @@ pub fn generate_update_tokens(macro_data: &MacroTokens, table_schema_data: &Stri ); let update_values: &[&dyn canyon_sql::crud::bounds::QueryParameter<'_>] = &[#(#update_values),*]; - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( + <#ty as canyon_sql::crud::Transaction<#ty>>::query( stmt, update_values, "" - ).await; + ).await?; - if let Err(e) = result { - Err(e) - } else { Ok(()) } + Ok(()) } @@ -64,13 +62,11 @@ pub fn generate_update_tokens(macro_data: &MacroTokens, table_schema_data: &Stri ); let update_values: &[&dyn canyon_sql::crud::bounds::QueryParameter<'_>] = &[#(#update_values_cloned),*]; - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( + <#ty as canyon_sql::crud::Transaction<#ty>>::query( stmt, update_values, datasource_name - ).await; + ).await?; - if let Err(e) = result { - Err(e) - } else { Ok(()) } + Ok(()) } } } else { From 4c908e01354a840ba1f34868dcf323fd7d51ff5d Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Mon, 10 Apr 2023 13:00:17 +0200 Subject: [PATCH 10/82] #upgrade The Canyon database connector has been reworked, simpliying the process. The monster trasmute for get a mutable reference to a connect due to the Tiberius query method has been eliminated --- bash_aliases.sh | 2 +- .../src/canyon_database_connector.rs | 49 +++++++++++-------- canyon_connection/src/lib.rs | 20 ++++---- canyon_crud/src/crud.rs | 48 ++++++++---------- 4 files changed, 59 insertions(+), 60 deletions(-) diff --git a/bash_aliases.sh b/bash_aliases.sh index 0466aac8..a67da429 100644 --- a/bash_aliases.sh +++ b/bash_aliases.sh @@ -4,7 +4,7 @@ # This alias avoid the usage of a bunch of commands for performn an integrated task that # depends on several concatenated commands. -# In order to run the script, simply type `$ . ./alias.sh` from the root of the project. +# In order to run the script, simply type `$ . ./bash_aliases.sh` from the root of the project. # (refreshing the current terminal session could be required) # Executes the docker compose script to wake up the postgres container diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 7da2c2ed..cac29826 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -31,10 +31,9 @@ pub struct SqlServerConnection { /// starts, Canyon gets the information about the desired datasources, /// process them and generates a pool of 1 to 1 database connection for /// every datasource defined. -pub struct DatabaseConnection { - pub postgres_connection: Option, - pub sqlserver_connection: Option, - pub database_type: DatabaseType, +pub enum DatabaseConnection { + Postgres(PostgreSqlConnection), + SqlServer(SqlServerConnection), } unsafe impl Send for DatabaseConnection {} @@ -65,14 +64,10 @@ impl DatabaseConnection { } }); - Ok(Self { - postgres_connection: Some(PostgreSqlConnection { - client: new_client, - // connection: new_connection, - }), - sqlserver_connection: None, - database_type: DatabaseType::PostgreSql, - }) + Ok(DatabaseConnection::Postgres(PostgreSqlConnection { + client: new_client, + // connection: new_connection, + })) } DatabaseType::SqlServer => { let mut config = Config::new(); @@ -106,18 +101,30 @@ impl DatabaseConnection { // Handling TLS, login and other details related to the SQL Server. let client = tiberius::Client::connect(config, tcp).await; - Ok(Self { - postgres_connection: None, - sqlserver_connection: Some(SqlServerConnection { - client: Box::leak(Box::new( - client.expect("A failure happened connecting to the database"), - )), - }), - database_type: DatabaseType::SqlServer, - }) + Ok(DatabaseConnection::SqlServer(SqlServerConnection { + client: Box::leak(Box::new( + client.expect("A failure happened connecting to the database"), + )), + })) } } } + + pub fn postgres_connection(&self) -> Option<&PostgreSqlConnection> { + if let DatabaseConnection::Postgres(conn) = self { + Some(conn) + } else { + None + } + } + + pub fn sqlserver_connection(&mut self) -> Option<&mut SqlServerConnection> { + if let DatabaseConnection::SqlServer(conn) = self { + Some(conn) + } else { + None + } + } } #[cfg(test)] diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index 9a4ebe90..240f87c5 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -32,7 +32,7 @@ lazy_static! { pub static ref DATASOURCES: Vec> = CONFIG_FILE.canyon_sql.datasources.clone(); - pub static ref CACHED_DATABASE_CONN: Mutex> = + pub static ref CACHED_DATABASE_CONN: Mutex> = Mutex::new(IndexMap::new()); } @@ -50,16 +50,14 @@ pub async fn init_connections_cache() { for datasource in DATASOURCES.iter() { CACHED_DATABASE_CONN.lock().await.insert( datasource.name, - Box::leak(Box::new( - DatabaseConnection::new(&datasource.properties) - .await - .unwrap_or_else(|_| { - panic!( - "Error pooling a new connection for the datasource: {:?}", - datasource.name - ) - }), - )), + DatabaseConnection::new(&datasource.properties) + .await + .unwrap_or_else(|_| { + panic!( + "Error pooling a new connection for the datasource: {:?}", + datasource.name + ) + }), ); } } diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index 134c41cc..36c37ea3 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -1,8 +1,8 @@ use std::fmt::Display; use async_trait::async_trait; -use canyon_connection::canyon_database_connector::DatabaseType; -use canyon_connection::CACHED_DATABASE_CONN; +use canyon_connection::canyon_database_connector::DatabaseConnection; +use canyon_connection::{CACHED_DATABASE_CONN, DATASOURCES}; use crate::bounds::QueryParameter; use crate::mapper::RowMapper; @@ -32,22 +32,24 @@ pub trait Transaction { S: AsRef + Display + Sync + Send + 'a, Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, { - let guarded_cache = CACHED_DATABASE_CONN.lock().await; + let mut guarded_cache = CACHED_DATABASE_CONN.lock().await; let database_conn = if datasource_name.is_empty() { guarded_cache - .values() - .next() - .expect("No default datasource found. Check your `canyon.toml` file") + .get_mut( + DATASOURCES.get(0) + .expect("We didn't found any valid datasource configuration. Check your `canyon.toml` file") + .name + ).unwrap_or_else(|| panic!("No default datasource found. Check your `canyon.toml` file")) } else { - guarded_cache.get(datasource_name) + guarded_cache.get_mut(datasource_name) .unwrap_or_else(|| panic!("Canyon couldn't find a datasource in the pool with the argument provided: {datasource_name}" )) }; - match database_conn.database_type { - DatabaseType::PostgreSql => { + match database_conn { + DatabaseConnection::Postgres(_) => { postgres_query_launcher::launch::( database_conn, stmt.to_string(), @@ -55,7 +57,7 @@ pub trait Transaction { ) .await } - DatabaseType::SqlServer => { + DatabaseConnection::SqlServer(_) => { sqlserver_query_launcher::launch::( database_conn, &mut stmt.to_string(), @@ -174,8 +176,7 @@ mod postgres_query_launcher { Ok(DatabaseResult::new_postgresql( db_conn - .postgres_connection - .as_ref() + .postgres_connection() .unwrap() .client .query(&stmt, m_params.as_slice()) @@ -185,8 +186,6 @@ mod postgres_query_launcher { } mod sqlserver_query_launcher { - use std::mem::transmute; - use canyon_connection::tiberius::Row; use crate::{ @@ -196,7 +195,7 @@ mod sqlserver_query_launcher { }; pub async fn launch<'a, T, Z>( - db_conn: &&mut DatabaseConnection, + db_conn: &mut DatabaseConnection, stmt: &mut String, params: Z, ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> @@ -208,10 +207,8 @@ mod sqlserver_query_launcher { let c = stmt.clone(); let temp = c .split_once("RETURNING") - .expect("An error happened generating an INSERT statement for a SQL SERVER client"); - let temp2 = temp.0.split_once("VALUES").expect( - "An error happened generating an INSERT statement for a SQL SERVER client [1]", - ); + .unwrap(); + let temp2 = temp.0.split_once("VALUES").unwrap(); *stmt = format!( "{} OUTPUT inserted.{} VALUES {}", @@ -227,16 +224,13 @@ mod sqlserver_query_launcher { .iter() .for_each(|param| mssql_query.bind(*param)); - #[allow(mutable_transmutes)] let _results: Vec = mssql_query .query( - unsafe { transmute::<&DatabaseConnection, &mut DatabaseConnection>(db_conn) } - .sqlserver_connection - .as_mut() - .expect("Error querying the MSSQL database") - .client, - ) - .await? + db_conn + .sqlserver_connection() + .expect("Error quering the MSSQL database") + .client + ).await? .into_results() .await? .into_iter() From 3b1dd0b61b58e71ee131db2b7fc463154e42380d Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Mon, 10 Apr 2023 13:41:04 +0200 Subject: [PATCH 11/82] db_type property of the properties of the datasource moved to the root of the configuration for a datasource --- .../src/canyon_database_connector.rs | 35 +++++++++---------- canyon_connection/src/datasources.rs | 11 +++--- canyon_connection/src/lib.rs | 2 +- canyon_crud/src/crud.rs | 1 - canyon_observer/src/migrations/handler.rs | 2 +- canyon_observer/src/migrations/memory.rs | 2 +- canyon_observer/src/migrations/processor.rs | 8 ++--- tests/canyon.toml | 4 +-- 8 files changed, 31 insertions(+), 34 deletions(-) diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index cac29826..d9b6d332 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -4,7 +4,7 @@ use serde::Deserialize; use tiberius::{AuthMethod, Config}; use tokio_postgres::{Client, NoTls}; -use crate::datasources::DatasourceProperties; +use crate::datasources::DatasourceConfig; /// Represents the current supported databases by Canyon #[derive(Deserialize, Debug, Eq, PartialEq, Clone, Copy, Default)] @@ -41,18 +41,18 @@ unsafe impl Sync for DatabaseConnection {} impl DatabaseConnection { pub async fn new( - datasource: &DatasourceProperties<'_>, + datasource: &DatasourceConfig<'_>, ) -> Result> { match datasource.db_type { DatabaseType::PostgreSql => { let (new_client, new_connection) = tokio_postgres::connect( &format!( "postgres://{user}:{pswd}@{host}:{port}/{db}", - user = datasource.username, - pswd = datasource.password, - host = datasource.host, - port = datasource.port.unwrap_or_default(), - db = datasource.db_name + user = datasource.properties.username, + pswd = datasource.properties.password, + host = datasource.properties.host, + port = datasource.properties.port.unwrap_or_default(), + db = datasource.properties.db_name )[..], NoTls, ) @@ -72,14 +72,14 @@ impl DatabaseConnection { DatabaseType::SqlServer => { let mut config = Config::new(); - config.host(datasource.host); - config.port(datasource.port.unwrap_or_default()); - config.database(datasource.db_name); + config.host(datasource.properties.host); + config.port(datasource.properties.port.unwrap_or_default()); + config.database(datasource.properties.db_name); // Using SQL Server authentication. config.authentication(AuthMethod::sql_server( - datasource.username, - datasource.password, + datasource.properties.username, + datasource.properties.password, )); // on production, it is not a good idea to do this. We should upgrade @@ -135,8 +135,8 @@ mod database_connection_handler { const CONFIG_FILE_MOCK_ALT: &str = r#" [canyon_sql] datasources = [ - {name = 'PostgresDS', properties.db_type = 'postgresql', properties.username = 'username', properties.password = 'random_pass', properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled'}, - {name = 'SqlServerDS', properties.db_type = 'sqlserver', properties.username = 'username2', properties.password = 'random_pass2', properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled'} + {name = 'PostgresDS', db_type = 'postgresql', properties.username = 'username', properties.password = 'random_pass', properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled'}, + {name = 'SqlServerDS', db_type = 'sqlserver', properties.username = 'username2', properties.password = 'random_pass2', properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled'} ] "#; @@ -146,10 +146,7 @@ mod database_connection_handler { let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) .expect("A failure happened retrieving the [canyon_sql] section"); - let psql_ds = &config.canyon_sql.datasources[0].properties; - let sqls_ds = &config.canyon_sql.datasources[1].properties; - - assert_eq!(psql_ds.db_type, DatabaseType::PostgreSql); - assert_eq!(sqls_ds.db_type, DatabaseType::SqlServer); + assert_eq!(config.canyon_sql.datasources[0].db_type, DatabaseType::PostgreSql); + assert_eq!(config.canyon_sql.datasources[1].db_type, DatabaseType::SqlServer); } } diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 7c87583d..daa9f8f5 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -8,8 +8,8 @@ fn load_ds_config_from_array() { const CONFIG_FILE_MOCK_ALT: &str = r#" [canyon_sql] datasources = [ - {name = 'PostgresDS', properties.db_type = 'postgresql', properties.username = 'username', properties.password = 'random_pass', properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations = 'enabled'}, - {name = 'SqlServerDS', properties.db_type = 'sqlserver', properties.username = 'username2', properties.password = 'random_pass2', properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2'} + {name = 'PostgresDS', db_type = 'postgresql', properties.username = 'username', properties.password = 'random_pass', properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations = 'enabled'}, + {name = 'SqlServerDS', db_type = 'sqlserver', properties.username = 'username2', properties.password = 'random_pass2', properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2'} ] "#; @@ -20,7 +20,7 @@ fn load_ds_config_from_array() { let ds_1 = &config.canyon_sql.datasources[1]; assert_eq!(ds_0.name, "PostgresDS"); - assert_eq!(ds_0.properties.db_type, DatabaseType::PostgreSql); + assert_eq!(ds_0.db_type, DatabaseType::PostgreSql); assert_eq!(ds_0.properties.username, "username"); assert_eq!(ds_0.properties.password, "random_pass"); assert_eq!(ds_0.properties.host, "localhost"); @@ -29,7 +29,7 @@ fn load_ds_config_from_array() { assert_eq!(ds_0.properties.migrations, Some(Migrations::Enabled)); assert_eq!(ds_1.name, "SqlServerDS"); - assert_eq!(ds_1.properties.db_type, DatabaseType::SqlServer); + assert_eq!(ds_1.db_type, DatabaseType::SqlServer); assert_eq!(ds_1.properties.username, "username2"); assert_eq!(ds_1.properties.password, "random_pass2"); assert_eq!(ds_1.properties.host, "192.168.0.250.1"); @@ -53,12 +53,13 @@ pub struct Datasources<'a> { pub struct DatasourceConfig<'a> { #[serde(borrow)] pub name: &'a str, + pub db_type: DatabaseType, pub properties: DatasourceProperties<'a>, } #[derive(Deserialize, Debug, Clone, Copy)] pub struct DatasourceProperties<'a> { - pub db_type: DatabaseType, + pub username: &'a str, pub password: &'a str, pub host: &'a str, diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index 240f87c5..6158a4eb 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -50,7 +50,7 @@ pub async fn init_connections_cache() { for datasource in DATASOURCES.iter() { CACHED_DATABASE_CONN.lock().await.insert( datasource.name, - DatabaseConnection::new(&datasource.properties) + DatabaseConnection::new(&datasource) .await .unwrap_or_else(|_| { panic!( diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index 36c37ea3..acfd7909 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -18,7 +18,6 @@ use crate::result::DatabaseResult; /// the result of the query and, if the user desires, /// automatically map it to an struct. #[async_trait] -#[allow(clippy::question_mark)] pub trait Transaction { /// Performs a query against the targeted database by the selected datasource. /// diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index eae26ef8..aa287c6c 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -52,7 +52,7 @@ impl Migrations { // Tracked entities that must be migrated whenever Canyon starts let schema_status = - Self::fetch_database(datasource.name, datasource.properties.db_type).await; + Self::fetch_database(datasource.name, datasource.db_type).await; let database_tables_schema_info = Self::map_rows(schema_status); // We filter the tables from the schema that aren't Canyon entities diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index 79a590a7..fd94bacb 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -64,7 +64,7 @@ impl CanyonMemory { canyon_entities: &[CanyonRegisterEntity<'_>], ) -> Self { // Creates the memory table if not exists - Self::create_memory(datasource.name, &datasource.properties.db_type).await; + Self::create_memory(datasource.name, &datasource.db_type).await; // Retrieve the last status data from the `canyon_memory` table let res = Self::query("SELECT * FROM canyon_memory", [], datasource.name) diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index 9a1e0294..32e7ecb5 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -35,7 +35,7 @@ impl MigrationsProcessor { datasource: &'_ DatasourceConfig<'static>, ) { // The database type formally represented in Canyon - let db_type = datasource.properties.db_type; + let db_type = datasource.db_type; // For each entity (table) on the register (Rust structs) for canyon_register_entity in canyon_entities { let entity_name = canyon_register_entity.entity_db_table_name; @@ -748,7 +748,7 @@ impl Transaction for TableOperation {} #[async_trait] impl DatabaseOperation for TableOperation { async fn generate_sql(&self, datasource: &DatasourceConfig<'static>) { - let db_type = datasource.properties.db_type; + let db_type = datasource.db_type; let stmt = match self { TableOperation::CreateTable(table_name, table_fields) => { @@ -904,7 +904,7 @@ impl Transaction for ColumnOperation {} #[async_trait] impl DatabaseOperation for ColumnOperation { async fn generate_sql(&self, datasource: &DatasourceConfig<'static>) { - let db_type = datasource.properties.db_type; + let db_type = datasource.db_type; let stmt = match self { ColumnOperation::CreateColumn(table_name, entity_field) => @@ -1012,7 +1012,7 @@ impl Transaction for SequenceOperation {} #[async_trait] impl DatabaseOperation for SequenceOperation { async fn generate_sql(&self, datasource: &DatasourceConfig<'static>) { - let db_type = datasource.properties.db_type; + let db_type = datasource.db_type; let stmt = match self { SequenceOperation::ModifySequence(table_name, entity_field) => { diff --git a/tests/canyon.toml b/tests/canyon.toml index 7bb56442..42ea4965 100644 --- a/tests/canyon.toml +++ b/tests/canyon.toml @@ -1,5 +1,5 @@ [canyon_sql] datasources = [ - {name = 'postgres_docker', properties.db_type = 'postgresql', properties.username = 'postgres', properties.password = 'postgres', properties.host = 'localhost', properties.port = 5438, properties.db_name = 'postgres'}, - {name = 'sqlserver_docker', properties.db_type = 'sqlserver', properties.username = 'sa', properties.password = 'SqlServer-10', properties.host = 'localhost', properties.port = 1434, properties.db_name = 'master'} + {name = 'postgres_docker', db_type = 'postgresql', properties.username = 'postgres', properties.password = 'postgres', properties.host = 'localhost', properties.port = 5438, properties.db_name = 'postgres'}, + {name = 'sqlserver_docker', db_type = 'sqlserver', properties.username = 'sa', properties.password = 'SqlServer-10', properties.host = 'localhost', properties.port = 1434, properties.db_name = 'master'} ] \ No newline at end of file From 70e42978f3a4d2a38cfc64846b515c0c190d2401 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Mon, 10 Apr 2023 15:31:31 +0200 Subject: [PATCH 12/82] Bumped the toml dependency to 0.7.3. This forced us to change from borrowed to owned types in the structs that holds the user configuration --- canyon_connection/Cargo.toml | 2 +- .../src/canyon_database_connector.rs | 10 +-- canyon_connection/src/datasources.rs | 32 ++++----- canyon_connection/src/lib.rs | 6 +- canyon_crud/src/crud.rs | 6 +- canyon_observer/src/lib.rs | 25 ++++++- canyon_observer/src/migrations/handler.rs | 2 +- canyon_observer/src/migrations/memory.rs | 16 ++--- canyon_observer/src/migrations/processor.rs | 66 +++---------------- tests/canyon.toml | 4 +- 10 files changed, 70 insertions(+), 99 deletions(-) diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 63c0a869..9a88cd2f 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -22,4 +22,4 @@ async-std = { version = "1.12.0" } lazy_static = "1.4.0" serde = { version = "1.0.138", features = ["derive"] } -toml = "0.5.9" \ No newline at end of file +toml = "0.7.3" diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index d9b6d332..5e6a110b 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -41,7 +41,7 @@ unsafe impl Sync for DatabaseConnection {} impl DatabaseConnection { pub async fn new( - datasource: &DatasourceConfig<'_>, + datasource: &DatasourceConfig, ) -> Result> { match datasource.db_type { DatabaseType::PostgreSql => { @@ -72,14 +72,14 @@ impl DatabaseConnection { DatabaseType::SqlServer => { let mut config = Config::new(); - config.host(datasource.properties.host); + config.host(&datasource.properties.host); config.port(datasource.properties.port.unwrap_or_default()); - config.database(datasource.properties.db_name); + config.database(&datasource.properties.db_name); // Using SQL Server authentication. config.authentication(AuthMethod::sql_server( - datasource.properties.username, - datasource.properties.password, + &datasource.properties.username, + &datasource.properties.password, )); // on production, it is not a good idea to do this. We should upgrade diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index daa9f8f5..72962e78 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -39,32 +39,28 @@ fn load_ds_config_from_array() { } /// #[derive(Deserialize, Debug, Clone)] -pub struct CanyonSqlConfig<'a> { - #[serde(borrow)] - pub canyon_sql: Datasources<'a>, +pub struct CanyonSqlConfig { + pub canyon_sql: Datasources, } #[derive(Deserialize, Debug, Clone)] -pub struct Datasources<'a> { - #[serde(borrow)] - pub datasources: Vec>, +pub struct Datasources { + pub datasources: Vec, } -#[derive(Deserialize, Debug, Clone, Copy)] -pub struct DatasourceConfig<'a> { - #[serde(borrow)] - pub name: &'a str, +#[derive(Deserialize, Debug, Clone)] +pub struct DatasourceConfig { + pub name: String, pub db_type: DatabaseType, - pub properties: DatasourceProperties<'a>, + pub properties: DatasourceProperties, } -#[derive(Deserialize, Debug, Clone, Copy)] -pub struct DatasourceProperties<'a> { - - pub username: &'a str, - pub password: &'a str, - pub host: &'a str, +#[derive(Deserialize, Debug, Clone)] +pub struct DatasourceProperties { + pub username: String, + pub password: String, + pub host: String, pub port: Option, - pub db_name: &'a str, + pub db_name: String, pub migrations: Option, } diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index 6158a4eb..fa17e03f 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -26,10 +26,10 @@ lazy_static! { static ref RAW_CONFIG_FILE: String = fs::read_to_string(CONFIG_FILE_IDENTIFIER) .expect("Error opening or reading the Canyon configuration file"); - static ref CONFIG_FILE: CanyonSqlConfig<'static> = toml::from_str(RAW_CONFIG_FILE.as_str()) + static ref CONFIG_FILE: CanyonSqlConfig = toml::from_str(RAW_CONFIG_FILE.as_str()) .expect("Error generating the configuration for Canyon-SQL"); - pub static ref DATASOURCES: Vec> = + pub static ref DATASOURCES: Vec = CONFIG_FILE.canyon_sql.datasources.clone(); pub static ref CACHED_DATABASE_CONN: Mutex> = @@ -49,7 +49,7 @@ lazy_static! { pub async fn init_connections_cache() { for datasource in DATASOURCES.iter() { CACHED_DATABASE_CONN.lock().await.insert( - datasource.name, + &datasource.name, DatabaseConnection::new(&datasource) .await .unwrap_or_else(|_| { diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index acfd7909..0bd78e47 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -36,9 +36,11 @@ pub trait Transaction { let database_conn = if datasource_name.is_empty() { guarded_cache .get_mut( - DATASOURCES.get(0) + DATASOURCES + .get(0) .expect("We didn't found any valid datasource configuration. Check your `canyon.toml` file") .name + .as_str() ).unwrap_or_else(|| panic!("No default datasource found. Check your `canyon.toml` file")) } else { guarded_cache.get_mut(datasource_name) @@ -227,7 +229,7 @@ mod sqlserver_query_launcher { .query( db_conn .sqlserver_connection() - .expect("Error quering the MSSQL database") + .expect("Error querying the MSSQL database") .client ).await? .into_results() diff --git a/canyon_observer/src/lib.rs b/canyon_observer/src/lib.rs index 3c9b9fa7..2490dfb7 100644 --- a/canyon_observer/src/lib.rs +++ b/canyon_observer/src/lib.rs @@ -23,8 +23,29 @@ use std::{collections::HashMap, sync::Mutex}; pub static CANYON_REGISTER_ENTITIES: Mutex>> = Mutex::new(Vec::new()); lazy_static! { - pub static ref QUERIES_TO_EXECUTE: Mutex>> = + pub static ref QUERIES_TO_EXECUTE: Mutex>> = Mutex::new(HashMap::new()); - pub static ref CM_QUERIES_TO_EXECUTE: Mutex>> = + pub static ref CM_QUERIES_TO_EXECUTE: Mutex>> = Mutex::new(HashMap::new()); } + +/// Stores a newly generated SQL statement from the migrations into the register +pub fn save_migrations_query_to_execute(stmt: String, ds_name: &str) { + if QUERIES_TO_EXECUTE + .lock() + .unwrap() + .contains_key(ds_name) + { + QUERIES_TO_EXECUTE + .lock() + .unwrap() + .get_mut(ds_name) + .unwrap() + .push(stmt); + } else { + QUERIES_TO_EXECUTE + .lock() + .unwrap() + .insert(ds_name.to_owned(), vec![stmt]); + } +} diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index aa287c6c..78ac1760 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -52,7 +52,7 @@ impl Migrations { // Tracked entities that must be migrated whenever Canyon starts let schema_status = - Self::fetch_database(datasource.name, datasource.db_type).await; + Self::fetch_database(&datasource.name, datasource.db_type).await; let database_tables_schema_info = Self::map_rows(schema_status); // We filter the tables from the schema that aren't Canyon entities diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index fd94bacb..60d9373e 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -60,14 +60,14 @@ impl CanyonMemory { #[cfg(not(cargo_check))] #[allow(clippy::nonminimal_bool)] pub async fn remember( - datasource: &DatasourceConfig<'static>, + datasource: &DatasourceConfig, canyon_entities: &[CanyonRegisterEntity<'_>], ) -> Self { // Creates the memory table if not exists - Self::create_memory(datasource.name, &datasource.db_type).await; + Self::create_memory(&datasource.name, &datasource.db_type).await; // Retrieve the last status data from the `canyon_memory` table - let res = Self::query("SELECT * FROM canyon_memory", [], datasource.name) + let res = Self::query("SELECT * FROM canyon_memory", [], &datasource.name) .await .expect("Error querying Canyon Memory"); let mem_results = res.as_canyon_rows(); @@ -112,7 +112,7 @@ impl CanyonMemory { WHERE id = {}", _struct.filepath, _struct.struct_name, _struct.declared_table_name, old.id ); - save_canyon_memory_query(stmt, datasource.name); + save_canyon_memory_query(stmt, &datasource.name); // if the updated element is the struct name, we add it to the table_rename Hashmap let rename_table = old.declared_table_name != _struct.declared_table_name; @@ -132,7 +132,7 @@ impl CanyonMemory { VALUES ('{}', '{}', '{}')", _struct.filepath, _struct.struct_name, _struct.declared_table_name ); - save_canyon_memory_query(stmt, datasource.name) + save_canyon_memory_query(stmt, &datasource.name) } } @@ -149,7 +149,7 @@ impl CanyonMemory { "DELETE FROM canyon_memory WHERE struct_name = '{}'", db_row.struct_name ), - datasource.name, + &datasource.name, ); } } @@ -230,7 +230,7 @@ impl CanyonMemory { } } -fn save_canyon_memory_query(stmt: String, ds_name: &'static str) { +fn save_canyon_memory_query(stmt: String, ds_name: &str) { use crate::CM_QUERIES_TO_EXECUTE; if CM_QUERIES_TO_EXECUTE.lock().unwrap().contains_key(ds_name) { @@ -244,7 +244,7 @@ fn save_canyon_memory_query(stmt: String, ds_name: &'static str) { CM_QUERIES_TO_EXECUTE .lock() .unwrap() - .insert(ds_name, vec![stmt]); + .insert(ds_name.to_owned(), vec![stmt]); } } diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index 32e7ecb5..6de664bd 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -9,7 +9,7 @@ use std::ops::Not; use crate::canyon_crud::{crud::Transaction, DatasourceConfig}; use crate::constants::regex_patterns; -use crate::QUERIES_TO_EXECUTE; +use crate::save_migrations_query_to_execute; use super::information_schema::{ColumnMetadata, TableMetadata}; use super::memory::CanyonMemory; @@ -32,7 +32,7 @@ impl MigrationsProcessor { canyon_memory: CanyonMemory, canyon_entities: Vec>, database_tables: Vec<&'a TableMetadata>, - datasource: &'_ DatasourceConfig<'static>, + datasource: &'_ DatasourceConfig, ) { // The database type formally represented in Canyon let db_type = datasource.db_type; @@ -723,7 +723,7 @@ mod migrations_helper_tests { /// Trait that enables implementors to generate the migration queries #[async_trait] trait DatabaseOperation: Debug { - async fn generate_sql(&self, datasource: &DatasourceConfig<'static>); + async fn generate_sql(&self, datasource: &DatasourceConfig); } /// Helper to relate the operations that Canyon should do when it's managing a schema @@ -747,7 +747,7 @@ impl Transaction for TableOperation {} #[async_trait] impl DatabaseOperation for TableOperation { - async fn generate_sql(&self, datasource: &DatasourceConfig<'static>) { + async fn generate_sql(&self, datasource: &DatasourceConfig) { let db_type = datasource.db_type; let stmt = match self { @@ -862,23 +862,7 @@ impl DatabaseOperation for TableOperation { } }; - if QUERIES_TO_EXECUTE - .lock() - .unwrap() - .contains_key(datasource.name) - { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .get_mut(datasource.name) - .unwrap() - .push(stmt); - } else { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .insert(datasource.name, vec![stmt]); - } + save_migrations_query_to_execute(stmt, &datasource.name); } } @@ -903,7 +887,7 @@ impl Transaction for ColumnOperation {} #[async_trait] impl DatabaseOperation for ColumnOperation { - async fn generate_sql(&self, datasource: &DatasourceConfig<'static>) { + async fn generate_sql(&self, datasource: &DatasourceConfig) { let db_type = datasource.db_type; let stmt = match self { @@ -980,23 +964,7 @@ impl DatabaseOperation for ColumnOperation { ), }; - if QUERIES_TO_EXECUTE - .lock() - .unwrap() - .contains_key(datasource.name) - { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .get_mut(datasource.name) - .unwrap() - .push(stmt); - } else { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .insert(datasource.name, vec![stmt]); - } + save_migrations_query_to_execute(stmt, &datasource.name); } } @@ -1011,7 +979,7 @@ impl Transaction for SequenceOperation {} #[async_trait] impl DatabaseOperation for SequenceOperation { - async fn generate_sql(&self, datasource: &DatasourceConfig<'static>) { + async fn generate_sql(&self, datasource: &DatasourceConfig) { let db_type = datasource.db_type; let stmt = match self { @@ -1029,22 +997,6 @@ impl DatabaseOperation for SequenceOperation { } }; - if QUERIES_TO_EXECUTE - .lock() - .unwrap() - .contains_key(datasource.name) - { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .get_mut(datasource.name) - .unwrap() - .push(stmt); - } else { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .insert(datasource.name, vec![stmt]); - } + save_migrations_query_to_execute(stmt, &datasource.name); } } diff --git a/tests/canyon.toml b/tests/canyon.toml index 42ea4965..50766b68 100644 --- a/tests/canyon.toml +++ b/tests/canyon.toml @@ -1,5 +1,5 @@ [canyon_sql] datasources = [ - {name = 'postgres_docker', db_type = 'postgresql', properties.username = 'postgres', properties.password = 'postgres', properties.host = 'localhost', properties.port = 5438, properties.db_name = 'postgres'}, - {name = 'sqlserver_docker', db_type = 'sqlserver', properties.username = 'sa', properties.password = 'SqlServer-10', properties.host = 'localhost', properties.port = 1434, properties.db_name = 'master'} + { name = 'postgres_docker', db_type = 'postgresql', properties.username = 'postgres', properties.password = 'postgres', properties.host = 'localhost', properties.port = 5438, properties.db_name = 'postgres' }, + { name = 'sqlserver_docker', db_type = 'sqlserver', properties.username = 'sa', properties.password = 'SqlServer-10', properties.host = 'localhost', properties.port = 1434, properties.db_name = 'master' } ] \ No newline at end of file From 02bcbc2a48d16fa914bd97657b15c2e85e21a0fa Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Mon, 10 Apr 2023 16:57:11 +0200 Subject: [PATCH 13/82] #feature - reworked the authentication properties for the configuration file, by giving them a unique spot --- .../src/canyon_database_connector.rs | 27 ++++++++++++----- canyon_connection/src/datasources.rs | 18 ++++++++---- canyon_connection/src/lib.rs | 4 +-- canyon_macros/src/query_operations/select.rs | 2 +- tests/canyon.toml | 29 ++++++++++++++++--- 5 files changed, 60 insertions(+), 20 deletions(-) diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 5e6a110b..4adb8346 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -45,11 +45,18 @@ impl DatabaseConnection { ) -> Result> { match datasource.db_type { DatabaseType::PostgreSql => { + let (username, password) = match datasource.auth.as_ref() { + Some(auth_method) => match auth_method { + crate::datasources::Auth::Basic { username, password }=> (username.as_str(), password.as_str()), + crate::datasources::Auth::Integrated => todo!("Auth method `Integrated` not supported in PostgreSQL databases"), + }, + None => ("postgres", "postgres"), + }; let (new_client, new_connection) = tokio_postgres::connect( &format!( "postgres://{user}:{pswd}@{host}:{port}/{db}", - user = datasource.properties.username, - pswd = datasource.properties.password, + user = username, + pswd = password, host = datasource.properties.host, port = datasource.properties.port.unwrap_or_default(), db = datasource.properties.db_name @@ -77,11 +84,17 @@ impl DatabaseConnection { config.database(&datasource.properties.db_name); // Using SQL Server authentication. - config.authentication(AuthMethod::sql_server( - &datasource.properties.username, - &datasource.properties.password, - )); - + config.authentication( + match datasource.auth.as_ref() { + Some(auth_method) => match auth_method { + crate::datasources::Auth::Basic { username, password } => + AuthMethod::sql_server(username, password), + crate::datasources::Auth::Integrated => AuthMethod::Integrated + }, + None => AuthMethod::Integrated, + } + ); + // on production, it is not a good idea to do this. We should upgrade // Canyon in future versions to allow the user take care about this // configuration diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 72962e78..8eb2777a 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -21,8 +21,8 @@ fn load_ds_config_from_array() { assert_eq!(ds_0.name, "PostgresDS"); assert_eq!(ds_0.db_type, DatabaseType::PostgreSql); - assert_eq!(ds_0.properties.username, "username"); - assert_eq!(ds_0.properties.password, "random_pass"); + // assert_eq!(ds_0.properties.username, "username"); + // assert_eq!(ds_0.properties.password, "random_pass"); assert_eq!(ds_0.properties.host, "localhost"); assert_eq!(ds_0.properties.port, None); assert_eq!(ds_0.properties.db_name, "triforce"); @@ -30,8 +30,7 @@ fn load_ds_config_from_array() { assert_eq!(ds_1.name, "SqlServerDS"); assert_eq!(ds_1.db_type, DatabaseType::SqlServer); - assert_eq!(ds_1.properties.username, "username2"); - assert_eq!(ds_1.properties.password, "random_pass2"); + // assert_eq!(ds_1.auth, Some(Auth::Basic("username2".to_string(), "random_pass2".to_string()))); assert_eq!(ds_1.properties.host, "192.168.0.250.1"); assert_eq!(ds_1.properties.port, Some(3340)); assert_eq!(ds_1.properties.db_name, "triforce2"); @@ -51,13 +50,20 @@ pub struct Datasources { pub struct DatasourceConfig { pub name: String, pub db_type: DatabaseType, + pub auth: Option, pub properties: DatasourceProperties, } +#[derive(Deserialize, Debug, Clone, PartialEq)] +pub enum Auth { + #[serde(alias = "Basic", alias = "basic")] + Basic {username: String, password: String}, + #[serde(alias = "Integrated", alias = "integrated")] + Integrated, +} + #[derive(Deserialize, Debug, Clone)] pub struct DatasourceProperties { - pub username: String, - pub password: String, pub host: String, pub port: Option, pub db_name: String, diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index fa17e03f..774c4057 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -40,10 +40,10 @@ lazy_static! { /// in the configuration file. /// /// This avoids Canyon to create a new connection to the database on every query, potentially avoiding bottlenecks -/// derivated from the instantiation of that new conn every time. +/// coming from the instantiation of that new conn every time. /// /// Note: We noticed with the integration tests that the [`tokio_postgres`] crate (PostgreSQL) is able to work in an async environment -/// with a new connection per query without no problem, but the [`tiberius`] crate (MSSQL) sufferes a lot when it has continuous +/// with a new connection per query without no problem, but the [`tiberius`] crate (MSSQL) suffers a lot when it has continuous /// statements with multiple queries, like and insert followed by a find by id to check if the insert query has done its /// job done. pub async fn init_connections_cache() { diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index 8c616034..761451c1 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -30,7 +30,7 @@ pub fn generate_find_all_unchecked_tokens( .get_entities::<#ty>() } - /// Performns a `SELECT * FROM table_name`, where `table_name` it's + /// Performs a `SELECT * FROM table_name`, where `table_name` it's /// the name of your entity but converted to the corresponding /// database convention. P.ej. PostgreSQL prefers table names declared /// with snake_case identifiers. diff --git a/tests/canyon.toml b/tests/canyon.toml index 50766b68..bb324c04 100644 --- a/tests/canyon.toml +++ b/tests/canyon.toml @@ -1,5 +1,26 @@ [canyon_sql] -datasources = [ - { name = 'postgres_docker', db_type = 'postgresql', properties.username = 'postgres', properties.password = 'postgres', properties.host = 'localhost', properties.port = 5438, properties.db_name = 'postgres' }, - { name = 'sqlserver_docker', db_type = 'sqlserver', properties.username = 'sa', properties.password = 'SqlServer-10', properties.host = 'localhost', properties.port = 1434, properties.db_name = 'master' } -] \ No newline at end of file + +[[canyon_sql.datasources]] +name = 'postgres_docker' +db_type = 'postgresql' + +[canyon_sql.datasources.auth] +basic = { username = 'postgres', password = 'postgres'} + +[canyon_sql.datasources.properties] +host = 'localhost' +port = 5438 +db_name = 'postgres' + + +[[canyon_sql.datasources]] +name = 'sqlserver_docker' +db_type = 'sqlserver' + +[canyon_sql.datasources.auth] +basic = { username = 'sa', password = 'SqlServer-10' } + +[canyon_sql.datasources.properties] +host = 'localhost' +port = 1434 +db_name = 'master' From 8835935fa60599fe023df9875d65acac0c580880 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Mon, 10 Apr 2023 17:46:57 +0200 Subject: [PATCH 14/82] #feature - Auth property is mandatory for every datasource, looking forward to get rid out of the db_type property, since it will be inferred from the auth declaration --- .../src/canyon_database_connector.rs | 48 +++++++++++-------- canyon_connection/src/datasources.rs | 47 +++++++++++++----- canyon_connection/src/lib.rs | 2 +- canyon_crud/src/crud.rs | 9 ++-- canyon_observer/src/lib.rs | 6 +-- canyon_observer/src/migrations/handler.rs | 3 +- tests/canyon.toml | 4 +- 7 files changed, 74 insertions(+), 45 deletions(-) diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 4adb8346..08d9db1b 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -45,12 +45,15 @@ impl DatabaseConnection { ) -> Result> { match datasource.db_type { DatabaseType::PostgreSql => { - let (username, password) = match datasource.auth.as_ref() { - Some(auth_method) => match auth_method { - crate::datasources::Auth::Basic { username, password }=> (username.as_str(), password.as_str()), - crate::datasources::Auth::Integrated => todo!("Auth method `Integrated` not supported in PostgreSQL databases"), + let (username, password) = match &datasource.auth { + crate::datasources::Auth::Postgres(postgres_auth) => match postgres_auth { + crate::datasources::PostgresAuth::Basic { username, password } => { + (username.as_str(), password.as_str()) + } }, - None => ("postgres", "postgres"), + crate::datasources::Auth::SqlServer(_) => { + panic!("Found SqlServer auth configuration for a PostgreSQL datasource") + } }; let (new_client, new_connection) = tokio_postgres::connect( &format!( @@ -84,17 +87,18 @@ impl DatabaseConnection { config.database(&datasource.properties.db_name); // Using SQL Server authentication. - config.authentication( - match datasource.auth.as_ref() { - Some(auth_method) => match auth_method { - crate::datasources::Auth::Basic { username, password } => - AuthMethod::sql_server(username, password), - crate::datasources::Auth::Integrated => AuthMethod::Integrated - }, - None => AuthMethod::Integrated, + config.authentication(match &datasource.auth { + crate::datasources::Auth::Postgres(_) => { + panic!("Found PostgreSQL auth configuration for a SqlServer database") } - ); - + crate::datasources::Auth::SqlServer(sql_server_auth) => match sql_server_auth { + crate::datasources::SqlServerAuth::Basic { username, password } => { + AuthMethod::sql_server(username, password) + } + crate::datasources::SqlServerAuth::Integrated => AuthMethod::Integrated, + }, + }); + // on production, it is not a good idea to do this. We should upgrade // Canyon in future versions to allow the user take care about this // configuration @@ -148,8 +152,8 @@ mod database_connection_handler { const CONFIG_FILE_MOCK_ALT: &str = r#" [canyon_sql] datasources = [ - {name = 'PostgresDS', db_type = 'postgresql', properties.username = 'username', properties.password = 'random_pass', properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled'}, - {name = 'SqlServerDS', db_type = 'sqlserver', properties.username = 'username2', properties.password = 'random_pass2', properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled'} + {name = 'PostgresDS', db_type = 'postgresql', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, + {name = 'SqlServerDS', db_type = 'sqlserver', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } ] "#; @@ -159,7 +163,13 @@ mod database_connection_handler { let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) .expect("A failure happened retrieving the [canyon_sql] section"); - assert_eq!(config.canyon_sql.datasources[0].db_type, DatabaseType::PostgreSql); - assert_eq!(config.canyon_sql.datasources[1].db_type, DatabaseType::SqlServer); + assert_eq!( + config.canyon_sql.datasources[0].db_type, + DatabaseType::PostgreSql + ); + assert_eq!( + config.canyon_sql.datasources[1].db_type, + DatabaseType::SqlServer + ); } } diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 8eb2777a..cd8f2907 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -6,11 +6,11 @@ use crate::canyon_database_connector::DatabaseType; #[test] fn load_ds_config_from_array() { const CONFIG_FILE_MOCK_ALT: &str = r#" - [canyon_sql] - datasources = [ - {name = 'PostgresDS', db_type = 'postgresql', properties.username = 'username', properties.password = 'random_pass', properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations = 'enabled'}, - {name = 'SqlServerDS', db_type = 'sqlserver', properties.username = 'username2', properties.password = 'random_pass2', properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2'} - ] + [canyon_sql] + datasources = [ + {name = 'PostgresDS', db_type = 'postgresql', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, + {name = 'SqlServerDS', db_type = 'sqlserver', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + ] "#; let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) @@ -21,8 +21,13 @@ fn load_ds_config_from_array() { assert_eq!(ds_0.name, "PostgresDS"); assert_eq!(ds_0.db_type, DatabaseType::PostgreSql); - // assert_eq!(ds_0.properties.username, "username"); - // assert_eq!(ds_0.properties.password, "random_pass"); + assert_eq!( + ds_0.auth, + Auth::Postgres(PostgresAuth::Basic { + username: "postgres".to_string(), + password: "postgres".to_string() + }) + ); assert_eq!(ds_0.properties.host, "localhost"); assert_eq!(ds_0.properties.port, None); assert_eq!(ds_0.properties.db_name, "triforce"); @@ -30,11 +35,17 @@ fn load_ds_config_from_array() { assert_eq!(ds_1.name, "SqlServerDS"); assert_eq!(ds_1.db_type, DatabaseType::SqlServer); - // assert_eq!(ds_1.auth, Some(Auth::Basic("username2".to_string(), "random_pass2".to_string()))); + assert_eq!( + ds_1.auth, + Auth::SqlServer(SqlServerAuth::Basic { + username: "sa".to_string(), + password: "SqlServer-10".to_string() + }) + ); assert_eq!(ds_1.properties.host, "192.168.0.250.1"); assert_eq!(ds_1.properties.port, Some(3340)); assert_eq!(ds_1.properties.db_name, "triforce2"); - assert_eq!(ds_1.properties.migrations, None); + assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); } /// #[derive(Deserialize, Debug, Clone)] @@ -50,14 +61,28 @@ pub struct Datasources { pub struct DatasourceConfig { pub name: String, pub db_type: DatabaseType, - pub auth: Option, + pub auth: Auth, pub properties: DatasourceProperties, } #[derive(Deserialize, Debug, Clone, PartialEq)] pub enum Auth { + #[serde(alias = "PostgreSQL", alias = "postgresql")] + Postgres(PostgresAuth), + #[serde(alias = "SqlServer", alias = "sqlserver", alias = "mssql")] + SqlServer(SqlServerAuth), +} + +#[derive(Deserialize, Debug, Clone, PartialEq)] +pub enum PostgresAuth { + #[serde(alias = "Basic", alias = "basic")] + Basic { username: String, password: String }, +} + +#[derive(Deserialize, Debug, Clone, PartialEq)] +pub enum SqlServerAuth { #[serde(alias = "Basic", alias = "basic")] - Basic {username: String, password: String}, + Basic { username: String, password: String }, #[serde(alias = "Integrated", alias = "integrated")] Integrated, } diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index 774c4057..1a8f7cab 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -50,7 +50,7 @@ pub async fn init_connections_cache() { for datasource in DATASOURCES.iter() { CACHED_DATABASE_CONN.lock().await.insert( &datasource.name, - DatabaseConnection::new(&datasource) + DatabaseConnection::new(datasource) .await .unwrap_or_else(|_| { panic!( diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index 0bd78e47..8f587a02 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -206,9 +206,7 @@ mod sqlserver_query_launcher { // Re-generate de insert statement to adequate it to the SQL SERVER syntax to retrieve the PK value(s) after insert if stmt.contains("RETURNING") { let c = stmt.clone(); - let temp = c - .split_once("RETURNING") - .unwrap(); + let temp = c.split_once("RETURNING").unwrap(); let temp2 = temp.0.split_once("VALUES").unwrap(); *stmt = format!( @@ -230,8 +228,9 @@ mod sqlserver_query_launcher { db_conn .sqlserver_connection() .expect("Error querying the MSSQL database") - .client - ).await? + .client, + ) + .await? .into_results() .await? .into_iter() diff --git a/canyon_observer/src/lib.rs b/canyon_observer/src/lib.rs index 2490dfb7..1a0766e5 100644 --- a/canyon_observer/src/lib.rs +++ b/canyon_observer/src/lib.rs @@ -31,11 +31,7 @@ lazy_static! { /// Stores a newly generated SQL statement from the migrations into the register pub fn save_migrations_query_to_execute(stmt: String, ds_name: &str) { - if QUERIES_TO_EXECUTE - .lock() - .unwrap() - .contains_key(ds_name) - { + if QUERIES_TO_EXECUTE.lock().unwrap().contains_key(ds_name) { QUERIES_TO_EXECUTE .lock() .unwrap() diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index 78ac1760..2775aa6e 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -51,8 +51,7 @@ impl Migrations { let canyon_memory = CanyonMemory::remember(datasource, &canyon_entities).await; // Tracked entities that must be migrated whenever Canyon starts - let schema_status = - Self::fetch_database(&datasource.name, datasource.db_type).await; + let schema_status = Self::fetch_database(&datasource.name, datasource.db_type).await; let database_tables_schema_info = Self::map_rows(schema_status); // We filter the tables from the schema that aren't Canyon entities diff --git a/tests/canyon.toml b/tests/canyon.toml index bb324c04..2149ea06 100644 --- a/tests/canyon.toml +++ b/tests/canyon.toml @@ -5,7 +5,7 @@ name = 'postgres_docker' db_type = 'postgresql' [canyon_sql.datasources.auth] -basic = { username = 'postgres', password = 'postgres'} +postgresql = { basic = { username = 'postgres', password = 'postgres'}} [canyon_sql.datasources.properties] host = 'localhost' @@ -18,7 +18,7 @@ name = 'sqlserver_docker' db_type = 'sqlserver' [canyon_sql.datasources.auth] -basic = { username = 'sa', password = 'SqlServer-10' } +sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } [canyon_sql.datasources.properties] host = 'localhost' From f9bcf6872fc88cd60b91b0bca0e992c019fedddc Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Mon, 10 Apr 2023 17:52:52 +0200 Subject: [PATCH 15/82] #feature - Removed the db_type field from the options of the configuration file. The in-use per datasource database type will be inferred from the auth key --- .../src/canyon_database_connector.rs | 10 +++++----- canyon_connection/src/datasources.rs | 18 +++++++++++++----- canyon_observer/src/migrations/handler.rs | 2 +- canyon_observer/src/migrations/memory.rs | 2 +- canyon_observer/src/migrations/processor.rs | 8 ++++---- tests/canyon.toml | 2 -- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 08d9db1b..e4d6cde3 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -43,7 +43,7 @@ impl DatabaseConnection { pub async fn new( datasource: &DatasourceConfig, ) -> Result> { - match datasource.db_type { + match datasource.get_db_type() { DatabaseType::PostgreSql => { let (username, password) = match &datasource.auth { crate::datasources::Auth::Postgres(postgres_auth) => match postgres_auth { @@ -152,8 +152,8 @@ mod database_connection_handler { const CONFIG_FILE_MOCK_ALT: &str = r#" [canyon_sql] datasources = [ - {name = 'PostgresDS', db_type = 'postgresql', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, - {name = 'SqlServerDS', db_type = 'sqlserver', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, + {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } ] "#; @@ -164,11 +164,11 @@ mod database_connection_handler { .expect("A failure happened retrieving the [canyon_sql] section"); assert_eq!( - config.canyon_sql.datasources[0].db_type, + config.canyon_sql.datasources[0].get_db_type(), DatabaseType::PostgreSql ); assert_eq!( - config.canyon_sql.datasources[1].db_type, + config.canyon_sql.datasources[1].get_db_type(), DatabaseType::SqlServer ); } diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index cd8f2907..c609f864 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -8,8 +8,8 @@ fn load_ds_config_from_array() { const CONFIG_FILE_MOCK_ALT: &str = r#" [canyon_sql] datasources = [ - {name = 'PostgresDS', db_type = 'postgresql', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, - {name = 'SqlServerDS', db_type = 'sqlserver', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, + {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } ] "#; @@ -20,7 +20,7 @@ fn load_ds_config_from_array() { let ds_1 = &config.canyon_sql.datasources[1]; assert_eq!(ds_0.name, "PostgresDS"); - assert_eq!(ds_0.db_type, DatabaseType::PostgreSql); + assert_eq!(ds_0.get_db_type(), DatabaseType::PostgreSql); assert_eq!( ds_0.auth, Auth::Postgres(PostgresAuth::Basic { @@ -34,7 +34,7 @@ fn load_ds_config_from_array() { assert_eq!(ds_0.properties.migrations, Some(Migrations::Enabled)); assert_eq!(ds_1.name, "SqlServerDS"); - assert_eq!(ds_1.db_type, DatabaseType::SqlServer); + assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); assert_eq!( ds_1.auth, Auth::SqlServer(SqlServerAuth::Basic { @@ -60,11 +60,19 @@ pub struct Datasources { #[derive(Deserialize, Debug, Clone)] pub struct DatasourceConfig { pub name: String, - pub db_type: DatabaseType, pub auth: Auth, pub properties: DatasourceProperties, } +impl DatasourceConfig { + pub fn get_db_type(&self) -> DatabaseType { + match self.auth { + Auth::Postgres(_) => DatabaseType::PostgreSql, + Auth::SqlServer(_) => DatabaseType::SqlServer, + } + } +} + #[derive(Deserialize, Debug, Clone, PartialEq)] pub enum Auth { #[serde(alias = "PostgreSQL", alias = "postgresql")] diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index 2775aa6e..80d5f1a3 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -51,7 +51,7 @@ impl Migrations { let canyon_memory = CanyonMemory::remember(datasource, &canyon_entities).await; // Tracked entities that must be migrated whenever Canyon starts - let schema_status = Self::fetch_database(&datasource.name, datasource.db_type).await; + let schema_status = Self::fetch_database(&datasource.name, datasource.get_db_type()).await; let database_tables_schema_info = Self::map_rows(schema_status); // We filter the tables from the schema that aren't Canyon entities diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index 60d9373e..0a4080c0 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -64,7 +64,7 @@ impl CanyonMemory { canyon_entities: &[CanyonRegisterEntity<'_>], ) -> Self { // Creates the memory table if not exists - Self::create_memory(&datasource.name, &datasource.db_type).await; + Self::create_memory(&datasource.name, &datasource.get_db_type()).await; // Retrieve the last status data from the `canyon_memory` table let res = Self::query("SELECT * FROM canyon_memory", [], &datasource.name) diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index 6de664bd..c3995bbf 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -35,7 +35,7 @@ impl MigrationsProcessor { datasource: &'_ DatasourceConfig, ) { // The database type formally represented in Canyon - let db_type = datasource.db_type; + let db_type = datasource.get_db_type(); // For each entity (table) on the register (Rust structs) for canyon_register_entity in canyon_entities { let entity_name = canyon_register_entity.entity_db_table_name; @@ -748,7 +748,7 @@ impl Transaction for TableOperation {} #[async_trait] impl DatabaseOperation for TableOperation { async fn generate_sql(&self, datasource: &DatasourceConfig) { - let db_type = datasource.db_type; + let db_type = datasource.get_db_type(); let stmt = match self { TableOperation::CreateTable(table_name, table_fields) => { @@ -888,7 +888,7 @@ impl Transaction for ColumnOperation {} #[async_trait] impl DatabaseOperation for ColumnOperation { async fn generate_sql(&self, datasource: &DatasourceConfig) { - let db_type = datasource.db_type; + let db_type = datasource.get_db_type(); let stmt = match self { ColumnOperation::CreateColumn(table_name, entity_field) => @@ -980,7 +980,7 @@ impl Transaction for SequenceOperation {} #[async_trait] impl DatabaseOperation for SequenceOperation { async fn generate_sql(&self, datasource: &DatasourceConfig) { - let db_type = datasource.db_type; + let db_type = datasource.get_db_type(); let stmt = match self { SequenceOperation::ModifySequence(table_name, entity_field) => { diff --git a/tests/canyon.toml b/tests/canyon.toml index 2149ea06..0b0614a4 100644 --- a/tests/canyon.toml +++ b/tests/canyon.toml @@ -2,7 +2,6 @@ [[canyon_sql.datasources]] name = 'postgres_docker' -db_type = 'postgresql' [canyon_sql.datasources.auth] postgresql = { basic = { username = 'postgres', password = 'postgres'}} @@ -15,7 +14,6 @@ db_name = 'postgres' [[canyon_sql.datasources]] name = 'sqlserver_docker' -db_type = 'sqlserver' [canyon_sql.datasources.auth] sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } From fbbb2060aad6b67dd270482985294da0cd75ac2a Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Mon, 10 Apr 2023 18:00:48 +0200 Subject: [PATCH 16/82] #feature - Added a test for the `SqlServerAuth` integrated auth option --- canyon_connection/src/datasources.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index c609f864..6f0d0c6e 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -9,7 +9,8 @@ fn load_ds_config_from_array() { [canyon_sql] datasources = [ {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, - {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' }, + {name = 'SqlServerDS', auth = { sqlserver = { integrated = {} } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } ] "#; @@ -18,6 +19,7 @@ fn load_ds_config_from_array() { let ds_0 = &config.canyon_sql.datasources[0]; let ds_1 = &config.canyon_sql.datasources[1]; + let ds_2 = &config.canyon_sql.datasources[2]; assert_eq!(ds_0.name, "PostgresDS"); assert_eq!(ds_0.get_db_type(), DatabaseType::PostgreSql); @@ -46,6 +48,8 @@ fn load_ds_config_from_array() { assert_eq!(ds_1.properties.port, Some(3340)); assert_eq!(ds_1.properties.db_name, "triforce2"); assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); + + assert_eq!(ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) } /// #[derive(Deserialize, Debug, Clone)] From a8c2f1ce7ca7714749cff9b3344609b41fb764a6 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 11 Apr 2023 10:18:26 +0200 Subject: [PATCH 17/82] Adding the required features for working with the tiberius integrated authentication system --- canyon_connection/Cargo.toml | 2 +- canyon_observer/src/migrations/handler.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 9a88cd2f..3955dac3 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -16,7 +16,7 @@ tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"] } futures = "0.3.25" indexmap = "1.9.1" -tiberius = { version = "0.11.3", features = ["tds73", "chrono"] } +tiberius = { version = "0.12.1", features = ["tds73", "chrono", "winauth", "integrated-auth-gssapi"] } async-std = { version = "1.12.0" } lazy_static = "1.4.0" diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index 80d5f1a3..d454128a 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -51,7 +51,8 @@ impl Migrations { let canyon_memory = CanyonMemory::remember(datasource, &canyon_entities).await; // Tracked entities that must be migrated whenever Canyon starts - let schema_status = Self::fetch_database(&datasource.name, datasource.get_db_type()).await; + let schema_status = + Self::fetch_database(&datasource.name, datasource.get_db_type()).await; let database_tables_schema_info = Self::map_rows(schema_status); // We filter the tables from the schema that aren't Canyon entities From 9021257ea9297b579a4ecf319c320d1379a0e479 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 11 Apr 2023 10:49:10 +0200 Subject: [PATCH 18/82] Separating with cfg features the conditional different libraries by target to enable integrated auth for tiberius (MSSQL) --- canyon_connection/Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 3955dac3..6d8b8b5f 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -16,10 +16,14 @@ tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"] } futures = "0.3.25" indexmap = "1.9.1" -tiberius = { version = "0.12.1", features = ["tds73", "chrono", "winauth", "integrated-auth-gssapi"] } +tiberius = { version = "0.12.1", features = ["tds73", "chrono", "integrated-auth-gssapi"] } async-std = { version = "1.12.0" } lazy_static = "1.4.0" serde = { version = "1.0.138", features = ["derive"] } toml = "0.7.3" + +[target.'cfg(windows)'.dependencies.tiberius] +version = "0.12.1" +features = ["tds73", "chrono", "winauth"] From 86dc02e846991a8cd39581ae44740e748a781bff Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 11 Apr 2023 11:06:27 +0200 Subject: [PATCH 19/82] Being more specific with targets for the CI process --- .github/workflows/code-quality.yml | 4 ++-- .github/workflows/continuous-integration.yml | 10 +++++++--- canyon_connection/Cargo.toml | 3 --- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index ff194b49..0ab43d54 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -22,7 +22,7 @@ jobs: - uses: hecrj/setup-rust-action@v1 with: components: clippy - - run: cargo clippy --workspace --all-targets --verbose --all-features -- -A clippy::question_mark + - run: cargo clippy --workspace --all-targets --verbose --all-features rustfmt: name: Verify code formatting runs-on: ubuntu-latest @@ -57,4 +57,4 @@ jobs: with: rust-version: nightly - - run: cargo rustdoc -p ${{ matrix.crate }} --all-features -- -D warnings + - run: cargo rustdoc --target=x86_64-unknown-linux-gnu -p ${{ matrix.crate }} --all-features -- -D warnings diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1201895c..0404e96c 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -43,12 +43,16 @@ jobs: - name: Load data for MSSQL tests if: ${{ matrix.os == 'ubuntu-latest' }} - run: cargo test initialize_sql_server_docker_instance -p tests --all-features --no-fail-fast -- --show-output --nocapture --include-ignored + run: cargo test initialize_sql_server_docker_instance -p tests --target=x86_64-unknown-linux-gnu --all-features --no-fail-fast -- --show-output --nocapture --include-ignored - name: Run all tests, UNIT and INTEGRATION for Linux targets if: ${{ matrix.os == 'ubuntu-latest' }} run: cargo test --verbose --workspace --all-features --no-fail-fast -- --show-output --test-threads=1 - - name: Run only UNIT tests for the rest of the defined targets - if: ${{ matrix.os != 'ubuntu-latest' }} + - name: Run only UNIT tests for Windows + if: ${{ matrix.os == 'windows-latest' }} + run: cargo test --verbose --workspace --target=x86_64-pc-windows-msvc --exclude tests --all-features --no-fail-fast -- --show-output- + + - name: Run only UNIT tests for MacOS + if: ${{ matrix.os == 'MacOS-latest' }} run: cargo test --verbose --workspace --exclude tests --all-features --no-fail-fast -- --show-output diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 6d8b8b5f..5af86ca7 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -24,6 +24,3 @@ lazy_static = "1.4.0" serde = { version = "1.0.138", features = ["derive"] } toml = "0.7.3" -[target.'cfg(windows)'.dependencies.tiberius] -version = "0.12.1" -features = ["tds73", "chrono", "winauth"] From df4d6a34dc4ac51496b002a24faf9eb107e271ee Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 11 Apr 2023 11:47:41 +0200 Subject: [PATCH 20/82] Upgrading the Rust version for the VMs --- .github/workflows/continuous-integration.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 0404e96c..bcf11843 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -23,6 +23,9 @@ jobs: - { rust: stable, os: windows-latest } steps: + - name: Upgrading Rust + run: rustup update + - name: Make the USER own the working directory if: ${{ matrix.os == 'ubuntu-latest' }} run: sudo chown -R $USER:$USER ${{ github.workspace }} From d66f7161770ad8bb47ce35c9f760c44583aed821 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 11 Apr 2023 11:54:10 +0200 Subject: [PATCH 21/82] Installing vendored OpenSSL --- .github/workflows/continuous-integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index bcf11843..e6ed0825 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -23,8 +23,8 @@ jobs: - { rust: stable, os: windows-latest } steps: - - name: Upgrading Rust - run: rustup update + - name: Installing vendored OpenSSL + run: cargo install cargo-generate --features vendored-openssl - name: Make the USER own the working directory if: ${{ matrix.os == 'ubuntu-latest' }} From dff5d8dc786600fd2216c81d923a34a638ce649f Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 13 Apr 2023 11:42:57 +0200 Subject: [PATCH 22/82] Trying for UNIX based systems to solve the issue with Kerberos by installing their missing system headers --- .github/workflows/continuous-integration.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index e6ed0825..7df6e0e8 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -26,9 +26,11 @@ jobs: - name: Installing vendored OpenSSL run: cargo install cargo-generate --features vendored-openssl - - name: Make the USER own the working directory + - name: Make the USER own the working directory. Installing `gssapi` headers if: ${{ matrix.os == 'ubuntu-latest' }} - run: sudo chown -R $USER:$USER ${{ github.workspace }} + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} + sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit - uses: actions/checkout@v3 From d353508c99bd36fb16113d5933e26271f9312ad8 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 13 Apr 2023 12:12:27 +0200 Subject: [PATCH 23/82] Getting rid out of the installation of the openssl vendored. Propagating the installation of the gssapi headers to the others UNIX based actions --- .github/workflows/code-coverage.yml | 6 ++++++ .github/workflows/code-quality.yml | 5 +++++ .github/workflows/continuous-integration.yml | 3 --- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 43fce450..144aa42e 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -25,6 +25,12 @@ jobs: run: | rustup toolchain install nightly rustup override set nightly + + - name: Make the USER own the working directory. Installing `gssapi` headers + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} + sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit - name: Caching cargo dependencies id: project-cache diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 0ab43d54..d9c60256 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -15,6 +15,11 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Make the USER own the working directory. Installing `gssapi` headers + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} + sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit + - name: Caching project dependencies id: project-cache uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 7df6e0e8..ac55011a 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -23,9 +23,6 @@ jobs: - { rust: stable, os: windows-latest } steps: - - name: Installing vendored OpenSSL - run: cargo install cargo-generate --features vendored-openssl - - name: Make the USER own the working directory. Installing `gssapi` headers if: ${{ matrix.os == 'ubuntu-latest' }} run: | From fd8d20c2d9ca9db47dccbae851371ab9c27aba21 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 13 Apr 2023 13:40:28 +0200 Subject: [PATCH 24/82] Adding the gssapi headers to the other steps. Bumped the syn deps --- .github/workflows/code-quality.yml | 10 ++++++++++ canyon_macros/Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index d9c60256..c72c0e5b 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -34,6 +34,11 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Make the USER own the working directory. Installing `gssapi` headers + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} + sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit + - name: Caching project dependencies id: project-cache uses: Swatinem/rust-cache@v2 @@ -54,6 +59,11 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Make the USER own the working directory. Installing `gssapi` headers + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} + sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit + - name: Caching project dependencies id: project-cache uses: Swatinem/rust-cache@v2 diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index 11e0c341..58b97550 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -12,7 +12,7 @@ description = "A Rust ORM and QueryBuilder" proc-macro = true [dependencies] -syn = { version = "1.0.86", features = ["full"] } +syn = { version = "1.0.109", features = ["full"] } quote = "1.0.9" proc-macro2 = "1.0.27" futures = "0.3.21" From 9d4692185482f2a8a356cf6e634dee6a4f014925 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 13 Apr 2023 13:56:15 +0200 Subject: [PATCH 25/82] Disabling a doc tests in the canyon-macro module that was provoking linker issues under msvc envs --- canyon_macros/src/utils/helpers.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/canyon_macros/src/utils/helpers.rs b/canyon_macros/src/utils/helpers.rs index 32e8fef3..81ac7dcd 100644 --- a/canyon_macros/src/utils/helpers.rs +++ b/canyon_macros/src/utils/helpers.rs @@ -103,6 +103,7 @@ pub fn _database_table_name_from_struct(ty: &Ident) -> String { /// Parses a syn::Identifier to create a defaulted snake case database table name #[test] +#[cfg(not(target_env = "msvc"))] fn test_entity_database_name_defaulter() { assert_eq!( default_database_table_name_from_entity_name("League"), From 8c37ef2c426830ed65501c35687e873103df7a04 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 13 Apr 2023 15:42:22 +0200 Subject: [PATCH 26/82] Correct typo in the CI action --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ac55011a..53f77132 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -53,7 +53,7 @@ jobs: - name: Run only UNIT tests for Windows if: ${{ matrix.os == 'windows-latest' }} - run: cargo test --verbose --workspace --target=x86_64-pc-windows-msvc --exclude tests --all-features --no-fail-fast -- --show-output- + run: cargo test --verbose --workspace --target=x86_64-pc-windows-msvc --exclude tests --all-features --no-fail-fast -- --show-output - name: Run only UNIT tests for MacOS if: ${{ matrix.os == 'MacOS-latest' }} From 61a854ab0f2d7196ec48326874358f61773c06ba Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 13 Apr 2023 15:53:36 +0200 Subject: [PATCH 27/82] v0.2.0 --- canyon_connection/Cargo.toml | 2 +- canyon_crud/Cargo.toml | 4 ++-- canyon_macros/Cargo.toml | 8 ++++---- canyon_observer/Cargo.toml | 6 +++--- canyon_sql/Cargo.toml | 10 +++++----- tests/Cargo.toml | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 5af86ca7..2db6bd2c 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_connection" -version = "0.1.2" +version = "0.2.0" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index 6b25867d..4c30408f 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_crud" -version = "0.1.2" +version = "0.2.0" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -12,4 +12,4 @@ description = "A Rust ORM and QueryBuilder" chrono = { version = "0.4", features = ["serde"] } async-trait = { version = "0.1.50" } -canyon_connection = { version = "0.1.2", path = "../canyon_connection" } +canyon_connection = { version = "0.2.0", path = "../canyon_connection" } diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index 58b97550..93695087 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_macros" -version = "0.1.2" +version = "0.2.0" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -18,6 +18,6 @@ proc-macro2 = "1.0.27" futures = "0.3.21" tokio = { version = "1.9.0", features = ["full"] } -canyon_observer = { version = "0.1.2", path = "../canyon_observer" } -canyon_crud = { version = "0.1.2", path = "../canyon_crud" } -canyon_connection = { version = "0.1.2", path = "../canyon_connection" } +canyon_observer = { version = "0.2.0", path = "../canyon_observer" } +canyon_crud = { version = "0.2.0", path = "../canyon_crud" } +canyon_connection = { version = "0.2.0", path = "../canyon_connection" } diff --git a/canyon_observer/Cargo.toml b/canyon_observer/Cargo.toml index d6424714..cb4bd353 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_observer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_observer" -version = "0.1.2" +version = "0.2.0" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -23,5 +23,5 @@ quote = "1.0.9" partialdebug = "0.2.0" # Internal dependencies -canyon_crud = { version = "0.1.2", path = "../canyon_crud" } -canyon_connection = { version = "0.1.2", path = "../canyon_connection" } +canyon_crud = { version = "0.2.0", path = "../canyon_crud" } +canyon_connection = { version = "0.2.0", path = "../canyon_connection" } diff --git a/canyon_sql/Cargo.toml b/canyon_sql/Cargo.toml index e2ae054f..0a13a101 100755 --- a/canyon_sql/Cargo.toml +++ b/canyon_sql/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_sql" -version = "0.1.2" +version = "0.2.0" edition = "2021" authors = ["Alex Vergara, Gonzalo Busto"] documentation = "https://zerodaycode.github.io/canyon-book/" @@ -13,7 +13,7 @@ description = "A Rust ORM and QueryBuilder" async-trait = { version = "0.1.50" } # Project crates -canyon_macros = { version = "0.1.2", path = "../canyon_macros" } -canyon_observer = { version = "0.1.2", path = "../canyon_observer" } -canyon_crud = { version = "0.1.2", path = "../canyon_crud" } -canyon_connection = { version = "0.1.2", path = "../canyon_connection" } +canyon_macros = { version = "0.2.0", path = "../canyon_macros" } +canyon_observer = { version = "0.2.0", path = "../canyon_observer" } +canyon_crud = { version = "0.2.0", path = "../canyon_crud" } +canyon_connection = { version = "0.2.0", path = "../canyon_connection" } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index bdb58930..f2e83953 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tests" -version = "0.1.2" +version = "0.2.0" edition = "2021" publish = false From 73db1366c0671fa9efe7c84033076895e41510c4 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 13 Apr 2023 16:17:01 +0200 Subject: [PATCH 28/82] #feature - #[cfg(feature = "mssql-integrated-auth")] --- canyon_connection/Cargo.toml | 3 +++ canyon_connection/src/canyon_database_connector.rs | 1 + canyon_connection/src/datasources.rs | 1 + 3 files changed, 5 insertions(+) diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 2db6bd2c..99058cf2 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -24,3 +24,6 @@ lazy_static = "1.4.0" serde = { version = "1.0.138", features = ["derive"] } toml = "0.7.3" +[features] +mssql-integrated-auth = [] + diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index e4d6cde3..71fd767e 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -95,6 +95,7 @@ impl DatabaseConnection { crate::datasources::SqlServerAuth::Basic { username, password } => { AuthMethod::sql_server(username, password) } + #[cfg(feature = "mssql-integrated-auth")] crate::datasources::SqlServerAuth::Integrated => AuthMethod::Integrated, }, }); diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 6f0d0c6e..81c4e611 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -95,6 +95,7 @@ pub enum PostgresAuth { pub enum SqlServerAuth { #[serde(alias = "Basic", alias = "basic")] Basic { username: String, password: String }, + #[cfg(feature = "mssql-integrated-auth")] #[serde(alias = "Integrated", alias = "integrated")] Integrated, } From ec474101f623eb1c71312c8a23c81f5ce669d089 Mon Sep 17 00:00:00 2001 From: Alex Vergara <68871459+Pyzyryab@users.noreply.github.com> Date: Thu, 13 Apr 2023 16:29:12 +0200 Subject: [PATCH 29/82] Reworking the Canyon Connector (#38) * #upgrade The Canyon database connector has been reworked, simpliying the process. The monster trasmute for get a mutable reference to a connect due to the Tiberius query method has been eliminated * db_type property of the properties of the datasource moved to the root of the configuration for a datasource * Bumped the toml dependency to 0.7.3. This forced us to change from borrowed to owned types in the structs that holds the user configuration * #feature - reworked the authentication properties for the configuration file, by giving them a unique spot * #feature - Auth property is mandatory for every datasource, looking forward to get rid out of the db_type property, since it will be inferred from the auth declaration * #feature - Removed the db_type field from the options of the configuration file. The in-use per datasource database type will be inferred from the auth key * #feature - Added a test for the `SqlServerAuth` integrated auth option * Adding the required features for working with the tiberius integrated authentication system * Separating with cfg features the conditional different libraries by target to enable integrated auth for tiberius (MSSQL) * Being more specific with targets for the CI process * Upgrading the Rust version for the VMs * Installing vendored OpenSSL * Trying for UNIX based systems to solve the issue with Kerberos by installing their missing system headers * Getting rid out of the installation of the openssl vendored. Propagating the installation of the gssapi headers to the others UNIX based actions * Adding the gssapi headers to the other steps. Bumped the syn deps * Disabling a doc tests in the canyon-macro module that was provoking linker issues under msvc envs * Correct typo in the CI action * v0.2.0 * #feature - #[cfg(feature = "mssql-integrated-auth")] --- .github/workflows/code-coverage.yml | 6 + .github/workflows/code-quality.yml | 19 ++- .github/workflows/continuous-integration.yml | 16 ++- bash_aliases.sh | 2 +- canyon_connection/Cargo.toml | 10 +- .../src/canyon_database_connector.rs | 114 +++++++++++------- canyon_connection/src/datasources.rs | 101 +++++++++++----- canyon_connection/src/lib.rs | 30 +++-- canyon_crud/Cargo.toml | 4 +- canyon_crud/src/crud.rs | 46 +++---- canyon_macros/Cargo.toml | 10 +- canyon_macros/src/query_operations/select.rs | 2 +- canyon_macros/src/utils/helpers.rs | 1 + canyon_observer/Cargo.toml | 6 +- canyon_observer/src/lib.rs | 21 +++- canyon_observer/src/migrations/handler.rs | 2 +- canyon_observer/src/migrations/memory.rs | 16 +-- canyon_observer/src/migrations/processor.rs | 74 ++---------- canyon_sql/Cargo.toml | 10 +- tests/Cargo.toml | 2 +- tests/canyon.toml | 27 ++++- 21 files changed, 300 insertions(+), 219 deletions(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 43fce450..144aa42e 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -25,6 +25,12 @@ jobs: run: | rustup toolchain install nightly rustup override set nightly + + - name: Make the USER own the working directory. Installing `gssapi` headers + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} + sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit - name: Caching cargo dependencies id: project-cache diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index ff194b49..c72c0e5b 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -15,6 +15,11 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Make the USER own the working directory. Installing `gssapi` headers + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} + sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit + - name: Caching project dependencies id: project-cache uses: Swatinem/rust-cache@v2 @@ -22,13 +27,18 @@ jobs: - uses: hecrj/setup-rust-action@v1 with: components: clippy - - run: cargo clippy --workspace --all-targets --verbose --all-features -- -A clippy::question_mark + - run: cargo clippy --workspace --all-targets --verbose --all-features rustfmt: name: Verify code formatting runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Make the USER own the working directory. Installing `gssapi` headers + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} + sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit + - name: Caching project dependencies id: project-cache uses: Swatinem/rust-cache@v2 @@ -49,6 +59,11 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Make the USER own the working directory. Installing `gssapi` headers + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} + sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit + - name: Caching project dependencies id: project-cache uses: Swatinem/rust-cache@v2 @@ -57,4 +72,4 @@ jobs: with: rust-version: nightly - - run: cargo rustdoc -p ${{ matrix.crate }} --all-features -- -D warnings + - run: cargo rustdoc --target=x86_64-unknown-linux-gnu -p ${{ matrix.crate }} --all-features -- -D warnings diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1201895c..53f77132 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -23,9 +23,11 @@ jobs: - { rust: stable, os: windows-latest } steps: - - name: Make the USER own the working directory + - name: Make the USER own the working directory. Installing `gssapi` headers if: ${{ matrix.os == 'ubuntu-latest' }} - run: sudo chown -R $USER:$USER ${{ github.workspace }} + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} + sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit - uses: actions/checkout@v3 @@ -43,12 +45,16 @@ jobs: - name: Load data for MSSQL tests if: ${{ matrix.os == 'ubuntu-latest' }} - run: cargo test initialize_sql_server_docker_instance -p tests --all-features --no-fail-fast -- --show-output --nocapture --include-ignored + run: cargo test initialize_sql_server_docker_instance -p tests --target=x86_64-unknown-linux-gnu --all-features --no-fail-fast -- --show-output --nocapture --include-ignored - name: Run all tests, UNIT and INTEGRATION for Linux targets if: ${{ matrix.os == 'ubuntu-latest' }} run: cargo test --verbose --workspace --all-features --no-fail-fast -- --show-output --test-threads=1 - - name: Run only UNIT tests for the rest of the defined targets - if: ${{ matrix.os != 'ubuntu-latest' }} + - name: Run only UNIT tests for Windows + if: ${{ matrix.os == 'windows-latest' }} + run: cargo test --verbose --workspace --target=x86_64-pc-windows-msvc --exclude tests --all-features --no-fail-fast -- --show-output + + - name: Run only UNIT tests for MacOS + if: ${{ matrix.os == 'MacOS-latest' }} run: cargo test --verbose --workspace --exclude tests --all-features --no-fail-fast -- --show-output diff --git a/bash_aliases.sh b/bash_aliases.sh index 0466aac8..a67da429 100644 --- a/bash_aliases.sh +++ b/bash_aliases.sh @@ -4,7 +4,7 @@ # This alias avoid the usage of a bunch of commands for performn an integrated task that # depends on several concatenated commands. -# In order to run the script, simply type `$ . ./alias.sh` from the root of the project. +# In order to run the script, simply type `$ . ./bash_aliases.sh` from the root of the project. # (refreshing the current terminal session could be required) # Executes the docker compose script to wake up the postgres container diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 63c0a869..99058cf2 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_connection" -version = "0.1.2" +version = "0.2.0" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -16,10 +16,14 @@ tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"] } futures = "0.3.25" indexmap = "1.9.1" -tiberius = { version = "0.11.3", features = ["tds73", "chrono"] } +tiberius = { version = "0.12.1", features = ["tds73", "chrono", "integrated-auth-gssapi"] } async-std = { version = "1.12.0" } lazy_static = "1.4.0" serde = { version = "1.0.138", features = ["derive"] } -toml = "0.5.9" \ No newline at end of file +toml = "0.7.3" + +[features] +mssql-integrated-auth = [] + diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 7da2c2ed..71fd767e 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -4,7 +4,7 @@ use serde::Deserialize; use tiberius::{AuthMethod, Config}; use tokio_postgres::{Client, NoTls}; -use crate::datasources::DatasourceProperties; +use crate::datasources::DatasourceConfig; /// Represents the current supported databases by Canyon #[derive(Deserialize, Debug, Eq, PartialEq, Clone, Copy, Default)] @@ -31,10 +31,9 @@ pub struct SqlServerConnection { /// starts, Canyon gets the information about the desired datasources, /// process them and generates a pool of 1 to 1 database connection for /// every datasource defined. -pub struct DatabaseConnection { - pub postgres_connection: Option, - pub sqlserver_connection: Option, - pub database_type: DatabaseType, +pub enum DatabaseConnection { + Postgres(PostgreSqlConnection), + SqlServer(SqlServerConnection), } unsafe impl Send for DatabaseConnection {} @@ -42,18 +41,28 @@ unsafe impl Sync for DatabaseConnection {} impl DatabaseConnection { pub async fn new( - datasource: &DatasourceProperties<'_>, + datasource: &DatasourceConfig, ) -> Result> { - match datasource.db_type { + match datasource.get_db_type() { DatabaseType::PostgreSql => { + let (username, password) = match &datasource.auth { + crate::datasources::Auth::Postgres(postgres_auth) => match postgres_auth { + crate::datasources::PostgresAuth::Basic { username, password } => { + (username.as_str(), password.as_str()) + } + }, + crate::datasources::Auth::SqlServer(_) => { + panic!("Found SqlServer auth configuration for a PostgreSQL datasource") + } + }; let (new_client, new_connection) = tokio_postgres::connect( &format!( "postgres://{user}:{pswd}@{host}:{port}/{db}", - user = datasource.username, - pswd = datasource.password, - host = datasource.host, - port = datasource.port.unwrap_or_default(), - db = datasource.db_name + user = username, + pswd = password, + host = datasource.properties.host, + port = datasource.properties.port.unwrap_or_default(), + db = datasource.properties.db_name )[..], NoTls, ) @@ -65,27 +74,31 @@ impl DatabaseConnection { } }); - Ok(Self { - postgres_connection: Some(PostgreSqlConnection { - client: new_client, - // connection: new_connection, - }), - sqlserver_connection: None, - database_type: DatabaseType::PostgreSql, - }) + Ok(DatabaseConnection::Postgres(PostgreSqlConnection { + client: new_client, + // connection: new_connection, + })) } DatabaseType::SqlServer => { let mut config = Config::new(); - config.host(datasource.host); - config.port(datasource.port.unwrap_or_default()); - config.database(datasource.db_name); + config.host(&datasource.properties.host); + config.port(datasource.properties.port.unwrap_or_default()); + config.database(&datasource.properties.db_name); // Using SQL Server authentication. - config.authentication(AuthMethod::sql_server( - datasource.username, - datasource.password, - )); + config.authentication(match &datasource.auth { + crate::datasources::Auth::Postgres(_) => { + panic!("Found PostgreSQL auth configuration for a SqlServer database") + } + crate::datasources::Auth::SqlServer(sql_server_auth) => match sql_server_auth { + crate::datasources::SqlServerAuth::Basic { username, password } => { + AuthMethod::sql_server(username, password) + } + #[cfg(feature = "mssql-integrated-auth")] + crate::datasources::SqlServerAuth::Integrated => AuthMethod::Integrated, + }, + }); // on production, it is not a good idea to do this. We should upgrade // Canyon in future versions to allow the user take care about this @@ -106,18 +119,30 @@ impl DatabaseConnection { // Handling TLS, login and other details related to the SQL Server. let client = tiberius::Client::connect(config, tcp).await; - Ok(Self { - postgres_connection: None, - sqlserver_connection: Some(SqlServerConnection { - client: Box::leak(Box::new( - client.expect("A failure happened connecting to the database"), - )), - }), - database_type: DatabaseType::SqlServer, - }) + Ok(DatabaseConnection::SqlServer(SqlServerConnection { + client: Box::leak(Box::new( + client.expect("A failure happened connecting to the database"), + )), + })) } } } + + pub fn postgres_connection(&self) -> Option<&PostgreSqlConnection> { + if let DatabaseConnection::Postgres(conn) = self { + Some(conn) + } else { + None + } + } + + pub fn sqlserver_connection(&mut self) -> Option<&mut SqlServerConnection> { + if let DatabaseConnection::SqlServer(conn) = self { + Some(conn) + } else { + None + } + } } #[cfg(test)] @@ -128,8 +153,8 @@ mod database_connection_handler { const CONFIG_FILE_MOCK_ALT: &str = r#" [canyon_sql] datasources = [ - {name = 'PostgresDS', properties.db_type = 'postgresql', properties.username = 'username', properties.password = 'random_pass', properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled'}, - {name = 'SqlServerDS', properties.db_type = 'sqlserver', properties.username = 'username2', properties.password = 'random_pass2', properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled'} + {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, + {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } ] "#; @@ -139,10 +164,13 @@ mod database_connection_handler { let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) .expect("A failure happened retrieving the [canyon_sql] section"); - let psql_ds = &config.canyon_sql.datasources[0].properties; - let sqls_ds = &config.canyon_sql.datasources[1].properties; - - assert_eq!(psql_ds.db_type, DatabaseType::PostgreSql); - assert_eq!(sqls_ds.db_type, DatabaseType::SqlServer); + assert_eq!( + config.canyon_sql.datasources[0].get_db_type(), + DatabaseType::PostgreSql + ); + assert_eq!( + config.canyon_sql.datasources[1].get_db_type(), + DatabaseType::SqlServer + ); } } diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 7c87583d..81c4e611 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -6,11 +6,12 @@ use crate::canyon_database_connector::DatabaseType; #[test] fn load_ds_config_from_array() { const CONFIG_FILE_MOCK_ALT: &str = r#" - [canyon_sql] - datasources = [ - {name = 'PostgresDS', properties.db_type = 'postgresql', properties.username = 'username', properties.password = 'random_pass', properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations = 'enabled'}, - {name = 'SqlServerDS', properties.db_type = 'sqlserver', properties.username = 'username2', properties.password = 'random_pass2', properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2'} - ] + [canyon_sql] + datasources = [ + {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, + {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' }, + {name = 'SqlServerDS', auth = { sqlserver = { integrated = {} } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + ] "#; let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) @@ -18,52 +19,92 @@ fn load_ds_config_from_array() { let ds_0 = &config.canyon_sql.datasources[0]; let ds_1 = &config.canyon_sql.datasources[1]; + let ds_2 = &config.canyon_sql.datasources[2]; assert_eq!(ds_0.name, "PostgresDS"); - assert_eq!(ds_0.properties.db_type, DatabaseType::PostgreSql); - assert_eq!(ds_0.properties.username, "username"); - assert_eq!(ds_0.properties.password, "random_pass"); + assert_eq!(ds_0.get_db_type(), DatabaseType::PostgreSql); + assert_eq!( + ds_0.auth, + Auth::Postgres(PostgresAuth::Basic { + username: "postgres".to_string(), + password: "postgres".to_string() + }) + ); assert_eq!(ds_0.properties.host, "localhost"); assert_eq!(ds_0.properties.port, None); assert_eq!(ds_0.properties.db_name, "triforce"); assert_eq!(ds_0.properties.migrations, Some(Migrations::Enabled)); assert_eq!(ds_1.name, "SqlServerDS"); - assert_eq!(ds_1.properties.db_type, DatabaseType::SqlServer); - assert_eq!(ds_1.properties.username, "username2"); - assert_eq!(ds_1.properties.password, "random_pass2"); + assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); + assert_eq!( + ds_1.auth, + Auth::SqlServer(SqlServerAuth::Basic { + username: "sa".to_string(), + password: "SqlServer-10".to_string() + }) + ); assert_eq!(ds_1.properties.host, "192.168.0.250.1"); assert_eq!(ds_1.properties.port, Some(3340)); assert_eq!(ds_1.properties.db_name, "triforce2"); - assert_eq!(ds_1.properties.migrations, None); + assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); + + assert_eq!(ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) } /// #[derive(Deserialize, Debug, Clone)] -pub struct CanyonSqlConfig<'a> { - #[serde(borrow)] - pub canyon_sql: Datasources<'a>, +pub struct CanyonSqlConfig { + pub canyon_sql: Datasources, +} +#[derive(Deserialize, Debug, Clone)] +pub struct Datasources { + pub datasources: Vec, } + #[derive(Deserialize, Debug, Clone)] -pub struct Datasources<'a> { - #[serde(borrow)] - pub datasources: Vec>, +pub struct DatasourceConfig { + pub name: String, + pub auth: Auth, + pub properties: DatasourceProperties, +} + +impl DatasourceConfig { + pub fn get_db_type(&self) -> DatabaseType { + match self.auth { + Auth::Postgres(_) => DatabaseType::PostgreSql, + Auth::SqlServer(_) => DatabaseType::SqlServer, + } + } } -#[derive(Deserialize, Debug, Clone, Copy)] -pub struct DatasourceConfig<'a> { - #[serde(borrow)] - pub name: &'a str, - pub properties: DatasourceProperties<'a>, +#[derive(Deserialize, Debug, Clone, PartialEq)] +pub enum Auth { + #[serde(alias = "PostgreSQL", alias = "postgresql")] + Postgres(PostgresAuth), + #[serde(alias = "SqlServer", alias = "sqlserver", alias = "mssql")] + SqlServer(SqlServerAuth), } -#[derive(Deserialize, Debug, Clone, Copy)] -pub struct DatasourceProperties<'a> { - pub db_type: DatabaseType, - pub username: &'a str, - pub password: &'a str, - pub host: &'a str, +#[derive(Deserialize, Debug, Clone, PartialEq)] +pub enum PostgresAuth { + #[serde(alias = "Basic", alias = "basic")] + Basic { username: String, password: String }, +} + +#[derive(Deserialize, Debug, Clone, PartialEq)] +pub enum SqlServerAuth { + #[serde(alias = "Basic", alias = "basic")] + Basic { username: String, password: String }, + #[cfg(feature = "mssql-integrated-auth")] + #[serde(alias = "Integrated", alias = "integrated")] + Integrated, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct DatasourceProperties { + pub host: String, pub port: Option, - pub db_name: &'a str, + pub db_name: String, pub migrations: Option, } diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index 9a4ebe90..1a8f7cab 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -26,13 +26,13 @@ lazy_static! { static ref RAW_CONFIG_FILE: String = fs::read_to_string(CONFIG_FILE_IDENTIFIER) .expect("Error opening or reading the Canyon configuration file"); - static ref CONFIG_FILE: CanyonSqlConfig<'static> = toml::from_str(RAW_CONFIG_FILE.as_str()) + static ref CONFIG_FILE: CanyonSqlConfig = toml::from_str(RAW_CONFIG_FILE.as_str()) .expect("Error generating the configuration for Canyon-SQL"); - pub static ref DATASOURCES: Vec> = + pub static ref DATASOURCES: Vec = CONFIG_FILE.canyon_sql.datasources.clone(); - pub static ref CACHED_DATABASE_CONN: Mutex> = + pub static ref CACHED_DATABASE_CONN: Mutex> = Mutex::new(IndexMap::new()); } @@ -40,26 +40,24 @@ lazy_static! { /// in the configuration file. /// /// This avoids Canyon to create a new connection to the database on every query, potentially avoiding bottlenecks -/// derivated from the instantiation of that new conn every time. +/// coming from the instantiation of that new conn every time. /// /// Note: We noticed with the integration tests that the [`tokio_postgres`] crate (PostgreSQL) is able to work in an async environment -/// with a new connection per query without no problem, but the [`tiberius`] crate (MSSQL) sufferes a lot when it has continuous +/// with a new connection per query without no problem, but the [`tiberius`] crate (MSSQL) suffers a lot when it has continuous /// statements with multiple queries, like and insert followed by a find by id to check if the insert query has done its /// job done. pub async fn init_connections_cache() { for datasource in DATASOURCES.iter() { CACHED_DATABASE_CONN.lock().await.insert( - datasource.name, - Box::leak(Box::new( - DatabaseConnection::new(&datasource.properties) - .await - .unwrap_or_else(|_| { - panic!( - "Error pooling a new connection for the datasource: {:?}", - datasource.name - ) - }), - )), + &datasource.name, + DatabaseConnection::new(datasource) + .await + .unwrap_or_else(|_| { + panic!( + "Error pooling a new connection for the datasource: {:?}", + datasource.name + ) + }), ); } } diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index 6b25867d..4c30408f 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_crud" -version = "0.1.2" +version = "0.2.0" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -12,4 +12,4 @@ description = "A Rust ORM and QueryBuilder" chrono = { version = "0.4", features = ["serde"] } async-trait = { version = "0.1.50" } -canyon_connection = { version = "0.1.2", path = "../canyon_connection" } +canyon_connection = { version = "0.2.0", path = "../canyon_connection" } diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index 134c41cc..8f587a02 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -1,8 +1,8 @@ use std::fmt::Display; use async_trait::async_trait; -use canyon_connection::canyon_database_connector::DatabaseType; -use canyon_connection::CACHED_DATABASE_CONN; +use canyon_connection::canyon_database_connector::DatabaseConnection; +use canyon_connection::{CACHED_DATABASE_CONN, DATASOURCES}; use crate::bounds::QueryParameter; use crate::mapper::RowMapper; @@ -18,7 +18,6 @@ use crate::result::DatabaseResult; /// the result of the query and, if the user desires, /// automatically map it to an struct. #[async_trait] -#[allow(clippy::question_mark)] pub trait Transaction { /// Performs a query against the targeted database by the selected datasource. /// @@ -32,22 +31,26 @@ pub trait Transaction { S: AsRef + Display + Sync + Send + 'a, Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, { - let guarded_cache = CACHED_DATABASE_CONN.lock().await; + let mut guarded_cache = CACHED_DATABASE_CONN.lock().await; let database_conn = if datasource_name.is_empty() { guarded_cache - .values() - .next() - .expect("No default datasource found. Check your `canyon.toml` file") + .get_mut( + DATASOURCES + .get(0) + .expect("We didn't found any valid datasource configuration. Check your `canyon.toml` file") + .name + .as_str() + ).unwrap_or_else(|| panic!("No default datasource found. Check your `canyon.toml` file")) } else { - guarded_cache.get(datasource_name) + guarded_cache.get_mut(datasource_name) .unwrap_or_else(|| panic!("Canyon couldn't find a datasource in the pool with the argument provided: {datasource_name}" )) }; - match database_conn.database_type { - DatabaseType::PostgreSql => { + match database_conn { + DatabaseConnection::Postgres(_) => { postgres_query_launcher::launch::( database_conn, stmt.to_string(), @@ -55,7 +58,7 @@ pub trait Transaction { ) .await } - DatabaseType::SqlServer => { + DatabaseConnection::SqlServer(_) => { sqlserver_query_launcher::launch::( database_conn, &mut stmt.to_string(), @@ -174,8 +177,7 @@ mod postgres_query_launcher { Ok(DatabaseResult::new_postgresql( db_conn - .postgres_connection - .as_ref() + .postgres_connection() .unwrap() .client .query(&stmt, m_params.as_slice()) @@ -185,8 +187,6 @@ mod postgres_query_launcher { } mod sqlserver_query_launcher { - use std::mem::transmute; - use canyon_connection::tiberius::Row; use crate::{ @@ -196,7 +196,7 @@ mod sqlserver_query_launcher { }; pub async fn launch<'a, T, Z>( - db_conn: &&mut DatabaseConnection, + db_conn: &mut DatabaseConnection, stmt: &mut String, params: Z, ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> @@ -206,12 +206,8 @@ mod sqlserver_query_launcher { // Re-generate de insert statement to adequate it to the SQL SERVER syntax to retrieve the PK value(s) after insert if stmt.contains("RETURNING") { let c = stmt.clone(); - let temp = c - .split_once("RETURNING") - .expect("An error happened generating an INSERT statement for a SQL SERVER client"); - let temp2 = temp.0.split_once("VALUES").expect( - "An error happened generating an INSERT statement for a SQL SERVER client [1]", - ); + let temp = c.split_once("RETURNING").unwrap(); + let temp2 = temp.0.split_once("VALUES").unwrap(); *stmt = format!( "{} OUTPUT inserted.{} VALUES {}", @@ -227,12 +223,10 @@ mod sqlserver_query_launcher { .iter() .for_each(|param| mssql_query.bind(*param)); - #[allow(mutable_transmutes)] let _results: Vec = mssql_query .query( - unsafe { transmute::<&DatabaseConnection, &mut DatabaseConnection>(db_conn) } - .sqlserver_connection - .as_mut() + db_conn + .sqlserver_connection() .expect("Error querying the MSSQL database") .client, ) diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index 11e0c341..93695087 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_macros" -version = "0.1.2" +version = "0.2.0" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -12,12 +12,12 @@ description = "A Rust ORM and QueryBuilder" proc-macro = true [dependencies] -syn = { version = "1.0.86", features = ["full"] } +syn = { version = "1.0.109", features = ["full"] } quote = "1.0.9" proc-macro2 = "1.0.27" futures = "0.3.21" tokio = { version = "1.9.0", features = ["full"] } -canyon_observer = { version = "0.1.2", path = "../canyon_observer" } -canyon_crud = { version = "0.1.2", path = "../canyon_crud" } -canyon_connection = { version = "0.1.2", path = "../canyon_connection" } +canyon_observer = { version = "0.2.0", path = "../canyon_observer" } +canyon_crud = { version = "0.2.0", path = "../canyon_crud" } +canyon_connection = { version = "0.2.0", path = "../canyon_connection" } diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index 8c616034..761451c1 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -30,7 +30,7 @@ pub fn generate_find_all_unchecked_tokens( .get_entities::<#ty>() } - /// Performns a `SELECT * FROM table_name`, where `table_name` it's + /// Performs a `SELECT * FROM table_name`, where `table_name` it's /// the name of your entity but converted to the corresponding /// database convention. P.ej. PostgreSQL prefers table names declared /// with snake_case identifiers. diff --git a/canyon_macros/src/utils/helpers.rs b/canyon_macros/src/utils/helpers.rs index 32e8fef3..81ac7dcd 100644 --- a/canyon_macros/src/utils/helpers.rs +++ b/canyon_macros/src/utils/helpers.rs @@ -103,6 +103,7 @@ pub fn _database_table_name_from_struct(ty: &Ident) -> String { /// Parses a syn::Identifier to create a defaulted snake case database table name #[test] +#[cfg(not(target_env = "msvc"))] fn test_entity_database_name_defaulter() { assert_eq!( default_database_table_name_from_entity_name("League"), diff --git a/canyon_observer/Cargo.toml b/canyon_observer/Cargo.toml index d6424714..cb4bd353 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_observer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_observer" -version = "0.1.2" +version = "0.2.0" edition = "2021" documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" @@ -23,5 +23,5 @@ quote = "1.0.9" partialdebug = "0.2.0" # Internal dependencies -canyon_crud = { version = "0.1.2", path = "../canyon_crud" } -canyon_connection = { version = "0.1.2", path = "../canyon_connection" } +canyon_crud = { version = "0.2.0", path = "../canyon_crud" } +canyon_connection = { version = "0.2.0", path = "../canyon_connection" } diff --git a/canyon_observer/src/lib.rs b/canyon_observer/src/lib.rs index 3c9b9fa7..1a0766e5 100644 --- a/canyon_observer/src/lib.rs +++ b/canyon_observer/src/lib.rs @@ -23,8 +23,25 @@ use std::{collections::HashMap, sync::Mutex}; pub static CANYON_REGISTER_ENTITIES: Mutex>> = Mutex::new(Vec::new()); lazy_static! { - pub static ref QUERIES_TO_EXECUTE: Mutex>> = + pub static ref QUERIES_TO_EXECUTE: Mutex>> = Mutex::new(HashMap::new()); - pub static ref CM_QUERIES_TO_EXECUTE: Mutex>> = + pub static ref CM_QUERIES_TO_EXECUTE: Mutex>> = Mutex::new(HashMap::new()); } + +/// Stores a newly generated SQL statement from the migrations into the register +pub fn save_migrations_query_to_execute(stmt: String, ds_name: &str) { + if QUERIES_TO_EXECUTE.lock().unwrap().contains_key(ds_name) { + QUERIES_TO_EXECUTE + .lock() + .unwrap() + .get_mut(ds_name) + .unwrap() + .push(stmt); + } else { + QUERIES_TO_EXECUTE + .lock() + .unwrap() + .insert(ds_name.to_owned(), vec![stmt]); + } +} diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index eae26ef8..d454128a 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -52,7 +52,7 @@ impl Migrations { // Tracked entities that must be migrated whenever Canyon starts let schema_status = - Self::fetch_database(datasource.name, datasource.properties.db_type).await; + Self::fetch_database(&datasource.name, datasource.get_db_type()).await; let database_tables_schema_info = Self::map_rows(schema_status); // We filter the tables from the schema that aren't Canyon entities diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index 79a590a7..0a4080c0 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -60,14 +60,14 @@ impl CanyonMemory { #[cfg(not(cargo_check))] #[allow(clippy::nonminimal_bool)] pub async fn remember( - datasource: &DatasourceConfig<'static>, + datasource: &DatasourceConfig, canyon_entities: &[CanyonRegisterEntity<'_>], ) -> Self { // Creates the memory table if not exists - Self::create_memory(datasource.name, &datasource.properties.db_type).await; + Self::create_memory(&datasource.name, &datasource.get_db_type()).await; // Retrieve the last status data from the `canyon_memory` table - let res = Self::query("SELECT * FROM canyon_memory", [], datasource.name) + let res = Self::query("SELECT * FROM canyon_memory", [], &datasource.name) .await .expect("Error querying Canyon Memory"); let mem_results = res.as_canyon_rows(); @@ -112,7 +112,7 @@ impl CanyonMemory { WHERE id = {}", _struct.filepath, _struct.struct_name, _struct.declared_table_name, old.id ); - save_canyon_memory_query(stmt, datasource.name); + save_canyon_memory_query(stmt, &datasource.name); // if the updated element is the struct name, we add it to the table_rename Hashmap let rename_table = old.declared_table_name != _struct.declared_table_name; @@ -132,7 +132,7 @@ impl CanyonMemory { VALUES ('{}', '{}', '{}')", _struct.filepath, _struct.struct_name, _struct.declared_table_name ); - save_canyon_memory_query(stmt, datasource.name) + save_canyon_memory_query(stmt, &datasource.name) } } @@ -149,7 +149,7 @@ impl CanyonMemory { "DELETE FROM canyon_memory WHERE struct_name = '{}'", db_row.struct_name ), - datasource.name, + &datasource.name, ); } } @@ -230,7 +230,7 @@ impl CanyonMemory { } } -fn save_canyon_memory_query(stmt: String, ds_name: &'static str) { +fn save_canyon_memory_query(stmt: String, ds_name: &str) { use crate::CM_QUERIES_TO_EXECUTE; if CM_QUERIES_TO_EXECUTE.lock().unwrap().contains_key(ds_name) { @@ -244,7 +244,7 @@ fn save_canyon_memory_query(stmt: String, ds_name: &'static str) { CM_QUERIES_TO_EXECUTE .lock() .unwrap() - .insert(ds_name, vec![stmt]); + .insert(ds_name.to_owned(), vec![stmt]); } } diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index 9a1e0294..c3995bbf 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -9,7 +9,7 @@ use std::ops::Not; use crate::canyon_crud::{crud::Transaction, DatasourceConfig}; use crate::constants::regex_patterns; -use crate::QUERIES_TO_EXECUTE; +use crate::save_migrations_query_to_execute; use super::information_schema::{ColumnMetadata, TableMetadata}; use super::memory::CanyonMemory; @@ -32,10 +32,10 @@ impl MigrationsProcessor { canyon_memory: CanyonMemory, canyon_entities: Vec>, database_tables: Vec<&'a TableMetadata>, - datasource: &'_ DatasourceConfig<'static>, + datasource: &'_ DatasourceConfig, ) { // The database type formally represented in Canyon - let db_type = datasource.properties.db_type; + let db_type = datasource.get_db_type(); // For each entity (table) on the register (Rust structs) for canyon_register_entity in canyon_entities { let entity_name = canyon_register_entity.entity_db_table_name; @@ -723,7 +723,7 @@ mod migrations_helper_tests { /// Trait that enables implementors to generate the migration queries #[async_trait] trait DatabaseOperation: Debug { - async fn generate_sql(&self, datasource: &DatasourceConfig<'static>); + async fn generate_sql(&self, datasource: &DatasourceConfig); } /// Helper to relate the operations that Canyon should do when it's managing a schema @@ -747,8 +747,8 @@ impl Transaction for TableOperation {} #[async_trait] impl DatabaseOperation for TableOperation { - async fn generate_sql(&self, datasource: &DatasourceConfig<'static>) { - let db_type = datasource.properties.db_type; + async fn generate_sql(&self, datasource: &DatasourceConfig) { + let db_type = datasource.get_db_type(); let stmt = match self { TableOperation::CreateTable(table_name, table_fields) => { @@ -862,23 +862,7 @@ impl DatabaseOperation for TableOperation { } }; - if QUERIES_TO_EXECUTE - .lock() - .unwrap() - .contains_key(datasource.name) - { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .get_mut(datasource.name) - .unwrap() - .push(stmt); - } else { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .insert(datasource.name, vec![stmt]); - } + save_migrations_query_to_execute(stmt, &datasource.name); } } @@ -903,8 +887,8 @@ impl Transaction for ColumnOperation {} #[async_trait] impl DatabaseOperation for ColumnOperation { - async fn generate_sql(&self, datasource: &DatasourceConfig<'static>) { - let db_type = datasource.properties.db_type; + async fn generate_sql(&self, datasource: &DatasourceConfig) { + let db_type = datasource.get_db_type(); let stmt = match self { ColumnOperation::CreateColumn(table_name, entity_field) => @@ -980,23 +964,7 @@ impl DatabaseOperation for ColumnOperation { ), }; - if QUERIES_TO_EXECUTE - .lock() - .unwrap() - .contains_key(datasource.name) - { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .get_mut(datasource.name) - .unwrap() - .push(stmt); - } else { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .insert(datasource.name, vec![stmt]); - } + save_migrations_query_to_execute(stmt, &datasource.name); } } @@ -1011,8 +979,8 @@ impl Transaction for SequenceOperation {} #[async_trait] impl DatabaseOperation for SequenceOperation { - async fn generate_sql(&self, datasource: &DatasourceConfig<'static>) { - let db_type = datasource.properties.db_type; + async fn generate_sql(&self, datasource: &DatasourceConfig) { + let db_type = datasource.get_db_type(); let stmt = match self { SequenceOperation::ModifySequence(table_name, entity_field) => { @@ -1029,22 +997,6 @@ impl DatabaseOperation for SequenceOperation { } }; - if QUERIES_TO_EXECUTE - .lock() - .unwrap() - .contains_key(datasource.name) - { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .get_mut(datasource.name) - .unwrap() - .push(stmt); - } else { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .insert(datasource.name, vec![stmt]); - } + save_migrations_query_to_execute(stmt, &datasource.name); } } diff --git a/canyon_sql/Cargo.toml b/canyon_sql/Cargo.toml index e2ae054f..0a13a101 100755 --- a/canyon_sql/Cargo.toml +++ b/canyon_sql/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "canyon_sql" -version = "0.1.2" +version = "0.2.0" edition = "2021" authors = ["Alex Vergara, Gonzalo Busto"] documentation = "https://zerodaycode.github.io/canyon-book/" @@ -13,7 +13,7 @@ description = "A Rust ORM and QueryBuilder" async-trait = { version = "0.1.50" } # Project crates -canyon_macros = { version = "0.1.2", path = "../canyon_macros" } -canyon_observer = { version = "0.1.2", path = "../canyon_observer" } -canyon_crud = { version = "0.1.2", path = "../canyon_crud" } -canyon_connection = { version = "0.1.2", path = "../canyon_connection" } +canyon_macros = { version = "0.2.0", path = "../canyon_macros" } +canyon_observer = { version = "0.2.0", path = "../canyon_observer" } +canyon_crud = { version = "0.2.0", path = "../canyon_crud" } +canyon_connection = { version = "0.2.0", path = "../canyon_connection" } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index bdb58930..f2e83953 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tests" -version = "0.1.2" +version = "0.2.0" edition = "2021" publish = false diff --git a/tests/canyon.toml b/tests/canyon.toml index 7bb56442..0b0614a4 100644 --- a/tests/canyon.toml +++ b/tests/canyon.toml @@ -1,5 +1,24 @@ [canyon_sql] -datasources = [ - {name = 'postgres_docker', properties.db_type = 'postgresql', properties.username = 'postgres', properties.password = 'postgres', properties.host = 'localhost', properties.port = 5438, properties.db_name = 'postgres'}, - {name = 'sqlserver_docker', properties.db_type = 'sqlserver', properties.username = 'sa', properties.password = 'SqlServer-10', properties.host = 'localhost', properties.port = 1434, properties.db_name = 'master'} -] \ No newline at end of file + +[[canyon_sql.datasources]] +name = 'postgres_docker' + +[canyon_sql.datasources.auth] +postgresql = { basic = { username = 'postgres', password = 'postgres'}} + +[canyon_sql.datasources.properties] +host = 'localhost' +port = 5438 +db_name = 'postgres' + + +[[canyon_sql.datasources]] +name = 'sqlserver_docker' + +[canyon_sql.datasources.auth] +sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } + +[canyon_sql.datasources.properties] +host = 'localhost' +port = 1434 +db_name = 'master' From 08a1c09e50375e5714a71daa57ccd346fe8e1055 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 13 Apr 2023 16:41:36 +0200 Subject: [PATCH 30/82] Added the gssapi headers to the Unix GH action for the release --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2efaf9ea..7c3342da 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,9 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 + - name: Installing `gssapi` headers + run: sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit + - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: From c8467efdccb0431b67bf5fd5100dc85934315e18 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 13 Apr 2023 17:10:18 +0200 Subject: [PATCH 31/82] Corrected the code coverage workflow. Updated the CHANGELOG.md --- .github/workflows/code-coverage.yml | 1 - CHANGELOG.md | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 144aa42e..cb3ecc98 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -27,7 +27,6 @@ jobs: rustup override set nightly - name: Make the USER own the working directory. Installing `gssapi` headers - if: ${{ matrix.os == 'ubuntu-latest' }} run: | sudo chown -R $USER:$USER ${{ github.workspace }} sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit diff --git a/CHANGELOG.md b/CHANGELOG.md index d4ef370f..d79ce967 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,22 @@ Year format is defined as: `YYYY-m-d` ## [Unreleased] -- Solved a bug in the canyon_entity proc macro that was wiring the incorrect user table name in the migrations +## [0.2.0] - 2023 - 04 - 13 + +### Feature [BREAKING CHANGES] + +- The configuration file has been reworked, by providing a whole category dedicated +to the authentication against the database server. +- We removed the database type property, since the database type can be inferred by +the new mandatory auth property +- Included support for the `MSSQL` integrated authentication via the cfg feature `mssql-integrated-auth` + +## [0.1.2] - 2023 - 03 - 28 + +### Update + +- Implemented bool types for QueryParameters<'_>. +- Minimal performance improvements ## [0.1.1] - 2023 - 03 - 20 From aee3e23f91cfbdeca199bdf06860d0a158f431f9 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Fri, 14 Apr 2023 10:45:25 +0200 Subject: [PATCH 32/82] #feature - Created two new cfg properties, for split the code per supported database. This means, that the user now will must attach specific features to the canyon-sql dependency, unless the default, which is PostgreSQL --- canyon_connection/Cargo.toml | 3 +++ .../src/canyon_database_connector.rs | 21 +++++++++++++------ canyon_connection/src/datasources.rs | 10 ++++++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 99058cf2..234ffee0 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -25,5 +25,8 @@ serde = { version = "1.0.138", features = ["derive"] } toml = "0.7.3" [features] +default = ["postgres"] +postgres = [] +mssql = [] mssql-integrated-auth = [] diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 71fd767e..563cfd5c 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -1,8 +1,8 @@ -use async_std::net::TcpStream; +#[cfg(feature = "mssql")] use async_std::net::TcpStream; use serde::Deserialize; -use tiberius::{AuthMethod, Config}; -use tokio_postgres::{Client, NoTls}; +#[cfg(feature = "mssql")] use tiberius::{AuthMethod, Config}; +#[cfg(feature = "postgres")] use tokio_postgres::{Client, NoTls}; use crate::datasources::DatasourceConfig; @@ -11,17 +11,21 @@ use crate::datasources::DatasourceConfig; pub enum DatabaseType { #[default] #[serde(alias = "postgres", alias = "postgresql")] + #[cfg(feature = "postgres")] PostgreSql, #[serde(alias = "sqlserver", alias = "mssql")] + #[cfg(feature = "mssql")] SqlServer, } /// A connection with a `PostgreSQL` database +#[cfg(feature = "postgres")] pub struct PostgreSqlConnection { pub client: Client, // pub connection: Connection, // TODO Hold it, or not to hold it... that's the question! } +#[cfg(feature = "mssql")] /// A connection with a `SqlServer` database pub struct SqlServerConnection { pub client: &'static mut tiberius::Client, @@ -32,8 +36,8 @@ pub struct SqlServerConnection { /// process them and generates a pool of 1 to 1 database connection for /// every datasource defined. pub enum DatabaseConnection { - Postgres(PostgreSqlConnection), - SqlServer(SqlServerConnection), + #[cfg(feature = "postgres")] Postgres(PostgreSqlConnection), + #[cfg(feature = "mssql")] SqlServer(SqlServerConnection), } unsafe impl Send for DatabaseConnection {} @@ -44,6 +48,7 @@ impl DatabaseConnection { datasource: &DatasourceConfig, ) -> Result> { match datasource.get_db_type() { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => { let (username, password) = match &datasource.auth { crate::datasources::Auth::Postgres(postgres_auth) => match postgres_auth { @@ -51,6 +56,7 @@ impl DatabaseConnection { (username.as_str(), password.as_str()) } }, + #[cfg(feature = "mssql")] crate::datasources::Auth::SqlServer(_) => { panic!("Found SqlServer auth configuration for a PostgreSQL datasource") } @@ -79,6 +85,7 @@ impl DatabaseConnection { // connection: new_connection, })) } + #[cfg(feature = "mssql")] DatabaseType::SqlServer => { let mut config = Config::new(); @@ -88,7 +95,7 @@ impl DatabaseConnection { // Using SQL Server authentication. config.authentication(match &datasource.auth { - crate::datasources::Auth::Postgres(_) => { + #[cfg(feature = "postgres")] crate::datasources::Auth::Postgres(_) => { panic!("Found PostgreSQL auth configuration for a SqlServer database") } crate::datasources::Auth::SqlServer(sql_server_auth) => match sql_server_auth { @@ -128,6 +135,7 @@ impl DatabaseConnection { } } + #[cfg(feature = "postgres")] pub fn postgres_connection(&self) -> Option<&PostgreSqlConnection> { if let DatabaseConnection::Postgres(conn) = self { Some(conn) @@ -136,6 +144,7 @@ impl DatabaseConnection { } } + #[cfg(feature = "mssql")] pub fn sqlserver_connection(&mut self) -> Option<&mut SqlServerConnection> { if let DatabaseConnection::SqlServer(conn) = self { Some(conn) diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 81c4e611..0486686a 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -49,7 +49,7 @@ fn load_ds_config_from_array() { assert_eq!(ds_1.properties.db_name, "triforce2"); assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); - assert_eq!(ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) + #[cfg(feature = "postgres")] assert_eq!(ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) } /// #[derive(Deserialize, Debug, Clone)] @@ -71,8 +71,8 @@ pub struct DatasourceConfig { impl DatasourceConfig { pub fn get_db_type(&self) -> DatabaseType { match self.auth { - Auth::Postgres(_) => DatabaseType::PostgreSql, - Auth::SqlServer(_) => DatabaseType::SqlServer, + #[cfg(feature = "postgres")] Auth::Postgres(_) => DatabaseType::PostgreSql, + #[cfg(feature = "mssql")] Auth::SqlServer(_) => DatabaseType::SqlServer, } } } @@ -80,18 +80,22 @@ impl DatasourceConfig { #[derive(Deserialize, Debug, Clone, PartialEq)] pub enum Auth { #[serde(alias = "PostgreSQL", alias = "postgresql")] + #[cfg(feature = "postgres")] Postgres(PostgresAuth), #[serde(alias = "SqlServer", alias = "sqlserver", alias = "mssql")] + #[cfg(feature = "mssql")] SqlServer(SqlServerAuth), } #[derive(Deserialize, Debug, Clone, PartialEq)] +#[cfg(feature = "postgres")] pub enum PostgresAuth { #[serde(alias = "Basic", alias = "basic")] Basic { username: String, password: String }, } #[derive(Deserialize, Debug, Clone, PartialEq)] +#[cfg(feature = "mssql")] pub enum SqlServerAuth { #[serde(alias = "Basic", alias = "basic")] Basic { username: String, password: String }, From 28129020193220944124c0ed5c23a49111c70099 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Fri, 14 Apr 2023 10:48:58 +0200 Subject: [PATCH 33/82] #fix - Small doc typos and reordering trait impl members --- canyon_connection/src/datasources.rs | 2 +- .../src/query_elements/query_builder.rs | 44 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 0486686a..409705dc 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -79,7 +79,7 @@ impl DatasourceConfig { #[derive(Deserialize, Debug, Clone, PartialEq)] pub enum Auth { - #[serde(alias = "PostgreSQL", alias = "postgresql")] + #[serde(alias = "PostgreSQL", alias = "postgresql", alias = "postgres")] #[cfg(feature = "postgres")] Postgres(PostgresAuth), #[serde(alias = "SqlServer", alias = "sqlserver", alias = "mssql")] diff --git a/canyon_crud/src/query_elements/query_builder.rs b/canyon_crud/src/query_elements/query_builder.rs index f0e68223..56ddda7c 100644 --- a/canyon_crud/src/query_elements/query_builder.rs +++ b/canyon_crud/src/query_elements/query_builder.rs @@ -324,7 +324,7 @@ where } /// Adds a *LEFT JOIN* SQL statement to the underlying - /// [`Query`] holded by the [`QueryBuilder`], where: + /// [`Query`] held by the [`QueryBuilder`], where: /// /// * `join_table` - The table target of the join operation /// * `col1` - The left side of the ON operator for the join @@ -340,7 +340,7 @@ where } /// Adds a *RIGHT JOIN* SQL statement to the underlying - /// [`Query`] holded by the [`QueryBuilder`], where: + /// [`Query`] held by the [`QueryBuilder`], where: /// /// * `join_table` - The table target of the join operation /// * `col1` - The left side of the ON operator for the join @@ -356,7 +356,7 @@ where } /// Adds a *RIGHT JOIN* SQL statement to the underlying - /// [`Query`] holded by the [`QueryBuilder`], where: + /// [`Query`] held by the [`QueryBuilder`], where: /// /// * `join_table` - The table target of the join operation /// * `col1` - The left side of the ON operator for the join @@ -372,7 +372,7 @@ where } /// Adds a *FULL JOIN* SQL statement to the underlying - /// [`Query`] holded by the [`QueryBuilder`], where: + /// [`Query`] held by the [`QueryBuilder`], where: /// /// * `join_table` - The table target of the join operation /// * `col1` - The left side of the ON operator for the join @@ -428,12 +428,6 @@ where self } - #[inline] - fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { - self._inner.or(column, op); - self - } - #[inline] fn or_values_in(&mut self, r#and: Z, values: &'a [Q]) -> &mut Self where @@ -444,6 +438,12 @@ where self } + #[inline] + fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { + self._inner.or(column, op); + self + } + #[inline] fn order_by>(&mut self, order_by: Z, desc: bool) -> &mut Self { self._inner.order_by(order_by, desc); @@ -565,12 +565,6 @@ where self } - #[inline] - fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { - self._inner.or(column, op); - self - } - #[inline] fn or_values_in(&mut self, r#or: Z, values: &'a [Q]) -> &mut Self where @@ -581,6 +575,12 @@ where self } + #[inline] + fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { + self._inner.or(column, op); + self + } + #[inline] fn order_by>(&mut self, order_by: Z, desc: bool) -> &mut Self { self._inner.order_by(order_by, desc); @@ -665,12 +665,6 @@ where self } - #[inline] - fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { - self._inner.or(column, op); - self - } - #[inline] fn or_values_in(&mut self, r#or: Z, values: &'a [Q]) -> &mut Self where @@ -681,6 +675,12 @@ where self } + #[inline] + fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { + self._inner.or(column, op); + self + } + #[inline] fn order_by>(&mut self, order_by: Z, desc: bool) -> &mut Self { self._inner.order_by(order_by, desc); From f49f5d6c7c895457bfd4a80aef2a24df4975e293 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Fri, 14 Apr 2023 14:47:49 +0200 Subject: [PATCH 34/82] #wip - Rewriting with conditinal compilation. Reworking the workspace --- .github/workflows/code-quality.yml | 2 +- canyon_sql/Cargo.toml => Caasdfadsrgo.tomlsda | 16 +- Cargo.toml | 35 ++- bash_aliases.sh | 2 +- canyon_connection/Cargo.toml | 45 ++- .../src/canyon_database_connector.rs | 4 +- canyon_connection/src/datasources.rs | 5 +- canyon_connection/src/lib.rs | 10 +- canyon_crud/Cargo.toml | 28 +- canyon_crud/src/bounds.rs | 257 ++++++++---------- canyon_crud/src/crud.rs | 65 +++-- canyon_crud/src/lib.rs | 1 - canyon_crud/src/mapper.rs | 3 +- .../src/query_elements/query_builder.rs | 3 +- canyon_crud/src/result.rs | 108 -------- canyon_macros/Cargo.toml | 15 +- canyon_macros/src/query_operations/insert.rs | 5 +- canyon_macros/src/query_operations/select.rs | 65 +++-- canyon_observer/Cargo.toml | 15 +- canyon_observer/src/manager/entity.rs | 4 +- canyon_observer/src/migrations/handler.rs | 9 +- canyon_observer/src/migrations/memory.rs | 5 +- {canyon_sql/src => src}/lib.rs | 1 - tests/Cargo.toml | 2 +- 24 files changed, 310 insertions(+), 395 deletions(-) rename canyon_sql/Cargo.toml => Caasdfadsrgo.tomlsda (55%) mode change 100755 => 100644 delete mode 100644 canyon_crud/src/result.rs rename {canyon_sql/src => src}/lib.rs (98%) mode change 100755 => 100644 diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index c72c0e5b..07ce16a2 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -55,7 +55,7 @@ jobs: strategy: fail-fast: false matrix: - crate: [canyon_connection, canyon_crud, canyon_macros, canyon_observer, canyon_sql] + crate: [canyon_connection, canyon_crud, canyon_macros, canyon_observer, canyon_sql_root] steps: - uses: actions/checkout@v3 diff --git a/canyon_sql/Cargo.toml b/Caasdfadsrgo.tomlsda old mode 100755 new mode 100644 similarity index 55% rename from canyon_sql/Cargo.toml rename to Caasdfadsrgo.tomlsda index 0a13a101..3e3c557e --- a/canyon_sql/Cargo.toml +++ b/Caasdfadsrgo.tomlsda @@ -1,13 +1,13 @@ [package] name = "canyon_sql" version = "0.2.0" -edition = "2021" -authors = ["Alex Vergara, Gonzalo Busto"] -documentation = "https://zerodaycode.github.io/canyon-book/" -homepage = "https://github.com/zerodaycode/Canyon-SQL" -readme = "../README.md" -license = "MIT" -description = "A Rust ORM and QueryBuilder" +edition.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true [dependencies] async-trait = { version = "0.1.50" } @@ -16,4 +16,4 @@ async-trait = { version = "0.1.50" } canyon_macros = { version = "0.2.0", path = "../canyon_macros" } canyon_observer = { version = "0.2.0", path = "../canyon_observer" } canyon_crud = { version = "0.2.0", path = "../canyon_crud" } -canyon_connection = { version = "0.2.0", path = "../canyon_connection" } +canyon_connection = { version = "0.2.0", path = "../canyon_connection", features = ["postgres"] } diff --git a/Cargo.toml b/Cargo.toml index 800ad578..70dea99d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,10 @@ -# This is the root Cargo.toml file that serves as manager for the workspace of the project +# This is the root Caasdfadsrgo.tomlsda file that serves as manager for the workspace of the project +[package] +name = "canyon_sql" +version = "0.2.0" [workspace] members = [ - "canyon_sql", "canyon_observer", "canyon_macros", "canyon_crud", @@ -10,3 +12,32 @@ members = [ "tests" ] + +[workspace.dependencies] +# Project crates +canyon_macros = { version = "0.2.0", path = "canyon_macros" } +canyon_observer = { version = "0.2.0", path = "canyon_observer" } +canyon_crud = { version = "0.2.0", path = "canyon_crud", features = ["postgres", "mssql"] } +canyon_connection = { version = "0.2.0", path = "canyon_connection", features = ["postgres", "mssql"] } + +tokio = { version = "1.21.2", features = ["full"] } +tokio-util = { version = "0.7.4", features = ["compat"] } +tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"] } +tiberius = { version = "0.12.1", features = ["tds73", "chrono", "integrated-auth-gssapi"] } + +futures = "0.3.25" +indexmap = "1.9.1" +async-std = "1.12.0" +lazy_static = "1.4.0" +serde = { version = "1.0.138", features = ["derive"] } +toml = "0.7.3" + +[workspace.package] +version = "0.2.0" +edition = "2021" +authors = ["Alex Vergara, Gonzalo Busto"] +documentation = "https://zerodaycode.github.io/canyon-book/" +homepage = "https://github.com/zerodaycode/Canyon-SQL" +readme = "../README.md" +license = "MIT" +description = "A Rust ORM and QueryBuilder" diff --git a/bash_aliases.sh b/bash_aliases.sh index a67da429..64e2d931 100644 --- a/bash_aliases.sh +++ b/bash_aliases.sh @@ -39,7 +39,7 @@ alias SqlServerInitializationLinux='cargo test initialize_sql_server_docker_inst # Publish Canyon-SQL to the registry with its dependencies -alias PublishCanyon='cargo publish -p canyon_connection && cargo publish -p canyon_crud && cargo publish -p canyon_observer && cargo publish -p canyon_macros && cargo publish -p canyon_sql' +alias PublishCanyon='cargo publish -p canyon_connection && cargo publish -p canyon_crud && cargo publish -p canyon_observer && cargo publish -p canyon_macros && cargo publish -p canyon_sql_root' # Collects the code coverage for the project (tests must run before this) alias CcEnvVars='export CARGO_INCREMENTAL=0 diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 234ffee0..323e91a3 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -1,32 +1,29 @@ [package] name = "canyon_connection" -version = "0.2.0" -edition = "2021" -documentation = "https://zerodaycode.github.io/canyon-book/" -homepage = "https://github.com/zerodaycode/Canyon-SQL" -readme = "../README.md" -license = "MIT" -description = "A Rust ORM and QueryBuilder" - +version.workspace = true +edition.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true [dependencies] -tokio = { version = "1.21.2", features = ["full"] } -tokio-util = { version = "0.7.4", features = ["compat"] } -tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"] } -futures = "0.3.25" -indexmap = "1.9.1" - -tiberius = { version = "0.12.1", features = ["tds73", "chrono", "integrated-auth-gssapi"] } -async-std = { version = "1.12.0" } +tokio = { workspace = true, features = ["full"], optional = true } +tokio-util = { workspace = true, features = ["compat"], optional = true } +tokio-postgres = { workspace = true, features = ["with-chrono-0_4"], optional = true } +tiberius = { workspace = true, features = ["tds73", "chrono", "integrated-auth-gssapi"], optional = true } -lazy_static = "1.4.0" - -serde = { version = "1.0.138", features = ["derive"] } -toml = "0.7.3" +futures = { workspace = true } +indexmap = { workspace = true } +async-std = { workspace = true } +lazy_static = { workspace = true } +serde = { workspace = true, features = ["derive"] } +toml = { workspace = true } [features] default = ["postgres"] -postgres = [] -mssql = [] -mssql-integrated-auth = [] - +postgres = ["tokio", "tokio-postgres", "tokio-util"] +mssql = ["tiberius", "tiberius/tds73", "tiberius/chrono"] +mssql-integrated-auth = ["mssql", "tiberius/integrated-auth-gssapi"] \ No newline at end of file diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 563cfd5c..27d59799 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -160,7 +160,7 @@ mod database_connection_handler { use crate::CanyonSqlConfig; const CONFIG_FILE_MOCK_ALT: &str = r#" - [canyon_sql] + [canyon_sql_root] datasources = [ {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } @@ -171,7 +171,7 @@ mod database_connection_handler { #[test] fn check_from_datasource() { let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) - .expect("A failure happened retrieving the [canyon_sql] section"); + .expect("A failure happened retrieving the [canyon_sql_root] section"); assert_eq!( config.canyon_sql.datasources[0].get_db_type(), diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 409705dc..4dc76dbb 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -6,7 +6,7 @@ use crate::canyon_database_connector::DatabaseType; #[test] fn load_ds_config_from_array() { const CONFIG_FILE_MOCK_ALT: &str = r#" - [canyon_sql] + [canyon_sql_root] datasources = [ {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' }, @@ -15,7 +15,7 @@ fn load_ds_config_from_array() { "#; let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) - .expect("A failure happened retrieving the [canyon_sql] section"); + .expect("A failure happened retrieving the [canyon_sql_root] section"); let ds_0 = &config.canyon_sql.datasources[0]; let ds_1 = &config.canyon_sql.datasources[1]; @@ -56,6 +56,7 @@ fn load_ds_config_from_array() { pub struct CanyonSqlConfig { pub canyon_sql: Datasources, } + #[derive(Deserialize, Debug, Clone)] pub struct Datasources { pub datasources: Vec, diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index 1a8f7cab..59a9dbba 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -1,10 +1,10 @@ -pub extern crate async_std; +#[cfg(feature = "mssql")] pub extern crate async_std; pub extern crate futures; pub extern crate lazy_static; -pub extern crate tiberius; -pub extern crate tokio; -pub extern crate tokio_postgres; -pub extern crate tokio_util; +#[cfg(feature = "mssql")] pub extern crate tiberius; +#[cfg(feature = "postgres")] pub extern crate tokio; +#[cfg(feature = "postgres")] pub extern crate tokio_postgres; +#[cfg(feature = "postgres")] pub extern crate tokio_util; pub mod canyon_database_connector; pub mod datasources; diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index 4c30408f..0e4f0854 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -1,15 +1,27 @@ [package] name = "canyon_crud" -version = "0.2.0" -edition = "2021" -documentation = "https://zerodaycode.github.io/canyon-book/" -homepage = "https://github.com/zerodaycode/Canyon-SQL" -readme = "../README.md" -license = "MIT" -description = "A Rust ORM and QueryBuilder" +version.workspace = true +edition.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true [dependencies] +tokio = { workspace = true, features = ["full"], optional = true } +tokio-util = { workspace = true, features = ["compat"], optional = true } +tokio-postgres = { workspace = true, features = ["with-chrono-0_4"], optional = true } +tiberius = { workspace = true, features = ["tds73", "chrono", "integrated-auth-gssapi"], optional = true } + chrono = { version = "0.4", features = ["serde"] } async-trait = { version = "0.1.50" } -canyon_connection = { version = "0.2.0", path = "../canyon_connection" } +canyon_connection = { version = "0.2.0", path = "../canyon_connection", features = ["postgres", "mssql"] } + +[features] +default = ["postgres"] +postgres = ["tokio", "tokio-postgres", "tokio-util"] +mssql = ["tiberius", "tiberius/tds73", "tiberius/chrono"] +mssql-integrated-auth = ["mssql", "tiberius/integrated-auth-gssapi"] \ No newline at end of file diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs index e484fe8c..813c05dd 100644 --- a/canyon_crud/src/bounds.rs +++ b/canyon_crud/src/bounds.rs @@ -4,15 +4,19 @@ use crate::{ crud::{CrudOperations, Transaction}, mapper::RowMapper, }; -use canyon_connection::{ - tiberius::{self, ColumnData, IntoSql}, - tokio_postgres::{self, types::ToSql}, -}; + +#[cfg(feature = "postgres")] +use canyon_connection::tokio_postgres::{self, types::ToSql}; + +#[cfg(feature = "mssql")] +use canyon_connection::tiberius::{self, ColumnData, IntoSql}; + use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use std::any::Any; +use tiberius::FromSql; /// Created for retrieve the field's name of a field of a struct, giving -/// the Canoyn's autogenerated enum with the variants that maps this +/// the Canyon's autogenerated enum with the variants that maps this /// fields. /// /// ``` @@ -87,18 +91,20 @@ pub trait InClauseValues: ToSql + ToString {} pub trait Row { fn as_any(&self) -> &dyn Any; } -impl Row for tokio_postgres::Row { + +#[cfg(feature = "postgres")] impl Row for tokio_postgres::Row { fn as_any(&self) -> &dyn Any { self } } - -impl Row for tiberius::Row { +#[cfg(feature = "mssql")] impl Row for tiberius::Row { fn as_any(&self) -> &dyn Any { self } } +/// Generic abstraction for hold a Column type that will be one of the Column +/// types present in the dependent crates pub struct Column<'a> { name: &'a str, type_: ColumnType, @@ -112,8 +118,8 @@ impl<'a> Column<'a> { } pub fn type_(&'a self) -> &'_ dyn Type { match &self.type_ { - ColumnType::Postgres(v) => v as &'a dyn Type, - ColumnType::SqlServer(v) => v as &'a dyn Type, + #[cfg(feature = "postgres")] ColumnType::Postgres(v) => v as &'a dyn Type, + #[cfg(feature = "mssql")] ColumnType::SqlServer(v) => v as &'a dyn Type, } } } @@ -121,20 +127,21 @@ impl<'a> Column<'a> { pub trait Type { fn as_any(&self) -> &dyn Any; } -impl Type for tokio_postgres::types::Type { +#[cfg(feature = "postgres")] impl Type for tokio_postgres::types::Type { fn as_any(&self) -> &dyn Any { self } } -impl Type for tiberius::ColumnType { +#[cfg(feature = "mssql")] impl Type for tiberius::ColumnType { fn as_any(&self) -> &dyn Any { self } } +/// Wrapper over the dependencies Column's types pub enum ColumnType { - Postgres(tokio_postgres::types::Type), - SqlServer(tiberius::ColumnType), + #[cfg(feature = "postgres")] Postgres(tokio_postgres::types::Type), + #[cfg(feature = "mssql")] SqlServer(tiberius::ColumnType), } pub trait RowOperations { @@ -168,6 +175,21 @@ impl RowOperations for &dyn Row { panic!() } + fn get_opt<'a, Output>(&'a self, col_name: &str) -> Option + where + Output: tokio_postgres::types::FromSql<'a> + tiberius::FromSql<'a>, + { + if let Some(row) = self.as_any().downcast_ref::() { + return row.get::<&str, Option>(col_name); + }; + if let Some(row) = self.as_any().downcast_ref::() { + return row + .try_get:where + .expect("Failed to obtain a row in the MSSQL migrations"); + }; + panic!() + } + fn columns(&self) -> Vec { let mut cols = vec![]; @@ -199,28 +221,13 @@ impl RowOperations for &dyn Row { cols } - - fn get_opt<'a, Output>(&'a self, col_name: &str) -> Option - where - Output: tokio_postgres::types::FromSql<'a> + tiberius::FromSql<'a>, - { - if let Some(row) = self.as_any().downcast_ref::() { - return row.get::<&str, Option>(col_name); - }; - if let Some(row) = self.as_any().downcast_ref::() { - return row - .try_get::(col_name) - .expect("Failed to obtain a row in the MSSQL migrations"); - }; - panic!() - } } /// Defines a trait for represent type bounds against the allowed -/// datatypes supported by Canyon to be used as query parameters. +/// data types supported by Canyon to be used as query parameters. pub trait QueryParameter<'a>: std::fmt::Debug + Sync + Send { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync); - fn as_sqlserver_param(&self) -> ColumnData<'_>; + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync); + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_>; } /// The implementation of the [`canyon_connection::tiberius`] [`IntoSql`] for the @@ -231,6 +238,7 @@ pub trait QueryParameter<'a>: std::fmt::Debug + Sync + Send { /// a collection of [`QueryParameter<'a>`], in order to allow a workflow /// that is not dependent of the specific type of the argument that holds /// the query parameters of the database connectors +#[cfg(feature = "mssql")] impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { fn into_sql(self) -> ColumnData<'a> { self.as_sqlserver_param() @@ -238,222 +246,198 @@ impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { } impl<'a> QueryParameter<'a> for bool { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::Bit(Some(*self)) } } impl<'a> QueryParameter<'a> for i16 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(*self)) } } impl<'a> QueryParameter<'a> for &i16 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(*self) } } impl<'a> QueryParameter<'a> for Option<&i16> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for i32 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self)) } } impl<'a> QueryParameter<'a> for &i32 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(*self) } } impl<'a> QueryParameter<'a> for Option<&i32> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for f32 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some(*self)) } } impl<'a> QueryParameter<'a> for &f32 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(*self) } } impl<'a> QueryParameter<'a> for Option<&f32> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some( *self.expect("Error on an f32 value on QueryParameter<'_>"), )) } } impl<'a> QueryParameter<'a> for f64 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some(*self)) } } impl<'a> QueryParameter<'a> for &f64 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(*self) } } impl<'a> QueryParameter<'a> for Option<&f64> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some( *self.expect("Error on an f64 value on QueryParameter<'_>"), )) } } impl<'a> QueryParameter<'a> for i64 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self)) } } impl<'a> QueryParameter<'a> for &i64 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(*self) } } impl<'a> QueryParameter<'a> for Option<&i64> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for String { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Owned(self.to_owned()))) } } impl<'a> QueryParameter<'a> for &String { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Borrowed(self))) } } impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { match self { Some(string) => ColumnData::String(Some(std::borrow::Cow::Owned(string.to_owned()))), None => ColumnData::String(None), @@ -461,11 +445,10 @@ impl<'a> QueryParameter<'a> for Option { } } impl<'a> QueryParameter<'a> for Option<&String> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { match self { Some(string) => ColumnData::String(Some(std::borrow::Cow::Borrowed(string))), None => ColumnData::String(None), @@ -473,20 +456,18 @@ impl<'a> QueryParameter<'a> for Option<&String> { } } impl<'a> QueryParameter<'_> for &'_ str { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Borrowed(*self))) } } impl<'a> QueryParameter<'a> for Option<&'_ str> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { match *self { Some(str) => ColumnData::String(Some(std::borrow::Cow::Borrowed(str))), None => ColumnData::String(None), @@ -494,92 +475,82 @@ impl<'a> QueryParameter<'a> for Option<&'_ str> { } } impl<'a> QueryParameter<'_> for NaiveDate { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for NaiveTime { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for NaiveDateTime { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for DateTime { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for DateTime { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for Option> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index 8f587a02..121efa91 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -9,10 +9,9 @@ use crate::mapper::RowMapper; use crate::query_elements::query_builder::{ DeleteQueryBuilder, SelectQueryBuilder, UpdateQueryBuilder, }; -use crate::result::DatabaseResult; /// This traits defines and implements a query against a database given -/// an statemt `stmt` and the params to pass the to the client. +/// an statement `stmt` and the params to pass the to the client. /// /// It returns a [`DatabaseResult`], which is the core Canyon type to wrap /// the result of the query and, if the user desires, @@ -26,10 +25,11 @@ pub trait Transaction { stmt: S, params: Z, datasource_name: &'a str, - ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> + ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> where S: AsRef + Display + Sync + Send + 'a, Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, + T: Transaction + RowMapper { let mut guarded_cache = CACHED_DATABASE_CONN.lock().await; @@ -50,7 +50,7 @@ pub trait Transaction { }; match database_conn { - DatabaseConnection::Postgres(_) => { + #[cfg(feature = "postgres")] DatabaseConnection::Postgres(_) => { postgres_query_launcher::launch::( database_conn, stmt.to_string(), @@ -58,7 +58,7 @@ pub trait Transaction { ) .await } - DatabaseConnection::SqlServer(_) => { + #[cfg(feature = "mssql")] DatabaseConnection::SqlServer(_) => { sqlserver_query_launcher::launch::( database_conn, &mut stmt.to_string(), @@ -84,7 +84,7 @@ pub trait Transaction { /// /// See it's definition and docs to see the implementations. /// Also, you can find the written macro-code that performs the auto-mapping -/// in the *canyon_sql::canyon_macros* crates, on the root of this project. +/// in the *canyon_sql_root::canyon_macros* crates, on the root of this project. #[async_trait] pub trait CrudOperations: Transaction where @@ -121,12 +121,12 @@ where async fn insert<'a>( &mut self, - ) -> Result<(), Box>; + ) -> Result<(), Box>; async fn insert_datasource<'a>( &mut self, datasource_name: &'a str, - ) -> Result<(), Box>; + ) -> Result<(), Box>; async fn multi_insert<'a>( instances: &'a mut [&'a mut T], @@ -137,71 +137,79 @@ where datasource_name: &'a str, ) -> Result<(), Box<(dyn std::error::Error + Send + Sync + 'static)>>; - async fn update(&self) -> Result<(), Box>; + async fn update(&self) -> Result<(), Box>; async fn update_datasource<'a>( &self, datasource_name: &'a str, - ) -> Result<(), Box>; + ) -> Result<(), Box>; fn update_query<'a>() -> UpdateQueryBuilder<'a, T>; fn update_query_datasource(datasource_name: &str) -> UpdateQueryBuilder<'_, T>; - async fn delete(&self) -> Result<(), Box>; + async fn delete(&self) -> Result<(), Box>; async fn delete_datasource<'a>( &self, datasource_name: &'a str, - ) -> Result<(), Box>; + ) -> Result<(), Box>; fn delete_query<'a>() -> DeleteQueryBuilder<'a, T>; fn delete_query_datasource(datasource_name: &str) -> DeleteQueryBuilder<'_, T>; } +#[cfg(feature = "postgres")] mod postgres_query_launcher { use crate::bounds::QueryParameter; - use crate::result::DatabaseResult; use canyon_connection::canyon_database_connector::DatabaseConnection; + use crate::crud::Transaction; + use crate::mapper::RowMapper; pub async fn launch<'a, T>( db_conn: &DatabaseConnection, stmt: String, params: &'a [&'_ dyn QueryParameter<'_>], - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> { + ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> + where T: Transaction + RowMapper + { let mut m_params = Vec::new(); for param in params { m_params.push(param.as_postgres_param()); } - Ok(DatabaseResult::new_postgresql( - db_conn - .postgres_connection() - .unwrap() - .client - .query(&stmt, m_params.as_slice()) - .await?, - )) + let r = db_conn + .postgres_connection() + .unwrap() + .client + .query(&stmt, m_params.as_slice()) + .await?; + + Ok( + r.iter().map(|row| T::deserialize_postgresql(row)).collect() + ) } } -mod sqlserver_query_launcher { - use canyon_connection::tiberius::Row; +#[cfg(feature = "mssql")] +mod sqlserver_query_launcher { use crate::{ bounds::QueryParameter, canyon_connection::{canyon_database_connector::DatabaseConnection, tiberius::Query}, - result::DatabaseResult, }; + use crate::crud::Transaction; + use crate::mapper::RowMapper; pub async fn launch<'a, T, Z>( db_conn: &mut DatabaseConnection, stmt: &mut String, params: Z, - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> + ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> where Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, + T: Transaction + RowMapper { // Re-generate de insert statement to adequate it to the SQL SERVER syntax to retrieve the PK value(s) after insert if stmt.contains("RETURNING") { @@ -223,7 +231,7 @@ mod sqlserver_query_launcher { .iter() .for_each(|param| mssql_query.bind(*param)); - let _results: Vec = mssql_query + let _results = mssql_query .query( db_conn .sqlserver_connection() @@ -235,8 +243,9 @@ mod sqlserver_query_launcher { .await? .into_iter() .flatten() + .map(|row| T::deserialize_sqlserver(&row)) .collect::>(); - Ok(DatabaseResult::new_sqlserver(_results)) + Ok(_results) } } diff --git a/canyon_crud/src/lib.rs b/canyon_crud/src/lib.rs index 8a20b48e..929dbea2 100644 --- a/canyon_crud/src/lib.rs +++ b/canyon_crud/src/lib.rs @@ -4,7 +4,6 @@ pub mod bounds; pub mod crud; pub mod mapper; pub mod query_elements; -pub mod result; pub use query_elements::operators::*; diff --git a/canyon_crud/src/mapper.rs b/canyon_crud/src/mapper.rs index 71303785..0114bd3a 100644 --- a/canyon_crud/src/mapper.rs +++ b/canyon_crud/src/mapper.rs @@ -1,4 +1,5 @@ -use canyon_connection::{tiberius, tokio_postgres}; +#[cfg(feature = "postgres")] use canyon_connection::tokio_postgres; +#[cfg(feature = "mssql")] use canyon_connection::tiberius; use crate::crud::Transaction; diff --git a/canyon_crud/src/query_elements/query_builder.rs b/canyon_crud/src/query_elements/query_builder.rs index 56ddda7c..c26c5642 100644 --- a/canyon_crud/src/query_elements/query_builder.rs +++ b/canyon_crud/src/query_elements/query_builder.rs @@ -173,8 +173,7 @@ where self.query.params.to_vec(), self.datasource_name, ) - .await? - .get_entities::()) + .await?) } pub fn r#where>(&mut self, r#where: Z, op: impl Operator) { diff --git a/canyon_crud/src/result.rs b/canyon_crud/src/result.rs deleted file mode 100644 index 1a2cae29..00000000 --- a/canyon_crud/src/result.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::{bounds::Row, crud::Transaction, mapper::RowMapper}; -use canyon_connection::{canyon_database_connector::DatabaseType, tiberius, tokio_postgres}; -use std::{fmt::Debug, marker::PhantomData}; - -/// Represents a database result after a query, by wrapping the `Vec` types that comes with the -/// results after the query. -/// and providing methods to deserialize this result into a **user defined struct** -#[derive(Debug)] -pub struct DatabaseResult { - pub postgres: Vec, - pub sqlserver: Vec, - pub active_ds: DatabaseType, - _phantom_data: std::marker::PhantomData, -} - -impl DatabaseResult { - pub fn new_postgresql(result: Vec) -> Self { - Self { - postgres: result, - sqlserver: Vec::with_capacity(0), - active_ds: DatabaseType::PostgreSql, - _phantom_data: PhantomData, - } - } - - pub fn new_sqlserver(results: Vec) -> Self { - Self { - postgres: Vec::with_capacity(0), - sqlserver: results, - active_ds: DatabaseType::SqlServer, - _phantom_data: PhantomData, - } - } - - /// Returns a [`Vec`] filled with instances of the type T. - /// Z param it's used to constraint the types that can call this method. - /// - /// Also, provides a way to statically call `Z::deserialize_` method, - /// which it's the implementation used by the macros to automatically - /// map database columns into the fields for T. - pub fn get_entities>(&self) -> Vec - where - T: Transaction, - { - match self.active_ds { - DatabaseType::PostgreSql => self.map_from_postgresql::(), - DatabaseType::SqlServer => self.map_from_sql_server::(), - } - } - - fn map_from_postgresql>(&self) -> Vec - where - T: Transaction, - { - let mut results = Vec::new(); - - self.postgres - .iter() - .for_each(|row| results.push(Z::deserialize_postgresql(row))); - - results - } - - fn map_from_sql_server>(&self) -> Vec - where - T: Transaction, - { - let mut results = Vec::new(); - - self.sqlserver - .iter() - .for_each(|row| results.push(Z::deserialize_sqlserver(row))); - - results - } - - pub fn as_canyon_rows(&self) -> Vec<&dyn Row> { - let mut results = Vec::new(); - - match self.active_ds { - DatabaseType::PostgreSql => { - self.postgres - .iter() - .for_each(|row| results.push(row as &dyn Row)); - } - DatabaseType::SqlServer => { - self.sqlserver - .iter() - .for_each(|row| results.push(row as &dyn Row)); - } - }; - - results - } - - /// Returns the active datasource - pub fn get_active_ds(&self) -> &DatabaseType { - &self.active_ds - } - - /// Returns how many rows contains the result of the query - pub fn number_of_results(&self) -> usize { - match self.active_ds { - DatabaseType::PostgreSql => self.postgres.len(), - DatabaseType::SqlServer => self.sqlserver.len(), - } - } -} diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index 93695087..a501440d 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "canyon_macros" -version = "0.2.0" -edition = "2021" -documentation = "https://zerodaycode.github.io/canyon-book/" -homepage = "https://github.com/zerodaycode/Canyon-SQL" -readme = "../README.md" -license = "MIT" -description = "A Rust ORM and QueryBuilder" +version.workspace = true +edition.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true [lib] proc-macro = true diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index 11890b31..543a5121 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -56,6 +56,7 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri datasource_name ).await; + // TODO Convertir a canyon rows match result { Ok(res) => { match res.get_active_ds() { @@ -296,7 +297,9 @@ pub fn generate_multiple_insert_tokens( datasource_name ).await; - match result { + match result { // TODO Falta el ds correcto + // TODO Recuperar datasource fuera del código cliente + /* .for_each(|row| results.push(row as &dyn Row)); */ Ok(res) => { match res.get_active_ds() { canyon_sql::crud::DatabaseType::PostgreSql => { diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index 761451c1..c782e8c2 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -16,7 +16,7 @@ pub fn generate_find_all_unchecked_tokens( let stmt = format!("SELECT * FROM {table_schema_data}"); quote! { - /// Performns a `SELECT * FROM table_name`, where `table_name` it's + /// Performs a `SELECT * FROM table_name`, where `table_name` it's /// the name of your entity but converted to the corresponding /// database convention. P.ej. PostgreSQL prefers table names declared /// with snake_case identifiers. @@ -27,7 +27,6 @@ pub fn generate_find_all_unchecked_tokens( "" ).await .unwrap() - .get_entities::<#ty>() } /// Performs a `SELECT * FROM table_name`, where `table_name` it's @@ -45,7 +44,6 @@ pub fn generate_find_all_unchecked_tokens( datasource_name ).await .unwrap() - .get_entities::<#ty>() } } } @@ -60,7 +58,7 @@ pub fn generate_find_all_tokens( let stmt = format!("SELECT * FROM {table_schema_data}"); quote! { - /// Performns a `SELECT * FROM table_name`, where `table_name` it's + /// Performs a `SELECT * FROM table_name`, where `table_name` it's /// the name of your entity but converted to the corresponding /// database convention. P.ej. PostgreSQL prefers table names declared /// with snake_case identifiers. @@ -73,11 +71,10 @@ pub fn generate_find_all_tokens( &[], "" ).await? - .get_entities::<#ty>() ) } - /// Performns a `SELECT * FROM table_name`, where `table_name` it's + /// Performs a `SELECT * FROM table_name`, where `table_name` it's /// the name of your entity but converted to the corresponding /// database convention. P.ej. PostgreSQL prefers table names declared /// with snake_case identifiers. @@ -98,7 +95,6 @@ pub fn generate_find_all_tokens( &[], datasource_name ).await? - .get_entities::<#ty>() ) } } @@ -151,25 +147,26 @@ pub fn generate_count_tokens( let stmt = format!("SELECT COUNT (*) FROM {table_schema_data}"); let result_handling = quote! { - match count.get_active_ds() { - canyon_sql::crud::DatabaseType::PostgreSql => { - Ok( - count.postgres.get(0) - .expect(&format!("Count operation failed for {:?}", #ty_str)) - .get::<&str, i64>("count") - .to_owned() - ) - }, - canyon_sql::crud::DatabaseType::SqlServer => { - Ok( - count.sqlserver.get(0) - .expect(&format!("Count operation failed for {:?}", #ty_str)) - .get::(0) - .expect(&format!("SQL Server failed to return the count values for {:?}", #ty_str)) - .into() - ) - } - } + // match count.get_active_ds() { + // canyon_sql_root::crud::DatabaseType::PostgreSql => { + // Ok( + // count.postgres.get(0) + // .expect(&format!("Count operation failed for {:?}", #ty_str)) + // .get::<&str, i64>("count") + // .to_owned() + // ) + // }, + // canyon_sql_root::crud::DatabaseType::SqlServer => { + // Ok( + // count.sqlserver.get(0) + // .expect(&format!("Count operation failed for {:?}", #ty_str)) + // .get::(0) + // .expect(&format!("SQL Server failed to return the count values for {:?}", #ty_str)) + // .into() + // ) + // } + // } + Ok(0 as i64) // TODO }; quote! { @@ -240,11 +237,12 @@ pub fn generate_find_by_pk_tokens( }; } + // TOODO no tenemos number_OF_results let result_handling = quote! { match result { - n if n.number_of_results() == 0 => Ok(None), + n if n.len() == 0 => Ok(None), _ => Ok( - Some(result.get_entities::<#ty>().remove(0)) + Some(result.remove(0)) ) } }; @@ -347,9 +345,10 @@ pub fn generate_find_by_foreign_key_tokens( ); let result_handler = quote! { match result { - n if n.number_of_results() == 0 => Ok(None), + // TODO Noof + n if n.len() == 0 => Ok(None), _ => Ok(Some( - result.get_entities::<#fk_ty>().remove(0) + result.remove(0) )) } }; @@ -448,8 +447,7 @@ pub fn generate_find_by_reverse_foreign_key_tokens( stmt, &[lookage_value], "" - ).await? - .get_entities::<#ty>()) + ).await?) } }, )); @@ -477,8 +475,7 @@ pub fn generate_find_by_reverse_foreign_key_tokens( stmt, &[lookage_value], datasource_name - ).await? - .get_entities::<#ty>()) + ).await?) } }, )); diff --git a/canyon_observer/Cargo.toml b/canyon_observer/Cargo.toml index cb4bd353..67918e37 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_observer/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "canyon_observer" -version = "0.2.0" -edition = "2021" -documentation = "https://zerodaycode.github.io/canyon-book/" -homepage = "https://github.com/zerodaycode/Canyon-SQL" -readme = "../README.md" -license = "MIT" -description = "A Rust ORM and QueryBuilder" +version.workspace = true +edition.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true [dependencies] tokio = { version = "1.9.0", features = ["full"] } diff --git a/canyon_observer/src/manager/entity.rs b/canyon_observer/src/manager/entity.rs index 78e2f157..7aaeb38e 100644 --- a/canyon_observer/src/manager/entity.rs +++ b/canyon_observer/src/manager/entity.rs @@ -71,7 +71,7 @@ impl CanyonEntity { /// Generates an implementation of the match pattern to find whatever variant /// is being requested when the method `.field_name_as_str(self)` it's invoked over some - /// instance that implements the `canyon_sql::crud::bounds::FieldIdentifier` trait + /// instance that implements the `canyon_sql_root::crud::bounds::FieldIdentifier` trait pub fn create_match_arm_for_get_variant_as_string( &self, enum_name: &Ident, @@ -91,7 +91,7 @@ impl CanyonEntity { /// Generates an implementation of the match pattern to find whatever variant /// is being requested when the method `.value()` it's invoked over some - /// instance that implements the `canyon_sql::crud::bounds::FieldValueIdentifier` trait + /// instance that implements the `canyon_sql_root::crud::bounds::FieldValueIdentifier` trait pub fn create_match_arm_for_relate_fields_with_values( &self, enum_name: &Ident, diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index d454128a..739b4cae 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -5,7 +5,6 @@ use crate::{ canyon_crud::{ bounds::{Column, Row, RowOperations}, crud::Transaction, - result::DatabaseResult, DatabaseType, }, constants, @@ -87,7 +86,7 @@ impl Migrations { async fn fetch_database( datasource_name: &str, db_type: DatabaseType, - ) -> DatabaseResult { + ) -> Vec { let query = match db_type { DatabaseType::PostgreSql => constants::postgresql_queries::FETCH_PUBLIC_SCHEMA, DatabaseType::SqlServer => constants::mssql_queries::FETCH_PUBLIC_SCHEMA, @@ -105,10 +104,12 @@ impl Migrations { /// Handler for parse the result of query the information of some database schema, /// and extract the content of the returned rows into custom structures with /// the data well organized for every entity present on that schema - fn map_rows(db_results: DatabaseResult) -> Vec { + fn map_rows(db_results: Vec) -> Vec { let mut schema_info: Vec = Vec::new(); - for res_row in db_results.as_canyon_rows().into_iter() { + for res_row in db_results.iter() + .map(|row| &row as &dyn Row) + { let unique_table = schema_info .iter_mut() .find(|table| table.table_name == *res_row.get::<&str>("table_name").to_owned()); diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index 0a4080c0..aac81d3b 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -4,6 +4,7 @@ use regex::Regex; use std::collections::HashMap; use std::fs; use walkdir::WalkDir; +use canyon_crud::bounds::Row; use super::register_types::CanyonRegisterEntity; @@ -70,11 +71,11 @@ impl CanyonMemory { let res = Self::query("SELECT * FROM canyon_memory", [], &datasource.name) .await .expect("Error querying Canyon Memory"); - let mem_results = res.as_canyon_rows(); + let mem_results = res.map(|row| &row as &dyn Row); // Manually maps the results let mut db_rows = Vec::new(); - for row in mem_results.iter() { + for row in mem_results { let db_row = CanyonMemoryRow { id: row.get::("id"), filepath: row.get::<&str>("filepath"), diff --git a/canyon_sql/src/lib.rs b/src/lib.rs old mode 100755 new mode 100644 similarity index 98% rename from canyon_sql/src/lib.rs rename to src/lib.rs index 330b8ed4..d3bf079c --- a/canyon_sql/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,6 @@ pub mod crud { pub use canyon_crud::bounds; pub use canyon_crud::crud::*; pub use canyon_crud::mapper::*; - pub use canyon_crud::result::*; pub use canyon_crud::DatabaseType; } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index f2e83953..54047bc4 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dev-dependencies] -canyon_sql = { path = "../canyon_sql" } +canyon_sql = { path = ".." } [[test]] name = "canyon_integration_tests" From 5723243f180d192b4ccd1aa86fad9cfdae334f43 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Sun, 16 Apr 2023 00:25:22 +0200 Subject: [PATCH 35/82] WIP - Introducing CanyonRows --- canyon_connection/src/lib.rs | 25 ++++++- canyon_crud/src/bounds.rs | 58 +++++++++------ canyon_crud/src/crud.rs | 59 ++++++--------- canyon_crud/src/lib.rs | 1 + canyon_crud/src/rows.rs | 76 ++++++++++++++++++++ canyon_macros/src/query_operations/insert.rs | 33 ++------- 6 files changed, 163 insertions(+), 89 deletions(-) create mode 100644 canyon_crud/src/rows.rs diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index 59a9dbba..535e59fd 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -15,7 +15,7 @@ use crate::datasources::{CanyonSqlConfig, DatasourceConfig}; use canyon_database_connector::DatabaseConnection; use indexmap::IndexMap; use lazy_static::lazy_static; -use tokio::sync::Mutex; +use tokio::sync::{Mutex, MutexGuard}; const CONFIG_FILE_IDENTIFIER: &str = "canyon.toml"; @@ -61,3 +61,26 @@ pub async fn init_connections_cache() { ); } } + + +/// +pub fn get_database_connection<'a>( + datasource_name: &str, + guarded_cache: &'a mut MutexGuard> +) -> &'a mut DatabaseConnection { + if datasource_name.is_empty() { + guarded_cache + .get_mut( + DATASOURCES + .get(0) + .expect("We didn't found any valid datasource configuration. Check your `canyon.toml` file") + .name + .as_str() + ).unwrap_or_else(|| panic!("No default datasource found. Check your `canyon.toml` file")) + } else { + guarded_cache.get_mut(datasource_name) + .unwrap_or_else(|| + panic!("Canyon couldn't find a datasource in the pool with the argument provided: {datasource_name}") + ) + } +} diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs index 813c05dd..3589cb65 100644 --- a/canyon_crud/src/bounds.rs +++ b/canyon_crud/src/bounds.rs @@ -145,55 +145,71 @@ pub enum ColumnType { } pub trait RowOperations { - /// Abstracts the different forms of use the common `get` row - /// function or method dynamically no matter what are the origin - /// type from any database client provider - fn get<'a, Output>(&'a self, col_name: &str) -> Output - where - Output: tokio_postgres::types::FromSql<'a> + tiberius::FromSql<'a>; + #[cfg(feature = "postgres")] + fn get_postgres<'a, Output>(&'a self, col_name: &str) -> Output + where Output: tokio_postgres::types::FromSql<'a>; + #[cfg(feature = "mssql")] + fn get_mssql<'a, Output>(&self, col_name: &str) -> Output + where Output: tiberius::FromSql<'a>; - fn get_opt<'a, Output>(&'a self, col_name: &str) -> Option - where - Output: tokio_postgres::types::FromSql<'a> + tiberius::FromSql<'a>; + #[cfg(feature = "postgres")] + fn get_postgres_opt<'a, Output>(&'a self, col_name: &str) -> Option + where Output: tokio_postgres::types::FromSql<'a>; + #[cfg(feature = "mssql")] + fn get_mssql_opt<'a, Output>(&'a self, col_name: &str) -> Option + where Output: tokio_postgres::types::FromSql<'a>; fn columns(&self) -> Vec; } impl RowOperations for &dyn Row { - fn get<'a, Output>(&'a self, col_name: &str) -> Output - where - Output: tokio_postgres::types::FromSql<'a> + tiberius::FromSql<'a>, + #[cfg(feature = "postgres")] + fn get_postgres<'a, Output>(&'a self, col_name: &str) -> Output + where Output: tokio_postgres::types::FromSql<'a> { if let Some(row) = self.as_any().downcast_ref::() { return row.get::<&str, Output>(col_name); }; + panic!() // TODO into result and propagate + } + #[cfg(feature = "mssql")] + fn get_mssql<'a, Output>(&'a self, col_name: &str) -> Output + where Output: tiberius::FromSql<'a> + { if let Some(row) = self.as_any().downcast_ref::() { return row .get::(col_name) .expect("Failed to obtain a row in the MSSQL migrations"); }; - panic!() + panic!() // TODO into result and propagate } - fn get_opt<'a, Output>(&'a self, col_name: &str) -> Option - where - Output: tokio_postgres::types::FromSql<'a> + tiberius::FromSql<'a>, + #[cfg(feature = "postgres")] + fn get_postgres_opt<'a, Output>(&'a self, col_name: &str) -> Option + where Output: tokio_postgres::types::FromSql<'a> { if let Some(row) = self.as_any().downcast_ref::() { return row.get::<&str, Option>(col_name); }; + panic!() // TODO into result and propagate + } + + #[cfg(feature = "mssql")] + fn get_mssql_opt<'a, Output>(&'a self, col_name: &str) -> Option + where Output: tiberius::FromSql<'a> + { if let Some(row) = self.as_any().downcast_ref::() { return row - .try_get:where - .expect("Failed to obtain a row in the MSSQL migrations"); + .try_get + .expect("Failed to obtain a row for MSSQL"); }; - panic!() + panic!() // TODO into result and propagate } fn columns(&self) -> Vec { let mut cols = vec![]; - if self.as_any().is::() { + /* if self.as_any().is::() { self.as_any() .downcast_ref::() .expect("Not a tokio postgres Row for column") @@ -217,7 +233,7 @@ impl RowOperations for &dyn Row { type_: ColumnType::SqlServer(c.column_type()), }) }) - }; + }; */ cols } diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index 121efa91..59fc5bd3 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -2,13 +2,14 @@ use std::fmt::Display; use async_trait::async_trait; use canyon_connection::canyon_database_connector::DatabaseConnection; -use canyon_connection::{CACHED_DATABASE_CONN, DATASOURCES}; +use canyon_connection::{CACHED_DATABASE_CONN, get_database_connection}; use crate::bounds::QueryParameter; use crate::mapper::RowMapper; use crate::query_elements::query_builder::{ DeleteQueryBuilder, SelectQueryBuilder, UpdateQueryBuilder, }; +use crate::rows::CanyonRows; /// This traits defines and implements a query against a database given /// an statement `stmt` and the params to pass the to the client. @@ -18,36 +19,22 @@ use crate::query_elements::query_builder::{ /// automatically map it to an struct. #[async_trait] pub trait Transaction { - /// Performs a query against the targeted database by the selected datasource. - /// - /// No datasource means take the entry zero + /// Performs a query against the targeted database by the selected or + /// the defaulted datasource, wrapping the resultant collection of entities + /// in [`super::rows::Rows`]. This ones provides custom operations that + /// facilitates the macro operations. async fn query<'a, S, Z>( stmt: S, params: Z, datasource_name: &'a str, - ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> - where - S: AsRef + Display + Sync + Send + 'a, - Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, - T: Transaction + RowMapper + ) -> Result> + where + S: AsRef + Display + Sync + Send + 'a, + Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, + T: Transaction + RowMapper { let mut guarded_cache = CACHED_DATABASE_CONN.lock().await; - - let database_conn = if datasource_name.is_empty() { - guarded_cache - .get_mut( - DATASOURCES - .get(0) - .expect("We didn't found any valid datasource configuration. Check your `canyon.toml` file") - .name - .as_str() - ).unwrap_or_else(|| panic!("No default datasource found. Check your `canyon.toml` file")) - } else { - guarded_cache.get_mut(datasource_name) - .unwrap_or_else(|| - panic!("Canyon couldn't find a datasource in the pool with the argument provided: {datasource_name}" - )) - }; + let database_conn = get_database_connection(datasource_name, &mut guarded_cache); match database_conn { #[cfg(feature = "postgres")] DatabaseConnection::Postgres(_) => { @@ -56,7 +43,7 @@ pub trait Transaction { stmt.to_string(), params.as_ref(), ) - .await + .await } #[cfg(feature = "mssql")] DatabaseConnection::SqlServer(_) => { sqlserver_query_launcher::launch::( @@ -64,7 +51,7 @@ pub trait Transaction { &mut stmt.to_string(), params, ) - .await + .await } } } @@ -166,12 +153,13 @@ mod postgres_query_launcher { use canyon_connection::canyon_database_connector::DatabaseConnection; use crate::crud::Transaction; use crate::mapper::RowMapper; + use crate::rows::CanyonRows; pub async fn launch<'a, T>( db_conn: &DatabaseConnection, stmt: String, params: &'a [&'_ dyn QueryParameter<'_>], - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> + ) -> Result> where T: Transaction + RowMapper { let mut m_params = Vec::new(); @@ -186,9 +174,7 @@ mod postgres_query_launcher { .query(&stmt, m_params.as_slice()) .await?; - Ok( - r.iter().map(|row| T::deserialize_postgresql(row)).collect() - ) + Ok(CanyonRows::Postgres(r)) } } @@ -201,12 +187,13 @@ mod sqlserver_query_launcher { }; use crate::crud::Transaction; use crate::mapper::RowMapper; + use crate::rows::CanyonRows; pub async fn launch<'a, T, Z>( db_conn: &mut DatabaseConnection, stmt: &mut String, params: Z, - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> + ) -> Result> where Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, T: Transaction + RowMapper @@ -240,12 +227,8 @@ mod sqlserver_query_launcher { ) .await? .into_results() - .await? - .into_iter() - .flatten() - .map(|row| T::deserialize_sqlserver(&row)) - .collect::>(); + .await?; - Ok(_results) + Ok(CanyonRows::Tiberius(_results)) } } diff --git a/canyon_crud/src/lib.rs b/canyon_crud/src/lib.rs index 929dbea2..ee856f6c 100644 --- a/canyon_crud/src/lib.rs +++ b/canyon_crud/src/lib.rs @@ -4,6 +4,7 @@ pub mod bounds; pub mod crud; pub mod mapper; pub mod query_elements; +pub mod rows; pub use query_elements::operators::*; diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs new file mode 100644 index 00000000..157fde81 --- /dev/null +++ b/canyon_crud/src/rows.rs @@ -0,0 +1,76 @@ +use tokio_postgres::types::FromSql; +use crate::bounds::{PrimaryKey, QueryParameter}; +use crate::crud::Transaction; +use crate::mapper::RowMapper; + +/// Lightweight wrapper over the collection of results of the different crates +/// supported by Canyon-SQL. +/// +/// Even tho the wrapping seems meaningless, this allows us to provide internal +/// operations that are too difficult or to ugly to implement in the macros that +/// will call the query method of Crud. +pub enum CanyonRows { + #[cfg(feature = "postgres")] Postgres(Vec), + #[cfg(feature = "mssql")] Tiberius(Vec>) +} + +impl CanyonRows { + // /// Type constructor, returning the correct variant of Self wrapping the collection of results + // /// by the given database connection + // pub fn new( + // conn: &DatabaseConnection, + // res: Vec + // ) -> Self { + // match conn { + // #[cfg(feature = "postgres")] DatabaseConnection::Postgres(_) => Self::Postgres(res), + // #[cfg(feature = "mssql")] DatabaseConnection::SqlServer(_) => Self::Tiberius(res) + // } + // } + + /// Consumes `self` and returns the wrapped [`std::vec::Vec`] with the instances of T + pub fn into_results(self) -> Vec where T: Transaction + RowMapper { + match self { + #[cfg(feature = "postgres")] Self::Postgres(v) => v + .iter() + .map(|row| T::deserialize_postgresql(row)) + .collect(), + #[cfg(feature = "mssql")] Self::Tiberius(v) => v + .iter() + .flatten() + .map(|row| T::deserialize_sqlserver(&row)) + .collect() + } + } + + /// + pub fn set_primary_key_after_insert<'a, T, PkType: PrimaryKey>(self, pk: &str) -> PkType { + match self { + #[cfg(feature = "postgres")] Self::Postgres(v) => { + v.get(0) + .expect("No value found on the returning clause") + .get::<&str, PkType>(pk) + // .to_owned(); + } + #[cfg(feature = "mssql")] Self::Tiberius(v) => { + v.into_iter() + .flatten() + .collect::>() + .remove(0) + .get::(pk) + .expect("SQL Server primary key type failed to be set as value") + // .to_owned() + } + } + } +} + +// r.iter().map(|row| T::deserialize_postgresql(row)).collect() +// .map(|row| T::deserialize_sqlserver(&row)) + + +// canyon_sql::crud::DatabaseType::SqlServer => { +// self.#pk_ident = res.sqlserver.get(0) +// .expect("No value found on the returning clause") +// .get::<#pk_type, &str>(#primary_key) +// .expect("SQL Server primary key type failed to be set as value") +// .to_owned(); diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index 543a5121..d33d5086 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -50,38 +50,13 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri #primary_key ); - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( + <#ty as canyon_sql::crud::Transaction<#ty>>::query_for_rows( stmt, values, datasource_name - ).await; - - // TODO Convertir a canyon rows - match result { - Ok(res) => { - match res.get_active_ds() { - canyon_sql::crud::DatabaseType::PostgreSql => { - self.#pk_ident = res.postgres.get(0) - .expect("No value found on the returning clause") - .get::<&str, #pk_type>(#primary_key) - .to_owned(); - - Ok(()) - }, - canyon_sql::crud::DatabaseType::SqlServer => { - self.#pk_ident = res.sqlserver.get(0) - .expect("No value found on the returning clause") - .get::<#pk_type, &str>(#primary_key) - .expect("SQL Server primary key type failed to be set as value") - .to_owned(); - - Ok(()) - } - } - }, - Err(e) => Err(e) - } - } + ).await + .set_primary_key_after_insert(); + } } else { quote! { let stmt = format!( From 32e611a5186cd129850f94cb8784ec788602b907 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Sun, 16 Apr 2023 21:08:57 +0200 Subject: [PATCH 36/82] WIP - Whole rework of the workspace, more cfg's --- Cargo.toml | 30 ++- canyon_connection/Cargo.toml | 15 +- .../src/canyon_database_connector.rs | 49 ++--- canyon_connection/src/datasources.rs | 14 +- canyon_connection/src/lib.rs | 10 +- canyon_crud/Cargo.toml | 23 +- canyon_crud/src/bounds.rs | 208 +++++++++--------- canyon_crud/src/crud.rs | 47 ++-- canyon_crud/src/mapper.rs | 9 +- .../src/query_elements/query_builder.rs | 6 +- canyon_crud/src/rows.rs | 61 ++--- canyon_macros/src/query_operations/insert.rs | 23 +- canyon_observer/Cargo.toml | 19 +- canyon_observer/src/lib.rs | 1 + canyon_observer/src/migrations/handler.rs | 15 +- .../src/migrations/information_schema.rs | 7 +- canyon_observer/src/migrations/memory.rs | 7 +- 17 files changed, 272 insertions(+), 272 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 70dea99d..01e1eafe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,32 +5,44 @@ version = "0.2.0" [workspace] members = [ + "canyon_connection", "canyon_observer", "canyon_macros", "canyon_crud", - "canyon_connection", "tests" ] -[workspace.dependencies] +[dependencies] # Project crates canyon_macros = { version = "0.2.0", path = "canyon_macros" } canyon_observer = { version = "0.2.0", path = "canyon_observer" } -canyon_crud = { version = "0.2.0", path = "canyon_crud", features = ["postgres", "mssql"] } -canyon_connection = { version = "0.2.0", path = "canyon_connection", features = ["postgres", "mssql"] } +canyon_crud = { version = "0.2.0", path = "canyon_crud" } +canyon_connection = { version = "0.2.0", path = "canyon_connection" } -tokio = { version = "1.21.2", features = ["full"] } + +#tokio = { workspace = true } +#tokio-util = { workspace = true } +#tokio-postgres = { workspace = true } +#tiberius = { worskpace = true } + +[workspace.dependencies] +canyon_crud = { version = "0.2.0", path = "canyon_crud" } +canyon_connection = { version = "0.2.0", path = "canyon_connection" } + +tokio = { version = "1.27.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["compat"] } tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"] } tiberius = { version = "0.12.1", features = ["tds73", "chrono", "integrated-auth-gssapi"] } +serde = { version = "1.0.138", features = ["derive"] } + futures = "0.3.25" indexmap = "1.9.1" async-std = "1.12.0" lazy_static = "1.4.0" -serde = { version = "1.0.138", features = ["derive"] } toml = "0.7.3" +async-trait = "0.1.68" [workspace.package] version = "0.2.0" @@ -41,3 +53,9 @@ homepage = "https://github.com/zerodaycode/Canyon-SQL" readme = "../README.md" license = "MIT" description = "A Rust ORM and QueryBuilder" + +[features] +default = ["postgres", "canyon_connection/tokio-postgres", "canyon_crud/tokio-postgres", "canyon_observer/tokio-postgres"] +postgres = ["canyon_connection/tokio-postgres", "canyon_crud/tokio-postgres", "canyon_observer/tokio-postgres"] +mssql = ["canyon_connection/tiberius"] +mssql-integrated-auth = ["mssql"] \ No newline at end of file diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 323e91a3..36fc8a97 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -10,20 +10,13 @@ license.workspace = true description.workspace = true [dependencies] -tokio = { workspace = true, features = ["full"], optional = true } -tokio-util = { workspace = true, features = ["compat"], optional = true } -tokio-postgres = { workspace = true, features = ["with-chrono-0_4"], optional = true } -tiberius = { workspace = true, features = ["tds73", "chrono", "integrated-auth-gssapi"], optional = true } - +tokio = { workspace = true } +tokio-util = { workspace = true } +tokio-postgres = { workspace = true, optional = true } +tiberius = { workspace = true, optional = true } futures = { workspace = true } indexmap = { workspace = true } async-std = { workspace = true } lazy_static = { workspace = true } serde = { workspace = true, features = ["derive"] } toml = { workspace = true } - -[features] -default = ["postgres"] -postgres = ["tokio", "tokio-postgres", "tokio-util"] -mssql = ["tiberius", "tiberius/tds73", "tiberius/chrono"] -mssql-integrated-auth = ["mssql", "tiberius/integrated-auth-gssapi"] \ No newline at end of file diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 27d59799..5e324cde 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -1,32 +1,31 @@ -#[cfg(feature = "mssql")] use async_std::net::TcpStream; +#[cfg(feature = "tiberius")] use async_std::net::TcpStream; use serde::Deserialize; -#[cfg(feature = "mssql")] use tiberius::{AuthMethod, Config}; -#[cfg(feature = "postgres")] use tokio_postgres::{Client, NoTls}; +#[cfg(feature = "tiberius")] use tiberius::{AuthMethod, Config}; +#[cfg(feature = "tokio-postgres")] use tokio_postgres::{Client, NoTls}; use crate::datasources::DatasourceConfig; /// Represents the current supported databases by Canyon -#[derive(Deserialize, Debug, Eq, PartialEq, Clone, Copy, Default)] +#[derive(Deserialize, Debug, Eq, PartialEq, Clone, Copy)] pub enum DatabaseType { - #[default] #[serde(alias = "postgres", alias = "postgresql")] - #[cfg(feature = "postgres")] + #[cfg(feature = "tokio-postgres")] PostgreSql, #[serde(alias = "sqlserver", alias = "mssql")] - #[cfg(feature = "mssql")] + #[cfg(feature = "tiberius")] SqlServer, } /// A connection with a `PostgreSQL` database -#[cfg(feature = "postgres")] +#[cfg(feature = "tokio-postgres")] pub struct PostgreSqlConnection { pub client: Client, // pub connection: Connection, // TODO Hold it, or not to hold it... that's the question! } -#[cfg(feature = "mssql")] /// A connection with a `SqlServer` database +#[cfg(feature = "tiberius")] pub struct SqlServerConnection { pub client: &'static mut tiberius::Client, } @@ -36,8 +35,8 @@ pub struct SqlServerConnection { /// process them and generates a pool of 1 to 1 database connection for /// every datasource defined. pub enum DatabaseConnection { - #[cfg(feature = "postgres")] Postgres(PostgreSqlConnection), - #[cfg(feature = "mssql")] SqlServer(SqlServerConnection), + #[cfg(feature = "tokio-postgres")] Postgres(PostgreSqlConnection), + #[cfg(feature = "tiberius")] SqlServer(SqlServerConnection), } unsafe impl Send for DatabaseConnection {} @@ -48,7 +47,7 @@ impl DatabaseConnection { datasource: &DatasourceConfig, ) -> Result> { match datasource.get_db_type() { - #[cfg(feature = "postgres")] + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => { let (username, password) = match &datasource.auth { crate::datasources::Auth::Postgres(postgres_auth) => match postgres_auth { @@ -56,7 +55,7 @@ impl DatabaseConnection { (username.as_str(), password.as_str()) } }, - #[cfg(feature = "mssql")] + #[cfg(feature = "tiberius")] crate::datasources::Auth::SqlServer(_) => { panic!("Found SqlServer auth configuration for a PostgreSQL datasource") } @@ -85,7 +84,7 @@ impl DatabaseConnection { // connection: new_connection, })) } - #[cfg(feature = "mssql")] + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => { let mut config = Config::new(); @@ -95,14 +94,14 @@ impl DatabaseConnection { // Using SQL Server authentication. config.authentication(match &datasource.auth { - #[cfg(feature = "postgres")] crate::datasources::Auth::Postgres(_) => { + #[cfg(feature = "tokio-postgres")] crate::datasources::Auth::Postgres(_) => { panic!("Found PostgreSQL auth configuration for a SqlServer database") } crate::datasources::Auth::SqlServer(sql_server_auth) => match sql_server_auth { crate::datasources::SqlServerAuth::Basic { username, password } => { AuthMethod::sql_server(username, password) } - #[cfg(feature = "mssql-integrated-auth")] + #[cfg(feature = "mssql-integrated-auth")] // TODO pending, or remove the cfg? crate::datasources::SqlServerAuth::Integrated => AuthMethod::Integrated, }, }); @@ -135,21 +134,19 @@ impl DatabaseConnection { } } - #[cfg(feature = "postgres")] + #[cfg(feature = "tokio-postgres")] pub fn postgres_connection(&self) -> Option<&PostgreSqlConnection> { - if let DatabaseConnection::Postgres(conn) = self { - Some(conn) - } else { - None + match self { + DatabaseConnection::Postgres(conn) => Some(conn), + _ => panic!() } } - #[cfg(feature = "mssql")] + #[cfg(feature = "tiberius")] pub fn sqlserver_connection(&mut self) -> Option<&mut SqlServerConnection> { - if let DatabaseConnection::SqlServer(conn) = self { - Some(conn) - } else { - None + match self { + DatabaseConnection::SqlServer(conn) => Some(conn), + _ => panic!() } } } diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 4dc76dbb..2a553cb3 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -49,7 +49,7 @@ fn load_ds_config_from_array() { assert_eq!(ds_1.properties.db_name, "triforce2"); assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); - #[cfg(feature = "postgres")] assert_eq!(ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) + #[cfg(feature = "tokio-postgres")] assert_eq!(ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) } /// #[derive(Deserialize, Debug, Clone)] @@ -72,8 +72,8 @@ pub struct DatasourceConfig { impl DatasourceConfig { pub fn get_db_type(&self) -> DatabaseType { match self.auth { - #[cfg(feature = "postgres")] Auth::Postgres(_) => DatabaseType::PostgreSql, - #[cfg(feature = "mssql")] Auth::SqlServer(_) => DatabaseType::SqlServer, + #[cfg(feature = "tokio-postgres")] Auth::Postgres(_) => DatabaseType::PostgreSql, + #[cfg(feature = "tiberius")] Auth::SqlServer(_) => DatabaseType::SqlServer, } } } @@ -81,22 +81,22 @@ impl DatasourceConfig { #[derive(Deserialize, Debug, Clone, PartialEq)] pub enum Auth { #[serde(alias = "PostgreSQL", alias = "postgresql", alias = "postgres")] - #[cfg(feature = "postgres")] + #[cfg(feature = "tokio-postgres")] Postgres(PostgresAuth), #[serde(alias = "SqlServer", alias = "sqlserver", alias = "mssql")] - #[cfg(feature = "mssql")] + #[cfg(feature = "tiberius")] SqlServer(SqlServerAuth), } #[derive(Deserialize, Debug, Clone, PartialEq)] -#[cfg(feature = "postgres")] +#[cfg(feature = "tokio-postgres")] pub enum PostgresAuth { #[serde(alias = "Basic", alias = "basic")] Basic { username: String, password: String }, } #[derive(Deserialize, Debug, Clone, PartialEq)] -#[cfg(feature = "mssql")] +#[cfg(feature = "tiberius")] pub enum SqlServerAuth { #[serde(alias = "Basic", alias = "basic")] Basic { username: String, password: String }, diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index 535e59fd..cc240034 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -1,10 +1,10 @@ -#[cfg(feature = "mssql")] pub extern crate async_std; +#[cfg(feature = "tiberius")] pub extern crate async_std; pub extern crate futures; pub extern crate lazy_static; -#[cfg(feature = "mssql")] pub extern crate tiberius; -#[cfg(feature = "postgres")] pub extern crate tokio; -#[cfg(feature = "postgres")] pub extern crate tokio_postgres; -#[cfg(feature = "postgres")] pub extern crate tokio_util; +#[cfg(feature = "tiberius")] pub extern crate tiberius; +pub extern crate tokio; +#[cfg(feature = "tokio-postgres")] pub extern crate tokio_postgres; +#[cfg(feature = "tokio-postgres")] pub extern crate tokio_util; pub mod canyon_database_connector; pub mod datasources; diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index 0e4f0854..6f6ee233 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -10,18 +10,19 @@ license.workspace = true description.workspace = true [dependencies] -tokio = { workspace = true, features = ["full"], optional = true } -tokio-util = { workspace = true, features = ["compat"], optional = true } -tokio-postgres = { workspace = true, features = ["with-chrono-0_4"], optional = true } -tiberius = { workspace = true, features = ["tds73", "chrono", "integrated-auth-gssapi"], optional = true } - +#tokio = { workspace = true, features = ["full"], optional = true } +#tokio-util = { workspace = true, features = ["compat"], optional = true } +#tokio-postgres = { workspace = true, features = ["with-chrono-0_4"], optional = true } +#tiberius = { workspace = true, features = ["tds73", "chrono", "integrated-auth-gssapi"], optional = true } +tokio-postgres = { workspace = true, optional = true } +tiberius = { workspace = true, optional = true } chrono = { version = "0.4", features = ["serde"] } async-trait = { version = "0.1.50" } -canyon_connection = { version = "0.2.0", path = "../canyon_connection", features = ["postgres", "mssql"] } +canyon_connection = { workspace = true, path = "../canyon_connection" } -[features] -default = ["postgres"] -postgres = ["tokio", "tokio-postgres", "tokio-util"] -mssql = ["tiberius", "tiberius/tds73", "tiberius/chrono"] -mssql-integrated-auth = ["mssql", "tiberius/integrated-auth-gssapi"] \ No newline at end of file +#[features] +#default = ["postgres"] +#postgres = ["tokio", "tokio-postgres", "tokio-util"] +#mssql = ["tiberius", "tiberius/tds73", "tiberius/chrono"] +#mssql-integrated-auth = ["mssql", "tiberius/integrated-auth-gssapi"] \ No newline at end of file diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs index 3589cb65..218f337b 100644 --- a/canyon_crud/src/bounds.rs +++ b/canyon_crud/src/bounds.rs @@ -1,19 +1,16 @@ -#![allow(clippy::extra_unused_lifetimes)] - use crate::{ crud::{CrudOperations, Transaction}, mapper::RowMapper, }; -#[cfg(feature = "postgres")] +#[cfg(feature = "tokio-postgres")] use canyon_connection::tokio_postgres::{self, types::ToSql}; -#[cfg(feature = "mssql")] -use canyon_connection::tiberius::{self, ColumnData, IntoSql}; +#[cfg(feature = "tiberius")] +use canyon_connection::tiberius::{self, ColumnData, FromSql, IntoSql}; use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use std::any::Any; -use tiberius::FromSql; /// Created for retrieve the field's name of a field of a struct, giving /// the Canyon's autogenerated enum with the variants that maps this @@ -83,21 +80,18 @@ pub trait ForeignKeyable { fn get_fk_column(&self, column: &str) -> Option<&dyn QueryParameter<'_>>; } -/// To define trait objects that helps to relates the necessary bounds in the 'IN` SQL clause -pub trait InClauseValues: ToSql + ToString {} - /// Generic abstraction to represent any of the Row types /// from the client crates pub trait Row { fn as_any(&self) -> &dyn Any; } -#[cfg(feature = "postgres")] impl Row for tokio_postgres::Row { +#[cfg(feature = "tokio-postgres")] impl Row for tokio_postgres::Row { fn as_any(&self) -> &dyn Any { self } } -#[cfg(feature = "mssql")] impl Row for tiberius::Row { +#[cfg(feature = "tiberius")] impl Row for tiberius::Row { fn as_any(&self) -> &dyn Any { self } @@ -105,6 +99,7 @@ pub trait Row { /// Generic abstraction for hold a Column type that will be one of the Column /// types present in the dependent crates +// #[derive(Copy, Clone)] pub struct Column<'a> { name: &'a str, type_: ColumnType, @@ -116,46 +111,47 @@ impl<'a> Column<'a> { pub fn column_type(&self) -> &ColumnType { &self.type_ } - pub fn type_(&'a self) -> &'_ dyn Type { - match &self.type_ { - #[cfg(feature = "postgres")] ColumnType::Postgres(v) => v as &'a dyn Type, - #[cfg(feature = "mssql")] ColumnType::SqlServer(v) => v as &'a dyn Type, - } - } + // pub fn type_(&'a self) -> &'_ dyn Type { + // match (*self).type_ { + // #[cfg(feature = "tokio-postgres")] ColumnType::Postgres(v) => v as &'a dyn Type, + // #[cfg(feature = "tiberius")] ColumnType::SqlServer(v) => v as &'a dyn Type, + // } + // } } pub trait Type { fn as_any(&self) -> &dyn Any; } -#[cfg(feature = "postgres")] impl Type for tokio_postgres::types::Type { +#[cfg(feature = "tokio-postgres")] impl Type for tokio_postgres::types::Type { fn as_any(&self) -> &dyn Any { self } } -#[cfg(feature = "mssql")] impl Type for tiberius::ColumnType { +#[cfg(feature = "tiberius")] impl Type for tiberius::ColumnType { fn as_any(&self) -> &dyn Any { self } } /// Wrapper over the dependencies Column's types +// #[derive(Copy)] pub enum ColumnType { - #[cfg(feature = "postgres")] Postgres(tokio_postgres::types::Type), - #[cfg(feature = "mssql")] SqlServer(tiberius::ColumnType), + #[cfg(feature = "tokio-postgres")] Postgres(tokio_postgres::types::Type), + #[cfg(feature = "tiberius")] SqlServer(tiberius::ColumnType), } pub trait RowOperations { - #[cfg(feature = "postgres")] + #[cfg(feature = "tokio-postgres")] fn get_postgres<'a, Output>(&'a self, col_name: &str) -> Output where Output: tokio_postgres::types::FromSql<'a>; - #[cfg(feature = "mssql")] + #[cfg(feature = "tiberius")] fn get_mssql<'a, Output>(&self, col_name: &str) -> Output where Output: tiberius::FromSql<'a>; - #[cfg(feature = "postgres")] + #[cfg(feature = "tokio-postgres")] fn get_postgres_opt<'a, Output>(&'a self, col_name: &str) -> Option where Output: tokio_postgres::types::FromSql<'a>; - #[cfg(feature = "mssql")] + #[cfg(feature = "tiberius")] fn get_mssql_opt<'a, Output>(&'a self, col_name: &str) -> Option where Output: tokio_postgres::types::FromSql<'a>; @@ -163,7 +159,7 @@ pub trait RowOperations { } impl RowOperations for &dyn Row { - #[cfg(feature = "postgres")] + #[cfg(feature = "tokio-postgres")] fn get_postgres<'a, Output>(&'a self, col_name: &str) -> Output where Output: tokio_postgres::types::FromSql<'a> { @@ -172,7 +168,7 @@ impl RowOperations for &dyn Row { }; panic!() // TODO into result and propagate } - #[cfg(feature = "mssql")] + #[cfg(feature = "tiberius")] fn get_mssql<'a, Output>(&'a self, col_name: &str) -> Output where Output: tiberius::FromSql<'a> { @@ -184,7 +180,7 @@ impl RowOperations for &dyn Row { panic!() // TODO into result and propagate } - #[cfg(feature = "postgres")] + #[cfg(feature = "tokio-postgres")] fn get_postgres_opt<'a, Output>(&'a self, col_name: &str) -> Option where Output: tokio_postgres::types::FromSql<'a> { @@ -194,7 +190,7 @@ impl RowOperations for &dyn Row { panic!() // TODO into result and propagate } - #[cfg(feature = "mssql")] + #[cfg(feature = "tiberius")] fn get_mssql_opt<'a, Output>(&'a self, col_name: &str) -> Option where Output: tiberius::FromSql<'a> { @@ -242,8 +238,8 @@ impl RowOperations for &dyn Row { /// Defines a trait for represent type bounds against the allowed /// data types supported by Canyon to be used as query parameters. pub trait QueryParameter<'a>: std::fmt::Debug + Sync + Send { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync); - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_>; + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync); + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_>; } /// The implementation of the [`canyon_connection::tiberius`] [`IntoSql`] for the @@ -254,7 +250,7 @@ pub trait QueryParameter<'a>: std::fmt::Debug + Sync + Send { /// a collection of [`QueryParameter<'a>`], in order to allow a workflow /// that is not dependent of the specific type of the argument that holds /// the query parameters of the database connectors -#[cfg(feature = "mssql")] +#[cfg(feature = "tiberius")] impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { fn into_sql(self) -> ColumnData<'a> { self.as_sqlserver_param() @@ -262,198 +258,198 @@ impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { } impl<'a> QueryParameter<'a> for bool { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::Bit(Some(*self)) } } impl<'a> QueryParameter<'a> for i16 { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(*self)) } } impl<'a> QueryParameter<'a> for &i16 { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(*self) } } impl<'a> QueryParameter<'a> for Option<&i16> { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for i32 { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self)) } } impl<'a> QueryParameter<'a> for &i32 { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(*self) } } impl<'a> QueryParameter<'a> for Option<&i32> { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for f32 { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some(*self)) } } impl<'a> QueryParameter<'a> for &f32 { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(*self) } } impl<'a> QueryParameter<'a> for Option<&f32> { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some( *self.expect("Error on an f32 value on QueryParameter<'_>"), )) } } impl<'a> QueryParameter<'a> for f64 { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some(*self)) } } impl<'a> QueryParameter<'a> for &f64 { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(*self) } } impl<'a> QueryParameter<'a> for Option<&f64> { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some( *self.expect("Error on an f64 value on QueryParameter<'_>"), )) } } impl<'a> QueryParameter<'a> for i64 { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self)) } } impl<'a> QueryParameter<'a> for &i64 { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(*self) } } impl<'a> QueryParameter<'a> for Option<&i64> { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for String { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Owned(self.to_owned()))) } } impl<'a> QueryParameter<'a> for &String { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Borrowed(self))) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { match self { Some(string) => ColumnData::String(Some(std::borrow::Cow::Owned(string.to_owned()))), None => ColumnData::String(None), @@ -461,10 +457,10 @@ impl<'a> QueryParameter<'a> for Option { } } impl<'a> QueryParameter<'a> for Option<&String> { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { match self { Some(string) => ColumnData::String(Some(std::borrow::Cow::Borrowed(string))), None => ColumnData::String(None), @@ -472,18 +468,18 @@ impl<'a> QueryParameter<'a> for Option<&String> { } } impl<'a> QueryParameter<'_> for &'_ str { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Borrowed(*self))) } } impl<'a> QueryParameter<'a> for Option<&'_ str> { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { match *self { Some(str) => ColumnData::String(Some(std::borrow::Cow::Borrowed(str))), None => ColumnData::String(None), @@ -491,82 +487,82 @@ impl<'a> QueryParameter<'a> for Option<&'_ str> { } } impl<'a> QueryParameter<'_> for NaiveDate { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for NaiveTime { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for NaiveDateTime { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for DateTime { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option> { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for DateTime { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for Option> { - #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index 59fc5bd3..b3395df5 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -19,25 +19,39 @@ use crate::rows::CanyonRows; /// automatically map it to an struct. #[async_trait] pub trait Transaction { + // /// Performs a query against the targeted database by the selected or + // /// the defaulted datasource, returning a collection of instances of *T* + // async fn query<'a, S, Z>( + // stmt: S, + // params: Z, + // datasource_name: &'a str, + // ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> + // where + // S: AsRef + Display + Sync + Send + 'a, + // Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, + // { + // Self::query_for_rows(stmt, params, datasource_name) + // .await + // .map(|res| res.into_results()) + // } + /// Performs a query against the targeted database by the selected or /// the defaulted datasource, wrapping the resultant collection of entities - /// in [`super::rows::Rows`]. This ones provides custom operations that - /// facilitates the macro operations. + /// in [`super::rows::Rows`] async fn query<'a, S, Z>( stmt: S, params: Z, datasource_name: &'a str, - ) -> Result> + ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> where S: AsRef + Display + Sync + Send + 'a, - Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, - T: Transaction + RowMapper + Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a { let mut guarded_cache = CACHED_DATABASE_CONN.lock().await; let database_conn = get_database_connection(datasource_name, &mut guarded_cache); - match database_conn { - #[cfg(feature = "postgres")] DatabaseConnection::Postgres(_) => { + match *database_conn { + #[cfg(feature = "tokio-postgres")] DatabaseConnection::Postgres(_) => { postgres_query_launcher::launch::( database_conn, stmt.to_string(), @@ -45,7 +59,7 @@ pub trait Transaction { ) .await } - #[cfg(feature = "mssql")] DatabaseConnection::SqlServer(_) => { + #[cfg(feature = "tiberius")] DatabaseConnection::SqlServer(_) => { sqlserver_query_launcher::launch::( database_conn, &mut stmt.to_string(), @@ -147,21 +161,17 @@ where fn delete_query_datasource(datasource_name: &str) -> DeleteQueryBuilder<'_, T>; } -#[cfg(feature = "postgres")] +#[cfg(feature = "tokio-postgres")] mod postgres_query_launcher { use crate::bounds::QueryParameter; use canyon_connection::canyon_database_connector::DatabaseConnection; - use crate::crud::Transaction; - use crate::mapper::RowMapper; use crate::rows::CanyonRows; pub async fn launch<'a, T>( db_conn: &DatabaseConnection, stmt: String, params: &'a [&'_ dyn QueryParameter<'_>], - ) -> Result> - where T: Transaction + RowMapper - { + ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> { let mut m_params = Vec::new(); for param in params { m_params.push(param.as_postgres_param()); @@ -179,24 +189,21 @@ mod postgres_query_launcher { } -#[cfg(feature = "mssql")] +#[cfg(feature = "tiberius")] mod sqlserver_query_launcher { use crate::{ bounds::QueryParameter, canyon_connection::{canyon_database_connector::DatabaseConnection, tiberius::Query}, }; - use crate::crud::Transaction; - use crate::mapper::RowMapper; use crate::rows::CanyonRows; pub async fn launch<'a, T, Z>( db_conn: &mut DatabaseConnection, stmt: &mut String, params: Z, - ) -> Result> + ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> where - Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, - T: Transaction + RowMapper + Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a { // Re-generate de insert statement to adequate it to the SQL SERVER syntax to retrieve the PK value(s) after insert if stmt.contains("RETURNING") { diff --git a/canyon_crud/src/mapper.rs b/canyon_crud/src/mapper.rs index 0114bd3a..7996c0fc 100644 --- a/canyon_crud/src/mapper.rs +++ b/canyon_crud/src/mapper.rs @@ -1,5 +1,5 @@ -#[cfg(feature = "postgres")] use canyon_connection::tokio_postgres; -#[cfg(feature = "mssql")] use canyon_connection::tiberius; +#[cfg(feature = "tokio-postgres")] use canyon_connection::tokio_postgres; +#[cfg(feature = "tiberius")] use canyon_connection::tiberius; use crate::crud::Transaction; @@ -7,7 +7,6 @@ use crate::crud::Transaction; /// from some supported database in Canyon-SQL into a user's defined /// type `T` pub trait RowMapper>: Sized { - fn deserialize_postgresql(row: &tokio_postgres::Row) -> T; - - fn deserialize_sqlserver(row: &tiberius::Row) -> T; + #[cfg(feature = "tokio-postgres")] fn deserialize_postgresql(row: &tokio_postgres::Row) -> T; + #[cfg(feature = "tiberius")] fn deserialize_sqlserver(row: &tiberius::Row) -> T; } diff --git a/canyon_crud/src/query_elements/query_builder.rs b/canyon_crud/src/query_elements/query_builder.rs index c26c5642..9d102f87 100644 --- a/canyon_crud/src/query_elements/query_builder.rs +++ b/canyon_crud/src/query_elements/query_builder.rs @@ -26,7 +26,7 @@ pub mod ops { /// hierarchy. /// /// For example, the [`super::QueryBuilder`] type holds the data - /// necessary for track the SQL sentece while it's being generated + /// necessary for track the SQL sentence while it's being generated /// thought the fluent builder, and provides the behaviour of /// the common elements defined in this trait. /// @@ -44,7 +44,7 @@ pub mod ops { /// just one type. pub trait QueryBuilder<'a, T> where - T: Debug + CrudOperations + Transaction + RowMapper, + T: CrudOperations + Transaction + RowMapper, { /// Returns a read-only reference to the underlying SQL sentence, /// with the same lifetime as self @@ -173,7 +173,7 @@ where self.query.params.to_vec(), self.datasource_name, ) - .await?) + .await?.into_results::()) } pub fn r#where>(&mut self, r#where: Z, op: impl Operator) { diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs index 157fde81..669407ae 100644 --- a/canyon_crud/src/rows.rs +++ b/canyon_crud/src/rows.rs @@ -1,5 +1,4 @@ -use tokio_postgres::types::FromSql; -use crate::bounds::{PrimaryKey, QueryParameter}; +use std::marker::PhantomData; use crate::crud::Transaction; use crate::mapper::RowMapper; @@ -9,12 +8,13 @@ use crate::mapper::RowMapper; /// Even tho the wrapping seems meaningless, this allows us to provide internal /// operations that are too difficult or to ugly to implement in the macros that /// will call the query method of Crud. -pub enum CanyonRows { - #[cfg(feature = "postgres")] Postgres(Vec), - #[cfg(feature = "mssql")] Tiberius(Vec>) +pub enum CanyonRows { + #[cfg(feature = "tokio-postgres")] Postgres(Vec), + #[cfg(feature = "tiberius")] Tiberius(Vec>), + UnusableTypeMarker(PhantomData) } -impl CanyonRows { +impl CanyonRows { // /// Type constructor, returning the correct variant of Self wrapping the collection of results // /// by the given database connection // pub fn new( @@ -22,55 +22,24 @@ impl CanyonRows { // res: Vec // ) -> Self { // match conn { - // #[cfg(feature = "postgres")] DatabaseConnection::Postgres(_) => Self::Postgres(res), - // #[cfg(feature = "mssql")] DatabaseConnection::SqlServer(_) => Self::Tiberius(res) + // #[cfg(feature = "tokio-postgres")] DatabaseConnection::Postgres(_) => Self::Postgres(res), + // #[cfg(feature = "tiberius")] DatabaseConnection::SqlServer(_) => Self::Tiberius(res) // } // } /// Consumes `self` and returns the wrapped [`std::vec::Vec`] with the instances of T - pub fn into_results(self) -> Vec where T: Transaction + RowMapper { + pub fn into_results>(self) -> Vec where T: Transaction { match self { - #[cfg(feature = "postgres")] Self::Postgres(v) => v + #[cfg(feature = "tokio-postgres")] Self::Postgres(v) => v .iter() - .map(|row| T::deserialize_postgresql(row)) + .map(|row| Z::deserialize_postgresql(row)) .collect(), - #[cfg(feature = "mssql")] Self::Tiberius(v) => v + #[cfg(feature = "tiberius")] Self::Tiberius(v) => v .iter() .flatten() - .map(|row| T::deserialize_sqlserver(&row)) - .collect() - } - } - - /// - pub fn set_primary_key_after_insert<'a, T, PkType: PrimaryKey>(self, pk: &str) -> PkType { - match self { - #[cfg(feature = "postgres")] Self::Postgres(v) => { - v.get(0) - .expect("No value found on the returning clause") - .get::<&str, PkType>(pk) - // .to_owned(); - } - #[cfg(feature = "mssql")] Self::Tiberius(v) => { - v.into_iter() - .flatten() - .collect::>() - .remove(0) - .get::(pk) - .expect("SQL Server primary key type failed to be set as value") - // .to_owned() - } + .map(|row| Z::deserialize_sqlserver(&row)) + .collect(), + _ => panic!("This branch will never ever should be reachable") } } } - -// r.iter().map(|row| T::deserialize_postgresql(row)).collect() -// .map(|row| T::deserialize_sqlserver(&row)) - - -// canyon_sql::crud::DatabaseType::SqlServer => { -// self.#pk_ident = res.sqlserver.get(0) -// .expect("No value found on the returning clause") -// .get::<#pk_type, &str>(#primary_key) -// .expect("SQL Server primary key type failed to be set as value") -// .to_owned(); diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index d33d5086..45ce1187 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -50,13 +50,28 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri #primary_key ); - <#ty as canyon_sql::crud::Transaction<#ty>>::query_for_rows( + let rows = <#ty as canyon_sql::crud::Transaction<#ty>>::query_for_rows( stmt, values, datasource_name - ).await - .set_primary_key_after_insert(); - } + ).await; + + match rows { + #[cfg(feature = "tokio-postgres")] Self::Postgres(v) => { + v.remove(0) + .expect("No value found on the returning clause for Postgres") + .get::<&str, #pk_type>(#primary_key) + } + #[cfg(feature = "tiberius")] Self::Tiberius(v) => { + v.into_iter() + .flatten() + .collect::>() + .remove(0) + .get::<#pk_type, &str>(#primary_key) + .expect("SQL Server primary key type failed to be set as value") + } + } + } } else { quote! { let stmt = format!( diff --git a/canyon_observer/Cargo.toml b/canyon_observer/Cargo.toml index 67918e37..9f59b093 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_observer/Cargo.toml @@ -10,19 +10,18 @@ license.workspace = true description.workspace = true [dependencies] -tokio = { version = "1.9.0", features = ["full"] } -tokio-postgres = { version = "0.7.2" , features=["with-chrono-0_4"] } -async-trait = { version = "0.1.50" } -regex = "1.5" -walkdir = "2" +canyon_crud = { workspace = true } +canyon_connection = { workspace = true } +tokio = { workspace = true } +tokio-postgres = { workspace = true, optional = true } +tiberius = { workspace = true, optional = true } +async-trait = { workspace = true } +# transform to opts with migrations feature +regex = "1.5" # opt +walkdir = "2" # opt proc-macro2 = "1.0.27" syn = { version = "1.0.86", features = ["full", "parsing"] } quote = "1.0.9" - -# Debug partialdebug = "0.2.0" -# Internal dependencies -canyon_crud = { version = "0.2.0", path = "../canyon_crud" } -canyon_connection = { version = "0.2.0", path = "../canyon_connection" } diff --git a/canyon_observer/src/lib.rs b/canyon_observer/src/lib.rs index 1a0766e5..41e0dd42 100644 --- a/canyon_observer/src/lib.rs +++ b/canyon_observer/src/lib.rs @@ -11,6 +11,7 @@ /// in order to perform the migrations pub mod migrations; +extern crate canyon_connection; extern crate canyon_crud; mod constants; diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index 739b4cae..aafc6fd7 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -52,7 +52,7 @@ impl Migrations { // Tracked entities that must be migrated whenever Canyon starts let schema_status = Self::fetch_database(&datasource.name, datasource.get_db_type()).await; - let database_tables_schema_info = Self::map_rows(schema_status); + let database_tables_schema_info = Self::map_rows(schema_status, datasource.get_db_type()); // We filter the tables from the schema that aren't Canyon entities let mut user_database_tables = vec![]; @@ -98,21 +98,26 @@ impl Migrations { panic!( "Error querying the schema information for the datasource: {datasource_name}" ) - }) + }).into_results() } /// Handler for parse the result of query the information of some database schema, /// and extract the content of the returned rows into custom structures with /// the data well organized for every entity present on that schema - fn map_rows(db_results: Vec) -> Vec { + fn map_rows(db_results: Vec, db_type: DatabaseType) -> Vec { let mut schema_info: Vec = Vec::new(); + let row_retriever_fn_ptr = match db_type { + DatabaseType::PostgreSql => RowOperations::get_postgres::<&str>, + DatabaseType::SqlServer => RowOperations::get_mssql::<&str>, + }; for res_row in db_results.iter() .map(|row| &row as &dyn Row) { let unique_table = schema_info .iter_mut() - .find(|table| table.table_name == *res_row.get::<&str>("table_name").to_owned()); + // TODO To be able to remove row from our code, use a match statement to get table name + .find(|table| table.table_name == *row_retriever_fn_ptr("table_name").to_owned()); match unique_table { Some(table) => { /* If a table entity it's already present on the collection, we add it @@ -124,7 +129,7 @@ impl Migrations { collection yet, we must create a new instance and attach it the founded columns data in this iteration */ let mut new_table = TableMetadata { - table_name: res_row.get::<&str>("table_name").to_owned(), + table_name: row_retriever_fn_ptr("table_name").to_owned(), columns: Vec::new(), }; Self::get_columns_metadata(res_row, &mut new_table); diff --git a/canyon_observer/src/migrations/information_schema.rs b/canyon_observer/src/migrations/information_schema.rs index bdf9f48e..527f4084 100644 --- a/canyon_observer/src/migrations/information_schema.rs +++ b/canyon_observer/src/migrations/information_schema.rs @@ -1,4 +1,5 @@ -use canyon_connection::{tiberius::ColumnType as TIB_TY, tokio_postgres::types::Type as TP_TYP}; +#[cfg(feature = "tokio-postgres")] use canyon_connection::tokio_postgres::types::Type as TP_TYP; +#[cfg(feature = "tiberius")] use canyon_connection::tiberius::ColumnType as TIB_TY; use canyon_crud::bounds::{Column, ColumnType, Row, RowOperations}; /// Model that represents the database entities that belongs to the current schema. @@ -40,7 +41,7 @@ impl ColumnMetadataTypeValue { /// Retrieves the value stored in a [`Column`] for a passed [`Row`] pub fn get_value(row: &dyn Row, col: &Column) -> Self { match col.column_type() { - ColumnType::Postgres(v) => { + #[cfg(feature = "tokio-postgres")] ColumnType::Postgres(v) => { match *v { TP_TYP::NAME | TP_TYP::VARCHAR | TP_TYP::TEXT => { Self::StringValue(row.get_opt::<&str>(col.name()).map(|opt| opt.to_owned())) @@ -49,7 +50,7 @@ impl ColumnMetadataTypeValue { _ => Self::NoneValue, // TODO watchout this one } } - ColumnType::SqlServer(v) => match v { + #[cfg(feature = "tiberius")] ColumnType::SqlServer(v) => match v { TIB_TY::NChar | TIB_TY::NVarchar | TIB_TY::BigChar | TIB_TY::BigVarChar => { Self::StringValue(row.get_opt::<&str>(col.name()).map(|opt| opt.to_owned())) } diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index aac81d3b..d5a1311a 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -219,10 +219,9 @@ impl CanyonMemory { /// Generates, if not exists the `canyon_memory` table #[cfg(not(cargo_check))] async fn create_memory(datasource_name: &str, database_type: &DatabaseType) { - let query = if database_type == &DatabaseType::PostgreSql { - constants::postgresql_queries::CANYON_MEMORY_TABLE - } else { - constants::mssql_queries::CANYON_MEMORY_TABLE + let query = match database_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => constants::postgresql_queries::CANYON_MEMORY_TABLE, + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => constants::mssql_queries::CANYON_MEMORY_TABLE }; Self::query(query, [], datasource_name) From 41843a4b87f05855d7a9077bac60bff89f5cf15f Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Mon, 17 Apr 2023 14:33:00 +0200 Subject: [PATCH 37/82] #wip - Addressing the issues of the CanyonMemory module to the new source code structure --- Cargo.toml | 4 +- canyon_crud/src/bounds.rs | 10 ++++ canyon_crud/src/rows.rs | 29 +++++++----- canyon_observer/src/migrations/handler.rs | 4 +- canyon_observer/src/migrations/memory.rs | 58 +++++++++++++++-------- 5 files changed, 71 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 01e1eafe..919cdf24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ license = "MIT" description = "A Rust ORM and QueryBuilder" [features] -default = ["postgres", "canyon_connection/tokio-postgres", "canyon_crud/tokio-postgres", "canyon_observer/tokio-postgres"] +default = ["postgres"] postgres = ["canyon_connection/tokio-postgres", "canyon_crud/tokio-postgres", "canyon_observer/tokio-postgres"] -mssql = ["canyon_connection/tiberius"] +mssql = ["canyon_connection/tiberius", "canyon_observer/tiberius", "canyon_observer/tiberius"] mssql-integrated-auth = ["mssql"] \ No newline at end of file diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs index 218f337b..93b00ed1 100644 --- a/canyon_crud/src/bounds.rs +++ b/canyon_crud/src/bounds.rs @@ -91,11 +91,21 @@ pub trait Row { self } } +#[cfg(feature = "tokio-postgres")] impl Row for &tokio_postgres::Row { + fn as_any(&self) -> &dyn Any { + *self + } +} #[cfg(feature = "tiberius")] impl Row for tiberius::Row { fn as_any(&self) -> &dyn Any { self } } +#[cfg(feature = "tiberius")] impl Row for &tiberius::Row { + fn as_any(&self) -> &dyn Any { + self + } +} /// Generic abstraction for hold a Column type that will be one of the Column /// types present in the dependent crates diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs index 669407ae..3760b76d 100644 --- a/canyon_crud/src/rows.rs +++ b/canyon_crud/src/rows.rs @@ -15,17 +15,24 @@ pub enum CanyonRows { } impl CanyonRows { - // /// Type constructor, returning the correct variant of Self wrapping the collection of results - // /// by the given database connection - // pub fn new( - // conn: &DatabaseConnection, - // res: Vec - // ) -> Self { - // match conn { - // #[cfg(feature = "tokio-postgres")] DatabaseConnection::Postgres(_) => Self::Postgres(res), - // #[cfg(feature = "tiberius")] DatabaseConnection::SqlServer(_) => Self::Tiberius(res) - // } - // } + #[cfg(feature = "tokio-postgres")] + pub fn get_postgres_rows(self) -> Vec { + match self { + Self::Postgres(v) => v, + _ => panic!("This branch will never ever should be reachable") + } + } + + #[cfg(feature = "tiberius")] + pub fn get_tiberius_rows(self) -> Vec { + match self { + Self::Tiberius(v) => v + .iter() + .flatten() + .collect(), + _ => panic!("This branch will never ever should be reachable") + } + } /// Consumes `self` and returns the wrapped [`std::vec::Vec`] with the instances of T pub fn into_results>(self) -> Vec where T: Transaction { diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index aafc6fd7..cb9f27ee 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -107,8 +107,8 @@ impl Migrations { fn map_rows(db_results: Vec, db_type: DatabaseType) -> Vec { let mut schema_info: Vec = Vec::new(); let row_retriever_fn_ptr = match db_type { - DatabaseType::PostgreSql => RowOperations::get_postgres::<&str>, - DatabaseType::SqlServer => RowOperations::get_mssql::<&str>, + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => RowOperations::get_postgres::<&str>, + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => RowOperations::get_mssql::<&str>, }; for res_row in db_results.iter() diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index d5a1311a..735b8d2c 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -1,10 +1,9 @@ use crate::constants; -use canyon_crud::{bounds::RowOperations, crud::Transaction, DatabaseType, DatasourceConfig}; +use canyon_crud::{crud::Transaction, DatabaseType, DatasourceConfig}; use regex::Regex; use std::collections::HashMap; use std::fs; use walkdir::WalkDir; -use canyon_crud::bounds::Row; use super::register_types::CanyonRegisterEntity; @@ -71,21 +70,42 @@ impl CanyonMemory { let res = Self::query("SELECT * FROM canyon_memory", [], &datasource.name) .await .expect("Error querying Canyon Memory"); - let mem_results = res.map(|row| &row as &dyn Row); // Manually maps the results let mut db_rows = Vec::new(); - for row in mem_results { - let db_row = CanyonMemoryRow { - id: row.get::("id"), - filepath: row.get::<&str>("filepath"), - struct_name: row.get::<&str>("struct_name"), - declared_table_name: row.get::<&str>("declared_table_name"), - }; - db_rows.push(db_row); + #[cfg(feature = "tokio-postgres")] { + let mem_results: Vec = res.get_postgres_rows(); + for row in mem_results { + let db_row = CanyonMemoryRow { + id: row.get::<&str, i32>("id"), + filepath: row.get::<&str, String>("filepath"), + struct_name: row.get::<&str, String>("struct_name").to_owned(), + declared_table_name: row.get::<&str, String>("declared_table_name").to_owned(), + }; + db_rows.push(db_row); + } + } + #[cfg(feature = "tiberius")] { + let mem_results: Vec = res.get_tiberius_rows(); + for row in mem_results { + let db_row = CanyonMemoryRow { + id: row.get::("id"), + filepath: row.get::<&str, &str>("filepath"), + struct_name: row.get::<&str, &str>("struct_name"), + declared_table_name: row.get::<&str, &str>("declared_table_name"), + }; + db_rows.push(db_row); + } } - // Parses the source code files looking for the #[canyon_entity] annotated classes + Self::populate_memory(datasource, canyon_entities, db_rows).await + } + + async fn populate_memory( + datasource: &DatasourceConfig, + canyon_entities: &[CanyonRegisterEntity<'_>], + db_rows: Vec + ) -> CanyonMemory { let mut mem = Self { memory: Vec::new(), renamed_entities: HashMap::new(), @@ -107,7 +127,7 @@ impl CanyonMemory { && old.struct_name == _struct.struct_name && old.declared_table_name == _struct.declared_table_name) { - updates.push(old.struct_name); + updates.push(&old.struct_name); let stmt = format!( "UPDATE canyon_memory SET filepath = '{}', struct_name = '{}', declared_table_name = '{}' \ WHERE id = {}", @@ -138,12 +158,12 @@ impl CanyonMemory { } // Deletes the records from canyon_memory, because they stopped to be tracked by Canyon - for db_row in db_rows.into_iter() { + for db_row in db_rows.iter() { if !mem .memory .iter() .any(|entity| entity.struct_name == db_row.struct_name) - && !updates.contains(&db_row.struct_name) + && !updates.contains(&&(db_row.struct_name)) { save_canyon_memory_query( format!( @@ -250,11 +270,11 @@ fn save_canyon_memory_query(stmt: String, ds_name: &str) { /// Represents a single row from the `canyon_memory` table #[derive(Debug)] -struct CanyonMemoryRow<'a> { +struct CanyonMemoryRow { id: i32, - filepath: &'a str, - struct_name: &'a str, - declared_table_name: &'a str, + filepath: String, + struct_name: String, + declared_table_name: String, } /// Represents the data that will be serialized in the `canyon_memory` table From ad9e1eab6c098608c184834d5f847acf742bdc60 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 18 Apr 2023 09:10:15 +0200 Subject: [PATCH 38/82] #wip - Addressing the issues of the Handler module to the new source code structure --- canyon_crud/src/crud.rs | 2 +- canyon_crud/src/rows.rs | 38 ++++++++++++++++--- canyon_observer/src/migrations/handler.rs | 26 ++++++------- .../src/migrations/information_schema.rs | 4 +- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index b3395df5..895019ca 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -236,6 +236,6 @@ mod sqlserver_query_launcher { .into_results() .await?; - Ok(CanyonRows::Tiberius(_results)) + Ok(CanyonRows::Tiberius(_results.iter().flatten().collect())) } } diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs index 3760b76d..98e00507 100644 --- a/canyon_crud/src/rows.rs +++ b/canyon_crud/src/rows.rs @@ -10,7 +10,7 @@ use crate::mapper::RowMapper; /// will call the query method of Crud. pub enum CanyonRows { #[cfg(feature = "tokio-postgres")] Postgres(Vec), - #[cfg(feature = "tiberius")] Tiberius(Vec>), + #[cfg(feature = "tiberius")] Tiberius(Vec), UnusableTypeMarker(PhantomData) } @@ -26,10 +26,7 @@ impl CanyonRows { #[cfg(feature = "tiberius")] pub fn get_tiberius_rows(self) -> Vec { match self { - Self::Tiberius(v) => v - .iter() - .flatten() - .collect(), + Self::Tiberius(v) => v, _ => panic!("This branch will never ever should be reachable") } } @@ -43,10 +40,39 @@ impl CanyonRows { .collect(), #[cfg(feature = "tiberius")] Self::Tiberius(v) => v .iter() - .flatten() .map(|row| Z::deserialize_sqlserver(&row)) .collect(), _ => panic!("This branch will never ever should be reachable") } } } + +#[cfg(feature = "tokio-postgres")] +impl IntoIterator for CanyonRows { + type Item = tokio_postgres::Row; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + #[cfg(feature = "tokio-postgres")] { + match self { + Self::Postgres(v) => v.into_iter(), + _ => panic!() + } + } + } +} + +#[cfg(feature = "tiberius")] +impl IntoIterator for CanyonRows { + type Item = tiberius::Row; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + #[cfg(feature = "tokio-postgres")] { + match self { + Self::Postgres(v) => v.into_iter(), + _ => panic!() + } + } + } +} diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index cb9f27ee..79152756 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -1,5 +1,6 @@ use canyon_connection::{datasources::Migrations as MigrationsStatus, DATASOURCES}; use partialdebug::placeholder::PartialDebug; +use canyon_crud::rows::CanyonRows; use crate::{ canyon_crud::{ @@ -86,38 +87,35 @@ impl Migrations { async fn fetch_database( datasource_name: &str, db_type: DatabaseType, - ) -> Vec { + ) -> CanyonRows { let query = match db_type { - DatabaseType::PostgreSql => constants::postgresql_queries::FETCH_PUBLIC_SCHEMA, - DatabaseType::SqlServer => constants::mssql_queries::FETCH_PUBLIC_SCHEMA, + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => constants::postgresql_queries::FETCH_PUBLIC_SCHEMA, + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => constants::mssql_queries::FETCH_PUBLIC_SCHEMA, }; - Self::query(query, [], datasource_name) - .await - .unwrap_or_else(|_| { - panic!( - "Error querying the schema information for the datasource: {datasource_name}" - ) - }).into_results() + Self::query(query, [], datasource_name).await + .unwrap_or_else(|_| {panic!( + "Error querying the schema information for the datasource: {datasource_name}" + )}) } /// Handler for parse the result of query the information of some database schema, /// and extract the content of the returned rows into custom structures with /// the data well organized for every entity present on that schema - fn map_rows(db_results: Vec, db_type: DatabaseType) -> Vec { + fn map_rows(db_results: CanyonRows, db_type: DatabaseType) -> Vec { let mut schema_info: Vec = Vec::new(); let row_retriever_fn_ptr = match db_type { #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => RowOperations::get_postgres::<&str>, #[cfg(feature = "tiberius")] DatabaseType::SqlServer => RowOperations::get_mssql::<&str>, }; - for res_row in db_results.iter() + for res_row in db_results.into_iter() .map(|row| &row as &dyn Row) { let unique_table = schema_info .iter_mut() // TODO To be able to remove row from our code, use a match statement to get table name - .find(|table| table.table_name == *row_retriever_fn_ptr("table_name").to_owned()); + .find(|table| table.table_name == row_retriever_fn_ptr(&res_row, "table_name")); match unique_table { Some(table) => { /* If a table entity it's already present on the collection, we add it @@ -129,7 +127,7 @@ impl Migrations { collection yet, we must create a new instance and attach it the founded columns data in this iteration */ let mut new_table = TableMetadata { - table_name: row_retriever_fn_ptr("table_name").to_owned(), + table_name: row_retriever_fn_ptr(&res_row, "table_name").to_string(), columns: Vec::new(), }; Self::get_columns_metadata(res_row, &mut new_table); diff --git a/canyon_observer/src/migrations/information_schema.rs b/canyon_observer/src/migrations/information_schema.rs index 527f4084..91bc4db4 100644 --- a/canyon_observer/src/migrations/information_schema.rs +++ b/canyon_observer/src/migrations/information_schema.rs @@ -44,9 +44,9 @@ impl ColumnMetadataTypeValue { #[cfg(feature = "tokio-postgres")] ColumnType::Postgres(v) => { match *v { TP_TYP::NAME | TP_TYP::VARCHAR | TP_TYP::TEXT => { - Self::StringValue(row.get_opt::<&str>(col.name()).map(|opt| opt.to_owned())) + Self::StringValue(row.get_postgres_opt::<&str>(col.name()).map(|opt| opt.to_owned())) } - TP_TYP::INT4 => Self::IntValue(row.get_opt::(col.name())), + TP_TYP::INT4 => Self::IntValue(row.get_postgres_opt::(col.name())), _ => Self::NoneValue, // TODO watchout this one } } From 6d9c66381ba2307b25e78989a41affbbd6b621a3 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 18 Apr 2023 12:42:26 +0200 Subject: [PATCH 39/82] #wip - Reaching the goal of the conditional compilation by database client --- .../src/canyon_database_connector.rs | 1 + canyon_crud/src/bounds.rs | 51 +-- canyon_crud/src/rows.rs | 16 +- canyon_observer/src/constants.rs | 5 +- canyon_observer/src/migrations/handler.rs | 36 +- .../src/migrations/information_schema.rs | 4 +- canyon_observer/src/migrations/processor.rs | 312 +++++++++--------- .../src/migrations/register_types.rs | 58 +--- src/lib.rs | 13 +- 9 files changed, 242 insertions(+), 254 deletions(-) diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 5e324cde..c58451fb 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -135,6 +135,7 @@ impl DatabaseConnection { } #[cfg(feature = "tokio-postgres")] + #[allow(unreachable_patterns)] pub fn postgres_connection(&self) -> Option<&PostgreSqlConnection> { match self { DatabaseConnection::Postgres(conn) => Some(conn), diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs index 93b00ed1..ce0b498b 100644 --- a/canyon_crud/src/bounds.rs +++ b/canyon_crud/src/bounds.rs @@ -215,31 +215,36 @@ impl RowOperations for &dyn Row { fn columns(&self) -> Vec { let mut cols = vec![]; - /* if self.as_any().is::() { - self.as_any() - .downcast_ref::() - .expect("Not a tokio postgres Row for column") - .columns() - .iter() - .for_each(|c| { - cols.push(Column { - name: c.name(), - type_: ColumnType::Postgres(c.type_().to_owned()), + #[cfg(feature = "tokio-postgres")] { + if self.as_any().is::() { + self.as_any() + .downcast_ref::() + .expect("Not a tokio postgres Row for column") + .columns() + .iter() + .for_each(|c| { + cols.push(Column { + name: c.name(), + type_: ColumnType::Postgres(c.type_().to_owned()), + }) }) - }) - } else { - self.as_any() - .downcast_ref::() - .expect("Not a Tiberius Row for column") - .columns() - .iter() - .for_each(|c| { - cols.push(Column { - name: c.name(), - type_: ColumnType::SqlServer(c.column_type()), + } + } + #[cfg(feature = "tiberius")] { + if self.as_any().is::() { + self.as_any() + .downcast_ref::() + .expect("Not a Tiberius Row for column") + .columns() + .iter() + .for_each(|c| { + cols.push(Column { + name: c.name(), + type_: ColumnType::SqlServer(c.column_type()), + }) }) - }) - }; */ + }; + } cols } diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs index 98e00507..efddfcb8 100644 --- a/canyon_crud/src/rows.rs +++ b/canyon_crud/src/rows.rs @@ -53,11 +53,9 @@ impl IntoIterator for CanyonRows { type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { - #[cfg(feature = "tokio-postgres")] { - match self { - Self::Postgres(v) => v.into_iter(), - _ => panic!() - } + match self { + Self::Postgres(v) => v.into_iter(), + _ => panic!() } } } @@ -68,11 +66,9 @@ impl IntoIterator for CanyonRows { type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { - #[cfg(feature = "tokio-postgres")] { - match self { - Self::Postgres(v) => v.into_iter(), - _ => panic!() - } + match self { + Self::Tiberius(v) => v.into_iter(), + _ => panic!() } } } diff --git a/canyon_observer/src/constants.rs b/canyon_observer/src/constants.rs index c9db74e8..ae746e6e 100644 --- a/canyon_observer/src/constants.rs +++ b/canyon_observer/src/constants.rs @@ -1,5 +1,6 @@ pub const NUMERIC_PK_DATATYPE: [&str; 6] = ["i16", "u16", "i32", "u32", "i64", "u64"]; +#[cfg(feature = "tokio-postgres")] pub mod postgresql_queries { pub static CANYON_MEMORY_TABLE: &str = "CREATE TABLE IF NOT EXISTS canyon_memory ( id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, @@ -35,6 +36,7 @@ pub mod postgresql_queries { table_schema = 'public';"; } +#[cfg(feature = "tiberius")] pub mod mssql_queries { pub static CANYON_MEMORY_TABLE: &str = "IF OBJECT_ID(N'[dbo].[canyon_memory]', N'U') IS NULL BEGIN @@ -142,7 +144,7 @@ pub mod rust_type { pub const OPT_NAIVE_DATE_TIME: &str = "Option"; } -/// TODO +#[cfg(feature = "tokio-postgres")] pub mod postgresql_type { pub const INT_8: &str = "int8"; pub const SMALL_INT: &str = "smallint"; @@ -155,6 +157,7 @@ pub mod postgresql_type { pub const DATETIME: &str = "timestamp without time zone"; } +#[cfg(feature = "tiberius")] pub mod sqlserver_type { pub const TINY_INT: &str = "TINY INT"; pub const SMALL_INT: &str = "SMALL INT"; diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index 79152756..884b86f4 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -104,33 +104,32 @@ impl Migrations { /// the data well organized for every entity present on that schema fn map_rows(db_results: CanyonRows, db_type: DatabaseType) -> Vec { let mut schema_info: Vec = Vec::new(); - let row_retriever_fn_ptr = match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => RowOperations::get_postgres::<&str>, - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => RowOperations::get_mssql::<&str>, - }; for res_row in db_results.into_iter() - .map(|row| &row as &dyn Row) + // .map(|row| &row as &dyn Row) { let unique_table = schema_info .iter_mut() // TODO To be able to remove row from our code, use a match statement to get table name - .find(|table| table.table_name == row_retriever_fn_ptr(&res_row, "table_name")); + .find(|table| check_for_table_name(table, &res_row as &dyn Row)); match unique_table { Some(table) => { /* If a table entity it's already present on the collection, we add it the founded columns related to the table */ - Self::get_columns_metadata(res_row, table); + Self::get_columns_metadata(&res_row as &dyn Row, table); } None => { /* If there's no table for a given "table_name" property on the collection yet, we must create a new instance and attach it the founded columns data in this iteration */ let mut new_table = TableMetadata { - table_name: row_retriever_fn_ptr(&res_row, "table_name").to_string(), + table_name: match db_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => get_table_name_from_tp_row(&res_row), + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => get_table_name_from_tib_row(&res_row), + }, columns: Vec::new(), }; - Self::get_columns_metadata(res_row, &mut new_table); + Self::get_columns_metadata(&res_row as &dyn Row, &mut new_table); schema_info.push(new_table); } }; @@ -223,3 +222,22 @@ impl Migrations { }; } } + + +#[cfg(feature = "tokio-postgres")] +fn get_table_name_from_tp_row(res_row: &tokio_postgres::Row) -> String { + res_row.get::<&str, String>("table_name") +} +#[cfg(feature = "tiberius")] +fn get_table_name_from_tib_row(res_row: &tiberius::Row) -> String { + res_row.get::<&str, &str>("table_name").unwrap_or_default().to_string() +} + +fn check_for_table_name(table: &&mut TableMetadata, res_row: &dyn Row) -> bool { + #[cfg(feature = "tokio-postgres")] { + table.table_name == res_row.get_postgres::<&str>("table_name") + } + #[cfg(feature = "tiberius")] { + table.table_name == row_retriever_fn_ptr(&res_row, "table_name") + } +} diff --git a/canyon_observer/src/migrations/information_schema.rs b/canyon_observer/src/migrations/information_schema.rs index 91bc4db4..d93c7007 100644 --- a/canyon_observer/src/migrations/information_schema.rs +++ b/canyon_observer/src/migrations/information_schema.rs @@ -52,10 +52,10 @@ impl ColumnMetadataTypeValue { } #[cfg(feature = "tiberius")] ColumnType::SqlServer(v) => match v { TIB_TY::NChar | TIB_TY::NVarchar | TIB_TY::BigChar | TIB_TY::BigVarChar => { - Self::StringValue(row.get_opt::<&str>(col.name()).map(|opt| opt.to_owned())) + Self::StringValue(row.get_mssql_opt::<&str>(col.name()).map(|opt| opt.to_owned())) } TIB_TY::Int2 | TIB_TY::Int4 | TIB_TY::Int8 | TIB_TY::Intn => { - Self::IntValue(row.get_opt::(col.name())) + Self::IntValue(row.get_mssql_opt::(col.name())) } _ => Self::NoneValue, }, diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index c3995bbf..ff89bdc9 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -169,7 +169,7 @@ impl MigrationsProcessor { entity_name: &'a str, entity_fields: Vec, current_table_metadata: Option<&'a TableMetadata>, - db_type: DatabaseType, + _db_type: DatabaseType, ) { if current_table_metadata.is_none() { return; @@ -188,12 +188,15 @@ impl MigrationsProcessor { .collect(); for column_metadata in columns_name_to_delete { - if db_type == DatabaseType::SqlServer && !column_metadata.is_nullable { - self.drop_column_not_null( - entity_name, - column_metadata.column_name.clone(), - MigrationsHelper::get_datatype_from_column_metadata(column_metadata), - ) + #[cfg(feature = "tiberius")] + { + if _db_type == DatabaseType::SqlServer && !column_metadata.is_nullable { + self.drop_column_not_null( + entity_name, + column_metadata.column_name.clone(), + MigrationsHelper::get_datatype_from_column_metadata(column_metadata), + ) + } } self.delete_column(entity_name, column_metadata.column_name.clone()); } @@ -243,7 +246,7 @@ impl MigrationsProcessor { ))); } - fn drop_column_not_null( + #[cfg(feature = "tiberius")] fn drop_column_not_null( &mut self, table_name: &str, column_name: String, @@ -619,6 +622,7 @@ impl MigrationsHelper { } } + #[cfg(feature = "tiberius")] fn get_datatype_from_column_metadata(current_column_metadata: &ColumnMetadata) -> String { // TODO Add all SQL Server text datatypes if vec!["nvarchar", "varchar"] @@ -640,20 +644,25 @@ impl MigrationsHelper { canyon_register_entity_field: &CanyonRegisterEntityField, current_column_metadata: &ColumnMetadata, ) -> bool { - if db_type == DatabaseType::PostgreSql { - canyon_register_entity_field - .to_postgres_alter_syntax() - .to_lowercase() - == current_column_metadata.datatype - } else if db_type == DatabaseType::SqlServer { - // TODO Search a better way to get the datatype without useless info (like "VARCHAR(MAX)") - canyon_register_entity_field - .to_sqlserver_alter_syntax() - .to_lowercase() - == current_column_metadata.datatype - } else { - todo!() + #[cfg(feature = "tokio-postgres")] { + if db_type == DatabaseType::PostgreSql { + return canyon_register_entity_field + .to_postgres_alter_syntax() + .to_lowercase() + == current_column_metadata.datatype; + } + } + #[cfg(feature = "tiberius")] { + if db_type == DatabaseType::SqlServer { + // TODO Search a better way to get the datatype without useless info (like "VARCHAR(MAX)") + return canyon_register_entity_field + .to_sqlserver_alter_syntax() + .to_lowercase() + == current_column_metadata.datatype; + } } + + return false; } fn extract_foreign_key_annotation(field_annotations: &[String]) -> (String, String) { @@ -752,60 +761,60 @@ impl DatabaseOperation for TableOperation { let stmt = match self { TableOperation::CreateTable(table_name, table_fields) => { - if db_type == DatabaseType::PostgreSql { - format!( - "CREATE TABLE \"{table_name}\" ({});", - table_fields - .iter() - .map(|entity_field| format!( - "\"{}\" {}", - entity_field.field_name, - entity_field.to_postgres_syntax() - )) - .collect::>() - .join(", ") - ) - } else if db_type == DatabaseType::SqlServer { - format!( - "CREATE TABLE {:?} ({:?});", - table_name, - table_fields - .iter() - .map(|entity_field| format!( - "{} {}", - entity_field.field_name, - entity_field.to_sqlserver_syntax() - )) - .collect::>() - .join(", ") - ) - .replace('"', "") - } else { - todo!("There's no other databases supported in Canyon-SQL right now") + match db_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => { + format!( + "CREATE TABLE \"{table_name}\" ({});", + table_fields + .iter() + .map(|entity_field| format!( + "\"{}\" {}", + entity_field.field_name, + entity_field.to_postgres_syntax() + )) + .collect::>() + .join(", ") + ) + } + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => { + format!( + "CREATE TABLE {:?} ({:?});", + table_name, + table_fields + .iter() + .map(|entity_field| format!( + "{} {}", + entity_field.field_name, + entity_field.to_sqlserver_syntax() + )) + .collect::>() + .join(", ") + ) + .replace('"', "") + } } } TableOperation::AlterTableName(old_table_name, new_table_name) => { - if db_type == DatabaseType::PostgreSql { - format!("ALTER TABLE {old_table_name} RENAME TO {new_table_name};") - } else if db_type == DatabaseType::SqlServer { - /* - Notes: Brackets around `old_table_name`, p.e. - exec sp_rename ['league'], 'leagues' // NOT VALID! - is only allowed for compound names split by a dot. - exec sp_rename ['random.league'], 'leagues' // OK - - CARE! This doesn't mean that we are including the schema. - exec sp_rename ['dbo.random.league'], 'leagues' // OK - exec sp_rename 'dbo.league', 'leagues' // OK - Schema doesn't need brackets - - Due to the automatic mapped name from Rust to DB and vice-versa, this won't - be an allowed behaviour for now, only with the table_name parameter on the - CanyonEntity annotation. - */ - format!("exec sp_rename '{old_table_name}', '{new_table_name}';") - } else { - todo!() + match db_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + format!("ALTER TABLE {old_table_name} RENAME TO {new_table_name};"), + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + /* + Notes: Brackets around `old_table_name`, p.e. + exec sp_rename ['league'], 'leagues' // NOT VALID! + is only allowed for compound names split by a dot. + exec sp_rename ['random.league'], 'leagues' // OK + + CARE! This doesn't mean that we are including the schema. + exec sp_rename ['dbo.random.league'], 'leagues' // OK + exec sp_rename 'dbo.league', 'leagues' // OK - Schema doesn't need brackets + + Due to the automatic mapped name from Rust to DB and vice-versa, this won't + be an allowed behaviour for now, only with the table_name parameter on the + CanyonEntity annotation. + */ + format!("exec sp_rename '{old_table_name}', '{new_table_name}';") } } @@ -816,48 +825,46 @@ impl DatabaseOperation for TableOperation { table_to_reference, column_to_reference, ) => { - if db_type == DatabaseType::PostgreSql { - format!( - "ALTER TABLE {table_name} ADD CONSTRAINT {foreign_key_name} \ - FOREIGN KEY ({column_foreign_key}) REFERENCES {table_to_reference} ({column_to_reference});" - ) - } else if db_type == DatabaseType::SqlServer { - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } else { - todo!() + match db_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + format!( + "ALTER TABLE {table_name} ADD CONSTRAINT {foreign_key_name} \ + FOREIGN KEY ({column_foreign_key}) REFERENCES {table_to_reference} ({column_to_reference});" + ), + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } TableOperation::DeleteTableForeignKey(table_with_foreign_key, constraint_name) => { - if db_type == DatabaseType::PostgreSql { - format!( - "ALTER TABLE {table_with_foreign_key} DROP CONSTRAINT {constraint_name};", - ) - } else if db_type == DatabaseType::SqlServer { - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } else { - todo!() + match db_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + format!( + "ALTER TABLE {table_with_foreign_key} DROP CONSTRAINT {constraint_name};", + ), + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } TableOperation::AddTablePrimaryKey(table_name, entity_field) => { - if db_type == DatabaseType::PostgreSql { - format!( - "ALTER TABLE \"{table_name}\" ADD PRIMARY KEY (\"{}\");", - entity_field.field_name - ) - } else if db_type == DatabaseType::SqlServer { - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } else { - todo!() + match db_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + format!( + "ALTER TABLE \"{table_name}\" ADD PRIMARY KEY (\"{}\");", + entity_field.field_name + ), + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } TableOperation::DeleteTablePrimaryKey(table_name, primary_key_name) => { - if db_type == DatabaseType::PostgreSql || db_type == DatabaseType::SqlServer { - format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;") - } else { - todo!() + match db_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;"), + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;") } } }; @@ -876,11 +883,11 @@ enum ColumnOperation { AlterColumnType(String, CanyonRegisterEntityField), AlterColumnDropNotNull(String, CanyonRegisterEntityField), // SQL server specific operation - SQL server can't drop a NOT NULL column - DropNotNullBeforeDropColumn(String, String, String), - AlterColumnSetNotNull(String, CanyonRegisterEntityField), + #[cfg(feature = "tiberius")] DropNotNullBeforeDropColumn(String, String, String), + #[cfg(feature = "tokio-postgres")] AlterColumnSetNotNull(String, CanyonRegisterEntityField), // TODO if implement through annotations, modify for both GENERATED {ALWAYS, BY DEFAULT} - AlterColumnAddIdentity(String, CanyonRegisterEntityField), - AlterColumnDropIdentity(String, CanyonRegisterEntityField), + #[cfg(feature = "tokio-postgres")] AlterColumnAddIdentity(String, CanyonRegisterEntityField), + #[cfg(feature = "tokio-postgres")] AlterColumnDropIdentity(String, CanyonRegisterEntityField), } impl Transaction for ColumnOperation {} @@ -892,51 +899,47 @@ impl DatabaseOperation for ColumnOperation { let stmt = match self { ColumnOperation::CreateColumn(table_name, entity_field) => - if db_type == DatabaseType::PostgreSql { - format!( - "ALTER TABLE \"{}\" ADD COLUMN \"{}\" {};", - table_name, - entity_field.field_name, - entity_field.to_postgres_syntax()) - } else if db_type == DatabaseType::SqlServer { - format!( - "ALTER TABLE {} ADD \"{}\" {};", - table_name, - entity_field.field_name, - entity_field.to_sqlserver_syntax() - ) - } else { - todo!() - }, + match db_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + format!( + "ALTER TABLE \"{}\" ADD COLUMN \"{}\" {};", + table_name, + entity_field.field_name, + entity_field.to_postgres_syntax() + ), + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + format!( + "ALTER TABLE {} ADD \"{}\" {};", + table_name, + entity_field.field_name, + entity_field.to_sqlserver_syntax() + ) + } ColumnOperation::DeleteColumn(table_name, column_name) => { // TODO Check if operation for SQL server is different format!("ALTER TABLE \"{table_name}\" DROP COLUMN \"{column_name}\";") }, ColumnOperation::AlterColumnType(table_name, entity_field) => - if db_type == DatabaseType::PostgreSql { - format!( - "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" TYPE {};", - entity_field.field_name, entity_field.to_postgres_alter_syntax() - ) - } else if db_type == DatabaseType::SqlServer { - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } else { - todo!() - } - , + match db_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + format!( + "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" TYPE {};", + entity_field.field_name, entity_field.to_postgres_alter_syntax() + ), + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") + } ColumnOperation::AlterColumnDropNotNull(table_name, entity_field) => - if db_type == DatabaseType::PostgreSql { - format!("ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" DROP NOT NULL;", entity_field.field_name) - } else if db_type == DatabaseType::SqlServer { - format!( - "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NULL", - entity_field.field_name, entity_field.to_sqlserver_alter_syntax() - ) - } else { - todo!() - } - - ColumnOperation::DropNotNullBeforeDropColumn(table_name, column_name, column_datatype) => + match db_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + format!("ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" DROP NOT NULL;", entity_field.field_name), + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + format!( + "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NULL", + entity_field.field_name, entity_field.to_sqlserver_alter_syntax() + ) + } + #[cfg(feature = "tiberius")] ColumnOperation::DropNotNullBeforeDropColumn(table_name, column_name, column_datatype) => format!( "ALTER TABLE {table_name} ALTER COLUMN {column_name} {column_datatype} NULL; DECLARE @tableName VARCHAR(MAX) = '{table_name}' DECLARE @columnName VARCHAR(MAX) = '{column_name}' @@ -955,11 +958,11 @@ impl DatabaseOperation for ColumnOperation { "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" SET NOT NULL;", entity_field.field_name ), - ColumnOperation::AlterColumnAddIdentity(table_name, entity_field) => format!( + #[cfg(feature = "tokio-postgres")] ColumnOperation::AlterColumnAddIdentity(table_name, entity_field) => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" ADD GENERATED ALWAYS AS IDENTITY;", entity_field.field_name ), - ColumnOperation::AlterColumnDropIdentity(table_name, entity_field) => format!( + #[cfg(feature = "tokio-postgres")] ColumnOperation::AlterColumnDropIdentity(table_name, entity_field) => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" DROP IDENTITY;", entity_field.field_name ), }; @@ -984,15 +987,14 @@ impl DatabaseOperation for SequenceOperation { let stmt = match self { SequenceOperation::ModifySequence(table_name, entity_field) => { - if db_type == DatabaseType::PostgreSql { - format!( - "SELECT setval(pg_get_serial_sequence('\"{table_name}\"', '{}'), max(\"{}\")) from \"{table_name}\";", - entity_field.field_name, entity_field.field_name - ) - } else if db_type == DatabaseType::SqlServer { - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } else { - todo!() + match db_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + format!( + "SELECT setval(pg_get_serial_sequence('\"{table_name}\"', '{}'), max(\"{}\")) from \"{table_name}\";", + entity_field.field_name, entity_field.field_name + ), + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } }; diff --git a/canyon_observer/src/migrations/register_types.rs b/canyon_observer/src/migrations/register_types.rs index 470944db..57ef6e39 100644 --- a/canyon_observer/src/migrations/register_types.rs +++ b/canyon_observer/src/migrations/register_types.rs @@ -1,8 +1,8 @@ use regex::Regex; -use crate::constants::{ - postgresql_type, regex_patterns, rust_type, sqlserver_type, NUMERIC_PK_DATATYPE, -}; +use crate::constants::{regex_patterns, rust_type, NUMERIC_PK_DATATYPE}; +#[cfg(feature = "tokio-postgres")] use crate::constants::postgresql_type; +#[cfg(feature = "tiberius")] use crate::constants::sqlserver_type; /// This file contains `Rust` types that represents an entry on the `CanyonRegister` /// where `Canyon` tracks the user types that has to manage @@ -28,7 +28,7 @@ pub struct CanyonRegisterEntityField { impl CanyonRegisterEntityField { /// Return the postgres datatype and parameters to create a column for a given rust type - pub fn to_postgres_syntax(&self) -> String { + #[cfg(feature = "tokio-postgres")] pub fn to_postgres_syntax(&self) -> String { let rust_type_clean = self.field_type.replace(' ', ""); match rust_type_clean.as_str() { @@ -74,7 +74,7 @@ impl CanyonRegisterEntityField { /// Return the postgres datatype and parameters to create a column for a given rust type /// for Microsoft SQL Server - pub fn to_sqlserver_syntax(&self) -> String { + #[cfg(feature = "tiberius")] pub fn to_sqlserver_syntax(&self) -> String { let rust_type_clean = self.field_type.replace(' ', ""); match rust_type_clean.as_str() { @@ -120,7 +120,7 @@ impl CanyonRegisterEntityField { } } - pub fn to_postgres_alter_syntax(&self) -> String { + #[cfg(feature = "tokio-postgres")] pub fn to_postgres_alter_syntax(&self) -> String { let mut rust_type_clean = self.field_type.replace(' ', ""); let rs_type_is_optional = self.field_type.to_uppercase().starts_with("OPTION"); @@ -162,7 +162,7 @@ impl CanyonRegisterEntityField { } } - pub fn to_sqlserver_alter_syntax(&self) -> String { + #[cfg(feature = "tiberius")] pub fn to_sqlserver_alter_syntax(&self) -> String { let mut rust_type_clean = self.field_type.replace(' ', ""); let rs_type_is_optional = self.field_type.to_uppercase().starts_with("OPTION"); @@ -200,50 +200,6 @@ impl CanyonRegisterEntityField { } } - /// Return the datatype and parameters to create an id column, given the corresponding "CanyonRegisterEntityField" - /// with the correct format for PostgreSQL - fn _to_postgres_id_syntax(&self) -> String { - let has_pk_annotation = self - .annotations - .iter() - .find(|a| a.starts_with("Annotation: PrimaryKey")); - - let pk_is_autoincremental = match has_pk_annotation { - Some(annotation) => annotation.contains("true"), - None => false, - }; - - let postgres_datatype_syntax = Self::to_postgres_syntax(self); - - if NUMERIC_PK_DATATYPE.contains(&self.field_type.as_str()) && pk_is_autoincremental { - format!("{postgres_datatype_syntax} PRIMARY KEY GENERATED ALWAYS AS IDENTITY") - } else { - format!("{postgres_datatype_syntax} PRIMARY KEY") - } - } - - /// Return the datatype and parameters to create an id column, given the corresponding "CanyonRegisterEntityField" - /// with the correct format for Microsoft SQL Server - fn _to_sqlserver_id_syntax(&self) -> String { - let has_pk_annotation = self - .annotations - .iter() - .find(|a| a.starts_with("Annotation: PrimaryKey")); - - let pk_is_autoincremental = match has_pk_annotation { - Some(annotation) => annotation.contains("true"), - None => false, - }; - - let sqlserver_datatype_syntax = Self::to_sqlserver_syntax(self); - - if NUMERIC_PK_DATATYPE.contains(&self.field_type.as_str()) && pk_is_autoincremental { - format!("{sqlserver_datatype_syntax} IDENTITY PRIMARY") - } else { - format!("{sqlserver_datatype_syntax} PRIMARY KEY") - } - } - /// Return if the field is autoincremental pub fn is_autoincremental(&self) -> bool { let has_pk_annotation = self diff --git a/src/lib.rs b/src/lib.rs index d3bf079c..cb8be374 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,13 @@ /// reaches the top most level, grouping them and making them visible /// through this crate, building the *public API* of the library +extern crate canyon_connection; +extern crate canyon_crud; +extern crate canyon_observer; +extern crate canyon_macros; + +// extern crate async_trait; + /// Reexported elements to the root of the public API pub mod migrations { pub use canyon_observer::migrations::{handler, processor}; @@ -15,7 +22,7 @@ pub use canyon_macros::main; /// Public API for the `Canyon-SQL` proc-macros, and for the external ones pub mod macros { - pub use async_trait::*; + // pub use async_trait::*; pub use canyon_macros::*; } @@ -36,8 +43,8 @@ pub mod query { /// Reexport the available database clients within Canyon pub mod db_clients { - pub use canyon_connection::tiberius; - pub use canyon_connection::tokio_postgres; + #[cfg(feature = "postgres")] pub use canyon_connection::tokio_postgres; + #[cfg(feature = "mssql")] pub use canyon_connection::tiberius; } /// Reexport the needed runtime dependencies From c0b0dd7f66e2eef63d50a18e789ba51f53c53261 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 18 Apr 2023 14:09:44 +0200 Subject: [PATCH 40/82] First compilable version since the rework for the conditional compilation --- canyon_macros/src/query_operations/insert.rs | 51 ++++++++++---------- canyon_macros/src/query_operations/select.rs | 37 +++++--------- 2 files changed, 38 insertions(+), 50 deletions(-) diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index 45ce1187..d1b4e7a4 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -34,7 +34,6 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri ._fields_with_types() .into_iter() .find(|(i, _t)| Some(i.to_string()) == primary_key); - let insert_transaction = if let Some(pk_data) = &pk_ident_type { let pk_ident = &pk_data.0; let pk_type = &pk_data.1; @@ -54,22 +53,25 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri stmt, values, datasource_name - ).await; + ).await?; match rows { - #[cfg(feature = "tokio-postgres")] Self::Postgres(v) => { - v.remove(0) - .expect("No value found on the returning clause for Postgres") - .get::<&str, #pk_type>(#primary_key) - } - #[cfg(feature = "tiberius")] Self::Tiberius(v) => { - v.into_iter() - .flatten() - .collect::>() - .remove(0) + #[cfg(feature = "tokio-postgres")] Self::Postgres(mut v) => { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") + .get::<&str, #pk_type>(#primary_key); + Ok(()) + }, + #[cfg(feature = "tiberius")] Self::Tiberius(mut v) => { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") .get::<#pk_type, &str>(#primary_key) - .expect("SQL Server primary key type failed to be set as value") - } + .expect("SQL Server primary key type failed to be set as value"); + Ok(()) + }, + _ => panic!() // TODO remove when the generics will be refactored } } } else { @@ -92,6 +94,7 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri } }; + quote! { /// Inserts into a database entity the current data in `self`, generating a new /// entry (row), returning the `PRIMARY KEY` = `self.` with the specified @@ -287,15 +290,12 @@ pub fn generate_multiple_insert_tokens( datasource_name ).await; - match result { // TODO Falta el ds correcto - // TODO Recuperar datasource fuera del código cliente - /* .for_each(|row| results.push(row as &dyn Row)); */ + match result { Ok(res) => { - match res.get_active_ds() { - canyon_sql::crud::DatabaseType::PostgreSql => { + match res { + #[cfg(feature = "tokio-postgres")] Self::Postgres(mut v) => { for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = res - .postgres + instance.#pk_ident = v .get(idx) .expect("Failed getting the returned IDs for a multi insert") .get::<&str, #pk_type>(#pk); @@ -303,18 +303,17 @@ pub fn generate_multiple_insert_tokens( Ok(()) }, - canyon_sql::crud::DatabaseType::SqlServer => { + #[cfg(feature = "tiberius")] Self::Tiberius(mut v) => for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = res - .sqlserver + instance.#pk_ident = v .get(idx) .expect("Failed getting the returned IDs for a multi insert") .get::<#pk_type, &str>(#pk) .expect("SQL Server primary key type failed to be set as value"); } - Ok(()) - } + Ok(()), + _ => panic!() // TODO remove when the generics will be refactored } }, Err(e) => Err(e) diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index c782e8c2..c5875f03 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -147,39 +147,28 @@ pub fn generate_count_tokens( let stmt = format!("SELECT COUNT (*) FROM {table_schema_data}"); let result_handling = quote! { - // match count.get_active_ds() { - // canyon_sql_root::crud::DatabaseType::PostgreSql => { - // Ok( - // count.postgres.get(0) - // .expect(&format!("Count operation failed for {:?}", #ty_str)) - // .get::<&str, i64>("count") - // .to_owned() - // ) - // }, - // canyon_sql_root::crud::DatabaseType::SqlServer => { - // Ok( - // count.sqlserver.get(0) - // .expect(&format!("Count operation failed for {:?}", #ty_str)) - // .get::(0) - // .expect(&format!("SQL Server failed to return the count values for {:?}", #ty_str)) - // .into() - // ) - // } - // } - Ok(0 as i64) // TODO + match count { + #[cfg(feature = "tokio-postgres")] Self::Postgres(mut v) => Ok( + v.remove(0).get::<&str, i64>("count") + ), + #[cfg(feature = "tiberius")] Self::Tiberius(mut v) => + v.remove(0) + .get::("count") + .ok_or(format!("Failure in the COUNT query for MSSQL for: {}", #ty_str).into()), + _ => panic!() // TODO remove when the generics will be refactored + } }; quote! { /// Performs a COUNT(*) query over some table, returning a [`Result`] rather than panicking, /// wrapping a possible success or error coming from the database async fn count() -> Result> { - let count = <#ty as canyon_sql::crud::Transaction<#ty>>::query( + <#ty as canyon_sql::crud::Transaction<#ty>>::query( #stmt, &[], "" - ).await?; - - #result_handling + ).await + .get_by_idx_and_key(0, "count") } /// Performs a COUNT(*) query over some table, returning a [`Result`] rather than panicking, From 2a7d64f4202c1a0edc41f5a0373cdf329aa61868 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 18 Apr 2023 15:59:59 +0200 Subject: [PATCH 41/82] Setting up the missing parts for msssql databases --- Cargo.toml | 2 +- .../src/canyon_database_connector.rs | 8 +- canyon_crud/src/bounds.rs | 37 +++--- canyon_crud/src/crud.rs | 6 +- canyon_crud/src/rows.rs | 68 ++++++----- canyon_macros/src/query_operations/insert.rs | 3 +- canyon_observer/src/migrations/handler.rs | 108 ++++++++++++------ canyon_observer/src/migrations/memory.rs | 12 +- 8 files changed, 141 insertions(+), 103 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 919cdf24..afe4e1c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,5 +57,5 @@ description = "A Rust ORM and QueryBuilder" [features] default = ["postgres"] postgres = ["canyon_connection/tokio-postgres", "canyon_crud/tokio-postgres", "canyon_observer/tokio-postgres"] -mssql = ["canyon_connection/tiberius", "canyon_observer/tiberius", "canyon_observer/tiberius"] +mssql = ["canyon_connection/tiberius", "canyon_crud/tiberius", "canyon_observer/tiberius"] mssql-integrated-auth = ["mssql"] \ No newline at end of file diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index c58451fb..7c448f0e 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -1,8 +1,8 @@ -#[cfg(feature = "tiberius")] use async_std::net::TcpStream; - use serde::Deserialize; -#[cfg(feature = "tiberius")] use tiberius::{AuthMethod, Config}; + #[cfg(feature = "tokio-postgres")] use tokio_postgres::{Client, NoTls}; +#[cfg(feature = "tiberius")] use tiberius::{AuthMethod, Config}; +#[cfg(feature = "tiberius")] use async_std::net::TcpStream; use crate::datasources::DatasourceConfig; @@ -158,7 +158,7 @@ mod database_connection_handler { use crate::CanyonSqlConfig; const CONFIG_FILE_MOCK_ALT: &str = r#" - [canyon_sql_root] + [canyon_sql] datasources = [ {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs index ce0b498b..78d679b3 100644 --- a/canyon_crud/src/bounds.rs +++ b/canyon_crud/src/bounds.rs @@ -7,7 +7,7 @@ use crate::{ use canyon_connection::tokio_postgres::{self, types::ToSql}; #[cfg(feature = "tiberius")] -use canyon_connection::tiberius::{self, ColumnData, FromSql, IntoSql}; +use canyon_connection::tiberius::{self, ColumnData, IntoSql}; use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use std::any::Any; @@ -91,21 +91,13 @@ pub trait Row { self } } -#[cfg(feature = "tokio-postgres")] impl Row for &tokio_postgres::Row { - fn as_any(&self) -> &dyn Any { - *self - } -} + #[cfg(feature = "tiberius")] impl Row for tiberius::Row { fn as_any(&self) -> &dyn Any { self } } -#[cfg(feature = "tiberius")] impl Row for &tiberius::Row { - fn as_any(&self) -> &dyn Any { - self - } -} + /// Generic abstraction for hold a Column type that will be one of the Column /// types present in the dependent crates @@ -144,7 +136,6 @@ pub trait Type { } /// Wrapper over the dependencies Column's types -// #[derive(Copy)] pub enum ColumnType { #[cfg(feature = "tokio-postgres")] Postgres(tokio_postgres::types::Type), #[cfg(feature = "tiberius")] SqlServer(tiberius::ColumnType), @@ -152,25 +143,25 @@ pub enum ColumnType { pub trait RowOperations { #[cfg(feature = "tokio-postgres")] - fn get_postgres<'a, Output>(&'a self, col_name: &str) -> Output + fn get_postgres<'a, Output>(&'a self, col_name: &'a str) -> Output where Output: tokio_postgres::types::FromSql<'a>; #[cfg(feature = "tiberius")] - fn get_mssql<'a, Output>(&self, col_name: &str) -> Output + fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output where Output: tiberius::FromSql<'a>; #[cfg(feature = "tokio-postgres")] - fn get_postgres_opt<'a, Output>(&'a self, col_name: &str) -> Option + fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option where Output: tokio_postgres::types::FromSql<'a>; #[cfg(feature = "tiberius")] - fn get_mssql_opt<'a, Output>(&'a self, col_name: &str) -> Option - where Output: tokio_postgres::types::FromSql<'a>; + fn get_mssql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option + where Output: tiberius::FromSql<'a>; fn columns(&self) -> Vec; } impl RowOperations for &dyn Row { #[cfg(feature = "tokio-postgres")] - fn get_postgres<'a, Output>(&'a self, col_name: &str) -> Output + fn get_postgres<'a, Output>(&'a self, col_name: &'a str) -> Output where Output: tokio_postgres::types::FromSql<'a> { if let Some(row) = self.as_any().downcast_ref::() { @@ -179,7 +170,7 @@ impl RowOperations for &dyn Row { panic!() // TODO into result and propagate } #[cfg(feature = "tiberius")] - fn get_mssql<'a, Output>(&'a self, col_name: &str) -> Output + fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output where Output: tiberius::FromSql<'a> { if let Some(row) = self.as_any().downcast_ref::() { @@ -191,7 +182,7 @@ impl RowOperations for &dyn Row { } #[cfg(feature = "tokio-postgres")] - fn get_postgres_opt<'a, Output>(&'a self, col_name: &str) -> Option + fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option where Output: tokio_postgres::types::FromSql<'a> { if let Some(row) = self.as_any().downcast_ref::() { @@ -201,13 +192,11 @@ impl RowOperations for &dyn Row { } #[cfg(feature = "tiberius")] - fn get_mssql_opt<'a, Output>(&'a self, col_name: &str) -> Option + fn get_mssql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option where Output: tiberius::FromSql<'a> { if let Some(row) = self.as_any().downcast_ref::() { - return row - .try_get - .expect("Failed to obtain a row for MSSQL"); + return row.get::(col_name); }; panic!() // TODO into result and propagate } diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index 895019ca..c025dd97 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -50,7 +50,7 @@ pub trait Transaction { let mut guarded_cache = CACHED_DATABASE_CONN.lock().await; let database_conn = get_database_connection(datasource_name, &mut guarded_cache); - match *database_conn { + match database_conn { #[cfg(feature = "tokio-postgres")] DatabaseConnection::Postgres(_) => { postgres_query_launcher::launch::( database_conn, @@ -58,7 +58,7 @@ pub trait Transaction { params.as_ref(), ) .await - } + }, #[cfg(feature = "tiberius")] DatabaseConnection::SqlServer(_) => { sqlserver_query_launcher::launch::( database_conn, @@ -236,6 +236,6 @@ mod sqlserver_query_launcher { .into_results() .await?; - Ok(CanyonRows::Tiberius(_results.iter().flatten().collect())) + Ok(CanyonRows::Tiberius(_results.into_iter().flatten().collect())) } } diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs index efddfcb8..5dfbf5bf 100644 --- a/canyon_crud/src/rows.rs +++ b/canyon_crud/src/rows.rs @@ -16,7 +16,7 @@ pub enum CanyonRows { impl CanyonRows { #[cfg(feature = "tokio-postgres")] - pub fn get_postgres_rows(self) -> Vec { + pub fn get_postgres_rows(&self) -> &Vec { match self { Self::Postgres(v) => v, _ => panic!("This branch will never ever should be reachable") @@ -24,7 +24,7 @@ impl CanyonRows { } #[cfg(feature = "tiberius")] - pub fn get_tiberius_rows(self) -> Vec { + pub fn get_tiberius_rows(&self) -> &Vec { match self { Self::Tiberius(v) => v, _ => panic!("This branch will never ever should be reachable") @@ -47,28 +47,44 @@ impl CanyonRows { } } -#[cfg(feature = "tokio-postgres")] -impl IntoIterator for CanyonRows { - type Item = tokio_postgres::Row; - type IntoIter = std::vec::IntoIter; +// #[cfg(feature = "tokio-postgres")] +// impl IntoIterator for CanyonRows { +// type Item = tokio_postgres::Row; +// type IntoIter = std::vec::IntoIter; +// +// fn into_iter(self) -> Self::IntoIter { +// match self { +// Self::Postgres(v) => v.into_iter(), +// _ => panic!() +// } +// } +// } +// +// #[cfg(feature = "tiberius")] +// impl IntoIterator for CanyonRows { +// type Item = tiberius::Row; +// type IntoIter = std::vec::IntoIter; +// +// fn into_iter(self) -> Self::IntoIter { +// match self { +// Self::Tiberius(v) => v.into_iter(), +// _ => panic!() +// } +// } +// } +// +// #[cfg(all(feature = "tokio-postgres", feature = "tiberius"))] +// impl IntoIterator for CanyonRows { +// if cfg!(feature = "tokio-postgres") { +// type Item = tokio_postgres::Row; +// } else { type Item = tiberius::Row; } +// type IntoIter = std::vec::IntoIter; +// +// fn into_iter(self) -> Self::IntoIter { +// match self { +// Self::Tiberius(v) => v.into_iter(), +// _ => panic!() +// } +// } +// } - fn into_iter(self) -> Self::IntoIter { - match self { - Self::Postgres(v) => v.into_iter(), - _ => panic!() - } - } -} - -#[cfg(feature = "tiberius")] -impl IntoIterator for CanyonRows { - type Item = tiberius::Row; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - match self { - Self::Tiberius(v) => v.into_iter(), - _ => panic!() - } - } -} diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index d1b4e7a4..64579a55 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -303,7 +303,7 @@ pub fn generate_multiple_insert_tokens( Ok(()) }, - #[cfg(feature = "tiberius")] Self::Tiberius(mut v) => + #[cfg(feature = "tiberius")] Self::Tiberius(mut v) => { for (idx, instance) in instances.iter_mut().enumerate() { instance.#pk_ident = v .get(idx) @@ -313,6 +313,7 @@ pub fn generate_multiple_insert_tokens( } Ok(()), + }, _ => panic!() // TODO remove when the generics will be refactored } }, diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index 884b86f4..407dc76e 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -103,39 +103,11 @@ impl Migrations { /// and extract the content of the returned rows into custom structures with /// the data well organized for every entity present on that schema fn map_rows(db_results: CanyonRows, db_type: DatabaseType) -> Vec { - let mut schema_info: Vec = Vec::new(); - - for res_row in db_results.into_iter() - // .map(|row| &row as &dyn Row) - { - let unique_table = schema_info - .iter_mut() - // TODO To be able to remove row from our code, use a match statement to get table name - .find(|table| check_for_table_name(table, &res_row as &dyn Row)); - match unique_table { - Some(table) => { - /* If a table entity it's already present on the collection, we add it - the founded columns related to the table */ - Self::get_columns_metadata(&res_row as &dyn Row, table); - } - None => { - /* If there's no table for a given "table_name" property on the - collection yet, we must create a new instance and attach it - the founded columns data in this iteration */ - let mut new_table = TableMetadata { - table_name: match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => get_table_name_from_tp_row(&res_row), - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => get_table_name_from_tib_row(&res_row), - }, - columns: Vec::new(), - }; - Self::get_columns_metadata(&res_row as &dyn Row, &mut new_table); - schema_info.push(new_table); - } - }; + match db_results { + #[cfg(feature = "tokio-postgres")] CanyonRows::Postgres(v) => Self::process_tp_rows(v, db_type), + #[cfg(feature = "tiberius")] CanyonRows::Tiberius(v) => Self::process_tib_rows(v, db_type), + _ => panic!() } - - schema_info } /// Parses all the [`Row`] after query the information of the targeted schema, @@ -221,6 +193,66 @@ impl Migrations { } }; } + + #[cfg(feature = "tokio-postgres")] + fn process_tp_rows(db_results: Vec, db_type: DatabaseType) -> Vec { + let mut schema_info: Vec = Vec::new(); + for res_row in db_results.iter() { + let unique_table = schema_info + .iter_mut() + .find(|table| check_for_table_name(table, db_type, res_row as &dyn Row)); + match unique_table { + Some(table) => { + /* If a table entity it's already present on the collection, we add it + the founded columns related to the table */ + Self::get_columns_metadata(res_row as &dyn Row, table); + } + None => { + /* If there's no table for a given "table_name" property on the + collection yet, we must create a new instance and attach it + the founded columns data in this iteration */ + let mut new_table = TableMetadata { + table_name: get_table_name_from_tp_row(res_row), + columns: Vec::new(), + }; + Self::get_columns_metadata(res_row as &dyn Row, &mut new_table); + schema_info.push(new_table); + } + }; + } + + schema_info + } + + #[cfg(feature = "tiberius")] + fn process_tib_rows(db_results: Vec, db_type: DatabaseType) -> Vec { + let mut schema_info: Vec = Vec::new(); + for res_row in db_results.iter() { + let unique_table = schema_info + .iter_mut() + .find(|table| check_for_table_name(table, db_type, res_row as &dyn Row)); + match unique_table { + Some(table) => { + /* If a table entity it's already present on the collection, we add it + the founded columns related to the table */ + Self::get_columns_metadata(res_row as &dyn Row, table); + } + None => { + /* If there's no table for a given "table_name" property on the + collection yet, we must create a new instance and attach it + the founded columns data in this iteration */ + let mut new_table = TableMetadata { + table_name: get_table_name_from_tib_row(res_row), + columns: Vec::new(), + }; + Self::get_columns_metadata(res_row as &dyn Row, &mut new_table); + schema_info.push(new_table); + } + }; + } + + schema_info + } } @@ -233,11 +265,11 @@ fn get_table_name_from_tib_row(res_row: &tiberius::Row) -> String { res_row.get::<&str, &str>("table_name").unwrap_or_default().to_string() } -fn check_for_table_name(table: &&mut TableMetadata, res_row: &dyn Row) -> bool { - #[cfg(feature = "tokio-postgres")] { - table.table_name == res_row.get_postgres::<&str>("table_name") - } - #[cfg(feature = "tiberius")] { - table.table_name == row_retriever_fn_ptr(&res_row, "table_name") +fn check_for_table_name(table: &&mut TableMetadata, db_type: DatabaseType, res_row: &dyn Row) -> bool { + match db_type { + #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + table.table_name == res_row.get_postgres::<&str>("table_name"), + #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + table.table_name == res_row.get_mssql::<&str>("table_name") } } diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index 735b8d2c..8c8fe8f4 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -74,7 +74,7 @@ impl CanyonMemory { // Manually maps the results let mut db_rows = Vec::new(); #[cfg(feature = "tokio-postgres")] { - let mem_results: Vec = res.get_postgres_rows(); + let mem_results: &Vec = res.get_postgres_rows(); for row in mem_results { let db_row = CanyonMemoryRow { id: row.get::<&str, i32>("id"), @@ -86,13 +86,13 @@ impl CanyonMemory { } } #[cfg(feature = "tiberius")] { - let mem_results: Vec = res.get_tiberius_rows(); + let mem_results: &Vec = res.get_tiberius_rows(); for row in mem_results { let db_row = CanyonMemoryRow { - id: row.get::("id"), - filepath: row.get::<&str, &str>("filepath"), - struct_name: row.get::<&str, &str>("struct_name"), - declared_table_name: row.get::<&str, &str>("declared_table_name"), + id: row.get::("id").unwrap(), + filepath: row.get::<&str, &str>("filepath").unwrap().to_string(), + struct_name: row.get::<&str, &str>("struct_name").unwrap().to_string(), + declared_table_name: row.get::<&str, &str>("declared_table_name").unwrap().to_string(), }; db_rows.push(db_row); } From e09557d4b6074ce7ce0545f076ea2a5ed28cc6e0 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 18 Apr 2023 16:18:15 +0200 Subject: [PATCH 42/82] Adecuating the doc-tests to the conditional compilation --- Cargo.toml | 9 +-------- canyon_connection/Cargo.toml | 2 +- canyon_connection/src/canyon_database_connector.rs | 4 ++-- canyon_connection/src/datasources.rs | 11 +++++------ tests/Cargo.toml | 4 ++-- tests/canyon_integration_tests.rs | 2 ++ 6 files changed, 13 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index afe4e1c6..085ee7b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,12 +20,6 @@ canyon_observer = { version = "0.2.0", path = "canyon_observer" } canyon_crud = { version = "0.2.0", path = "canyon_crud" } canyon_connection = { version = "0.2.0", path = "canyon_connection" } - -#tokio = { workspace = true } -#tokio-util = { workspace = true } -#tokio-postgres = { workspace = true } -#tiberius = { worskpace = true } - [workspace.dependencies] canyon_crud = { version = "0.2.0", path = "canyon_crud" } canyon_connection = { version = "0.2.0", path = "canyon_connection" } @@ -50,7 +44,7 @@ edition = "2021" authors = ["Alex Vergara, Gonzalo Busto"] documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" -readme = "../README.md" +readme = "README.md" license = "MIT" description = "A Rust ORM and QueryBuilder" @@ -58,4 +52,3 @@ description = "A Rust ORM and QueryBuilder" default = ["postgres"] postgres = ["canyon_connection/tokio-postgres", "canyon_crud/tokio-postgres", "canyon_observer/tokio-postgres"] mssql = ["canyon_connection/tiberius", "canyon_crud/tiberius", "canyon_observer/tiberius"] -mssql-integrated-auth = ["mssql"] \ No newline at end of file diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 36fc8a97..77fea282 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -13,7 +13,7 @@ description.workspace = true tokio = { workspace = true } tokio-util = { workspace = true } tokio-postgres = { workspace = true, optional = true } -tiberius = { workspace = true, optional = true } +tiberius = { workspace = true, optional = true} futures = { workspace = true } indexmap = { workspace = true } async-std = { workspace = true } diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 7c448f0e..9ec8612f 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -171,11 +171,11 @@ mod database_connection_handler { let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) .expect("A failure happened retrieving the [canyon_sql_root] section"); - assert_eq!( + #[cfg(feature = "tokio-postgres")] assert_eq!( config.canyon_sql.datasources[0].get_db_type(), DatabaseType::PostgreSql ); - assert_eq!( + #[cfg(feature = "tiberius")] assert_eq!( config.canyon_sql.datasources[1].get_db_type(), DatabaseType::SqlServer ); diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 2a553cb3..e50b0521 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -19,7 +19,7 @@ fn load_ds_config_from_array() { let ds_0 = &config.canyon_sql.datasources[0]; let ds_1 = &config.canyon_sql.datasources[1]; - let ds_2 = &config.canyon_sql.datasources[2]; + let _ds_2 = &config.canyon_sql.datasources[2]; assert_eq!(ds_0.name, "PostgresDS"); assert_eq!(ds_0.get_db_type(), DatabaseType::PostgreSql); @@ -35,9 +35,9 @@ fn load_ds_config_from_array() { assert_eq!(ds_0.properties.db_name, "triforce"); assert_eq!(ds_0.properties.migrations, Some(Migrations::Enabled)); - assert_eq!(ds_1.name, "SqlServerDS"); - assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); - assert_eq!( + #[cfg(feature = "tiberius")] assert_eq!(ds_1.name, "SqlServerDS"); + #[cfg(feature = "tiberius")] assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); + #[cfg(feature = "tiberius")] assert_eq!( ds_1.auth, Auth::SqlServer(SqlServerAuth::Basic { username: "sa".to_string(), @@ -49,7 +49,7 @@ fn load_ds_config_from_array() { assert_eq!(ds_1.properties.db_name, "triforce2"); assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); - #[cfg(feature = "tokio-postgres")] assert_eq!(ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) + #[cfg(feature = "tiberius")] assert_eq!(_ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) } /// #[derive(Deserialize, Debug, Clone)] @@ -100,7 +100,6 @@ pub enum PostgresAuth { pub enum SqlServerAuth { #[serde(alias = "Basic", alias = "basic")] Basic { username: String, password: String }, - #[cfg(feature = "mssql-integrated-auth")] #[serde(alias = "Integrated", alias = "integrated")] Integrated, } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 54047bc4..212c0505 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tests" -version = "0.2.0" -edition = "2021" +version.workspace = true +edition.workspace = true publish = false [dev-dependencies] diff --git a/tests/canyon_integration_tests.rs b/tests/canyon_integration_tests.rs index 8120ee8f..30687987 100644 --- a/tests/canyon_integration_tests.rs +++ b/tests/canyon_integration_tests.rs @@ -1,3 +1,5 @@ +extern crate canyon_sql; + use std::error::Error; ///! Integration tests for the heart of a Canyon-SQL application, the CRUD operations. From 5d99d499ec370671ce6616ee6e4078879dc835b2 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 18 Apr 2023 17:31:59 +0200 Subject: [PATCH 43/82] Applying format with rustfmt --- Caasdfadsrgo.tomlsda | 19 -- Cargo.toml | 1 - .../src/canyon_database_connector.rs | 31 +- canyon_connection/src/datasources.rs | 18 +- canyon_connection/src/lib.rs | 15 +- canyon_crud/src/bounds.rs | 278 ++++++++++++------ canyon_crud/src/crud.rs | 35 +-- canyon_crud/src/mapper.rs | 12 +- .../src/query_elements/query_builder.rs | 3 +- canyon_crud/src/rows.rs | 34 +-- canyon_macros/src/query_operations/insert.rs | 1 - canyon_observer/src/migrations/handler.rs | 60 ++-- .../src/migrations/information_schema.rs | 24 +- canyon_observer/src/migrations/memory.rs | 19 +- canyon_observer/src/migrations/processor.rs | 21 +- .../src/migrations/register_types.rs | 18 +- src/lib.rs | 9 +- tests/crud/mod.rs | 6 +- 18 files changed, 374 insertions(+), 230 deletions(-) delete mode 100644 Caasdfadsrgo.tomlsda diff --git a/Caasdfadsrgo.tomlsda b/Caasdfadsrgo.tomlsda deleted file mode 100644 index 3e3c557e..00000000 --- a/Caasdfadsrgo.tomlsda +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "canyon_sql" -version = "0.2.0" -edition.workspace = true -authors.workspace = true -documentation.workspace = true -homepage.workspace = true -readme.workspace = true -license.workspace = true -description.workspace = true - -[dependencies] -async-trait = { version = "0.1.50" } - -# Project crates -canyon_macros = { version = "0.2.0", path = "../canyon_macros" } -canyon_observer = { version = "0.2.0", path = "../canyon_observer" } -canyon_crud = { version = "0.2.0", path = "../canyon_crud" } -canyon_connection = { version = "0.2.0", path = "../canyon_connection", features = ["postgres"] } diff --git a/Cargo.toml b/Cargo.toml index 085ee7b2..501c3fa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,3 @@ -# This is the root Caasdfadsrgo.tomlsda file that serves as manager for the workspace of the project [package] name = "canyon_sql" version = "0.2.0" diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 9ec8612f..5330e5e4 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -1,8 +1,11 @@ use serde::Deserialize; -#[cfg(feature = "tokio-postgres")] use tokio_postgres::{Client, NoTls}; -#[cfg(feature = "tiberius")] use tiberius::{AuthMethod, Config}; -#[cfg(feature = "tiberius")] use async_std::net::TcpStream; +#[cfg(feature = "tiberius")] +use async_std::net::TcpStream; +#[cfg(feature = "tiberius")] +use tiberius::{AuthMethod, Config}; +#[cfg(feature = "tokio-postgres")] +use tokio_postgres::{Client, NoTls}; use crate::datasources::DatasourceConfig; @@ -35,8 +38,10 @@ pub struct SqlServerConnection { /// process them and generates a pool of 1 to 1 database connection for /// every datasource defined. pub enum DatabaseConnection { - #[cfg(feature = "tokio-postgres")] Postgres(PostgreSqlConnection), - #[cfg(feature = "tiberius")] SqlServer(SqlServerConnection), + #[cfg(feature = "tokio-postgres")] + Postgres(PostgreSqlConnection), + #[cfg(feature = "tiberius")] + SqlServer(SqlServerConnection), } unsafe impl Send for DatabaseConnection {} @@ -94,14 +99,16 @@ impl DatabaseConnection { // Using SQL Server authentication. config.authentication(match &datasource.auth { - #[cfg(feature = "tokio-postgres")] crate::datasources::Auth::Postgres(_) => { + #[cfg(feature = "tokio-postgres")] + crate::datasources::Auth::Postgres(_) => { panic!("Found PostgreSQL auth configuration for a SqlServer database") } crate::datasources::Auth::SqlServer(sql_server_auth) => match sql_server_auth { crate::datasources::SqlServerAuth::Basic { username, password } => { AuthMethod::sql_server(username, password) } - #[cfg(feature = "mssql-integrated-auth")] // TODO pending, or remove the cfg? + #[cfg(feature = "mssql-integrated-auth")] + // TODO pending, or remove the cfg? crate::datasources::SqlServerAuth::Integrated => AuthMethod::Integrated, }, }); @@ -139,7 +146,7 @@ impl DatabaseConnection { pub fn postgres_connection(&self) -> Option<&PostgreSqlConnection> { match self { DatabaseConnection::Postgres(conn) => Some(conn), - _ => panic!() + _ => panic!(), } } @@ -147,7 +154,7 @@ impl DatabaseConnection { pub fn sqlserver_connection(&mut self) -> Option<&mut SqlServerConnection> { match self { DatabaseConnection::SqlServer(conn) => Some(conn), - _ => panic!() + _ => panic!(), } } } @@ -171,11 +178,13 @@ mod database_connection_handler { let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) .expect("A failure happened retrieving the [canyon_sql_root] section"); - #[cfg(feature = "tokio-postgres")] assert_eq!( + #[cfg(feature = "tokio-postgres")] + assert_eq!( config.canyon_sql.datasources[0].get_db_type(), DatabaseType::PostgreSql ); - #[cfg(feature = "tiberius")] assert_eq!( + #[cfg(feature = "tiberius")] + assert_eq!( config.canyon_sql.datasources[1].get_db_type(), DatabaseType::SqlServer ); diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index e50b0521..c2be5aa5 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -35,9 +35,12 @@ fn load_ds_config_from_array() { assert_eq!(ds_0.properties.db_name, "triforce"); assert_eq!(ds_0.properties.migrations, Some(Migrations::Enabled)); - #[cfg(feature = "tiberius")] assert_eq!(ds_1.name, "SqlServerDS"); - #[cfg(feature = "tiberius")] assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); - #[cfg(feature = "tiberius")] assert_eq!( + #[cfg(feature = "tiberius")] + assert_eq!(ds_1.name, "SqlServerDS"); + #[cfg(feature = "tiberius")] + assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); + #[cfg(feature = "tiberius")] + assert_eq!( ds_1.auth, Auth::SqlServer(SqlServerAuth::Basic { username: "sa".to_string(), @@ -49,7 +52,8 @@ fn load_ds_config_from_array() { assert_eq!(ds_1.properties.db_name, "triforce2"); assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); - #[cfg(feature = "tiberius")] assert_eq!(_ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) + #[cfg(feature = "tiberius")] + assert_eq!(_ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) } /// #[derive(Deserialize, Debug, Clone)] @@ -72,8 +76,10 @@ pub struct DatasourceConfig { impl DatasourceConfig { pub fn get_db_type(&self) -> DatabaseType { match self.auth { - #[cfg(feature = "tokio-postgres")] Auth::Postgres(_) => DatabaseType::PostgreSql, - #[cfg(feature = "tiberius")] Auth::SqlServer(_) => DatabaseType::SqlServer, + #[cfg(feature = "tokio-postgres")] + Auth::Postgres(_) => DatabaseType::PostgreSql, + #[cfg(feature = "tiberius")] + Auth::SqlServer(_) => DatabaseType::SqlServer, } } } diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index cc240034..64960537 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -1,10 +1,14 @@ -#[cfg(feature = "tiberius")] pub extern crate async_std; +#[cfg(feature = "tiberius")] +pub extern crate async_std; pub extern crate futures; pub extern crate lazy_static; -#[cfg(feature = "tiberius")] pub extern crate tiberius; +#[cfg(feature = "tiberius")] +pub extern crate tiberius; pub extern crate tokio; -#[cfg(feature = "tokio-postgres")] pub extern crate tokio_postgres; -#[cfg(feature = "tokio-postgres")] pub extern crate tokio_util; +#[cfg(feature = "tokio-postgres")] +pub extern crate tokio_postgres; +#[cfg(feature = "tokio-postgres")] +pub extern crate tokio_util; pub mod canyon_database_connector; pub mod datasources; @@ -62,11 +66,10 @@ pub async fn init_connections_cache() { } } - /// pub fn get_database_connection<'a>( datasource_name: &str, - guarded_cache: &'a mut MutexGuard> + guarded_cache: &'a mut MutexGuard>, ) -> &'a mut DatabaseConnection { if datasource_name.is_empty() { guarded_cache diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs index 78d679b3..6a6842ba 100644 --- a/canyon_crud/src/bounds.rs +++ b/canyon_crud/src/bounds.rs @@ -86,19 +86,20 @@ pub trait Row { fn as_any(&self) -> &dyn Any; } -#[cfg(feature = "tokio-postgres")] impl Row for tokio_postgres::Row { +#[cfg(feature = "tokio-postgres")] +impl Row for tokio_postgres::Row { fn as_any(&self) -> &dyn Any { self } } -#[cfg(feature = "tiberius")] impl Row for tiberius::Row { +#[cfg(feature = "tiberius")] +impl Row for tiberius::Row { fn as_any(&self) -> &dyn Any { self } } - /// Generic abstraction for hold a Column type that will be one of the Column /// types present in the dependent crates // #[derive(Copy, Clone)] @@ -124,12 +125,14 @@ impl<'a> Column<'a> { pub trait Type { fn as_any(&self) -> &dyn Any; } -#[cfg(feature = "tokio-postgres")] impl Type for tokio_postgres::types::Type { +#[cfg(feature = "tokio-postgres")] +impl Type for tokio_postgres::types::Type { fn as_any(&self) -> &dyn Any { self } } -#[cfg(feature = "tiberius")] impl Type for tiberius::ColumnType { +#[cfg(feature = "tiberius")] +impl Type for tiberius::ColumnType { fn as_any(&self) -> &dyn Any { self } @@ -137,24 +140,30 @@ pub trait Type { /// Wrapper over the dependencies Column's types pub enum ColumnType { - #[cfg(feature = "tokio-postgres")] Postgres(tokio_postgres::types::Type), - #[cfg(feature = "tiberius")] SqlServer(tiberius::ColumnType), + #[cfg(feature = "tokio-postgres")] + Postgres(tokio_postgres::types::Type), + #[cfg(feature = "tiberius")] + SqlServer(tiberius::ColumnType), } pub trait RowOperations { #[cfg(feature = "tokio-postgres")] fn get_postgres<'a, Output>(&'a self, col_name: &'a str) -> Output - where Output: tokio_postgres::types::FromSql<'a>; + where + Output: tokio_postgres::types::FromSql<'a>; #[cfg(feature = "tiberius")] fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output - where Output: tiberius::FromSql<'a>; + where + Output: tiberius::FromSql<'a>; #[cfg(feature = "tokio-postgres")] fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option - where Output: tokio_postgres::types::FromSql<'a>; + where + Output: tokio_postgres::types::FromSql<'a>; #[cfg(feature = "tiberius")] fn get_mssql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option - where Output: tiberius::FromSql<'a>; + where + Output: tiberius::FromSql<'a>; fn columns(&self) -> Vec; } @@ -162,7 +171,8 @@ pub trait RowOperations { impl RowOperations for &dyn Row { #[cfg(feature = "tokio-postgres")] fn get_postgres<'a, Output>(&'a self, col_name: &'a str) -> Output - where Output: tokio_postgres::types::FromSql<'a> + where + Output: tokio_postgres::types::FromSql<'a>, { if let Some(row) = self.as_any().downcast_ref::() { return row.get::<&str, Output>(col_name); @@ -171,7 +181,8 @@ impl RowOperations for &dyn Row { } #[cfg(feature = "tiberius")] fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output - where Output: tiberius::FromSql<'a> + where + Output: tiberius::FromSql<'a>, { if let Some(row) = self.as_any().downcast_ref::() { return row @@ -183,7 +194,8 @@ impl RowOperations for &dyn Row { #[cfg(feature = "tokio-postgres")] fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option - where Output: tokio_postgres::types::FromSql<'a> + where + Output: tokio_postgres::types::FromSql<'a>, { if let Some(row) = self.as_any().downcast_ref::() { return row.get::<&str, Option>(col_name); @@ -193,7 +205,8 @@ impl RowOperations for &dyn Row { #[cfg(feature = "tiberius")] fn get_mssql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option - where Output: tiberius::FromSql<'a> + where + Output: tiberius::FromSql<'a>, { if let Some(row) = self.as_any().downcast_ref::() { return row.get::(col_name); @@ -204,7 +217,8 @@ impl RowOperations for &dyn Row { fn columns(&self) -> Vec { let mut cols = vec![]; - #[cfg(feature = "tokio-postgres")] { + #[cfg(feature = "tokio-postgres")] + { if self.as_any().is::() { self.as_any() .downcast_ref::() @@ -219,7 +233,8 @@ impl RowOperations for &dyn Row { }) } } - #[cfg(feature = "tiberius")] { + #[cfg(feature = "tiberius")] + { if self.as_any().is::() { self.as_any() .downcast_ref::() @@ -242,8 +257,10 @@ impl RowOperations for &dyn Row { /// Defines a trait for represent type bounds against the allowed /// data types supported by Canyon to be used as query parameters. pub trait QueryParameter<'a>: std::fmt::Debug + Sync + Send { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync); - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_>; + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync); + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_>; } /// The implementation of the [`canyon_connection::tiberius`] [`IntoSql`] for the @@ -262,198 +279,247 @@ impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { } impl<'a> QueryParameter<'a> for bool { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::Bit(Some(*self)) } } impl<'a> QueryParameter<'a> for i16 { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(*self)) } } impl<'a> QueryParameter<'a> for &i16 { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(*self) } } impl<'a> QueryParameter<'a> for Option<&i16> { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for i32 { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self)) } } impl<'a> QueryParameter<'a> for &i32 { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(*self) } } impl<'a> QueryParameter<'a> for Option<&i32> { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for f32 { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some(*self)) } } impl<'a> QueryParameter<'a> for &f32 { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(*self) } } impl<'a> QueryParameter<'a> for Option<&f32> { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some( *self.expect("Error on an f32 value on QueryParameter<'_>"), )) } } impl<'a> QueryParameter<'a> for f64 { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some(*self)) } } impl<'a> QueryParameter<'a> for &f64 { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(*self) } } impl<'a> QueryParameter<'a> for Option<&f64> { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some( *self.expect("Error on an f64 value on QueryParameter<'_>"), )) } } impl<'a> QueryParameter<'a> for i64 { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self)) } } impl<'a> QueryParameter<'a> for &i64 { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(*self) } } impl<'a> QueryParameter<'a> for Option<&i64> { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for String { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Owned(self.to_owned()))) } } impl<'a> QueryParameter<'a> for &String { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Borrowed(self))) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { match self { Some(string) => ColumnData::String(Some(std::borrow::Cow::Owned(string.to_owned()))), None => ColumnData::String(None), @@ -461,10 +527,12 @@ impl<'a> QueryParameter<'a> for Option { } } impl<'a> QueryParameter<'a> for Option<&String> { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { match self { Some(string) => ColumnData::String(Some(std::borrow::Cow::Borrowed(string))), None => ColumnData::String(None), @@ -472,18 +540,22 @@ impl<'a> QueryParameter<'a> for Option<&String> { } } impl<'a> QueryParameter<'_> for &'_ str { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Borrowed(*self))) } } impl<'a> QueryParameter<'a> for Option<&'_ str> { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { match *self { Some(str) => ColumnData::String(Some(std::borrow::Cow::Borrowed(str))), None => ColumnData::String(None), @@ -491,82 +563,102 @@ impl<'a> QueryParameter<'a> for Option<&'_ str> { } } impl<'a> QueryParameter<'_> for NaiveDate { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for NaiveTime { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for NaiveDateTime { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for DateTime { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option> { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for DateTime { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for Option> { - #[cfg(feature = "tokio-postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + #[cfg(feature = "tokio-postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] fn as_sqlserver_param(&self) -> ColumnData<'_> { + #[cfg(feature = "tiberius")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index c025dd97..c38ea10a 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use async_trait::async_trait; use canyon_connection::canyon_database_connector::DatabaseConnection; -use canyon_connection::{CACHED_DATABASE_CONN, get_database_connection}; +use canyon_connection::{get_database_connection, CACHED_DATABASE_CONN}; use crate::bounds::QueryParameter; use crate::mapper::RowMapper; @@ -43,29 +43,31 @@ pub trait Transaction { params: Z, datasource_name: &'a str, ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> - where - S: AsRef + Display + Sync + Send + 'a, - Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a + where + S: AsRef + Display + Sync + Send + 'a, + Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, { let mut guarded_cache = CACHED_DATABASE_CONN.lock().await; let database_conn = get_database_connection(datasource_name, &mut guarded_cache); match database_conn { - #[cfg(feature = "tokio-postgres")] DatabaseConnection::Postgres(_) => { + #[cfg(feature = "tokio-postgres")] + DatabaseConnection::Postgres(_) => { postgres_query_launcher::launch::( database_conn, stmt.to_string(), params.as_ref(), ) - .await - }, - #[cfg(feature = "tiberius")] DatabaseConnection::SqlServer(_) => { + .await + } + #[cfg(feature = "tiberius")] + DatabaseConnection::SqlServer(_) => { sqlserver_query_launcher::launch::( database_conn, &mut stmt.to_string(), params, ) - .await + .await } } } @@ -120,9 +122,7 @@ where datasource_name: &'a str, ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>>; - async fn insert<'a>( - &mut self, - ) -> Result<(), Box>; + async fn insert<'a>(&mut self) -> Result<(), Box>; async fn insert_datasource<'a>( &mut self, @@ -164,8 +164,8 @@ where #[cfg(feature = "tokio-postgres")] mod postgres_query_launcher { use crate::bounds::QueryParameter; - use canyon_connection::canyon_database_connector::DatabaseConnection; use crate::rows::CanyonRows; + use canyon_connection::canyon_database_connector::DatabaseConnection; pub async fn launch<'a, T>( db_conn: &DatabaseConnection, @@ -188,14 +188,13 @@ mod postgres_query_launcher { } } - #[cfg(feature = "tiberius")] mod sqlserver_query_launcher { + use crate::rows::CanyonRows; use crate::{ bounds::QueryParameter, canyon_connection::{canyon_database_connector::DatabaseConnection, tiberius::Query}, }; - use crate::rows::CanyonRows; pub async fn launch<'a, T, Z>( db_conn: &mut DatabaseConnection, @@ -203,7 +202,7 @@ mod sqlserver_query_launcher { params: Z, ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> where - Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a + Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, { // Re-generate de insert statement to adequate it to the SQL SERVER syntax to retrieve the PK value(s) after insert if stmt.contains("RETURNING") { @@ -236,6 +235,8 @@ mod sqlserver_query_launcher { .into_results() .await?; - Ok(CanyonRows::Tiberius(_results.into_iter().flatten().collect())) + Ok(CanyonRows::Tiberius( + _results.into_iter().flatten().collect(), + )) } } diff --git a/canyon_crud/src/mapper.rs b/canyon_crud/src/mapper.rs index 7996c0fc..cc944f1d 100644 --- a/canyon_crud/src/mapper.rs +++ b/canyon_crud/src/mapper.rs @@ -1,5 +1,7 @@ -#[cfg(feature = "tokio-postgres")] use canyon_connection::tokio_postgres; -#[cfg(feature = "tiberius")] use canyon_connection::tiberius; +#[cfg(feature = "tiberius")] +use canyon_connection::tiberius; +#[cfg(feature = "tokio-postgres")] +use canyon_connection::tokio_postgres; use crate::crud::Transaction; @@ -7,6 +9,8 @@ use crate::crud::Transaction; /// from some supported database in Canyon-SQL into a user's defined /// type `T` pub trait RowMapper>: Sized { - #[cfg(feature = "tokio-postgres")] fn deserialize_postgresql(row: &tokio_postgres::Row) -> T; - #[cfg(feature = "tiberius")] fn deserialize_sqlserver(row: &tiberius::Row) -> T; + #[cfg(feature = "tokio-postgres")] + fn deserialize_postgresql(row: &tokio_postgres::Row) -> T; + #[cfg(feature = "tiberius")] + fn deserialize_sqlserver(row: &tiberius::Row) -> T; } diff --git a/canyon_crud/src/query_elements/query_builder.rs b/canyon_crud/src/query_elements/query_builder.rs index 9d102f87..92146542 100644 --- a/canyon_crud/src/query_elements/query_builder.rs +++ b/canyon_crud/src/query_elements/query_builder.rs @@ -173,7 +173,8 @@ where self.query.params.to_vec(), self.datasource_name, ) - .await?.into_results::()) + .await? + .into_results::()) } pub fn r#where>(&mut self, r#where: Z, op: impl Operator) { diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs index 5dfbf5bf..02322971 100644 --- a/canyon_crud/src/rows.rs +++ b/canyon_crud/src/rows.rs @@ -1,6 +1,6 @@ -use std::marker::PhantomData; use crate::crud::Transaction; use crate::mapper::RowMapper; +use std::marker::PhantomData; /// Lightweight wrapper over the collection of results of the different crates /// supported by Canyon-SQL. @@ -9,9 +9,11 @@ use crate::mapper::RowMapper; /// operations that are too difficult or to ugly to implement in the macros that /// will call the query method of Crud. pub enum CanyonRows { - #[cfg(feature = "tokio-postgres")] Postgres(Vec), - #[cfg(feature = "tiberius")] Tiberius(Vec), - UnusableTypeMarker(PhantomData) + #[cfg(feature = "tokio-postgres")] + Postgres(Vec), + #[cfg(feature = "tiberius")] + Tiberius(Vec), + UnusableTypeMarker(PhantomData), } impl CanyonRows { @@ -19,7 +21,7 @@ impl CanyonRows { pub fn get_postgres_rows(&self) -> &Vec { match self { Self::Postgres(v) => v, - _ => panic!("This branch will never ever should be reachable") + _ => panic!("This branch will never ever should be reachable"), } } @@ -27,22 +29,21 @@ impl CanyonRows { pub fn get_tiberius_rows(&self) -> &Vec { match self { Self::Tiberius(v) => v, - _ => panic!("This branch will never ever should be reachable") + _ => panic!("This branch will never ever should be reachable"), } } /// Consumes `self` and returns the wrapped [`std::vec::Vec`] with the instances of T - pub fn into_results>(self) -> Vec where T: Transaction { + pub fn into_results>(self) -> Vec + where + T: Transaction, + { match self { - #[cfg(feature = "tokio-postgres")] Self::Postgres(v) => v - .iter() - .map(|row| Z::deserialize_postgresql(row)) - .collect(), - #[cfg(feature = "tiberius")] Self::Tiberius(v) => v - .iter() - .map(|row| Z::deserialize_sqlserver(&row)) - .collect(), - _ => panic!("This branch will never ever should be reachable") + #[cfg(feature = "tokio-postgres")] + Self::Postgres(v) => v.iter().map(|row| Z::deserialize_postgresql(row)).collect(), + #[cfg(feature = "tiberius")] + Self::Tiberius(v) => v.iter().map(|row| Z::deserialize_sqlserver(&row)).collect(), + _ => panic!("This branch will never ever should be reachable"), } } } @@ -87,4 +88,3 @@ impl CanyonRows { // } // } // } - diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index 64579a55..18cf89f8 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -94,7 +94,6 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri } }; - quote! { /// Inserts into a database entity the current data in `self`, generating a new /// entry (row), returning the `PRIMARY KEY` = `self.` with the specified diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index 407dc76e..87dbd6a1 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -1,6 +1,6 @@ use canyon_connection::{datasources::Migrations as MigrationsStatus, DATASOURCES}; -use partialdebug::placeholder::PartialDebug; use canyon_crud::rows::CanyonRows; +use partialdebug::placeholder::PartialDebug; use crate::{ canyon_crud::{ @@ -53,7 +53,8 @@ impl Migrations { // Tracked entities that must be migrated whenever Canyon starts let schema_status = Self::fetch_database(&datasource.name, datasource.get_db_type()).await; - let database_tables_schema_info = Self::map_rows(schema_status, datasource.get_db_type()); + let database_tables_schema_info = + Self::map_rows(schema_status, datasource.get_db_type()); // We filter the tables from the schema that aren't Canyon entities let mut user_database_tables = vec![]; @@ -89,14 +90,19 @@ impl Migrations { db_type: DatabaseType, ) -> CanyonRows { let query = match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => constants::postgresql_queries::FETCH_PUBLIC_SCHEMA, - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => constants::mssql_queries::FETCH_PUBLIC_SCHEMA, + #[cfg(feature = "tokio-postgres")] + DatabaseType::PostgreSql => constants::postgresql_queries::FETCH_PUBLIC_SCHEMA, + #[cfg(feature = "tiberius")] + DatabaseType::SqlServer => constants::mssql_queries::FETCH_PUBLIC_SCHEMA, }; - Self::query(query, [], datasource_name).await - .unwrap_or_else(|_| {panic!( - "Error querying the schema information for the datasource: {datasource_name}" - )}) + Self::query(query, [], datasource_name) + .await + .unwrap_or_else(|_| { + panic!( + "Error querying the schema information for the datasource: {datasource_name}" + ) + }) } /// Handler for parse the result of query the information of some database schema, @@ -104,9 +110,11 @@ impl Migrations { /// the data well organized for every entity present on that schema fn map_rows(db_results: CanyonRows, db_type: DatabaseType) -> Vec { match db_results { - #[cfg(feature = "tokio-postgres")] CanyonRows::Postgres(v) => Self::process_tp_rows(v, db_type), - #[cfg(feature = "tiberius")] CanyonRows::Tiberius(v) => Self::process_tib_rows(v, db_type), - _ => panic!() + #[cfg(feature = "tokio-postgres")] + CanyonRows::Postgres(v) => Self::process_tp_rows(v, db_type), + #[cfg(feature = "tiberius")] + CanyonRows::Tiberius(v) => Self::process_tib_rows(v, db_type), + _ => panic!(), } } @@ -195,7 +203,10 @@ impl Migrations { } #[cfg(feature = "tokio-postgres")] - fn process_tp_rows(db_results: Vec, db_type: DatabaseType) -> Vec { + fn process_tp_rows( + db_results: Vec, + db_type: DatabaseType, + ) -> Vec { let mut schema_info: Vec = Vec::new(); for res_row in db_results.iter() { let unique_table = schema_info @@ -225,7 +236,10 @@ impl Migrations { } #[cfg(feature = "tiberius")] - fn process_tib_rows(db_results: Vec, db_type: DatabaseType) -> Vec { + fn process_tib_rows( + db_results: Vec, + db_type: DatabaseType, + ) -> Vec { let mut schema_info: Vec = Vec::new(); for res_row in db_results.iter() { let unique_table = schema_info @@ -255,21 +269,27 @@ impl Migrations { } } - #[cfg(feature = "tokio-postgres")] fn get_table_name_from_tp_row(res_row: &tokio_postgres::Row) -> String { res_row.get::<&str, String>("table_name") } #[cfg(feature = "tiberius")] fn get_table_name_from_tib_row(res_row: &tiberius::Row) -> String { - res_row.get::<&str, &str>("table_name").unwrap_or_default().to_string() + res_row + .get::<&str, &str>("table_name") + .unwrap_or_default() + .to_string() } -fn check_for_table_name(table: &&mut TableMetadata, db_type: DatabaseType, res_row: &dyn Row) -> bool { +fn check_for_table_name( + table: &&mut TableMetadata, + db_type: DatabaseType, + res_row: &dyn Row, +) -> bool { match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => - table.table_name == res_row.get_postgres::<&str>("table_name"), - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => - table.table_name == res_row.get_mssql::<&str>("table_name") + #[cfg(feature = "tokio-postgres")] + DatabaseType::PostgreSql => table.table_name == res_row.get_postgres::<&str>("table_name"), + #[cfg(feature = "tiberius")] + DatabaseType::SqlServer => table.table_name == res_row.get_mssql::<&str>("table_name"), } } diff --git a/canyon_observer/src/migrations/information_schema.rs b/canyon_observer/src/migrations/information_schema.rs index d93c7007..06eb6a3e 100644 --- a/canyon_observer/src/migrations/information_schema.rs +++ b/canyon_observer/src/migrations/information_schema.rs @@ -1,5 +1,7 @@ -#[cfg(feature = "tokio-postgres")] use canyon_connection::tokio_postgres::types::Type as TP_TYP; -#[cfg(feature = "tiberius")] use canyon_connection::tiberius::ColumnType as TIB_TY; +#[cfg(feature = "tiberius")] +use canyon_connection::tiberius::ColumnType as TIB_TY; +#[cfg(feature = "tokio-postgres")] +use canyon_connection::tokio_postgres::types::Type as TP_TYP; use canyon_crud::bounds::{Column, ColumnType, Row, RowOperations}; /// Model that represents the database entities that belongs to the current schema. @@ -41,18 +43,24 @@ impl ColumnMetadataTypeValue { /// Retrieves the value stored in a [`Column`] for a passed [`Row`] pub fn get_value(row: &dyn Row, col: &Column) -> Self { match col.column_type() { - #[cfg(feature = "tokio-postgres")] ColumnType::Postgres(v) => { + #[cfg(feature = "tokio-postgres")] + ColumnType::Postgres(v) => { match *v { - TP_TYP::NAME | TP_TYP::VARCHAR | TP_TYP::TEXT => { - Self::StringValue(row.get_postgres_opt::<&str>(col.name()).map(|opt| opt.to_owned())) - } + TP_TYP::NAME | TP_TYP::VARCHAR | TP_TYP::TEXT => Self::StringValue( + row.get_postgres_opt::<&str>(col.name()) + .map(|opt| opt.to_owned()), + ), TP_TYP::INT4 => Self::IntValue(row.get_postgres_opt::(col.name())), _ => Self::NoneValue, // TODO watchout this one } } - #[cfg(feature = "tiberius")] ColumnType::SqlServer(v) => match v { + #[cfg(feature = "tiberius")] + ColumnType::SqlServer(v) => match v { TIB_TY::NChar | TIB_TY::NVarchar | TIB_TY::BigChar | TIB_TY::BigVarChar => { - Self::StringValue(row.get_mssql_opt::<&str>(col.name()).map(|opt| opt.to_owned())) + Self::StringValue( + row.get_mssql_opt::<&str>(col.name()) + .map(|opt| opt.to_owned()), + ) } TIB_TY::Int2 | TIB_TY::Int4 | TIB_TY::Int8 | TIB_TY::Intn => { Self::IntValue(row.get_mssql_opt::(col.name())) diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index 8c8fe8f4..d22383a0 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -73,7 +73,8 @@ impl CanyonMemory { // Manually maps the results let mut db_rows = Vec::new(); - #[cfg(feature = "tokio-postgres")] { + #[cfg(feature = "tokio-postgres")] + { let mem_results: &Vec = res.get_postgres_rows(); for row in mem_results { let db_row = CanyonMemoryRow { @@ -85,14 +86,18 @@ impl CanyonMemory { db_rows.push(db_row); } } - #[cfg(feature = "tiberius")] { + #[cfg(feature = "tiberius")] + { let mem_results: &Vec = res.get_tiberius_rows(); for row in mem_results { let db_row = CanyonMemoryRow { id: row.get::("id").unwrap(), filepath: row.get::<&str, &str>("filepath").unwrap().to_string(), struct_name: row.get::<&str, &str>("struct_name").unwrap().to_string(), - declared_table_name: row.get::<&str, &str>("declared_table_name").unwrap().to_string(), + declared_table_name: row + .get::<&str, &str>("declared_table_name") + .unwrap() + .to_string(), }; db_rows.push(db_row); } @@ -104,7 +109,7 @@ impl CanyonMemory { async fn populate_memory( datasource: &DatasourceConfig, canyon_entities: &[CanyonRegisterEntity<'_>], - db_rows: Vec + db_rows: Vec, ) -> CanyonMemory { let mut mem = Self { memory: Vec::new(), @@ -240,8 +245,10 @@ impl CanyonMemory { #[cfg(not(cargo_check))] async fn create_memory(datasource_name: &str, database_type: &DatabaseType) { let query = match database_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => constants::postgresql_queries::CANYON_MEMORY_TABLE, - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => constants::mssql_queries::CANYON_MEMORY_TABLE + #[cfg(feature = "tokio-postgres")] + DatabaseType::PostgreSql => constants::postgresql_queries::CANYON_MEMORY_TABLE, + #[cfg(feature = "tiberius")] + DatabaseType::SqlServer => constants::mssql_queries::CANYON_MEMORY_TABLE, }; Self::query(query, [], datasource_name) diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index ff89bdc9..e068a3d4 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -246,7 +246,8 @@ impl MigrationsProcessor { ))); } - #[cfg(feature = "tiberius")] fn drop_column_not_null( + #[cfg(feature = "tiberius")] + fn drop_column_not_null( &mut self, table_name: &str, column_name: String, @@ -644,7 +645,8 @@ impl MigrationsHelper { canyon_register_entity_field: &CanyonRegisterEntityField, current_column_metadata: &ColumnMetadata, ) -> bool { - #[cfg(feature = "tokio-postgres")] { + #[cfg(feature = "tokio-postgres")] + { if db_type == DatabaseType::PostgreSql { return canyon_register_entity_field .to_postgres_alter_syntax() @@ -652,7 +654,8 @@ impl MigrationsHelper { == current_column_metadata.datatype; } } - #[cfg(feature = "tiberius")] { + #[cfg(feature = "tiberius")] + { if db_type == DatabaseType::SqlServer { // TODO Search a better way to get the datatype without useless info (like "VARCHAR(MAX)") return canyon_register_entity_field @@ -883,11 +886,15 @@ enum ColumnOperation { AlterColumnType(String, CanyonRegisterEntityField), AlterColumnDropNotNull(String, CanyonRegisterEntityField), // SQL server specific operation - SQL server can't drop a NOT NULL column - #[cfg(feature = "tiberius")] DropNotNullBeforeDropColumn(String, String, String), - #[cfg(feature = "tokio-postgres")] AlterColumnSetNotNull(String, CanyonRegisterEntityField), + #[cfg(feature = "tiberius")] + DropNotNullBeforeDropColumn(String, String, String), + #[cfg(feature = "tokio-postgres")] + AlterColumnSetNotNull(String, CanyonRegisterEntityField), // TODO if implement through annotations, modify for both GENERATED {ALWAYS, BY DEFAULT} - #[cfg(feature = "tokio-postgres")] AlterColumnAddIdentity(String, CanyonRegisterEntityField), - #[cfg(feature = "tokio-postgres")] AlterColumnDropIdentity(String, CanyonRegisterEntityField), + #[cfg(feature = "tokio-postgres")] + AlterColumnAddIdentity(String, CanyonRegisterEntityField), + #[cfg(feature = "tokio-postgres")] + AlterColumnDropIdentity(String, CanyonRegisterEntityField), } impl Transaction for ColumnOperation {} diff --git a/canyon_observer/src/migrations/register_types.rs b/canyon_observer/src/migrations/register_types.rs index 57ef6e39..b0cbf48d 100644 --- a/canyon_observer/src/migrations/register_types.rs +++ b/canyon_observer/src/migrations/register_types.rs @@ -1,8 +1,10 @@ use regex::Regex; +#[cfg(feature = "tokio-postgres")] +use crate::constants::postgresql_type; +#[cfg(feature = "tiberius")] +use crate::constants::sqlserver_type; use crate::constants::{regex_patterns, rust_type, NUMERIC_PK_DATATYPE}; -#[cfg(feature = "tokio-postgres")] use crate::constants::postgresql_type; -#[cfg(feature = "tiberius")] use crate::constants::sqlserver_type; /// This file contains `Rust` types that represents an entry on the `CanyonRegister` /// where `Canyon` tracks the user types that has to manage @@ -28,7 +30,8 @@ pub struct CanyonRegisterEntityField { impl CanyonRegisterEntityField { /// Return the postgres datatype and parameters to create a column for a given rust type - #[cfg(feature = "tokio-postgres")] pub fn to_postgres_syntax(&self) -> String { + #[cfg(feature = "tokio-postgres")] + pub fn to_postgres_syntax(&self) -> String { let rust_type_clean = self.field_type.replace(' ', ""); match rust_type_clean.as_str() { @@ -74,7 +77,8 @@ impl CanyonRegisterEntityField { /// Return the postgres datatype and parameters to create a column for a given rust type /// for Microsoft SQL Server - #[cfg(feature = "tiberius")] pub fn to_sqlserver_syntax(&self) -> String { + #[cfg(feature = "tiberius")] + pub fn to_sqlserver_syntax(&self) -> String { let rust_type_clean = self.field_type.replace(' ', ""); match rust_type_clean.as_str() { @@ -120,7 +124,8 @@ impl CanyonRegisterEntityField { } } - #[cfg(feature = "tokio-postgres")] pub fn to_postgres_alter_syntax(&self) -> String { + #[cfg(feature = "tokio-postgres")] + pub fn to_postgres_alter_syntax(&self) -> String { let mut rust_type_clean = self.field_type.replace(' ', ""); let rs_type_is_optional = self.field_type.to_uppercase().starts_with("OPTION"); @@ -162,7 +167,8 @@ impl CanyonRegisterEntityField { } } - #[cfg(feature = "tiberius")] pub fn to_sqlserver_alter_syntax(&self) -> String { + #[cfg(feature = "tiberius")] + pub fn to_sqlserver_alter_syntax(&self) -> String { let mut rust_type_clean = self.field_type.replace(' ', ""); let rs_type_is_optional = self.field_type.to_uppercase().starts_with("OPTION"); diff --git a/src/lib.rs b/src/lib.rs index cb8be374..20dac23f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,11 +3,10 @@ /// Here it's where all the available functionalities and features /// reaches the top most level, grouping them and making them visible /// through this crate, building the *public API* of the library - extern crate canyon_connection; extern crate canyon_crud; -extern crate canyon_observer; extern crate canyon_macros; +extern crate canyon_observer; // extern crate async_trait; @@ -43,8 +42,10 @@ pub mod query { /// Reexport the available database clients within Canyon pub mod db_clients { - #[cfg(feature = "postgres")] pub use canyon_connection::tokio_postgres; - #[cfg(feature = "mssql")] pub use canyon_connection::tiberius; + #[cfg(feature = "mssql")] + pub use canyon_connection::tiberius; + #[cfg(feature = "postgres")] + pub use canyon_connection::tokio_postgres; } /// Reexport the needed runtime dependencies diff --git a/tests/crud/mod.rs b/tests/crud/mod.rs index 7526c8f6..c0f6afee 100644 --- a/tests/crud/mod.rs +++ b/tests/crud/mod.rs @@ -29,10 +29,10 @@ use canyon_sql::runtime::tokio_util::compat::TokioAsyncWriteCompatExt; #[canyon_sql::macros::canyon_tokio_test] #[ignore] fn initialize_sql_server_docker_instance() { - canyon_sql::runtime::futures::executor::block_on(async { - static CONN_STR: &str = - "server=tcp:localhost,1434;User Id=SA;Password=SqlServer-10;TrustServerCertificate=true"; + static CONN_STR: &str = + "server=tcp:localhost,1434;User Id=SA;Password=SqlServer-10;TrustServerCertificate=true"; + canyon_sql::runtime::futures::executor::block_on(async { let config = Config::from_ado_string(CONN_STR).unwrap(); let tcp = TcpStream::connect(config.get_addr()).await.unwrap(); From f6c4408b884b10fee463bc51f1b452e837f7f6ac Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Tue, 18 Apr 2023 18:12:12 +0200 Subject: [PATCH 44/82] Discarded the integrated auth cfg feature flag, it will be included directly with the mssql feature --- canyon_connection/src/canyon_database_connector.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 5330e5e4..1ef74c6b 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -107,8 +107,6 @@ impl DatabaseConnection { crate::datasources::SqlServerAuth::Basic { username, password } => { AuthMethod::sql_server(username, password) } - #[cfg(feature = "mssql-integrated-auth")] - // TODO pending, or remove the cfg? crate::datasources::SqlServerAuth::Integrated => AuthMethod::Integrated, }, }); From c7893e346db62380987c58d6d256f8bdd5ae15d8 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Wed, 19 Apr 2023 12:39:21 +0200 Subject: [PATCH 45/82] Addressing issues on the macros with the new code structure - [select - count] --- Cargo.toml | 6 ++-- tests/canyon.toml => canyon.toml | 0 canyon_connection/Cargo.toml | 4 +-- canyon_connection/src/datasources.rs | 2 +- canyon_crud/src/crud.rs | 2 +- canyon_crud/src/rows.rs | 11 ++++++ canyon_macros/src/query_operations/insert.rs | 20 +++++++---- canyon_macros/src/query_operations/select.rs | 38 ++++++++++++-------- canyon_observer/src/migrations/memory.rs | 1 - src/lib.rs | 16 ++++++--- tests/migrations/mod.rs | 4 +-- 11 files changed, 69 insertions(+), 35 deletions(-) rename tests/canyon.toml => canyon.toml (100%) diff --git a/Cargo.toml b/Cargo.toml index 501c3fa6..baef5115 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "canyon_sql" -version = "0.2.0" +version.workspace = true +edition.workspace = true [workspace] members = [ "canyon_connection", + "canyon_crud", "canyon_observer", "canyon_macros", - "canyon_crud", "tests" ] @@ -18,6 +19,7 @@ canyon_macros = { version = "0.2.0", path = "canyon_macros" } canyon_observer = { version = "0.2.0", path = "canyon_observer" } canyon_crud = { version = "0.2.0", path = "canyon_crud" } canyon_connection = { version = "0.2.0", path = "canyon_connection" } +async-trait = "0.1.68" [workspace.dependencies] canyon_crud = { version = "0.2.0", path = "canyon_crud" } diff --git a/tests/canyon.toml b/canyon.toml similarity index 100% rename from tests/canyon.toml rename to canyon.toml diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 77fea282..15aea6d9 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -12,8 +12,8 @@ description.workspace = true [dependencies] tokio = { workspace = true } tokio-util = { workspace = true } -tokio-postgres = { workspace = true, optional = true } -tiberius = { workspace = true, optional = true} +tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"], optional = true } +tiberius = { version = "0.12.1", features = ["tds73", "chrono", "integrated-auth-gssapi"], optional = true } futures = { workspace = true } indexmap = { workspace = true } async-std = { workspace = true } diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index c2be5aa5..feb50e61 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -6,7 +6,7 @@ use crate::canyon_database_connector::DatabaseType; #[test] fn load_ds_config_from_array() { const CONFIG_FILE_MOCK_ALT: &str = r#" - [canyon_sql_root] + [canyon_sql] datasources = [ {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' }, diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index c38ea10a..53d4728a 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -50,7 +50,7 @@ pub trait Transaction { let mut guarded_cache = CACHED_DATABASE_CONN.lock().await; let database_conn = get_database_connection(datasource_name, &mut guarded_cache); - match database_conn { + match *database_conn { #[cfg(feature = "tokio-postgres")] DatabaseConnection::Postgres(_) => { postgres_query_launcher::launch::( diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs index 02322971..bbf096b1 100644 --- a/canyon_crud/src/rows.rs +++ b/canyon_crud/src/rows.rs @@ -46,6 +46,17 @@ impl CanyonRows { _ => panic!("This branch will never ever should be reachable"), } } + + /// Returns the number of elements present on the wrapped collection + pub fn len(&self) -> usize { + match self { + #[cfg(feature = "tokio-postgres")] + Self::Postgres(v) => v.len(), + #[cfg(feature = "tiberius")] + Self::Tiberius(v) => v.len(), + _ => panic!("This branch will never ever should be reachable"), + } + } } // #[cfg(feature = "tokio-postgres")] diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index 18cf89f8..213315e5 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -49,21 +49,25 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri #primary_key ); - let rows = <#ty as canyon_sql::crud::Transaction<#ty>>::query_for_rows( + let rows = <#ty as canyon_sql::crud::Transaction<#ty>>::query( stmt, values, datasource_name ).await?; - match rows { - #[cfg(feature = "tokio-postgres")] Self::Postgres(mut v) => { + Ok(()) + + /* match rows { + // #[cfg(feature = "tokio-postgres")] + canyon_sql::connection::Postgres(mut v) => { instance.#pk_ident = v .get(idx) .expect("Failed getting the returned IDs for a multi insert") .get::<&str, #pk_type>(#primary_key); Ok(()) }, - #[cfg(feature = "tiberius")] Self::Tiberius(mut v) => { + // #[cfg(feature = "tiberius")] + canyon_sql::connection::Tiberius(mut v) => { instance.#pk_ident = v .get(idx) .expect("Failed getting the returned IDs for a multi insert") @@ -72,7 +76,7 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri Ok(()) }, _ => panic!() // TODO remove when the generics will be refactored - } + } */ } } else { quote! { @@ -441,7 +445,8 @@ pub fn generate_multiple_insert_tokens( let mut mapped_fields: String = String::new(); - #multi_insert_transaction + // #multi_insert_transaction + Ok(()) } /// Inserts multiple instances of some type `T` into its related table with the specified @@ -497,7 +502,8 @@ pub fn generate_multiple_insert_tokens( let mut mapped_fields: String = String::new(); - #multi_insert_transaction + // #multi_insert_transaction + Ok(()) } } } diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index c5875f03..fd136fd4 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -27,6 +27,7 @@ pub fn generate_find_all_unchecked_tokens( "" ).await .unwrap() + .into_results::<#ty>() } /// Performs a `SELECT * FROM table_name`, where `table_name` it's @@ -44,6 +45,7 @@ pub fn generate_find_all_unchecked_tokens( datasource_name ).await .unwrap() + .into_results::<#ty>() } } } @@ -71,6 +73,7 @@ pub fn generate_find_all_tokens( &[], "" ).await? + .into_results::<#ty>() ) } @@ -95,6 +98,7 @@ pub fn generate_find_all_tokens( &[], datasource_name ).await? + .into_results::<#ty>() ) } } @@ -148,27 +152,33 @@ pub fn generate_count_tokens( let result_handling = quote! { match count { - #[cfg(feature = "tokio-postgres")] Self::Postgres(mut v) => Ok( + // #[cfg(feature = "tokio-postgres")] + canyon_sql::crud::CanyonRows::Postgres(mut v) => Ok( v.remove(0).get::<&str, i64>("count") ), - #[cfg(feature = "tiberius")] Self::Tiberius(mut v) => + // #[cfg(feature = "tiberius")] + canyon_sql::crud::CanyonRows::Tiberius(mut v) => v.remove(0) - .get::("count") - .ok_or(format!("Failure in the COUNT query for MSSQL for: {}", #ty_str).into()), + .get::(0) + .map(|c| c as i64) + .ok_or(format!("Failure in the COUNT query for MSSQL for: {}", #ty_str).into()) + .into(), _ => panic!() // TODO remove when the generics will be refactored } + // Ok(0 as i64) }; quote! { /// Performs a COUNT(*) query over some table, returning a [`Result`] rather than panicking, /// wrapping a possible success or error coming from the database async fn count() -> Result> { - <#ty as canyon_sql::crud::Transaction<#ty>>::query( + let count = <#ty as canyon_sql::crud::Transaction<#ty>>::query( #stmt, &[], "" - ).await - .get_by_idx_and_key(0, "count") + ).await?; + + #result_handling } /// Performs a COUNT(*) query over some table, returning a [`Result`] rather than panicking, @@ -226,12 +236,11 @@ pub fn generate_find_by_pk_tokens( }; } - // TOODO no tenemos number_OF_results let result_handling = quote! { match result { n if n.len() == 0 => Ok(None), _ => Ok( - Some(result.remove(0)) + Some(result.into_results::<#ty>().remove(0)) ) } }; @@ -334,10 +343,9 @@ pub fn generate_find_by_foreign_key_tokens( ); let result_handler = quote! { match result { - // TODO Noof n if n.len() == 0 => Ok(None), _ => Ok(Some( - result.remove(0) + result.into_results::<#fk_ty>().remove(0) )) } }; @@ -422,8 +430,8 @@ pub fn generate_find_by_reverse_foreign_key_tokens( #quoted_method_signature { let lookage_value = value.get_fk_column(#column) - .expect(format!( - "Column: {:?} not found in type: {:?}", #column, #table + .expect(format!( + "Column: {:?} not found in type: {:?}", #column, #table ).as_str()); let stmt = format!( @@ -436,7 +444,7 @@ pub fn generate_find_by_reverse_foreign_key_tokens( stmt, &[lookage_value], "" - ).await?) + ).await?.into_results::<#ty>()) } }, )); @@ -464,7 +472,7 @@ pub fn generate_find_by_reverse_foreign_key_tokens( stmt, &[lookage_value], datasource_name - ).await?) + ).await?.into_results::<#ty>()) } }, )); diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index d22383a0..912bf6dc 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -242,7 +242,6 @@ impl CanyonMemory { } /// Generates, if not exists the `canyon_memory` table - #[cfg(not(cargo_check))] async fn create_memory(datasource_name: &str, database_type: &DatabaseType) { let query = match database_type { #[cfg(feature = "tokio-postgres")] diff --git a/src/lib.rs b/src/lib.rs index 20dac23f..8253cea9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ extern crate canyon_crud; extern crate canyon_macros; extern crate canyon_observer; -// extern crate async_trait; +extern crate async_trait; /// Reexported elements to the root of the public API pub mod migrations { @@ -21,10 +21,17 @@ pub use canyon_macros::main; /// Public API for the `Canyon-SQL` proc-macros, and for the external ones pub mod macros { - // pub use async_trait::*; + pub use async_trait::*; pub use canyon_macros::*; } +/// connection module serves to reexport the public elements of the `canyon_connection` crate, +/// exposing them through the public API +pub mod connection { + pub use canyon_connection::canyon_database_connector::DatabaseConnection::Postgres; + pub use canyon_connection::canyon_database_connector::DatabaseConnection::SqlServer; +} + /// Crud module serves to reexport the public elements of the `canyon_crud` crate, /// exposing them through the public API pub mod crud { @@ -32,6 +39,7 @@ pub mod crud { pub use canyon_crud::crud::*; pub use canyon_crud::mapper::*; pub use canyon_crud::DatabaseType; + pub use canyon_crud::rows::CanyonRows; } /// Re-exports the query elements from the `crud`crate @@ -42,10 +50,10 @@ pub mod query { /// Reexport the available database clients within Canyon pub mod db_clients { - #[cfg(feature = "mssql")] - pub use canyon_connection::tiberius; #[cfg(feature = "postgres")] pub use canyon_connection::tokio_postgres; + #[cfg(feature = "mssql")] + pub use canyon_connection::tiberius; } /// Reexport the needed runtime dependencies diff --git a/tests/migrations/mod.rs b/tests/migrations/mod.rs index 17b19c35..12dfa111 100644 --- a/tests/migrations/mod.rs +++ b/tests/migrations/mod.rs @@ -9,8 +9,8 @@ fn test_migrations_postgresql_status_query() { let results = Migrations::query(constants::FETCH_PUBLIC_SCHEMA, [], constants::PSQL_DS).await; assert!(results.is_ok()); - let public_schema_info = results.ok().unwrap().postgres; - + let res = results.unwrap(); + let public_schema_info = res.get_postgres_rows(); let first_result = public_schema_info.get(0).unwrap(); assert_eq!(first_result.columns().get(0).unwrap().name(), "table_name"); From b576757541bdf9cb896454ac7186d37869bbb225 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Wed, 19 Apr 2023 12:58:11 +0200 Subject: [PATCH 46/82] Addressing issues on the macros with the new code structure - [insert - insert(s) + multi_insert(s)] --- canyon_macros/src/query_operations/insert.rs | 45 +++++++++----------- canyon_macros/src/query_operations/select.rs | 1 - src/lib.rs | 4 +- tests/canyon.toml | 24 +++++++++++ 4 files changed, 46 insertions(+), 28 deletions(-) create mode 100644 tests/canyon.toml diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index 213315e5..063df25c 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -55,28 +55,26 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri datasource_name ).await?; - Ok(()) - - /* match rows { + match rows { // #[cfg(feature = "tokio-postgres")] - canyon_sql::connection::Postgres(mut v) => { - instance.#pk_ident = v - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") + canyon_sql::crud::CanyonRows::Postgres(mut v) => { + self.#pk_ident = v + .get(0) + .expect("Failed getting the returned IDs for an insert") .get::<&str, #pk_type>(#primary_key); Ok(()) }, // #[cfg(feature = "tiberius")] - canyon_sql::connection::Tiberius(mut v) => { - instance.#pk_ident = v - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") + canyon_sql::crud::CanyonRows::Tiberius(mut v) => { + self.#pk_ident = v + .get(0) + .expect("Failed getting the returned IDs for an insert") .get::<#pk_type, &str>(#primary_key) .expect("SQL Server primary key type failed to be set as value"); Ok(()) }, _ => panic!() // TODO remove when the generics will be refactored - } */ + } } } else { quote! { @@ -296,7 +294,8 @@ pub fn generate_multiple_insert_tokens( match result { Ok(res) => { match res { - #[cfg(feature = "tokio-postgres")] Self::Postgres(mut v) => { + // #[cfg(feature = "tokio-postgres")] + canyon_sql::crud::CanyonRows::Postgres(mut v) => { for (idx, instance) in instances.iter_mut().enumerate() { instance.#pk_ident = v .get(idx) @@ -306,7 +305,8 @@ pub fn generate_multiple_insert_tokens( Ok(()) }, - #[cfg(feature = "tiberius")] Self::Tiberius(mut v) => { + // #[cfg(feature = "tiberius")] + canyon_sql::crud::CanyonRows::Tiberius(mut v) => { for (idx, instance) in instances.iter_mut().enumerate() { instance.#pk_ident = v .get(idx) @@ -315,7 +315,7 @@ pub fn generate_multiple_insert_tokens( .expect("SQL Server primary key type failed to be set as value"); } - Ok(()), + Ok(()) }, _ => panic!() // TODO remove when the generics will be refactored } @@ -378,16 +378,13 @@ pub fn generate_multiple_insert_tokens( } } - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( + <#ty as canyon_sql::crud::Transaction<#ty>>::query( stmt, v_arr, datasource_name - ).await; + ).await?; - match result { - Ok(res) => Ok(()), - Err(e) => Err(e) - } + Ok(()) } }; @@ -445,8 +442,7 @@ pub fn generate_multiple_insert_tokens( let mut mapped_fields: String = String::new(); - // #multi_insert_transaction - Ok(()) + #multi_insert_transaction } /// Inserts multiple instances of some type `T` into its related table with the specified @@ -502,8 +498,7 @@ pub fn generate_multiple_insert_tokens( let mut mapped_fields: String = String::new(); - // #multi_insert_transaction - Ok(()) + #multi_insert_transaction } } } diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index fd136fd4..3086aea5 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -165,7 +165,6 @@ pub fn generate_count_tokens( .into(), _ => panic!() // TODO remove when the generics will be refactored } - // Ok(0 as i64) }; quote! { diff --git a/src/lib.rs b/src/lib.rs index 8253cea9..3aaf6ea9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,8 +28,8 @@ pub mod macros { /// connection module serves to reexport the public elements of the `canyon_connection` crate, /// exposing them through the public API pub mod connection { - pub use canyon_connection::canyon_database_connector::DatabaseConnection::Postgres; - pub use canyon_connection::canyon_database_connector::DatabaseConnection::SqlServer; + #[cfg(feature = "postgres")] pub use canyon_connection::canyon_database_connector::DatabaseConnection::Postgres; + #[cfg(feature = "mssql")] pub use canyon_connection::canyon_database_connector::DatabaseConnection::SqlServer; } /// Crud module serves to reexport the public elements of the `canyon_crud` crate, diff --git a/tests/canyon.toml b/tests/canyon.toml new file mode 100644 index 00000000..0b0614a4 --- /dev/null +++ b/tests/canyon.toml @@ -0,0 +1,24 @@ +[canyon_sql] + +[[canyon_sql.datasources]] +name = 'postgres_docker' + +[canyon_sql.datasources.auth] +postgresql = { basic = { username = 'postgres', password = 'postgres'}} + +[canyon_sql.datasources.properties] +host = 'localhost' +port = 5438 +db_name = 'postgres' + + +[[canyon_sql.datasources]] +name = 'sqlserver_docker' + +[canyon_sql.datasources.auth] +sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } + +[canyon_sql.datasources.properties] +host = 'localhost' +port = 1434 +db_name = 'master' From 8558c9862edfac66071a99cb30c5baebfa94e8dc Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Wed, 19 Apr 2023 18:08:37 +0200 Subject: [PATCH 47/82] WIP - Reworked the way of how we was thinking about our features. Trying to conditionaly compilate client code by feature enabled --- Cargo.toml | 10 +- canyon_connection/Cargo.toml | 5 + .../src/canyon_database_connector.rs | 34 +-- canyon_connection/src/datasources.rs | 20 +- canyon_connection/src/lib.rs | 8 +- canyon_crud/Cargo.toml | 8 +- canyon_crud/src/bounds.rs | 196 +++++++++--------- canyon_crud/src/crud.rs | 8 +- canyon_crud/src/mapper.rs | 8 +- canyon_crud/src/rows.rs | 20 +- canyon_macros/Cargo.toml | 6 +- canyon_macros/src/query_operations/insert.rs | 53 +++-- canyon_macros/src/query_operations/select.rs | 4 +- canyon_observer/Cargo.toml | 5 + canyon_observer/src/constants.rs | 4 +- canyon_observer/src/migrations/handler.rs | 10 +- .../src/migrations/information_schema.rs | 4 +- canyon_observer/src/migrations/memory.rs | 4 +- canyon_observer/src/migrations/processor.rs | 32 +-- .../src/migrations/register_types.rs | 6 +- src/lib.rs | 7 +- tests/Cargo.toml | 7 +- 22 files changed, 246 insertions(+), 213 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index baef5115..e7cf2500 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,15 +15,17 @@ members = [ [dependencies] # Project crates -canyon_macros = { version = "0.2.0", path = "canyon_macros" } -canyon_observer = { version = "0.2.0", path = "canyon_observer" } +canyon_connection = { version = "0.2.0", path = "canyon_connection", optional = true } canyon_crud = { version = "0.2.0", path = "canyon_crud" } -canyon_connection = { version = "0.2.0", path = "canyon_connection" } +canyon_observer = { version = "0.2.0", path = "canyon_observer" } +canyon_macros = { version = "0.2.0", path = "canyon_macros" } async-trait = "0.1.68" [workspace.dependencies] canyon_crud = { version = "0.2.0", path = "canyon_crud" } canyon_connection = { version = "0.2.0", path = "canyon_connection" } +canyon_observer = { version = "0.2.0", path = "canyon_observer" } +canyon_macros = { version = "0.2.0", path = "canyon_macros" } tokio = { version = "1.27.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["compat"] } @@ -51,5 +53,5 @@ description = "A Rust ORM and QueryBuilder" [features] default = ["postgres"] -postgres = ["canyon_connection/tokio-postgres", "canyon_crud/tokio-postgres", "canyon_observer/tokio-postgres"] +postgres = ["canyon_connection/postgres", "canyon_connection/tokio-postgres"] mssql = ["canyon_connection/tiberius", "canyon_crud/tiberius", "canyon_observer/tiberius"] diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 15aea6d9..9bdacbc2 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -20,3 +20,8 @@ async-std = { workspace = true } lazy_static = { workspace = true } serde = { workspace = true, features = ["derive"] } toml = { workspace = true } + +[features] +default = ["postgres"] +postgres = ["tokio-postgres"] +mssql = ["tiberius"] diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 1ef74c6b..bcb07d8e 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -1,10 +1,10 @@ use serde::Deserialize; -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] use async_std::net::TcpStream; -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] use tiberius::{AuthMethod, Config}; -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] use tokio_postgres::{Client, NoTls}; use crate::datasources::DatasourceConfig; @@ -13,22 +13,22 @@ use crate::datasources::DatasourceConfig; #[derive(Deserialize, Debug, Eq, PartialEq, Clone, Copy)] pub enum DatabaseType { #[serde(alias = "postgres", alias = "postgresql")] - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] PostgreSql, #[serde(alias = "sqlserver", alias = "mssql")] - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] SqlServer, } /// A connection with a `PostgreSQL` database -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] pub struct PostgreSqlConnection { pub client: Client, // pub connection: Connection, // TODO Hold it, or not to hold it... that's the question! } /// A connection with a `SqlServer` database -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] pub struct SqlServerConnection { pub client: &'static mut tiberius::Client, } @@ -38,9 +38,9 @@ pub struct SqlServerConnection { /// process them and generates a pool of 1 to 1 database connection for /// every datasource defined. pub enum DatabaseConnection { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] Postgres(PostgreSqlConnection), - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] SqlServer(SqlServerConnection), } @@ -52,7 +52,7 @@ impl DatabaseConnection { datasource: &DatasourceConfig, ) -> Result> { match datasource.get_db_type() { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => { let (username, password) = match &datasource.auth { crate::datasources::Auth::Postgres(postgres_auth) => match postgres_auth { @@ -60,7 +60,7 @@ impl DatabaseConnection { (username.as_str(), password.as_str()) } }, - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] crate::datasources::Auth::SqlServer(_) => { panic!("Found SqlServer auth configuration for a PostgreSQL datasource") } @@ -89,7 +89,7 @@ impl DatabaseConnection { // connection: new_connection, })) } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] DatabaseType::SqlServer => { let mut config = Config::new(); @@ -99,7 +99,7 @@ impl DatabaseConnection { // Using SQL Server authentication. config.authentication(match &datasource.auth { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] crate::datasources::Auth::Postgres(_) => { panic!("Found PostgreSQL auth configuration for a SqlServer database") } @@ -139,7 +139,7 @@ impl DatabaseConnection { } } - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] #[allow(unreachable_patterns)] pub fn postgres_connection(&self) -> Option<&PostgreSqlConnection> { match self { @@ -148,7 +148,7 @@ impl DatabaseConnection { } } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] pub fn sqlserver_connection(&mut self) -> Option<&mut SqlServerConnection> { match self { DatabaseConnection::SqlServer(conn) => Some(conn), @@ -176,12 +176,12 @@ mod database_connection_handler { let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) .expect("A failure happened retrieving the [canyon_sql_root] section"); - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] assert_eq!( config.canyon_sql.datasources[0].get_db_type(), DatabaseType::PostgreSql ); - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] assert_eq!( config.canyon_sql.datasources[1].get_db_type(), DatabaseType::SqlServer diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index feb50e61..2dd3913c 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -35,11 +35,11 @@ fn load_ds_config_from_array() { assert_eq!(ds_0.properties.db_name, "triforce"); assert_eq!(ds_0.properties.migrations, Some(Migrations::Enabled)); - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] assert_eq!(ds_1.name, "SqlServerDS"); - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] assert_eq!( ds_1.auth, Auth::SqlServer(SqlServerAuth::Basic { @@ -52,7 +52,7 @@ fn load_ds_config_from_array() { assert_eq!(ds_1.properties.db_name, "triforce2"); assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] assert_eq!(_ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) } /// @@ -76,9 +76,9 @@ pub struct DatasourceConfig { impl DatasourceConfig { pub fn get_db_type(&self) -> DatabaseType { match self.auth { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] Auth::Postgres(_) => DatabaseType::PostgreSql, - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] Auth::SqlServer(_) => DatabaseType::SqlServer, } } @@ -87,22 +87,22 @@ impl DatasourceConfig { #[derive(Deserialize, Debug, Clone, PartialEq)] pub enum Auth { #[serde(alias = "PostgreSQL", alias = "postgresql", alias = "postgres")] - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] Postgres(PostgresAuth), #[serde(alias = "SqlServer", alias = "sqlserver", alias = "mssql")] - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] SqlServer(SqlServerAuth), } #[derive(Deserialize, Debug, Clone, PartialEq)] -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] pub enum PostgresAuth { #[serde(alias = "Basic", alias = "basic")] Basic { username: String, password: String }, } #[derive(Deserialize, Debug, Clone, PartialEq)] -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] pub enum SqlServerAuth { #[serde(alias = "Basic", alias = "basic")] Basic { username: String, password: String }, diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index 64960537..699341d6 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -1,13 +1,13 @@ -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] pub extern crate async_std; pub extern crate futures; pub extern crate lazy_static; -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] pub extern crate tiberius; pub extern crate tokio; -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] pub extern crate tokio_postgres; -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] pub extern crate tokio_util; pub mod canyon_database_connector; diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index 6f6ee233..33213847 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -21,8 +21,8 @@ async-trait = { version = "0.1.50" } canyon_connection = { workspace = true, path = "../canyon_connection" } -#[features] -#default = ["postgres"] -#postgres = ["tokio", "tokio-postgres", "tokio-util"] -#mssql = ["tiberius", "tiberius/tds73", "tiberius/chrono"] +[features] +default = ["postgres"] +postgres = ["tokio-postgres"] +mssql = ["tiberius"] #mssql-integrated-auth = ["mssql", "tiberius/integrated-auth-gssapi"] \ No newline at end of file diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs index 6a6842ba..7ed83f1b 100644 --- a/canyon_crud/src/bounds.rs +++ b/canyon_crud/src/bounds.rs @@ -3,10 +3,10 @@ use crate::{ mapper::RowMapper, }; -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] use canyon_connection::tokio_postgres::{self, types::ToSql}; -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] use canyon_connection::tiberius::{self, ColumnData, IntoSql}; use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc}; @@ -86,14 +86,14 @@ pub trait Row { fn as_any(&self) -> &dyn Any; } -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] impl Row for tokio_postgres::Row { fn as_any(&self) -> &dyn Any { self } } -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] impl Row for tiberius::Row { fn as_any(&self) -> &dyn Any { self @@ -116,8 +116,8 @@ impl<'a> Column<'a> { } // pub fn type_(&'a self) -> &'_ dyn Type { // match (*self).type_ { - // #[cfg(feature = "tokio-postgres")] ColumnType::Postgres(v) => v as &'a dyn Type, - // #[cfg(feature = "tiberius")] ColumnType::SqlServer(v) => v as &'a dyn Type, + // #[cfg(feature = "postgres")] ColumnType::Postgres(v) => v as &'a dyn Type, + // #[cfg(feature = "mssql")] ColumnType::SqlServer(v) => v as &'a dyn Type, // } // } } @@ -125,13 +125,13 @@ impl<'a> Column<'a> { pub trait Type { fn as_any(&self) -> &dyn Any; } -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] impl Type for tokio_postgres::types::Type { fn as_any(&self) -> &dyn Any { self } } -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] impl Type for tiberius::ColumnType { fn as_any(&self) -> &dyn Any { self @@ -140,27 +140,27 @@ impl Type for tiberius::ColumnType { /// Wrapper over the dependencies Column's types pub enum ColumnType { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] Postgres(tokio_postgres::types::Type), - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] SqlServer(tiberius::ColumnType), } pub trait RowOperations { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn get_postgres<'a, Output>(&'a self, col_name: &'a str) -> Output where Output: tokio_postgres::types::FromSql<'a>; - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output where Output: tiberius::FromSql<'a>; - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option where Output: tokio_postgres::types::FromSql<'a>; - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn get_mssql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option where Output: tiberius::FromSql<'a>; @@ -169,7 +169,7 @@ pub trait RowOperations { } impl RowOperations for &dyn Row { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn get_postgres<'a, Output>(&'a self, col_name: &'a str) -> Output where Output: tokio_postgres::types::FromSql<'a>, @@ -179,7 +179,7 @@ impl RowOperations for &dyn Row { }; panic!() // TODO into result and propagate } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output where Output: tiberius::FromSql<'a>, @@ -192,7 +192,7 @@ impl RowOperations for &dyn Row { panic!() // TODO into result and propagate } - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option where Output: tokio_postgres::types::FromSql<'a>, @@ -203,7 +203,7 @@ impl RowOperations for &dyn Row { panic!() // TODO into result and propagate } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn get_mssql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option where Output: tiberius::FromSql<'a>, @@ -217,7 +217,7 @@ impl RowOperations for &dyn Row { fn columns(&self) -> Vec { let mut cols = vec![]; - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] { if self.as_any().is::() { self.as_any() @@ -233,7 +233,7 @@ impl RowOperations for &dyn Row { }) } } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] { if self.as_any().is::() { self.as_any() @@ -257,9 +257,9 @@ impl RowOperations for &dyn Row { /// Defines a trait for represent type bounds against the allowed /// data types supported by Canyon to be used as query parameters. pub trait QueryParameter<'a>: std::fmt::Debug + Sync + Send { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync); - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_>; } @@ -271,7 +271,7 @@ pub trait QueryParameter<'a>: std::fmt::Debug + Sync + Send { /// a collection of [`QueryParameter<'a>`], in order to allow a workflow /// that is not dependent of the specific type of the argument that holds /// the query parameters of the database connectors -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { fn into_sql(self) -> ColumnData<'a> { self.as_sqlserver_param() @@ -279,131 +279,131 @@ impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { } impl<'a> QueryParameter<'a> for bool { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::Bit(Some(*self)) } } impl<'a> QueryParameter<'a> for i16 { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(*self)) } } impl<'a> QueryParameter<'a> for &i16 { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(*self) } } impl<'a> QueryParameter<'a> for Option<&i16> { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for i32 { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self)) } } impl<'a> QueryParameter<'a> for &i32 { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(*self) } } impl<'a> QueryParameter<'a> for Option<&i32> { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for f32 { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some(*self)) } } impl<'a> QueryParameter<'a> for &f32 { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(*self) } } impl<'a> QueryParameter<'a> for Option<&f32> { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some( *self.expect("Error on an f32 value on QueryParameter<'_>"), @@ -411,42 +411,42 @@ impl<'a> QueryParameter<'a> for Option<&f32> { } } impl<'a> QueryParameter<'a> for f64 { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some(*self)) } } impl<'a> QueryParameter<'a> for &f64 { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(*self) } } impl<'a> QueryParameter<'a> for Option<&f64> { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some( *self.expect("Error on an f64 value on QueryParameter<'_>"), @@ -454,71 +454,71 @@ impl<'a> QueryParameter<'a> for Option<&f64> { } } impl<'a> QueryParameter<'a> for i64 { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self)) } } impl<'a> QueryParameter<'a> for &i64 { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(*self) } } impl<'a> QueryParameter<'a> for Option<&i64> { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for String { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Owned(self.to_owned()))) } } impl<'a> QueryParameter<'a> for &String { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Borrowed(self))) } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { match self { Some(string) => ColumnData::String(Some(std::borrow::Cow::Owned(string.to_owned()))), @@ -527,11 +527,11 @@ impl<'a> QueryParameter<'a> for Option { } } impl<'a> QueryParameter<'a> for Option<&String> { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { match self { Some(string) => ColumnData::String(Some(std::borrow::Cow::Borrowed(string))), @@ -540,21 +540,21 @@ impl<'a> QueryParameter<'a> for Option<&String> { } } impl<'a> QueryParameter<'_> for &'_ str { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Borrowed(*self))) } } impl<'a> QueryParameter<'a> for Option<&'_ str> { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { match *self { Some(str) => ColumnData::String(Some(std::borrow::Cow::Borrowed(str))), @@ -563,101 +563,101 @@ impl<'a> QueryParameter<'a> for Option<&'_ str> { } } impl<'a> QueryParameter<'_> for NaiveDate { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for NaiveTime { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for NaiveDateTime { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for DateTime { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option> { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for DateTime { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'_> for Option> { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index 53d4728a..b06edfda 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -51,7 +51,7 @@ pub trait Transaction { let database_conn = get_database_connection(datasource_name, &mut guarded_cache); match *database_conn { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] DatabaseConnection::Postgres(_) => { postgres_query_launcher::launch::( database_conn, @@ -60,7 +60,7 @@ pub trait Transaction { ) .await } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] DatabaseConnection::SqlServer(_) => { sqlserver_query_launcher::launch::( database_conn, @@ -161,7 +161,7 @@ where fn delete_query_datasource(datasource_name: &str) -> DeleteQueryBuilder<'_, T>; } -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] mod postgres_query_launcher { use crate::bounds::QueryParameter; use crate::rows::CanyonRows; @@ -188,7 +188,7 @@ mod postgres_query_launcher { } } -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] mod sqlserver_query_launcher { use crate::rows::CanyonRows; use crate::{ diff --git a/canyon_crud/src/mapper.rs b/canyon_crud/src/mapper.rs index cc944f1d..66cb91d2 100644 --- a/canyon_crud/src/mapper.rs +++ b/canyon_crud/src/mapper.rs @@ -1,6 +1,6 @@ -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] use canyon_connection::tiberius; -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] use canyon_connection::tokio_postgres; use crate::crud::Transaction; @@ -9,8 +9,8 @@ use crate::crud::Transaction; /// from some supported database in Canyon-SQL into a user's defined /// type `T` pub trait RowMapper>: Sized { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn deserialize_postgresql(row: &tokio_postgres::Row) -> T; - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn deserialize_sqlserver(row: &tiberius::Row) -> T; } diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs index bbf096b1..056e136a 100644 --- a/canyon_crud/src/rows.rs +++ b/canyon_crud/src/rows.rs @@ -9,15 +9,15 @@ use std::marker::PhantomData; /// operations that are too difficult or to ugly to implement in the macros that /// will call the query method of Crud. pub enum CanyonRows { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] Postgres(Vec), - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] Tiberius(Vec), UnusableTypeMarker(PhantomData), } impl CanyonRows { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] pub fn get_postgres_rows(&self) -> &Vec { match self { Self::Postgres(v) => v, @@ -25,7 +25,7 @@ impl CanyonRows { } } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] pub fn get_tiberius_rows(&self) -> &Vec { match self { Self::Tiberius(v) => v, @@ -39,9 +39,9 @@ impl CanyonRows { T: Transaction, { match self { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] Self::Postgres(v) => v.iter().map(|row| Z::deserialize_postgresql(row)).collect(), - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] Self::Tiberius(v) => v.iter().map(|row| Z::deserialize_sqlserver(&row)).collect(), _ => panic!("This branch will never ever should be reachable"), } @@ -50,16 +50,16 @@ impl CanyonRows { /// Returns the number of elements present on the wrapped collection pub fn len(&self) -> usize { match self { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] Self::Postgres(v) => v.len(), - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] Self::Tiberius(v) => v.len(), _ => panic!("This branch will never ever should be reachable"), } } } -// #[cfg(feature = "tokio-postgres")] +// #[cfg(feature = "postgres")] // impl IntoIterator for CanyonRows { // type Item = tokio_postgres::Row; // type IntoIter = std::vec::IntoIter; @@ -72,7 +72,7 @@ impl CanyonRows { // } // } // -// #[cfg(feature = "tiberius")] +// #[cfg(feature = "mssql")] // impl IntoIterator for CanyonRows { // type Item = tiberius::Row; // type IntoIter = std::vec::IntoIter; diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index a501440d..d095e7ff 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -19,6 +19,6 @@ proc-macro2 = "1.0.27" futures = "0.3.21" tokio = { version = "1.9.0", features = ["full"] } -canyon_observer = { version = "0.2.0", path = "../canyon_observer" } -canyon_crud = { version = "0.2.0", path = "../canyon_crud" } -canyon_connection = { version = "0.2.0", path = "../canyon_connection" } +canyon_observer = { workspace = true, path = "../canyon_observer" } +canyon_crud = { workspace = true, path = "../canyon_crud" } +canyon_connection = { workspace = true, path = "../canyon_connection" } diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index 063df25c..3093bc4c 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -36,8 +36,36 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri .find(|(i, _t)| Some(i.to_string()) == primary_key); let insert_transaction = if let Some(pk_data) = &pk_ident_type { let pk_ident = &pk_data.0; + let pk_ident_str = &pk_data.0.to_string(); let pk_type = &pk_data.1; + let postgres_db_conn_match_arm = if cfg!(feature = "canyon_sql/postgres") { + quote! { + canyon_sql::crud::CanyonRows::Postgres(mut v) => { + self.#pk_ident = v + .get(0) + .expect("Failed getting the returned IDs for an insert") + .get::<&str, #pk_type>(#primary_key); + Ok(()) + } + } + } else { + println!("No feature postgres detected for: {:?}", pk_ident_str); + quote! {} + }; + + let mssql_db_conn_match_arm = if cfg!(feature = "mssql") { + quote! { + canyon_sql::crud::CanyonRows::Postgres(mut v) => { + self.#pk_ident = v + .get(0) + .expect("Failed getting the returned IDs for an insert") + .get::<&str, #pk_type>(#primary_key); + Ok(()) + } + } + } else { quote! {} }; + quote! { #remove_pk_value_from_fn_entry; @@ -56,24 +84,9 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri ).await?; match rows { - // #[cfg(feature = "tokio-postgres")] - canyon_sql::crud::CanyonRows::Postgres(mut v) => { - self.#pk_ident = v - .get(0) - .expect("Failed getting the returned IDs for an insert") - .get::<&str, #pk_type>(#primary_key); - Ok(()) - }, - // #[cfg(feature = "tiberius")] - canyon_sql::crud::CanyonRows::Tiberius(mut v) => { - self.#pk_ident = v - .get(0) - .expect("Failed getting the returned IDs for an insert") - .get::<#pk_type, &str>(#primary_key) - .expect("SQL Server primary key type failed to be set as value"); - Ok(()) - }, - _ => panic!() // TODO remove when the generics will be refactored + #postgres_db_conn_match_arm + #mssql_db_conn_match_arm + _ => panic!("Reached the panic match arm of insert for the DatabaseConnection type") // TODO remove when the generics will be refactored } } } else { @@ -294,7 +307,7 @@ pub fn generate_multiple_insert_tokens( match result { Ok(res) => { match res { - // #[cfg(feature = "tokio-postgres")] + // #[cfg(feature = "postgres")] canyon_sql::crud::CanyonRows::Postgres(mut v) => { for (idx, instance) in instances.iter_mut().enumerate() { instance.#pk_ident = v @@ -305,7 +318,7 @@ pub fn generate_multiple_insert_tokens( Ok(()) }, - // #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] canyon_sql::crud::CanyonRows::Tiberius(mut v) => { for (idx, instance) in instances.iter_mut().enumerate() { instance.#pk_ident = v diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index 3086aea5..55006e5d 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -152,11 +152,11 @@ pub fn generate_count_tokens( let result_handling = quote! { match count { - // #[cfg(feature = "tokio-postgres")] + // #[cfg(feature = "postgres")] canyon_sql::crud::CanyonRows::Postgres(mut v) => Ok( v.remove(0).get::<&str, i64>("count") ), - // #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] canyon_sql::crud::CanyonRows::Tiberius(mut v) => v.remove(0) .get::(0) diff --git a/canyon_observer/Cargo.toml b/canyon_observer/Cargo.toml index 9f59b093..2282f494 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_observer/Cargo.toml @@ -25,3 +25,8 @@ syn = { version = "1.0.86", features = ["full", "parsing"] } quote = "1.0.9" partialdebug = "0.2.0" +[features] +default = ["postgres"] +postgres = ["tokio-postgres"] +mssql = ["tiberius"] +#mssql-integrated-auth = ["mssql", "tiberius/integrated-auth-gssapi"] diff --git a/canyon_observer/src/constants.rs b/canyon_observer/src/constants.rs index ae746e6e..997a4bb3 100644 --- a/canyon_observer/src/constants.rs +++ b/canyon_observer/src/constants.rs @@ -1,6 +1,6 @@ pub const NUMERIC_PK_DATATYPE: [&str; 6] = ["i16", "u16", "i32", "u32", "i64", "u64"]; -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] pub mod postgresql_queries { pub static CANYON_MEMORY_TABLE: &str = "CREATE TABLE IF NOT EXISTS canyon_memory ( id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, @@ -144,7 +144,7 @@ pub mod rust_type { pub const OPT_NAIVE_DATE_TIME: &str = "Option"; } -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] pub mod postgresql_type { pub const INT_8: &str = "int8"; pub const SMALL_INT: &str = "smallint"; diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index 87dbd6a1..cd09ee28 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -90,7 +90,7 @@ impl Migrations { db_type: DatabaseType, ) -> CanyonRows { let query = match db_type { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => constants::postgresql_queries::FETCH_PUBLIC_SCHEMA, #[cfg(feature = "tiberius")] DatabaseType::SqlServer => constants::mssql_queries::FETCH_PUBLIC_SCHEMA, @@ -110,7 +110,7 @@ impl Migrations { /// the data well organized for every entity present on that schema fn map_rows(db_results: CanyonRows, db_type: DatabaseType) -> Vec { match db_results { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] CanyonRows::Postgres(v) => Self::process_tp_rows(v, db_type), #[cfg(feature = "tiberius")] CanyonRows::Tiberius(v) => Self::process_tib_rows(v, db_type), @@ -202,7 +202,7 @@ impl Migrations { }; } - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] fn process_tp_rows( db_results: Vec, db_type: DatabaseType, @@ -269,7 +269,7 @@ impl Migrations { } } -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] fn get_table_name_from_tp_row(res_row: &tokio_postgres::Row) -> String { res_row.get::<&str, String>("table_name") } @@ -287,7 +287,7 @@ fn check_for_table_name( res_row: &dyn Row, ) -> bool { match db_type { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => table.table_name == res_row.get_postgres::<&str>("table_name"), #[cfg(feature = "tiberius")] DatabaseType::SqlServer => table.table_name == res_row.get_mssql::<&str>("table_name"), diff --git a/canyon_observer/src/migrations/information_schema.rs b/canyon_observer/src/migrations/information_schema.rs index 06eb6a3e..98ad01b9 100644 --- a/canyon_observer/src/migrations/information_schema.rs +++ b/canyon_observer/src/migrations/information_schema.rs @@ -1,6 +1,6 @@ #[cfg(feature = "tiberius")] use canyon_connection::tiberius::ColumnType as TIB_TY; -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] use canyon_connection::tokio_postgres::types::Type as TP_TYP; use canyon_crud::bounds::{Column, ColumnType, Row, RowOperations}; @@ -43,7 +43,7 @@ impl ColumnMetadataTypeValue { /// Retrieves the value stored in a [`Column`] for a passed [`Row`] pub fn get_value(row: &dyn Row, col: &Column) -> Self { match col.column_type() { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] ColumnType::Postgres(v) => { match *v { TP_TYP::NAME | TP_TYP::VARCHAR | TP_TYP::TEXT => Self::StringValue( diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index 912bf6dc..a07e60d5 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -73,7 +73,7 @@ impl CanyonMemory { // Manually maps the results let mut db_rows = Vec::new(); - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] { let mem_results: &Vec = res.get_postgres_rows(); for row in mem_results { @@ -244,7 +244,7 @@ impl CanyonMemory { /// Generates, if not exists the `canyon_memory` table async fn create_memory(datasource_name: &str, database_type: &DatabaseType) { let query = match database_type { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => constants::postgresql_queries::CANYON_MEMORY_TABLE, #[cfg(feature = "tiberius")] DatabaseType::SqlServer => constants::mssql_queries::CANYON_MEMORY_TABLE, diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index e068a3d4..a4627bdc 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -645,7 +645,7 @@ impl MigrationsHelper { canyon_register_entity_field: &CanyonRegisterEntityField, current_column_metadata: &ColumnMetadata, ) -> bool { - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] { if db_type == DatabaseType::PostgreSql { return canyon_register_entity_field @@ -765,7 +765,7 @@ impl DatabaseOperation for TableOperation { let stmt = match self { TableOperation::CreateTable(table_name, table_fields) => { match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => { format!( "CREATE TABLE \"{table_name}\" ({});", table_fields @@ -800,7 +800,7 @@ impl DatabaseOperation for TableOperation { TableOperation::AlterTableName(old_table_name, new_table_name) => { match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!("ALTER TABLE {old_table_name} RENAME TO {new_table_name};"), #[cfg(feature = "tiberius")] DatabaseType::SqlServer => /* @@ -829,7 +829,7 @@ impl DatabaseOperation for TableOperation { column_to_reference, ) => { match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( "ALTER TABLE {table_name} ADD CONSTRAINT {foreign_key_name} \ FOREIGN KEY ({column_foreign_key}) REFERENCES {table_to_reference} ({column_to_reference});" @@ -841,7 +841,7 @@ impl DatabaseOperation for TableOperation { TableOperation::DeleteTableForeignKey(table_with_foreign_key, constraint_name) => { match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( "ALTER TABLE {table_with_foreign_key} DROP CONSTRAINT {constraint_name};", ), @@ -852,7 +852,7 @@ impl DatabaseOperation for TableOperation { TableOperation::AddTablePrimaryKey(table_name, entity_field) => { match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( "ALTER TABLE \"{table_name}\" ADD PRIMARY KEY (\"{}\");", entity_field.field_name @@ -864,7 +864,7 @@ impl DatabaseOperation for TableOperation { TableOperation::DeleteTablePrimaryKey(table_name, primary_key_name) => { match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;"), #[cfg(feature = "tiberius")] DatabaseType::SqlServer => format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;") @@ -888,12 +888,12 @@ enum ColumnOperation { // SQL server specific operation - SQL server can't drop a NOT NULL column #[cfg(feature = "tiberius")] DropNotNullBeforeDropColumn(String, String, String), - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] AlterColumnSetNotNull(String, CanyonRegisterEntityField), // TODO if implement through annotations, modify for both GENERATED {ALWAYS, BY DEFAULT} - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] AlterColumnAddIdentity(String, CanyonRegisterEntityField), - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] AlterColumnDropIdentity(String, CanyonRegisterEntityField), } @@ -907,7 +907,7 @@ impl DatabaseOperation for ColumnOperation { let stmt = match self { ColumnOperation::CreateColumn(table_name, entity_field) => match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( "ALTER TABLE \"{}\" ADD COLUMN \"{}\" {};", table_name, @@ -928,7 +928,7 @@ impl DatabaseOperation for ColumnOperation { }, ColumnOperation::AlterColumnType(table_name, entity_field) => match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" TYPE {};", entity_field.field_name, entity_field.to_postgres_alter_syntax() @@ -938,7 +938,7 @@ impl DatabaseOperation for ColumnOperation { } ColumnOperation::AlterColumnDropNotNull(table_name, entity_field) => match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!("ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" DROP NOT NULL;", entity_field.field_name), #[cfg(feature = "tiberius")] DatabaseType::SqlServer => format!( @@ -965,11 +965,11 @@ impl DatabaseOperation for ColumnOperation { "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" SET NOT NULL;", entity_field.field_name ), - #[cfg(feature = "tokio-postgres")] ColumnOperation::AlterColumnAddIdentity(table_name, entity_field) => format!( + #[cfg(feature = "postgres")] ColumnOperation::AlterColumnAddIdentity(table_name, entity_field) => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" ADD GENERATED ALWAYS AS IDENTITY;", entity_field.field_name ), - #[cfg(feature = "tokio-postgres")] ColumnOperation::AlterColumnDropIdentity(table_name, entity_field) => format!( + #[cfg(feature = "postgres")] ColumnOperation::AlterColumnDropIdentity(table_name, entity_field) => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" DROP IDENTITY;", entity_field.field_name ), }; @@ -995,7 +995,7 @@ impl DatabaseOperation for SequenceOperation { let stmt = match self { SequenceOperation::ModifySequence(table_name, entity_field) => { match db_type { - #[cfg(feature = "tokio-postgres")] DatabaseType::PostgreSql => + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( "SELECT setval(pg_get_serial_sequence('\"{table_name}\"', '{}'), max(\"{}\")) from \"{table_name}\";", entity_field.field_name, entity_field.field_name diff --git a/canyon_observer/src/migrations/register_types.rs b/canyon_observer/src/migrations/register_types.rs index b0cbf48d..313be8ea 100644 --- a/canyon_observer/src/migrations/register_types.rs +++ b/canyon_observer/src/migrations/register_types.rs @@ -1,6 +1,6 @@ use regex::Regex; -#[cfg(feature = "tokio-postgres")] +#[cfg(feature = "postgres")] use crate::constants::postgresql_type; #[cfg(feature = "tiberius")] use crate::constants::sqlserver_type; @@ -30,7 +30,7 @@ pub struct CanyonRegisterEntityField { impl CanyonRegisterEntityField { /// Return the postgres datatype and parameters to create a column for a given rust type - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] pub fn to_postgres_syntax(&self) -> String { let rust_type_clean = self.field_type.replace(' ', ""); @@ -124,7 +124,7 @@ impl CanyonRegisterEntityField { } } - #[cfg(feature = "tokio-postgres")] + #[cfg(feature = "postgres")] pub fn to_postgres_alter_syntax(&self) -> String { let mut rust_type_clean = self.field_type.replace(' ', ""); let rs_type_is_optional = self.field_type.to_uppercase().starts_with("OPTION"); diff --git a/src/lib.rs b/src/lib.rs index 3aaf6ea9..c1a034bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,8 +28,11 @@ pub mod macros { /// connection module serves to reexport the public elements of the `canyon_connection` crate, /// exposing them through the public API pub mod connection { - #[cfg(feature = "postgres")] pub use canyon_connection::canyon_database_connector::DatabaseConnection::Postgres; - #[cfg(feature = "mssql")] pub use canyon_connection::canyon_database_connector::DatabaseConnection::SqlServer; + #[cfg(feature = "postgres")] + pub use canyon_connection::canyon_database_connector::DatabaseConnection::Postgres; + + #[cfg(feature = "mssql")] + pub use canyon_connection::canyon_database_connector::DatabaseConnection::SqlServer; } /// Crud module serves to reexport the public elements of the `canyon_crud` crate, diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 212c0505..08f9a557 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -9,4 +9,9 @@ canyon_sql = { path = ".." } [[test]] name = "canyon_integration_tests" -path = "canyon_integration_tests.rs" \ No newline at end of file +path = "canyon_integration_tests.rs" + +[features] +default = ["postgres"] +postgres = ["canyon_sql/postgres"] +mssql = [] From 51b97d52bffab92820da92303b080190b9c46d6b Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 20 Apr 2023 08:59:24 +0200 Subject: [PATCH 48/82] Moved the IT to initialize MSSQL databases to its own crate --- tests/crud/init_mssql.rs | 64 ++++++++++++++++++++++++++++++++++++++++ tests/crud/mod.rs | 64 +--------------------------------------- 2 files changed, 65 insertions(+), 63 deletions(-) create mode 100644 tests/crud/init_mssql.rs diff --git a/tests/crud/init_mssql.rs b/tests/crud/init_mssql.rs new file mode 100644 index 00000000..cf8f8071 --- /dev/null +++ b/tests/crud/init_mssql.rs @@ -0,0 +1,64 @@ +#![cfg_attr(feature = "canyon_sql/mssql")] + +use crate::constants::SQL_SERVER_CREATE_TABLES; +use crate::constants::SQL_SERVER_DS; +use crate::constants::SQL_SERVER_FILL_TABLE_VALUES; +use crate::tests_models::league::League; + +use canyon_sql::crud::CrudOperations; +use canyon_sql::db_clients::tiberius::{Client, Config}; +use canyon_sql::runtime::tokio::net::TcpStream; +use canyon_sql::runtime::tokio_util::compat::TokioAsyncWriteCompatExt; + +/// In order to initialize data on `SqlServer`. we must manually insert it +/// when the docker starts. SqlServer official docker from Microsoft does +/// not allow you to run `.sql` files against the database (not at least, without) +/// using a workaround. So, we are going to query the `SqlServer` to check if already +/// has some data (other processes, persistence or multi-threading envs), af if not, +/// we are going to retrieve the inserted data on the `postgreSQL` at start-up and +/// inserting into the `SqlServer` instance. +/// +/// This will be marked as `#[ignore]`, so we can force to run first the marked as +/// ignored, check the data available, perform the necessary init operations and +/// then *cargo test * the real integration tests +#[canyon_sql::macros::canyon_tokio_test] +#[ignore] +fn initialize_sql_server_docker_instance() { + static CONN_STR: &str = + "server=tcp:localhost,1434;User Id=SA;Password=SqlServer-10;TrustServerCertificate=true"; + + canyon_sql::runtime::futures::executor::block_on(async { + let config = Config::from_ado_string(CONN_STR).unwrap(); + + let tcp = TcpStream::connect(config.get_addr()).await.unwrap(); + let tcp2 = TcpStream::connect(config.get_addr()).await.unwrap(); + tcp.set_nodelay(true).ok(); + + let mut client = Client::connect(config.clone(), tcp.compat_write()) + .await + .unwrap(); + + // Create the tables + let query_result = client.query(SQL_SERVER_CREATE_TABLES, &[]).await; + assert!(query_result.is_ok()); + + let leagues_sql = League::find_all_datasource(SQL_SERVER_DS).await; + println!("LSQL ERR: {leagues_sql:?}"); + assert!(leagues_sql.is_ok()); + + match leagues_sql { + Ok(ref leagues) => { + let leagues_len = leagues.len(); + println!("Leagues already inserted on SQLSERVER: {:?}", &leagues_len); + if leagues.len() < 10 { + let mut client2 = Client::connect(config, tcp2.compat_write()) + .await + .expect("Can't connect to MSSQL"); + let result = client2.query(SQL_SERVER_FILL_TABLE_VALUES, &[]).await; + assert!(result.is_ok()); + } + } + Err(e) => eprintln!("Error retrieving the leagues: {e}"), + } + }); +} diff --git a/tests/crud/mod.rs b/tests/crud/mod.rs index c0f6afee..97bb67bb 100644 --- a/tests/crud/mod.rs +++ b/tests/crud/mod.rs @@ -4,66 +4,4 @@ pub mod insert_operations; pub mod querybuilder_operations; pub mod select_operations; pub mod update_operations; - -use crate::constants::SQL_SERVER_CREATE_TABLES; -use crate::constants::SQL_SERVER_DS; -use crate::constants::SQL_SERVER_FILL_TABLE_VALUES; -use crate::tests_models::league::League; - -use canyon_sql::crud::CrudOperations; -use canyon_sql::db_clients::tiberius::{Client, Config}; -use canyon_sql::runtime::tokio::net::TcpStream; -use canyon_sql::runtime::tokio_util::compat::TokioAsyncWriteCompatExt; - -/// In order to initialize data on `SqlServer`. we must manually insert it -/// when the docker starts. SqlServer official docker from Microsoft does -/// not allow you to run `.sql` files against the database (not at least, without) -/// using a workaround. So, we are going to query the `SqlServer` to check if already -/// has some data (other processes, persistence or multi-threading envs), af if not, -/// we are going to retrieve the inserted data on the `postgreSQL` at start-up and -/// inserting into the `SqlServer` instance. -/// -/// This will be marked as `#[ignore]`, so we can force to run first the marked as -/// ignored, check the data available, perform the necessary init operations and -/// then *cargo test * the real integration tests -#[canyon_sql::macros::canyon_tokio_test] -#[ignore] -fn initialize_sql_server_docker_instance() { - static CONN_STR: &str = - "server=tcp:localhost,1434;User Id=SA;Password=SqlServer-10;TrustServerCertificate=true"; - - canyon_sql::runtime::futures::executor::block_on(async { - let config = Config::from_ado_string(CONN_STR).unwrap(); - - let tcp = TcpStream::connect(config.get_addr()).await.unwrap(); - let tcp2 = TcpStream::connect(config.get_addr()).await.unwrap(); - tcp.set_nodelay(true).ok(); - - let mut client = Client::connect(config.clone(), tcp.compat_write()) - .await - .unwrap(); - - // Create the tables - let query_result = client.query(SQL_SERVER_CREATE_TABLES, &[]).await; - assert!(query_result.is_ok()); - - let leagues_sql = League::find_all_datasource(SQL_SERVER_DS).await; - println!("LSQL ERR: {leagues_sql:?}"); - assert!(leagues_sql.is_ok()); - - match leagues_sql { - Ok(ref leagues) => { - let leagues_len = leagues.len(); - println!("Leagues already inserted on SQLSERVER: {:?}", &leagues_len); - if leagues.len() < 10 { - let mut client2 = Client::connect(config, tcp2.compat_write()) - .await - .expect("Can't connect to MSSQL"); - let result = client2.query(SQL_SERVER_FILL_TABLE_VALUES, &[]).await; - assert!(result.is_ok()); - } - } - Err(e) => eprintln!("Error retrieving the leagues: {e}"), - } - }); -} +#[cfg(feature = "canyon_sql/mssql")] pub mod init_mssql; From cc9a3ef287139177df506aadf5a3d089e4c6b024 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 20 Apr 2023 11:52:57 +0200 Subject: [PATCH 49/82] A bunch of major changes to stabilize the new conditional APIs into the new compilation model --- Cargo.toml | 18 ++- canyon_connection/Cargo.toml | 8 +- .../src/canyon_database_connector.rs | 70 +++++--- canyon_connection/src/datasources.rs | 103 ++++++------ canyon_crud/Cargo.toml | 7 +- canyon_macros/Cargo.toml | 5 + canyon_macros/src/lib.rs | 122 ++++++++------ canyon_macros/src/query_operations/insert.rs | 152 ++++++++++++------ canyon_observer/Cargo.toml | 5 +- canyon_observer/src/constants.rs | 4 +- canyon_observer/src/migrations/handler.rs | 10 +- .../src/migrations/information_schema.rs | 4 +- canyon_observer/src/migrations/memory.rs | 4 +- canyon_observer/src/migrations/processor.rs | 32 ++-- .../src/migrations/register_types.rs | 6 +- src/lib.rs | 4 +- tests/Cargo.toml | 4 +- tests/constants.rs | 10 +- tests/crud/delete_operations.rs | 5 +- tests/crud/foreign_key_operations.rs | 8 +- tests/crud/init_mssql.rs | 2 - tests/crud/insert_operations.rs | 8 +- tests/crud/mod.rs | 2 +- tests/crud/querybuilder_operations.rs | 12 +- tests/crud/select_operations.rs | 11 +- tests/crud/update_operations.rs | 4 +- 26 files changed, 390 insertions(+), 230 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e7cf2500..c2e7ffd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,17 +15,19 @@ members = [ [dependencies] # Project crates -canyon_connection = { version = "0.2.0", path = "canyon_connection", optional = true } -canyon_crud = { version = "0.2.0", path = "canyon_crud" } -canyon_observer = { version = "0.2.0", path = "canyon_observer" } -canyon_macros = { version = "0.2.0", path = "canyon_macros" } -async-trait = "0.1.68" +canyon_connection = { workspace = true, path = "canyon_connection" } +canyon_crud = { workspace = true, path = "canyon_crud" } +canyon_observer = { workspace = true, path = "canyon_observer" } +canyon_macros = { woorkspace = true, path = "canyon_macros" } + +# To be marked as opt deps +tokio-postgres = { workspace = true, optional = true } +tiberius = { workspace = true, optional = true } [workspace.dependencies] canyon_crud = { version = "0.2.0", path = "canyon_crud" } canyon_connection = { version = "0.2.0", path = "canyon_connection" } canyon_observer = { version = "0.2.0", path = "canyon_observer" } -canyon_macros = { version = "0.2.0", path = "canyon_macros" } tokio = { version = "1.27.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["compat"] } @@ -53,5 +55,5 @@ description = "A Rust ORM and QueryBuilder" [features] default = ["postgres"] -postgres = ["canyon_connection/postgres", "canyon_connection/tokio-postgres"] -mssql = ["canyon_connection/tiberius", "canyon_crud/tiberius", "canyon_observer/tiberius"] +postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_connection/postgres", "canyon_observer/postgres", "canyon_macros/postgres"] +mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql", "canyon_observer/mssql", "canyon_macros/mssql"] diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 9bdacbc2..4e140655 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -12,16 +12,18 @@ description.workspace = true [dependencies] tokio = { workspace = true } tokio-util = { workspace = true } + tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"], optional = true } tiberius = { version = "0.12.1", features = ["tds73", "chrono", "integrated-auth-gssapi"], optional = true } + futures = { workspace = true } indexmap = { workspace = true } -async-std = { workspace = true } lazy_static = { workspace = true } -serde = { workspace = true, features = ["derive"] } toml = { workspace = true } +serde = { workspace = true, features = ["derive"] } +async-std = { workspace = true, optional = true } [features] default = ["postgres"] postgres = ["tokio-postgres"] -mssql = ["tiberius"] +mssql = ["tiberius", "async-std"] diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index bcb07d8e..96d88154 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -162,29 +162,57 @@ mod database_connection_handler { use super::*; use crate::CanyonSqlConfig; - const CONFIG_FILE_MOCK_ALT: &str = r#" - [canyon_sql] - datasources = [ - {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, - {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } - ] - "#; - /// Tests the behaviour of the `DatabaseType::from_datasource(...)` #[test] fn check_from_datasource() { - let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) - .expect("A failure happened retrieving the [canyon_sql_root] section"); - - #[cfg(feature = "postgres")] - assert_eq!( - config.canyon_sql.datasources[0].get_db_type(), - DatabaseType::PostgreSql - ); - #[cfg(feature = "mssql")] - assert_eq!( - config.canyon_sql.datasources[1].get_db_type(), - DatabaseType::SqlServer - ); + #[cfg(all(feature = "postgres", feature = "mssql"))] { + const CONFIG_FILE_MOCK_ALT_ALL: &str = r#" + [canyon_sql] + datasources = [ + {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, + {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + ] + "#; + let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_ALL) + .expect("A failure happened retrieving the [canyon_sql] section"); + assert_eq!( + config.canyon_sql.datasources[0].get_db_type(), + DatabaseType::PostgreSql + ); + assert_eq!( + config.canyon_sql.datasources[1].get_db_type(), + DatabaseType::SqlServer + ); + } + + #[cfg(feature = "postgres")] { + const CONFIG_FILE_MOCK_ALT_PG: &str = r#" + [canyon_sql] + datasources = [ + {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, + ] + "#; + let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_PG) + .expect("A failure happened retrieving the [canyon_sql] section"); + assert_eq!( + config.canyon_sql.datasources[0].get_db_type(), + DatabaseType::PostgreSql + ); + } + + #[cfg(feature = "mssql")] { + const CONFIG_FILE_MOCK_ALT_MSSQL: &str = r#" + [canyon_sql] + datasources = [ + {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + ] + "#; + let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_MSSQL) + .expect("A failure happened retrieving the [canyon_sql] section"); + assert_eq!( + config.canyon_sql.datasources[0].get_db_type(), + DatabaseType::SqlServer + ); + } } } diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 2dd3913c..82775fd7 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -5,55 +5,64 @@ use crate::canyon_database_connector::DatabaseType; /// ``` #[test] fn load_ds_config_from_array() { - const CONFIG_FILE_MOCK_ALT: &str = r#" - [canyon_sql] - datasources = [ - {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, - {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' }, - {name = 'SqlServerDS', auth = { sqlserver = { integrated = {} } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } - ] - "#; - - let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) - .expect("A failure happened retrieving the [canyon_sql_root] section"); - - let ds_0 = &config.canyon_sql.datasources[0]; - let ds_1 = &config.canyon_sql.datasources[1]; - let _ds_2 = &config.canyon_sql.datasources[2]; - - assert_eq!(ds_0.name, "PostgresDS"); - assert_eq!(ds_0.get_db_type(), DatabaseType::PostgreSql); - assert_eq!( - ds_0.auth, - Auth::Postgres(PostgresAuth::Basic { - username: "postgres".to_string(), - password: "postgres".to_string() - }) - ); - assert_eq!(ds_0.properties.host, "localhost"); - assert_eq!(ds_0.properties.port, None); - assert_eq!(ds_0.properties.db_name, "triforce"); - assert_eq!(ds_0.properties.migrations, Some(Migrations::Enabled)); + #[cfg(feature = "postgres")] { + const CONFIG_FILE_MOCK_ALT_PG: &str = r#" + [canyon_sql] + datasources = [ + {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, + ] + "#; + let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_PG) + .expect("A failure happened retrieving the [canyon_sql] section"); - #[cfg(feature = "mssql")] - assert_eq!(ds_1.name, "SqlServerDS"); - #[cfg(feature = "mssql")] - assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); - #[cfg(feature = "mssql")] - assert_eq!( - ds_1.auth, - Auth::SqlServer(SqlServerAuth::Basic { - username: "sa".to_string(), - password: "SqlServer-10".to_string() - }) - ); - assert_eq!(ds_1.properties.host, "192.168.0.250.1"); - assert_eq!(ds_1.properties.port, Some(3340)); - assert_eq!(ds_1.properties.db_name, "triforce2"); - assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); + let ds_0 = &config.canyon_sql.datasources[0]; - #[cfg(feature = "mssql")] - assert_eq!(_ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) + assert_eq!(ds_0.name, "PostgresDS"); + assert_eq!(ds_0.get_db_type(), DatabaseType::PostgreSql); + assert_eq!( + ds_0.auth, + Auth::Postgres(PostgresAuth::Basic { + username: "postgres".to_string(), + password: "postgres".to_string() + }) + ); + assert_eq!(ds_0.properties.host, "localhost"); + assert_eq!(ds_0.properties.port, None); + assert_eq!(ds_0.properties.db_name, "triforce"); + assert_eq!(ds_0.properties.migrations, Some(Migrations::Enabled)); + } + + #[cfg(feature = "mssql")] { + const CONFIG_FILE_MOCK_ALT_MSSQL: &str = r#" + [canyon_sql] + datasources = [ + {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' }, + {name = 'SqlServerDS', auth = { sqlserver = { integrated = {} } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + ] + "#; + let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_MSSQL) + .expect("A failure happened retrieving the [canyon_sql] section"); + + let ds_1 = &config.canyon_sql.datasources[0]; + let ds_2 = &config.canyon_sql.datasources[1]; + + + assert_eq!(ds_1.name, "SqlServerDS"); + assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); + assert_eq!( + ds_1.auth, + Auth::SqlServer(SqlServerAuth::Basic { + username: "sa".to_string(), + password: "SqlServer-10".to_string() + }) + ); + assert_eq!(ds_1.properties.host, "192.168.0.250.1"); + assert_eq!(ds_1.properties.port, Some(3340)); + assert_eq!(ds_1.properties.db_name, "triforce2"); + assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); + + assert_eq!(ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)); + } } /// #[derive(Deserialize, Debug, Clone)] diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index 33213847..cbe44ad9 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -17,12 +17,11 @@ description.workspace = true tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } chrono = { version = "0.4", features = ["serde"] } -async-trait = { version = "0.1.50" } +async-trait = { workspace = true } canyon_connection = { workspace = true, path = "../canyon_connection" } [features] default = ["postgres"] -postgres = ["tokio-postgres"] -mssql = ["tiberius"] -#mssql-integrated-auth = ["mssql", "tiberius/integrated-auth-gssapi"] \ No newline at end of file +postgres = ["tokio-postgres", "canyon_connection/postgres"] +mssql = ["tiberius", "canyon_connection/mssql"] diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index d095e7ff..e306dcab 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -22,3 +22,8 @@ tokio = { version = "1.9.0", features = ["full"] } canyon_observer = { workspace = true, path = "../canyon_observer" } canyon_crud = { workspace = true, path = "../canyon_crud" } canyon_connection = { workspace = true, path = "../canyon_connection" } + +[features] +default = ["postgres"] +postgres = ["canyon_connection/postgres", "canyon_crud/postgres", "canyon_observer/postgres"] +mssql = ["canyon_connection/mssql", "canyon_crud/mssql", "canyon_observer/mssql"] diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index 34a166e8..54c4c873 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -486,103 +486,133 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac } }); - // TODO rework this ugly piece of code in the upcoming versions let init_field_values_sqlserver = fields.iter().map(|(_vis, ident, ty)| { - let ident_name = ident.to_string(); + let ident_name = ident.to_string(); - if get_field_type_as_string(ty) == "String" { - quote! { + if get_field_type_as_string(ty) == "String" { + quote! { #ident: row.get::<&str, &str>(#ident_name) .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) .to_string() } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::<&str, &str>(#ident_name) .map( |x| x.to_owned() ) } - } else if get_field_type_as_string(ty) == "NaiveDate" { - quote! { + } else if get_field_type_as_string(ty) == "NaiveDate" { + quote! { #ident: row.get::(#ident_name) .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty) == "NaiveTime" { - quote! { + } else if get_field_type_as_string(ty) == "NaiveTime" { + quote! { #ident: row.get::(#ident_name) .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty) == "NaiveDateTime" { - quote! { + } else if get_field_type_as_string(ty) == "NaiveDateTime" { + quote! { #ident: row.get::(#ident_name) .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty) == "DateTime" { - quote! { + } else if get_field_type_as_string(ty) == "DateTime" { + quote! { #ident: row.get::(#ident_name) .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else { - quote! { + } else { + quote! { #ident: row.get::<#ty, &str>(#ident_name) .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) } - } - }); + } + }); // The type of the Struct let ty = ast.ident; - let tokens = quote! { - impl canyon_sql::crud::RowMapper for #ty - { - fn deserialize_postgresql(row: &canyon_sql::db_clients::tokio_postgres::Row) -> #ty { - Self { - #(#init_field_values),* + let postgres_enabled = cfg!(feature = "postgres"); + let mssql_enabled = cfg!(feature = "mssql"); + + let tokens = if postgres_enabled && mssql_enabled { + quote! { + impl canyon_sql::crud::RowMapper for #ty { + fn deserialize_postgresql(row: &canyon_sql::db_clients::tokio_postgres::Row) -> #ty { + Self { + #(#init_field_values),* + } + } + fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { + Self { + #(#init_field_values_sqlserver),* + } } } - - fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { - Self { - #(#init_field_values_sqlserver),* + } + } else if postgres_enabled { + quote! { + impl canyon_sql::crud::RowMapper for #ty { + fn deserialize_postgresql(row: &canyon_sql::db_clients::tokio_postgres::Row) -> #ty { + Self { + #(#init_field_values),* + } + } + } + } + } else if mssql_enabled { + quote! { + impl canyon_sql::crud::RowMapper for #ty { + fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { + Self { + #(#init_field_values_sqlserver),* + } } } } + } else { + quote! { + panic!( + "Reached a branch in the implementation of the Row Mapper macro that should never be reached.\ + This is a severe bug of Canyon-SQL. Please, open us an issue at \ + https://github.com/zerodaycode/Canyon-SQL/issues and let us know about that failure." + ) + } }; tokens.into() diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index 3093bc4c..329399f0 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -36,35 +36,59 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri .find(|(i, _t)| Some(i.to_string()) == primary_key); let insert_transaction = if let Some(pk_data) = &pk_ident_type { let pk_ident = &pk_data.0; - let pk_ident_str = &pk_data.0.to_string(); let pk_type = &pk_data.1; - let postgres_db_conn_match_arm = if cfg!(feature = "canyon_sql/postgres") { + let postgres_enabled = cfg!(feature = "postgres"); + let mssql_enabled = cfg!(feature = "mssql"); + + let match_rows = if postgres_enabled && mssql_enabled { quote! { canyon_sql::crud::CanyonRows::Postgres(mut v) => { self.#pk_ident = v .get(0) - .expect("Failed getting the returned IDs for an insert") + .ok_or("Failed getting the returned IDs for an insert")? .get::<&str, #pk_type>(#primary_key); Ok(()) } + canyon_sql::crud::CanyonRows::Tiberius(mut v) => { + self.#pk_ident = v + .get(0) + .ok_or("Failed getting the returned IDs for a multi insert")? + .get::<#pk_type, &str>(#primary_key) + .ok_or("SQL Server primary key type failed to be set as value")?; + Ok(()) + } } - } else { - println!("No feature postgres detected for: {:?}", pk_ident_str); - quote! {} - }; - - let mssql_db_conn_match_arm = if cfg!(feature = "mssql") { + } else if postgres_enabled { quote! { canyon_sql::crud::CanyonRows::Postgres(mut v) => { self.#pk_ident = v .get(0) - .expect("Failed getting the returned IDs for an insert") + .ok_or("Failed getting the returned IDs for an insert")? .get::<&str, #pk_type>(#primary_key); Ok(()) } } - } else { quote! {} }; + } else if mssql_enabled { + quote! { + canyon_sql::crud::CanyonRows::Tiberius(mut v) => { + self.#pk_ident = v + .get(0) + .ok_or("Failed getting the returned IDs for a multi insert")? + .get::<#pk_type, &str>(#primary_key) + .ok_or("SQL Server primary key type failed to be set as value")?; + Ok(()) + } + } + } else { + quote! { + panic!( + "Reached a branch in the implementation of the Row Mapper macro that should never be reached.\ + This is a severe bug of Canyon-SQL. Please, open us an issue at \ + https://github.com/zerodaycode/Canyon-SQL/issues and let us know about that failure." + ) + } + }; quote! { #remove_pk_value_from_fn_entry; @@ -84,8 +108,7 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri ).await?; match rows { - #postgres_db_conn_match_arm - #mssql_db_conn_match_arm + #match_rows _ => panic!("Reached the panic match arm of insert for the DatabaseConnection type") // TODO remove when the generics will be refactored } } @@ -236,6 +259,70 @@ pub fn generate_multiple_insert_tokens( let pk_ident = &pk_data.0; let pk_type = &pk_data.1; + let postgres_enabled = cfg!(feature = "postgres"); + let mssql_enabled = cfg!(feature = "mssql"); + + let match_multi_insert_rows = if postgres_enabled && mssql_enabled { + quote! { + canyon_sql::crud::CanyonRows::Postgres(mut v) => { + for (idx, instance) in instances.iter_mut().enumerate() { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") + .get::<&str, #pk_type>(#pk); + } + + Ok(()) + } + canyon_sql::crud::CanyonRows::Tiberius(mut v) => { + for (idx, instance) in instances.iter_mut().enumerate() { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") + .get::<#pk_type, &str>(#pk) + .expect("SQL Server primary key type failed to be set as value"); + } + + Ok(()) + } + } + } else if postgres_enabled { + quote! { + canyon_sql::crud::CanyonRows::Postgres(mut v) => { + for (idx, instance) in instances.iter_mut().enumerate() { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") + .get::<&str, #pk_type>(#pk); + } + + Ok(()) + } + } + } else if mssql_enabled { + quote! { + canyon_sql::crud::CanyonRows::Tiberius(mut v) => { + for (idx, instance) in instances.iter_mut().enumerate() { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") + .get::<#pk_type, &str>(#pk) + .expect("SQL Server primary key type failed to be set as value"); + } + + Ok(()) + } + } + } else { + quote! { + panic!( + "Reached a branch in the implementation of the Row Mapper macro that should never be reached.\ + This is a severe bug of Canyon-SQL. Please, open us an issue at \ + https://github.com/zerodaycode/Canyon-SQL/issues and let us know about that failure." + ) + } + }; + quote! { mapped_fields = #column_names .split(", ") @@ -298,42 +385,15 @@ pub fn generate_multiple_insert_tokens( } } - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( + let multi_insert_result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( stmt, v_arr, datasource_name - ).await; - - match result { - Ok(res) => { - match res { - // #[cfg(feature = "postgres")] - canyon_sql::crud::CanyonRows::Postgres(mut v) => { - for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = v - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") - .get::<&str, #pk_type>(#pk); - } - - Ok(()) - }, - #[cfg(feature = "mssql")] - canyon_sql::crud::CanyonRows::Tiberius(mut v) => { - for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = v - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") - .get::<#pk_type, &str>(#pk) - .expect("SQL Server primary key type failed to be set as value"); - } - - Ok(()) - }, - _ => panic!() // TODO remove when the generics will be refactored - } - }, - Err(e) => Err(e) + ).await?; + + match multi_insert_result { + #match_multi_insert_rows + _ => panic!() // TODO remove when the generics will be refactored } } } else { diff --git a/canyon_observer/Cargo.toml b/canyon_observer/Cargo.toml index 2282f494..7a616e0c 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_observer/Cargo.toml @@ -27,6 +27,5 @@ partialdebug = "0.2.0" [features] default = ["postgres"] -postgres = ["tokio-postgres"] -mssql = ["tiberius"] -#mssql-integrated-auth = ["mssql", "tiberius/integrated-auth-gssapi"] +postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres"] +mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql"] diff --git a/canyon_observer/src/constants.rs b/canyon_observer/src/constants.rs index 997a4bb3..3928da4f 100644 --- a/canyon_observer/src/constants.rs +++ b/canyon_observer/src/constants.rs @@ -36,7 +36,7 @@ pub mod postgresql_queries { table_schema = 'public';"; } -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] pub mod mssql_queries { pub static CANYON_MEMORY_TABLE: &str = "IF OBJECT_ID(N'[dbo].[canyon_memory]', N'U') IS NULL BEGIN @@ -157,7 +157,7 @@ pub mod postgresql_type { pub const DATETIME: &str = "timestamp without time zone"; } -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] pub mod sqlserver_type { pub const TINY_INT: &str = "TINY INT"; pub const SMALL_INT: &str = "SMALL INT"; diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index cd09ee28..9ce3c4e8 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -92,7 +92,7 @@ impl Migrations { let query = match db_type { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => constants::postgresql_queries::FETCH_PUBLIC_SCHEMA, - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] DatabaseType::SqlServer => constants::mssql_queries::FETCH_PUBLIC_SCHEMA, }; @@ -112,7 +112,7 @@ impl Migrations { match db_results { #[cfg(feature = "postgres")] CanyonRows::Postgres(v) => Self::process_tp_rows(v, db_type), - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] CanyonRows::Tiberius(v) => Self::process_tib_rows(v, db_type), _ => panic!(), } @@ -235,7 +235,7 @@ impl Migrations { schema_info } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn process_tib_rows( db_results: Vec, db_type: DatabaseType, @@ -273,7 +273,7 @@ impl Migrations { fn get_table_name_from_tp_row(res_row: &tokio_postgres::Row) -> String { res_row.get::<&str, String>("table_name") } -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] fn get_table_name_from_tib_row(res_row: &tiberius::Row) -> String { res_row .get::<&str, &str>("table_name") @@ -289,7 +289,7 @@ fn check_for_table_name( match db_type { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => table.table_name == res_row.get_postgres::<&str>("table_name"), - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] DatabaseType::SqlServer => table.table_name == res_row.get_mssql::<&str>("table_name"), } } diff --git a/canyon_observer/src/migrations/information_schema.rs b/canyon_observer/src/migrations/information_schema.rs index 98ad01b9..74709619 100644 --- a/canyon_observer/src/migrations/information_schema.rs +++ b/canyon_observer/src/migrations/information_schema.rs @@ -1,4 +1,4 @@ -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] use canyon_connection::tiberius::ColumnType as TIB_TY; #[cfg(feature = "postgres")] use canyon_connection::tokio_postgres::types::Type as TP_TYP; @@ -54,7 +54,7 @@ impl ColumnMetadataTypeValue { _ => Self::NoneValue, // TODO watchout this one } } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] ColumnType::SqlServer(v) => match v { TIB_TY::NChar | TIB_TY::NVarchar | TIB_TY::BigChar | TIB_TY::BigVarChar => { Self::StringValue( diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index a07e60d5..18f6eb31 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -86,7 +86,7 @@ impl CanyonMemory { db_rows.push(db_row); } } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] { let mem_results: &Vec = res.get_tiberius_rows(); for row in mem_results { @@ -246,7 +246,7 @@ impl CanyonMemory { let query = match database_type { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => constants::postgresql_queries::CANYON_MEMORY_TABLE, - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] DatabaseType::SqlServer => constants::mssql_queries::CANYON_MEMORY_TABLE, }; diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index a4627bdc..e6c23bb4 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -188,7 +188,7 @@ impl MigrationsProcessor { .collect(); for column_metadata in columns_name_to_delete { - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] { if _db_type == DatabaseType::SqlServer && !column_metadata.is_nullable { self.drop_column_not_null( @@ -246,7 +246,7 @@ impl MigrationsProcessor { ))); } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn drop_column_not_null( &mut self, table_name: &str, @@ -623,7 +623,7 @@ impl MigrationsHelper { } } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] fn get_datatype_from_column_metadata(current_column_metadata: &ColumnMetadata) -> String { // TODO Add all SQL Server text datatypes if vec!["nvarchar", "varchar"] @@ -654,7 +654,7 @@ impl MigrationsHelper { == current_column_metadata.datatype; } } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] { if db_type == DatabaseType::SqlServer { // TODO Search a better way to get the datatype without useless info (like "VARCHAR(MAX)") @@ -779,7 +779,7 @@ impl DatabaseOperation for TableOperation { .join(", ") ) } - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => { + #[cfg(feature = "mssql")] DatabaseType::SqlServer => { format!( "CREATE TABLE {:?} ({:?});", table_name, @@ -802,7 +802,7 @@ impl DatabaseOperation for TableOperation { match db_type { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!("ALTER TABLE {old_table_name} RENAME TO {new_table_name};"), - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + #[cfg(feature = "mssql")] DatabaseType::SqlServer => /* Notes: Brackets around `old_table_name`, p.e. exec sp_rename ['league'], 'leagues' // NOT VALID! @@ -834,7 +834,7 @@ impl DatabaseOperation for TableOperation { "ALTER TABLE {table_name} ADD CONSTRAINT {foreign_key_name} \ FOREIGN KEY ({column_foreign_key}) REFERENCES {table_to_reference} ({column_to_reference});" ), - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + #[cfg(feature = "mssql")] DatabaseType::SqlServer => todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } @@ -845,7 +845,7 @@ impl DatabaseOperation for TableOperation { format!( "ALTER TABLE {table_with_foreign_key} DROP CONSTRAINT {constraint_name};", ), - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + #[cfg(feature = "mssql")] DatabaseType::SqlServer => todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } @@ -857,7 +857,7 @@ impl DatabaseOperation for TableOperation { "ALTER TABLE \"{table_name}\" ADD PRIMARY KEY (\"{}\");", entity_field.field_name ), - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + #[cfg(feature = "mssql")] DatabaseType::SqlServer => todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } @@ -866,7 +866,7 @@ impl DatabaseOperation for TableOperation { match db_type { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;"), - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + #[cfg(feature = "mssql")] DatabaseType::SqlServer => format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;") } } @@ -886,7 +886,7 @@ enum ColumnOperation { AlterColumnType(String, CanyonRegisterEntityField), AlterColumnDropNotNull(String, CanyonRegisterEntityField), // SQL server specific operation - SQL server can't drop a NOT NULL column - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] DropNotNullBeforeDropColumn(String, String, String), #[cfg(feature = "postgres")] AlterColumnSetNotNull(String, CanyonRegisterEntityField), @@ -914,7 +914,7 @@ impl DatabaseOperation for ColumnOperation { entity_field.field_name, entity_field.to_postgres_syntax() ), - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + #[cfg(feature = "mssql")] DatabaseType::SqlServer => format!( "ALTER TABLE {} ADD \"{}\" {};", table_name, @@ -933,20 +933,20 @@ impl DatabaseOperation for ColumnOperation { "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" TYPE {};", entity_field.field_name, entity_field.to_postgres_alter_syntax() ), - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + #[cfg(feature = "mssql")] DatabaseType::SqlServer => todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } ColumnOperation::AlterColumnDropNotNull(table_name, entity_field) => match db_type { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!("ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" DROP NOT NULL;", entity_field.field_name), - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + #[cfg(feature = "mssql")] DatabaseType::SqlServer => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NULL", entity_field.field_name, entity_field.to_sqlserver_alter_syntax() ) } - #[cfg(feature = "tiberius")] ColumnOperation::DropNotNullBeforeDropColumn(table_name, column_name, column_datatype) => + #[cfg(feature = "mssql")] ColumnOperation::DropNotNullBeforeDropColumn(table_name, column_name, column_datatype) => format!( "ALTER TABLE {table_name} ALTER COLUMN {column_name} {column_datatype} NULL; DECLARE @tableName VARCHAR(MAX) = '{table_name}' DECLARE @columnName VARCHAR(MAX) = '{column_name}' @@ -1000,7 +1000,7 @@ impl DatabaseOperation for SequenceOperation { "SELECT setval(pg_get_serial_sequence('\"{table_name}\"', '{}'), max(\"{}\")) from \"{table_name}\";", entity_field.field_name, entity_field.field_name ), - #[cfg(feature = "tiberius")] DatabaseType::SqlServer => + #[cfg(feature = "mssql")] DatabaseType::SqlServer => todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } diff --git a/canyon_observer/src/migrations/register_types.rs b/canyon_observer/src/migrations/register_types.rs index 313be8ea..14481c13 100644 --- a/canyon_observer/src/migrations/register_types.rs +++ b/canyon_observer/src/migrations/register_types.rs @@ -2,7 +2,7 @@ use regex::Regex; #[cfg(feature = "postgres")] use crate::constants::postgresql_type; -#[cfg(feature = "tiberius")] +#[cfg(feature = "mssql")] use crate::constants::sqlserver_type; use crate::constants::{regex_patterns, rust_type, NUMERIC_PK_DATATYPE}; @@ -77,7 +77,7 @@ impl CanyonRegisterEntityField { /// Return the postgres datatype and parameters to create a column for a given rust type /// for Microsoft SQL Server - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] pub fn to_sqlserver_syntax(&self) -> String { let rust_type_clean = self.field_type.replace(' ', ""); @@ -167,7 +167,7 @@ impl CanyonRegisterEntityField { } } - #[cfg(feature = "tiberius")] + #[cfg(feature = "mssql")] pub fn to_sqlserver_alter_syntax(&self) -> String { let mut rust_type_clean = self.field_type.replace(' ', ""); let rs_type_is_optional = self.field_type.to_uppercase().starts_with("OPTION"); diff --git a/src/lib.rs b/src/lib.rs index c1a034bb..e40d9d3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,8 +8,6 @@ extern crate canyon_crud; extern crate canyon_macros; extern crate canyon_observer; -extern crate async_trait; - /// Reexported elements to the root of the public API pub mod migrations { pub use canyon_observer::migrations::{handler, processor}; @@ -21,7 +19,7 @@ pub use canyon_macros::main; /// Public API for the `Canyon-SQL` proc-macros, and for the external ones pub mod macros { - pub use async_trait::*; + pub use canyon_crud::async_trait::*; pub use canyon_macros::*; } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 08f9a557..e606e3c9 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true publish = false [dev-dependencies] -canyon_sql = { path = ".." } +canyon_sql = { path = "..", default-features = false, features = ["mssql"] } [[test]] name = "canyon_integration_tests" @@ -13,5 +13,5 @@ path = "canyon_integration_tests.rs" [features] default = ["postgres"] -postgres = ["canyon_sql/postgres"] +postgres = [] mssql = [] diff --git a/tests/constants.rs b/tests/constants.rs index f7804e43..8fb86a44 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -1,8 +1,9 @@ ///! Constant values to share across the integration tests -pub const PSQL_DS: &str = "postgres_docker"; -pub const SQL_SERVER_DS: &str = "sqlserver_docker"; -pub static FETCH_PUBLIC_SCHEMA: &str = +#[cfg(feature = "postgres")] pub const PSQL_DS: &str = "postgres_docker"; +#[cfg(feature = "mssql")] pub const SQL_SERVER_DS: &str = "sqlserver_docker"; + +#[cfg(feature = "postgres")] pub static FETCH_PUBLIC_SCHEMA: &str = "SELECT gi.table_name, gi.column_name, @@ -33,7 +34,7 @@ LEFT JOIN pg_catalog.pg_constraint AS con on WHERE table_schema = 'public';"; -pub const SQL_SERVER_CREATE_TABLES: &str = " +#[cfg(feature = "mssql")] pub const SQL_SERVER_CREATE_TABLES: &str = " IF OBJECT_ID(N'[dbo].[league]', N'U') IS NULL BEGIN CREATE TABLE dbo.league ( @@ -87,6 +88,7 @@ BEGIN END; "; +#[cfg(feature = "mssql")] pub const SQL_SERVER_FILL_TABLE_VALUES: &str = " -- Values for league table -- Values for league table diff --git a/tests/crud/delete_operations.rs b/tests/crud/delete_operations.rs index 46d1bcaf..fb2e07e9 100644 --- a/tests/crud/delete_operations.rs +++ b/tests/crud/delete_operations.rs @@ -2,7 +2,8 @@ ///! generates and executes *INSERT* statements use canyon_sql::crud::CrudOperations; -use crate::constants::{PSQL_DS, SQL_SERVER_DS}; +#[cfg(feature = "postgres")] use crate::constants::PSQL_DS; +#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; /// Deletes a row from the database that is mapped into some instance of a `T` entity. @@ -14,6 +15,7 @@ use crate::tests_models::league::*; /// /// Attempt of usage the `t.delete(&self)` method on an entity without `#[primary_key]` /// will raise a runtime error. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_delete_method_operation() { // For test the delete, we will insert a new instance of the database, and then, @@ -58,6 +60,7 @@ fn test_crud_delete_method_operation() { } /// Same as the delete test, but performing the operations with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_delete_datasource_method_operation() { // For test the delete, we will insert a new instance of the database, and then, diff --git a/tests/crud/foreign_key_operations.rs b/tests/crud/foreign_key_operations.rs index b58df802..b74f6852 100644 --- a/tests/crud/foreign_key_operations.rs +++ b/tests/crud/foreign_key_operations.rs @@ -10,13 +10,14 @@ ///! For more info: TODO -> Link to the docs of the foreign key chapter use canyon_sql::crud::CrudOperations; -use crate::constants::SQL_SERVER_DS; +#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; use crate::tests_models::tournament::*; /// Given an entity `T` which has some field declaring a foreign key relation -/// with some another entity `U`, for example, performns a search to find +/// with some another entity `U`, for example, performs a search to find /// what is the parent type `U` of `T` +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_search_by_foreign_key() { let some_tournament: Tournament = Tournament::find_by_pk(&1) @@ -38,6 +39,7 @@ fn test_crud_search_by_foreign_key() { } /// Same as the search by foreign key, but with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_search_by_foreign_key_datasource() { let some_tournament: Tournament = Tournament::find_by_pk_datasource(&10, SQL_SERVER_DS) @@ -67,6 +69,7 @@ fn test_crud_search_by_foreign_key_datasource() { /// to `U`. /// /// For this to work, `U`, the parent, must have derived the `ForeignKeyable` proc macro +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_search_reverse_side_foreign_key() { let some_league: League = League::find_by_pk(&1) @@ -87,6 +90,7 @@ fn test_crud_search_reverse_side_foreign_key() { /// Same as the search by the reverse side of a foreign key relation /// but with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_search_reverse_side_foreign_key_datasource() { let some_league: League = League::find_by_pk_datasource(&1, SQL_SERVER_DS) diff --git a/tests/crud/init_mssql.rs b/tests/crud/init_mssql.rs index cf8f8071..19b08549 100644 --- a/tests/crud/init_mssql.rs +++ b/tests/crud/init_mssql.rs @@ -1,5 +1,3 @@ -#![cfg_attr(feature = "canyon_sql/mssql")] - use crate::constants::SQL_SERVER_CREATE_TABLES; use crate::constants::SQL_SERVER_DS; use crate::constants::SQL_SERVER_FILL_TABLE_VALUES; diff --git a/tests/crud/insert_operations.rs b/tests/crud/insert_operations.rs index 29c0c9fa..06ffbcbf 100644 --- a/tests/crud/insert_operations.rs +++ b/tests/crud/insert_operations.rs @@ -2,7 +2,7 @@ ///! generates and executes *INSERT* statements use canyon_sql::crud::CrudOperations; -use crate::constants::SQL_SERVER_DS; +#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; /// Inserts a new record on the database, given an entity that is @@ -25,7 +25,8 @@ use crate::tests_models::league::*; /// /// If the type hasn't a `#[primary_key]` annotation, or the annotation contains /// an argument specifying not autoincremental behaviour, all the fields will be -/// inserted on the database and no returning value will be placed in any field. +/// inserted on the database and no returning value will be placed in any field. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_insert_operation() { let mut new_league: League = League { @@ -54,6 +55,7 @@ fn test_crud_insert_operation() { /// Same as the insert operation above, but targeting the database defined in /// the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_insert_datasource_operation() { let mut new_league: League = League { @@ -93,6 +95,7 @@ fn test_crud_insert_datasource_operation() { /// /// The instances without `#[primary_key]` inserts all the values on the instaqce fields /// on the database. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_multi_insert_operation() { let mut new_league_mi: League = League { @@ -154,6 +157,7 @@ fn test_crud_multi_insert_operation() { } /// Same as the multi insert above, but with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_multi_insert_datasource_operation() { let mut new_league_mi: League = League { diff --git a/tests/crud/mod.rs b/tests/crud/mod.rs index 97bb67bb..5b11a7ed 100644 --- a/tests/crud/mod.rs +++ b/tests/crud/mod.rs @@ -4,4 +4,4 @@ pub mod insert_operations; pub mod querybuilder_operations; pub mod select_operations; pub mod update_operations; -#[cfg(feature = "canyon_sql/mssql")] pub mod init_mssql; +#[cfg(feature = "mssql")] pub mod init_mssql; diff --git a/tests/crud/querybuilder_operations.rs b/tests/crud/querybuilder_operations.rs index 4700f598..8f9d1659 100644 --- a/tests/crud/querybuilder_operations.rs +++ b/tests/crud/querybuilder_operations.rs @@ -9,10 +9,10 @@ use canyon_sql::{ query::{operators::Comp, ops::QueryBuilder}, }; -use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; -use crate::tests_models::player::*; use crate::tests_models::tournament::*; +#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; +#[cfg(feature = "mssql")] use crate::tests_models::player::*; /// Builds a new SQL statement for retrieves entities of the `T` type, filtered /// with the parameters that modifies the base SQL to SELECT * FROM @@ -38,6 +38,7 @@ fn test_generated_sql_by_the_select_querybuilder() { /// Builds a new SQL statement for retrieves entities of the `T` type, filtered /// with the parameters that modifies the base SQL to SELECT * FROM +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_with_querybuilder() { // Find all the leagues with ID less or equals that 7 @@ -57,6 +58,7 @@ fn test_crud_find_with_querybuilder() { } /// Same than the above but with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_with_querybuilder_datasource() { // Find all the players where its ID column value is greater that 50 @@ -70,6 +72,7 @@ fn test_crud_find_with_querybuilder_datasource() { /// Updates the values of the range on entries defined by the constraint parameters /// in the database entity +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_update_with_querybuilder() { // Find all the leagues with ID less or equals that 7 @@ -82,7 +85,7 @@ fn test_crud_update_with_querybuilder() { .r#where(LeagueFieldValue::id(&1), Comp::Gt) .and(LeagueFieldValue::id(&8), Comp::Lt); - /* Family of QueryBuilders are clone, useful in case of need to read the generated SQL + /* NOTE: Family of QueryBuilders are clone, useful in case of need to read the generated SQL let qpr = q.clone(); println!("PSQL: {:?}", qpr.read_sql()); */ @@ -105,6 +108,7 @@ fn test_crud_update_with_querybuilder() { } /// Same as above, but with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_update_with_querybuilder_datasource() { // Find all the leagues with ID less or equals that 7 @@ -139,6 +143,7 @@ fn test_crud_update_with_querybuilder_datasource() { /// Note if the database is persisted (not created and destroyed on every docker or /// GitHub Action wake up), it won't delete things that already have been deleted, /// but this isn't an error. They just don't exists. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_delete_with_querybuilder() { Tournament::delete_query() @@ -152,6 +157,7 @@ fn test_crud_delete_with_querybuilder() { } /// Same as the above delete, but with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_delete_with_querybuilder_datasource() { Player::delete_query_datasource(SQL_SERVER_DS) diff --git a/tests/crud/select_operations.rs b/tests/crud/select_operations.rs index 26e0e5f2..5c20e958 100644 --- a/tests/crud/select_operations.rs +++ b/tests/crud/select_operations.rs @@ -1,6 +1,6 @@ #![allow(clippy::nonminimal_bool)] -use crate::constants::SQL_SERVER_DS; +#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; ///! Integration tests for the CRUD operations available in `Canyon` that ///! generates and executes *SELECT* statements use crate::Error; @@ -12,6 +12,7 @@ use crate::tests_models::player::*; /// Tests the behaviour of a SELECT * FROM {table_name} within Canyon, through the /// `::find_all()` associated function derived with the `CanyonCrud` derive proc-macro /// and using the *default datasource* +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_all() { let find_all_result: Result, Box> = @@ -28,6 +29,7 @@ fn test_crud_find_all() { /// Same as the `find_all()`, but with the unchecked variant, which directly returns `Vec` not /// `Result` wrapped +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_all_unchecked() { let find_all_result: Vec = League::find_all_unchecked().await; @@ -37,6 +39,7 @@ fn test_crud_find_all_unchecked() { /// Tests the behaviour of a SELECT * FROM {table_name} within Canyon, through the /// `::find_all()` associated function derived with the `CanyonCrud` derive proc-macro /// and using the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_all_datasource() { let find_all_result: Result, Box> = @@ -48,6 +51,7 @@ fn test_crud_find_all_datasource() { /// Same as the `find_all_datasource()`, but with the unchecked variant and the specified dataosource, /// returning directly `Vec` and not `Result, Err>` +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_all_unchecked_datasource() { let find_all_result: Vec = League::find_all_unchecked_datasource(SQL_SERVER_DS).await; @@ -58,6 +62,7 @@ fn test_crud_find_all_unchecked_datasource() { /// defined with the #[primary_key] attribute over some field of the type. /// /// Uses the *default datasource*. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_by_pk() { let find_by_pk_result: Result, Box> = @@ -80,6 +85,8 @@ fn test_crud_find_by_pk() { /// defined with the #[primary_key] attribute over some field of the type. /// /// Uses the *specified datasource* in the second parameter of the function call. +#[cfg(feature = "postgres")] +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_by_pk_datasource() { let find_by_pk_result: Result, Box> = @@ -99,6 +106,7 @@ fn test_crud_find_by_pk_datasource() { } /// Counts how many rows contains an entity on the target database. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_count_operation() { assert_eq!( @@ -109,6 +117,7 @@ fn test_crud_count_operation() { /// Counts how many rows contains an entity on the target database using /// the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_count_datasource_operation() { assert_eq!( diff --git a/tests/crud/update_operations.rs b/tests/crud/update_operations.rs index fc7ae733..eee448cc 100644 --- a/tests/crud/update_operations.rs +++ b/tests/crud/update_operations.rs @@ -2,7 +2,7 @@ ///! generates and executes *UPDATE* statements use canyon_sql::crud::CrudOperations; -use crate::constants::SQL_SERVER_DS; +#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; /// Update operation is a *CRUD* method defined for some entity `T`, that works by appliying @@ -15,6 +15,7 @@ use crate::tests_models::league::*; /// /// Attempt of usage the `t.update(&self)` method on an entity without `#[primary_key]` /// will raise a runtime error. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_update_method_operation() { // We first retrieve some entity from the database. Note that we must make @@ -55,6 +56,7 @@ fn test_crud_update_method_operation() { } /// Same as the above test, but with the specified datasource. +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_update_datasource_method_operation() { // We first retrieve some entity from the database. Note that we must make From f77aa14c19bc4bb8020221a5d7a193c9f211a3f3 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 20 Apr 2023 13:33:59 +0200 Subject: [PATCH 50/82] All the APIs stabilized for making use of the conditional compilation across all the Canyon-SQL crates --- Cargo.toml | 7 +- canyon_connection/Cargo.toml | 1 - .../src/canyon_database_connector.rs | 13 +-- canyon_connection/src/lib.rs | 3 +- canyon_crud/Cargo.toml | 7 +- canyon_crud/src/crud.rs | 2 - canyon_crud/src/lib.rs | 1 + canyon_macros/Cargo.toml | 7 +- canyon_macros/src/lib.rs | 2 +- canyon_macros/src/query_operations/select.rs | 42 ++++++- canyon_observer/Cargo.toml | 1 - canyon_observer/src/migrations/processor.rs | 109 ++++++++++-------- tests/Cargo.toml | 3 +- tests/crud/mod.rs | 2 + tests/migrations/mod.rs | 2 + 15 files changed, 117 insertions(+), 85 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c2e7ffd2..216ab313 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ canyon_connection = { workspace = true, path = "canyon_connection" } canyon_crud = { workspace = true, path = "canyon_crud" } canyon_observer = { workspace = true, path = "canyon_observer" } -canyon_macros = { woorkspace = true, path = "canyon_macros" } +canyon_macros = { workspace = true, path = "canyon_macros" } # To be marked as opt deps tokio-postgres = { workspace = true, optional = true } @@ -28,12 +28,14 @@ tiberius = { workspace = true, optional = true } canyon_crud = { version = "0.2.0", path = "canyon_crud" } canyon_connection = { version = "0.2.0", path = "canyon_connection" } canyon_observer = { version = "0.2.0", path = "canyon_observer" } +canyon_macros = { version = "0.2.0", path = "canyon_macros" } tokio = { version = "1.27.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["compat"] } tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"] } tiberius = { version = "0.12.1", features = ["tds73", "chrono", "integrated-auth-gssapi"] } +chrono = { version = "0.4", features = ["serde"] } # Just from TP better? serde = { version = "1.0.138", features = ["derive"] } futures = "0.3.25" @@ -54,6 +56,5 @@ license = "MIT" description = "A Rust ORM and QueryBuilder" [features] -default = ["postgres"] -postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_connection/postgres", "canyon_observer/postgres", "canyon_macros/postgres"] +postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres", "canyon_observer/postgres", "canyon_macros/postgres"] mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql", "canyon_observer/mssql", "canyon_macros/mssql"] diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 4e140655..c736d124 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -24,6 +24,5 @@ serde = { workspace = true, features = ["derive"] } async-std = { workspace = true, optional = true } [features] -default = ["postgres"] postgres = ["tokio-postgres"] mssql = ["tiberius", "async-std"] diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 96d88154..7042cc71 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -140,19 +140,18 @@ impl DatabaseConnection { } #[cfg(feature = "postgres")] - #[allow(unreachable_patterns)] - pub fn postgres_connection(&self) -> Option<&PostgreSqlConnection> { + pub fn postgres_connection(&self) -> &PostgreSqlConnection { match self { - DatabaseConnection::Postgres(conn) => Some(conn), - _ => panic!(), + DatabaseConnection::Postgres(conn) => conn, + #[cfg(all(feature = "postgres", feature = "mssql"))] _ => panic!(), } } #[cfg(feature = "mssql")] - pub fn sqlserver_connection(&mut self) -> Option<&mut SqlServerConnection> { + pub fn sqlserver_connection(&mut self) -> &mut SqlServerConnection { match self { - DatabaseConnection::SqlServer(conn) => Some(conn), - _ => panic!(), + DatabaseConnection::SqlServer(conn) => conn, + #[cfg(all(feature = "postgres", feature = "mssql"))] _ => panic!(), } } } diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index 699341d6..434433d4 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -5,10 +5,9 @@ pub extern crate lazy_static; #[cfg(feature = "mssql")] pub extern crate tiberius; pub extern crate tokio; +pub extern crate tokio_util; #[cfg(feature = "postgres")] pub extern crate tokio_postgres; -#[cfg(feature = "postgres")] -pub extern crate tokio_util; pub mod canyon_database_connector; pub mod datasources; diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index cbe44ad9..eaefae18 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -10,18 +10,13 @@ license.workspace = true description.workspace = true [dependencies] -#tokio = { workspace = true, features = ["full"], optional = true } -#tokio-util = { workspace = true, features = ["compat"], optional = true } -#tokio-postgres = { workspace = true, features = ["with-chrono-0_4"], optional = true } -#tiberius = { workspace = true, features = ["tds73", "chrono", "integrated-auth-gssapi"], optional = true } tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } -chrono = { version = "0.4", features = ["serde"] } +chrono = { workspace = true, features = ["serde"] } async-trait = { workspace = true } canyon_connection = { workspace = true, path = "../canyon_connection" } [features] -default = ["postgres"] postgres = ["tokio-postgres", "canyon_connection/postgres"] mssql = ["tiberius", "canyon_connection/mssql"] diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index b06edfda..bf6b5e90 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -179,7 +179,6 @@ mod postgres_query_launcher { let r = db_conn .postgres_connection() - .unwrap() .client .query(&stmt, m_params.as_slice()) .await?; @@ -228,7 +227,6 @@ mod sqlserver_query_launcher { .query( db_conn .sqlserver_connection() - .expect("Error querying the MSSQL database") .client, ) .await? diff --git a/canyon_crud/src/lib.rs b/canyon_crud/src/lib.rs index ee856f6c..cea474cb 100644 --- a/canyon_crud/src/lib.rs +++ b/canyon_crud/src/lib.rs @@ -1,3 +1,4 @@ +pub extern crate async_trait; extern crate canyon_connection; pub mod bounds; diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index e306dcab..40682671 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -19,11 +19,10 @@ proc-macro2 = "1.0.27" futures = "0.3.21" tokio = { version = "1.9.0", features = ["full"] } -canyon_observer = { workspace = true, path = "../canyon_observer" } -canyon_crud = { workspace = true, path = "../canyon_crud" } -canyon_connection = { workspace = true, path = "../canyon_connection" } +canyon_observer = { workspace = true } +canyon_crud = { workspace = true } +canyon_connection = { workspace = true } [features] -default = ["postgres"] postgres = ["canyon_connection/postgres", "canyon_crud/postgres", "canyon_observer/postgres"] mssql = ["canyon_connection/mssql", "canyon_crud/mssql", "canyon_observer/mssql"] diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index 54c4c873..d6b68bac 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -322,7 +322,7 @@ fn impl_crud_operations_trait_for_struct( _search_by_revese_fk_tokens.iter().map(|(_, m_impl)| m_impl); // The autogenerated name for the trait that holds the fk and rev fk searches - let fk_trait_ident = proc_macro2::Ident::new( + let fk_trait_ident = Ident::new( &format!("{}FkOperations", &ty.to_string()), proc_macro2::Span::call_site(), ); diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index 55006e5d..0f70ab4d 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -150,13 +150,14 @@ pub fn generate_count_tokens( let ty_str = &ty.to_string(); let stmt = format!("SELECT COUNT (*) FROM {table_schema_data}"); - let result_handling = quote! { - match count { - // #[cfg(feature = "postgres")] + let postgres_enabled = cfg!(feature = "postgres"); + let mssql_enabled = cfg!(feature = "mssql"); + + let result_handling = if postgres_enabled && mssql_enabled { + quote! { canyon_sql::crud::CanyonRows::Postgres(mut v) => Ok( v.remove(0).get::<&str, i64>("count") ), - #[cfg(feature = "mssql")] canyon_sql::crud::CanyonRows::Tiberius(mut v) => v.remove(0) .get::(0) @@ -165,6 +166,31 @@ pub fn generate_count_tokens( .into(), _ => panic!() // TODO remove when the generics will be refactored } + } else if postgres_enabled { + quote! { + canyon_sql::crud::CanyonRows::Postgres(mut v) => Ok( + v.remove(0).get::<&str, i64>("count") + ), + _ => panic!() // TODO remove when the generics will be refactored + } + } else if mssql_enabled { + quote! { + canyon_sql::crud::CanyonRows::Tiberius(mut v) => + v.remove(0) + .get::(0) + .map(|c| c as i64) + .ok_or(format!("Failure in the COUNT query for MSSQL for: {}", #ty_str).into()) + .into(), + _ => panic!() // TODO remove when the generics will be refactored + } + } else { + quote! { + panic!( + "Reached a branch in the implementation of the Row Mapper macro that should never be reached.\ + This is a severe bug of Canyon-SQL. Please, open us an issue at \ + https://github.com/zerodaycode/Canyon-SQL/issues and let us know about that failure." + ) + } }; quote! { @@ -177,7 +203,9 @@ pub fn generate_count_tokens( "" ).await?; - #result_handling + match count { + #result_handling + } } /// Performs a COUNT(*) query over some table, returning a [`Result`] rather than panicking, @@ -189,7 +217,9 @@ pub fn generate_count_tokens( datasource_name ).await?; - #result_handling + match count { + #result_handling + } } } } diff --git a/canyon_observer/Cargo.toml b/canyon_observer/Cargo.toml index 7a616e0c..41cb3076 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_observer/Cargo.toml @@ -26,6 +26,5 @@ quote = "1.0.9" partialdebug = "0.2.0" [features] -default = ["postgres"] postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres"] mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql"] diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index e6c23bb4..b8e1de59 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -318,8 +318,10 @@ impl MigrationsProcessor { if attr.starts_with("Annotation: PrimaryKey") { Self::add_primary_key(self, entity_name, canyon_register_entity_field.clone()); - if canyon_register_entity_field.is_autoincremental() { - Self::add_identity(self, entity_name, canyon_register_entity_field.clone()); + #[cfg(feature = "postgres")] { + if canyon_register_entity_field.is_autoincremental() { + Self::add_identity(self, entity_name, canyon_register_entity_field.clone()); + } } } } @@ -355,6 +357,7 @@ impl MigrationsProcessor { ))); } + #[cfg(feature = "postgres")] fn add_identity(&mut self, entity_name: &str, field: CanyonRegisterEntityField) { self.constraints_operations .push(Box::new(ColumnOperation::AlterColumnAddIdentity( @@ -390,19 +393,22 @@ impl MigrationsProcessor { if field_is_primary_key && current_column_metadata.primary_key_info.is_none() { Self::add_primary_key(self, entity_name, canyon_register_entity_field.clone()); - if canyon_register_entity_field.is_autoincremental() { - Self::add_identity(self, entity_name, canyon_register_entity_field.clone()); + #[cfg(feature = "postgres")] { + if canyon_register_entity_field.is_autoincremental() { + Self::add_identity(self, entity_name, canyon_register_entity_field.clone()); + } } } // Case when the field contains a primary key annotation, and it's already on the database else if field_is_primary_key && current_column_metadata.primary_key_info.is_some() { - let is_autoincr_rust = canyon_register_entity_field.is_autoincremental(); - let is_autoincr_in_db = current_column_metadata.is_identity; - - if !is_autoincr_rust && is_autoincr_in_db { - Self::drop_identity(self, entity_name, canyon_register_entity_field.clone()) - } else if is_autoincr_rust && !is_autoincr_in_db { - Self::add_identity(self, entity_name, canyon_register_entity_field.clone()) + #[cfg(feature = "postgres")] { + let is_autoincr_rust = canyon_register_entity_field.is_autoincremental(); + let is_autoincr_in_db = current_column_metadata.is_identity; + if !is_autoincr_rust && is_autoincr_in_db { + Self::drop_identity(self, entity_name, canyon_register_entity_field.clone()) + } else if is_autoincr_rust && !is_autoincr_in_db { + Self::add_identity(self, entity_name, canyon_register_entity_field.clone()) + } } } // Case when field doesn't contains a primary key annotation, but there is one in the database column @@ -417,8 +423,10 @@ impl MigrationsProcessor { .to_string(), ); - if current_column_metadata.is_identity { - Self::drop_identity(self, entity_name, canyon_register_entity_field.clone()); + #[cfg(feature = "postgres")] { + if current_column_metadata.is_identity { + Self::drop_identity(self, entity_name, canyon_register_entity_field.clone()); + } } } @@ -531,6 +539,7 @@ impl MigrationsProcessor { ))); } + #[cfg(feature = "postgres")] fn drop_identity( &mut self, entity_name: &str, @@ -822,40 +831,40 @@ impl DatabaseOperation for TableOperation { } TableOperation::AddTableForeignKey( - table_name, - foreign_key_name, - column_foreign_key, - table_to_reference, - column_to_reference, + _table_name, + _foreign_key_name, + _column_foreign_key, + _table_to_reference, + _column_to_reference, ) => { match db_type { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( - "ALTER TABLE {table_name} ADD CONSTRAINT {foreign_key_name} \ - FOREIGN KEY ({column_foreign_key}) REFERENCES {table_to_reference} ({column_to_reference});" + "ALTER TABLE {_table_name} ADD CONSTRAINT {_foreign_key_name} \ + FOREIGN KEY ({_column_foreign_key}) REFERENCES {_table_to_reference} ({_column_to_reference});" ), #[cfg(feature = "mssql")] DatabaseType::SqlServer => todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } - TableOperation::DeleteTableForeignKey(table_with_foreign_key, constraint_name) => { + TableOperation::DeleteTableForeignKey(_table_with_foreign_key, _constraint_name) => { match db_type { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( - "ALTER TABLE {table_with_foreign_key} DROP CONSTRAINT {constraint_name};", + "ALTER TABLE {_table_with_foreign_key} DROP CONSTRAINT {_constraint_name};", ), #[cfg(feature = "mssql")] DatabaseType::SqlServer => todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } - TableOperation::AddTablePrimaryKey(table_name, entity_field) => { + TableOperation::AddTablePrimaryKey(_table_name, _entity_field) => { match db_type { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( - "ALTER TABLE \"{table_name}\" ADD PRIMARY KEY (\"{}\");", - entity_field.field_name + "ALTER TABLE \"{_table_name}\" ADD PRIMARY KEY (\"{}\");", + _entity_field.field_name ), #[cfg(feature = "mssql")] DatabaseType::SqlServer => todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") @@ -885,12 +894,10 @@ enum ColumnOperation { // AlterColumnName, AlterColumnType(String, CanyonRegisterEntityField), AlterColumnDropNotNull(String, CanyonRegisterEntityField), - // SQL server specific operation - SQL server can't drop a NOT NULL column - #[cfg(feature = "mssql")] - DropNotNullBeforeDropColumn(String, String, String), - #[cfg(feature = "postgres")] AlterColumnSetNotNull(String, CanyonRegisterEntityField), - // TODO if implement through annotations, modify for both GENERATED {ALWAYS, BY DEFAULT} + + #[cfg(feature = "mssql")] // SQL server specific operation - SQL server can't drop a NOT NULL column + DropNotNullBeforeDropColumn(String, String, String), #[cfg(feature = "postgres")] AlterColumnAddIdentity(String, CanyonRegisterEntityField), #[cfg(feature = "postgres")] @@ -926,12 +933,12 @@ impl DatabaseOperation for ColumnOperation { // TODO Check if operation for SQL server is different format!("ALTER TABLE \"{table_name}\" DROP COLUMN \"{column_name}\";") }, - ColumnOperation::AlterColumnType(table_name, entity_field) => + ColumnOperation::AlterColumnType(_table_name, _entity_field) => match db_type { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( - "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" TYPE {};", - entity_field.field_name, entity_field.to_postgres_alter_syntax() + "ALTER TABLE \"{_table_name}\" ALTER COLUMN \"{}\" TYPE {};", + _entity_field.field_name, _entity_field.to_postgres_alter_syntax() ), #[cfg(feature = "mssql")] DatabaseType::SqlServer => todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") @@ -961,9 +968,18 @@ impl DatabaseOperation for ColumnOperation { EXEC('ALTER TABLE '+@tableName+' DROP CONSTRAINT ' + @ConstraintName);" ), - ColumnOperation::AlterColumnSetNotNull(table_name, entity_field) => format!( - "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" SET NOT NULL;", entity_field.field_name - ), + ColumnOperation::AlterColumnSetNotNull(table_name, entity_field) => { + match db_type { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( + "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" SET NOT NULL;", entity_field.field_name + ), + #[cfg(feature = "mssql")] DatabaseType::SqlServer => format!( + "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NOT NULL", + entity_field.field_name, + entity_field.to_sqlserver_alter_syntax() + ) + } + } #[cfg(feature = "postgres")] ColumnOperation::AlterColumnAddIdentity(table_name, entity_field) => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" ADD GENERATED ALWAYS AS IDENTITY;", entity_field.field_name @@ -979,33 +995,26 @@ impl DatabaseOperation for ColumnOperation { } /// Helper for operations involving sequences +#[cfg(feature = "postgres")] #[derive(Debug)] -#[allow(dead_code)] enum SequenceOperation { ModifySequence(String, CanyonRegisterEntityField), } - +#[cfg(feature = "postgres")] impl Transaction for SequenceOperation {} +#[cfg(feature = "postgres")] #[async_trait] impl DatabaseOperation for SequenceOperation { async fn generate_sql(&self, datasource: &DatasourceConfig) { - let db_type = datasource.get_db_type(); - let stmt = match self { SequenceOperation::ModifySequence(table_name, entity_field) => { - match db_type { - #[cfg(feature = "postgres")] DatabaseType::PostgreSql => - format!( - "SELECT setval(pg_get_serial_sequence('\"{table_name}\"', '{}'), max(\"{}\")) from \"{table_name}\";", - entity_field.field_name, entity_field.field_name - ), - #[cfg(feature = "mssql")] DatabaseType::SqlServer => - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } + format!( + "SELECT setval(pg_get_serial_sequence('\"{table_name}\"', '{}'), max(\"{}\")) from \"{table_name}\";", + entity_field.field_name, entity_field.field_name + ) } }; - save_migrations_query_to_execute(stmt, &datasource.name); } } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index e606e3c9..8d96ac44 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -5,13 +5,12 @@ edition.workspace = true publish = false [dev-dependencies] -canyon_sql = { path = "..", default-features = false, features = ["mssql"] } +canyon_sql = { path = "..", features = ["postgres", "mssql"] } [[test]] name = "canyon_integration_tests" path = "canyon_integration_tests.rs" [features] -default = ["postgres"] postgres = [] mssql = [] diff --git a/tests/crud/mod.rs b/tests/crud/mod.rs index 5b11a7ed..82fdfd0b 100644 --- a/tests/crud/mod.rs +++ b/tests/crud/mod.rs @@ -1,3 +1,5 @@ +#![allow(unused_imports)] + pub mod delete_operations; pub mod foreign_key_operations; pub mod insert_operations; diff --git a/tests/migrations/mod.rs b/tests/migrations/mod.rs index 12dfa111..47f82566 100644 --- a/tests/migrations/mod.rs +++ b/tests/migrations/mod.rs @@ -1,9 +1,11 @@ +#![allow(unused_imports)] ///! Integration tests for the migrations feature of `Canyon-SQL` use canyon_sql::{crud::Transaction, migrations::handler::Migrations}; use crate::constants; /// Brings the information of the `PostgreSQL` requested schema +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_migrations_postgresql_status_query() { let results = Migrations::query(constants::FETCH_PUBLIC_SCHEMA, [], constants::PSQL_DS).await; From 8a3decfd0ed75f0cabab96cc802da4d9e475e065 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 20 Apr 2023 14:05:17 +0200 Subject: [PATCH 51/82] Setting up a better version for looking for the Canyon configuration file across directories --- Cargo.toml | 1 + canyon.toml | 24 ------- canyon_connection/Cargo.toml | 2 + .../src/canyon_database_connector.rs | 15 ++-- canyon_connection/src/datasources.rs | 7 +- canyon_connection/src/lib.rs | 26 +++++-- canyon_crud/src/bounds.rs | 14 ++-- canyon_crud/src/crud.rs | 6 +- canyon_crud/src/rows.rs | 54 ++++---------- canyon_macros/src/lib.rs | 70 +++++++++---------- canyon_observer/Cargo.toml | 2 +- canyon_observer/src/migrations/processor.rs | 17 +++-- src/lib.rs | 6 +- tests/Cargo.toml | 6 +- tests/canyon.toml | 20 +++--- tests/constants.rs | 12 ++-- tests/crud/delete_operations.rs | 6 +- tests/crud/foreign_key_operations.rs | 3 +- tests/crud/insert_operations.rs | 3 +- tests/crud/mod.rs | 3 +- tests/crud/querybuilder_operations.rs | 6 +- tests/crud/select_operations.rs | 3 +- tests/crud/update_operations.rs | 3 +- 23 files changed, 148 insertions(+), 161 deletions(-) delete mode 100644 canyon.toml diff --git a/Cargo.toml b/Cargo.toml index 216ab313..005b8648 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ async-std = "1.12.0" lazy_static = "1.4.0" toml = "0.7.3" async-trait = "0.1.68" +walkdir = "2.3.3" [workspace.package] version = "0.2.0" diff --git a/canyon.toml b/canyon.toml deleted file mode 100644 index 0b0614a4..00000000 --- a/canyon.toml +++ /dev/null @@ -1,24 +0,0 @@ -[canyon_sql] - -[[canyon_sql.datasources]] -name = 'postgres_docker' - -[canyon_sql.datasources.auth] -postgresql = { basic = { username = 'postgres', password = 'postgres'}} - -[canyon_sql.datasources.properties] -host = 'localhost' -port = 5438 -db_name = 'postgres' - - -[[canyon_sql.datasources]] -name = 'sqlserver_docker' - -[canyon_sql.datasources.auth] -sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } - -[canyon_sql.datasources.properties] -host = 'localhost' -port = 1434 -db_name = 'master' diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index c736d124..886971bb 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -22,6 +22,8 @@ lazy_static = { workspace = true } toml = { workspace = true } serde = { workspace = true, features = ["derive"] } async-std = { workspace = true, optional = true } +walkdir = { workspace = true } + [features] postgres = ["tokio-postgres"] diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 7042cc71..7196e948 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -143,7 +143,8 @@ impl DatabaseConnection { pub fn postgres_connection(&self) -> &PostgreSqlConnection { match self { DatabaseConnection::Postgres(conn) => conn, - #[cfg(all(feature = "postgres", feature = "mssql"))] _ => panic!(), + #[cfg(all(feature = "postgres", feature = "mssql"))] + _ => panic!(), } } @@ -151,7 +152,8 @@ impl DatabaseConnection { pub fn sqlserver_connection(&mut self) -> &mut SqlServerConnection { match self { DatabaseConnection::SqlServer(conn) => conn, - #[cfg(all(feature = "postgres", feature = "mssql"))] _ => panic!(), + #[cfg(all(feature = "postgres", feature = "mssql"))] + _ => panic!(), } } } @@ -164,7 +166,8 @@ mod database_connection_handler { /// Tests the behaviour of the `DatabaseType::from_datasource(...)` #[test] fn check_from_datasource() { - #[cfg(all(feature = "postgres", feature = "mssql"))] { + #[cfg(all(feature = "postgres", feature = "mssql"))] + { const CONFIG_FILE_MOCK_ALT_ALL: &str = r#" [canyon_sql] datasources = [ @@ -184,7 +187,8 @@ mod database_connection_handler { ); } - #[cfg(feature = "postgres")] { + #[cfg(feature = "postgres")] + { const CONFIG_FILE_MOCK_ALT_PG: &str = r#" [canyon_sql] datasources = [ @@ -199,7 +203,8 @@ mod database_connection_handler { ); } - #[cfg(feature = "mssql")] { + #[cfg(feature = "mssql")] + { const CONFIG_FILE_MOCK_ALT_MSSQL: &str = r#" [canyon_sql] datasources = [ diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 82775fd7..9571c343 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -5,7 +5,8 @@ use crate::canyon_database_connector::DatabaseType; /// ``` #[test] fn load_ds_config_from_array() { - #[cfg(feature = "postgres")] { + #[cfg(feature = "postgres")] + { const CONFIG_FILE_MOCK_ALT_PG: &str = r#" [canyon_sql] datasources = [ @@ -32,7 +33,8 @@ fn load_ds_config_from_array() { assert_eq!(ds_0.properties.migrations, Some(Migrations::Enabled)); } - #[cfg(feature = "mssql")] { + #[cfg(feature = "mssql")] + { const CONFIG_FILE_MOCK_ALT_MSSQL: &str = r#" [canyon_sql] datasources = [ @@ -46,7 +48,6 @@ fn load_ds_config_from_array() { let ds_1 = &config.canyon_sql.datasources[0]; let ds_2 = &config.canyon_sql.datasources[1]; - assert_eq!(ds_1.name, "SqlServerDS"); assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); assert_eq!( diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index 434433d4..fed9f31f 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -5,29 +5,29 @@ pub extern crate lazy_static; #[cfg(feature = "mssql")] pub extern crate tiberius; pub extern crate tokio; -pub extern crate tokio_util; #[cfg(feature = "postgres")] pub extern crate tokio_postgres; +pub extern crate tokio_util; pub mod canyon_database_connector; pub mod datasources; use std::fs; +use std::path::PathBuf; use crate::datasources::{CanyonSqlConfig, DatasourceConfig}; use canyon_database_connector::DatabaseConnection; use indexmap::IndexMap; use lazy_static::lazy_static; use tokio::sync::{Mutex, MutexGuard}; - -const CONFIG_FILE_IDENTIFIER: &str = "canyon.toml"; +use walkdir::WalkDir; lazy_static! { pub static ref CANYON_TOKIO_RUNTIME: tokio::runtime::Runtime = tokio::runtime::Runtime::new() // TODO Make the config with the builder .expect("Failed initializing the Canyon-SQL Tokio Runtime"); - static ref RAW_CONFIG_FILE: String = fs::read_to_string(CONFIG_FILE_IDENTIFIER) + static ref RAW_CONFIG_FILE: String = fs::read_to_string(find_canyon_config_file()) .expect("Error opening or reading the Canyon configuration file"); static ref CONFIG_FILE: CanyonSqlConfig = toml::from_str(RAW_CONFIG_FILE.as_str()) .expect("Error generating the configuration for Canyon-SQL"); @@ -39,6 +39,24 @@ lazy_static! { Mutex::new(IndexMap::new()); } +fn find_canyon_config_file() -> PathBuf { + for e in WalkDir::new(".") + .max_depth(2) + .into_iter() + .filter_map(|e| e.ok()) + { + let filename = e.file_name().to_str().unwrap(); + if e.metadata().unwrap().is_file() + && filename.starts_with("canyon") + && filename.ends_with(".toml") + { + return e.path().to_path_buf(); + } + } + + panic!() +} + /// Convenient free function to initialize a kind of connection pool based on the datasources present defined /// in the configuration file. /// diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs index 7ed83f1b..d46bf863 100644 --- a/canyon_crud/src/bounds.rs +++ b/canyon_crud/src/bounds.rs @@ -539,7 +539,7 @@ impl<'a> QueryParameter<'a> for Option<&String> { } } } -impl<'a> QueryParameter<'_> for &'_ str { +impl<'a> QueryParameter<'a> for &'_ str { #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self @@ -562,7 +562,7 @@ impl<'a> QueryParameter<'a> for Option<&'_ str> { } } } -impl<'a> QueryParameter<'_> for NaiveDate { +impl<'a> QueryParameter<'a> for NaiveDate { #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self @@ -582,7 +582,7 @@ impl<'a> QueryParameter<'a> for Option { self.into_sql() } } -impl<'a> QueryParameter<'_> for NaiveTime { +impl<'a> QueryParameter<'a> for NaiveTime { #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self @@ -602,7 +602,7 @@ impl<'a> QueryParameter<'a> for Option { self.into_sql() } } -impl<'a> QueryParameter<'_> for NaiveDateTime { +impl<'a> QueryParameter<'a> for NaiveDateTime { #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self @@ -622,7 +622,7 @@ impl<'a> QueryParameter<'a> for Option { self.into_sql() } } -impl<'a> QueryParameter<'_> for DateTime { +impl<'a> QueryParameter<'a> for DateTime { #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self @@ -642,7 +642,7 @@ impl<'a> QueryParameter<'a> for Option> { self.into_sql() } } -impl<'a> QueryParameter<'_> for DateTime { +impl<'a> QueryParameter<'a> for DateTime { #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self @@ -652,7 +652,7 @@ impl<'a> QueryParameter<'_> for DateTime { self.into_sql() } } -impl<'a> QueryParameter<'_> for Option> { +impl<'a> QueryParameter<'a> for Option> { #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index bf6b5e90..8509a91e 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -224,11 +224,7 @@ mod sqlserver_query_launcher { .for_each(|param| mssql_query.bind(*param)); let _results = mssql_query - .query( - db_conn - .sqlserver_connection() - .client, - ) + .query(db_conn.sqlserver_connection().client) .await? .into_results() .await?; diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs index 056e136a..d8d35070 100644 --- a/canyon_crud/src/rows.rs +++ b/canyon_crud/src/rows.rs @@ -42,7 +42,7 @@ impl CanyonRows { #[cfg(feature = "postgres")] Self::Postgres(v) => v.iter().map(|row| Z::deserialize_postgresql(row)).collect(), #[cfg(feature = "mssql")] - Self::Tiberius(v) => v.iter().map(|row| Z::deserialize_sqlserver(&row)).collect(), + Self::Tiberius(v) => v.iter().map(|row| Z::deserialize_sqlserver(row)).collect(), _ => panic!("This branch will never ever should be reachable"), } } @@ -57,45 +57,15 @@ impl CanyonRows { _ => panic!("This branch will never ever should be reachable"), } } -} -// #[cfg(feature = "postgres")] -// impl IntoIterator for CanyonRows { -// type Item = tokio_postgres::Row; -// type IntoIter = std::vec::IntoIter; -// -// fn into_iter(self) -> Self::IntoIter { -// match self { -// Self::Postgres(v) => v.into_iter(), -// _ => panic!() -// } -// } -// } -// -// #[cfg(feature = "mssql")] -// impl IntoIterator for CanyonRows { -// type Item = tiberius::Row; -// type IntoIter = std::vec::IntoIter; -// -// fn into_iter(self) -> Self::IntoIter { -// match self { -// Self::Tiberius(v) => v.into_iter(), -// _ => panic!() -// } -// } -// } -// -// #[cfg(all(feature = "tokio-postgres", feature = "tiberius"))] -// impl IntoIterator for CanyonRows { -// if cfg!(feature = "tokio-postgres") { -// type Item = tokio_postgres::Row; -// } else { type Item = tiberius::Row; } -// type IntoIter = std::vec::IntoIter; -// -// fn into_iter(self) -> Self::IntoIter { -// match self { -// Self::Tiberius(v) => v.into_iter(), -// _ => panic!() -// } -// } -// } + /// Returns true whenever the wrapped collection of Rows does not contains any elements + pub fn is_empty(&self) -> bool { + match self { + #[cfg(feature = "postgres")] + Self::Postgres(v) => v.is_empty(), + #[cfg(feature = "mssql")] + Self::Tiberius(v) => v.is_empty(), + _ => panic!("This branch will never ever should be reachable"), + } + } +} diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index d6b68bac..ce03cc58 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -487,82 +487,82 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac }); let init_field_values_sqlserver = fields.iter().map(|(_vis, ident, ty)| { - let ident_name = ident.to_string(); + let ident_name = ident.to_string(); - if get_field_type_as_string(ty) == "String" { - quote! { + if get_field_type_as_string(ty) == "String" { + quote! { #ident: row.get::<&str, &str>(#ident_name) .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) .to_string() } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::<&str, &str>(#ident_name) .map( |x| x.to_owned() ) } - } else if get_field_type_as_string(ty) == "NaiveDate" { - quote! { + } else if get_field_type_as_string(ty) == "NaiveDate" { + quote! { #ident: row.get::(#ident_name) .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty) == "NaiveTime" { - quote! { + } else if get_field_type_as_string(ty) == "NaiveTime" { + quote! { #ident: row.get::(#ident_name) .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty) == "NaiveDateTime" { - quote! { + } else if get_field_type_as_string(ty) == "NaiveDateTime" { + quote! { #ident: row.get::(#ident_name) .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else if get_field_type_as_string(ty) == "DateTime" { - quote! { + } else if get_field_type_as_string(ty) == "DateTime" { + quote! { #ident: row.get::(#ident_name) .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { + } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { + quote! { #ident: row.get::(#ident_name) } - } else { - quote! { + } else { + quote! { #ident: row.get::<#ty, &str>(#ident_name) .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) } - } - }); + } + }); // The type of the Struct let ty = ast.ident; diff --git a/canyon_observer/Cargo.toml b/canyon_observer/Cargo.toml index 41cb3076..c1b090ab 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_observer/Cargo.toml @@ -19,7 +19,7 @@ async-trait = { workspace = true } # transform to opts with migrations feature regex = "1.5" # opt -walkdir = "2" # opt +walkdir = { workspace = true } proc-macro2 = "1.0.27" syn = { version = "1.0.86", features = ["full", "parsing"] } quote = "1.0.9" diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index b8e1de59..b096b828 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -318,7 +318,8 @@ impl MigrationsProcessor { if attr.starts_with("Annotation: PrimaryKey") { Self::add_primary_key(self, entity_name, canyon_register_entity_field.clone()); - #[cfg(feature = "postgres")] { + #[cfg(feature = "postgres")] + { if canyon_register_entity_field.is_autoincremental() { Self::add_identity(self, entity_name, canyon_register_entity_field.clone()); } @@ -393,7 +394,8 @@ impl MigrationsProcessor { if field_is_primary_key && current_column_metadata.primary_key_info.is_none() { Self::add_primary_key(self, entity_name, canyon_register_entity_field.clone()); - #[cfg(feature = "postgres")] { + #[cfg(feature = "postgres")] + { if canyon_register_entity_field.is_autoincremental() { Self::add_identity(self, entity_name, canyon_register_entity_field.clone()); } @@ -401,7 +403,8 @@ impl MigrationsProcessor { } // Case when the field contains a primary key annotation, and it's already on the database else if field_is_primary_key && current_column_metadata.primary_key_info.is_some() { - #[cfg(feature = "postgres")] { + #[cfg(feature = "postgres")] + { let is_autoincr_rust = canyon_register_entity_field.is_autoincremental(); let is_autoincr_in_db = current_column_metadata.is_identity; if !is_autoincr_rust && is_autoincr_in_db { @@ -423,7 +426,8 @@ impl MigrationsProcessor { .to_string(), ); - #[cfg(feature = "postgres")] { + #[cfg(feature = "postgres")] + { if current_column_metadata.is_identity { Self::drop_identity(self, entity_name, canyon_register_entity_field.clone()); } @@ -674,7 +678,7 @@ impl MigrationsHelper { } } - return false; + false } fn extract_foreign_key_annotation(field_annotations: &[String]) -> (String, String) { @@ -896,7 +900,8 @@ enum ColumnOperation { AlterColumnDropNotNull(String, CanyonRegisterEntityField), AlterColumnSetNotNull(String, CanyonRegisterEntityField), - #[cfg(feature = "mssql")] // SQL server specific operation - SQL server can't drop a NOT NULL column + #[cfg(feature = "mssql")] + // SQL server specific operation - SQL server can't drop a NOT NULL column DropNotNullBeforeDropColumn(String, String, String), #[cfg(feature = "postgres")] AlterColumnAddIdentity(String, CanyonRegisterEntityField), diff --git a/src/lib.rs b/src/lib.rs index e40d9d3b..33a2c82b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,8 +39,8 @@ pub mod crud { pub use canyon_crud::bounds; pub use canyon_crud::crud::*; pub use canyon_crud::mapper::*; - pub use canyon_crud::DatabaseType; pub use canyon_crud::rows::CanyonRows; + pub use canyon_crud::DatabaseType; } /// Re-exports the query elements from the `crud`crate @@ -51,10 +51,10 @@ pub mod query { /// Reexport the available database clients within Canyon pub mod db_clients { - #[cfg(feature = "postgres")] - pub use canyon_connection::tokio_postgres; #[cfg(feature = "mssql")] pub use canyon_connection::tiberius; + #[cfg(feature = "postgres")] + pub use canyon_connection::tokio_postgres; } /// Reexport the needed runtime dependencies diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 8d96ac44..da6b0dfc 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -5,12 +5,12 @@ edition.workspace = true publish = false [dev-dependencies] -canyon_sql = { path = "..", features = ["postgres", "mssql"] } +canyon_sql = { path = ".." } [[test]] name = "canyon_integration_tests" path = "canyon_integration_tests.rs" [features] -postgres = [] -mssql = [] +postgres = ["canyon_sql/postgres"] +mssql = ["canyon_sql/mssql"] diff --git a/tests/canyon.toml b/tests/canyon.toml index 0b0614a4..dfa4a666 100644 --- a/tests/canyon.toml +++ b/tests/canyon.toml @@ -12,13 +12,13 @@ port = 5438 db_name = 'postgres' -[[canyon_sql.datasources]] -name = 'sqlserver_docker' - -[canyon_sql.datasources.auth] -sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } - -[canyon_sql.datasources.properties] -host = 'localhost' -port = 1434 -db_name = 'master' +#[[canyon_sql.datasources]] +#name = 'sqlserver_docker' +# +#[canyon_sql.datasources.auth] +#sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } +# +#[canyon_sql.datasources.properties] +#host = 'localhost' +#port = 1434 +#db_name = 'master' diff --git a/tests/constants.rs b/tests/constants.rs index 8fb86a44..1c9c8044 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -1,9 +1,12 @@ ///! Constant values to share across the integration tests -#[cfg(feature = "postgres")] pub const PSQL_DS: &str = "postgres_docker"; -#[cfg(feature = "mssql")] pub const SQL_SERVER_DS: &str = "sqlserver_docker"; +#[cfg(feature = "postgres")] +pub const PSQL_DS: &str = "postgres_docker"; +#[cfg(feature = "mssql")] +pub const SQL_SERVER_DS: &str = "sqlserver_docker"; -#[cfg(feature = "postgres")] pub static FETCH_PUBLIC_SCHEMA: &str = +#[cfg(feature = "postgres")] +pub static FETCH_PUBLIC_SCHEMA: &str = "SELECT gi.table_name, gi.column_name, @@ -34,7 +37,8 @@ LEFT JOIN pg_catalog.pg_constraint AS con on WHERE table_schema = 'public';"; -#[cfg(feature = "mssql")] pub const SQL_SERVER_CREATE_TABLES: &str = " +#[cfg(feature = "mssql")] +pub const SQL_SERVER_CREATE_TABLES: &str = " IF OBJECT_ID(N'[dbo].[league]', N'U') IS NULL BEGIN CREATE TABLE dbo.league ( diff --git a/tests/crud/delete_operations.rs b/tests/crud/delete_operations.rs index fb2e07e9..6420e553 100644 --- a/tests/crud/delete_operations.rs +++ b/tests/crud/delete_operations.rs @@ -2,8 +2,10 @@ ///! generates and executes *INSERT* statements use canyon_sql::crud::CrudOperations; -#[cfg(feature = "postgres")] use crate::constants::PSQL_DS; -#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; +#[cfg(feature = "postgres")] +use crate::constants::PSQL_DS; +#[cfg(feature = "mssql")] +use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; /// Deletes a row from the database that is mapped into some instance of a `T` entity. diff --git a/tests/crud/foreign_key_operations.rs b/tests/crud/foreign_key_operations.rs index b74f6852..471dd639 100644 --- a/tests/crud/foreign_key_operations.rs +++ b/tests/crud/foreign_key_operations.rs @@ -10,7 +10,8 @@ ///! For more info: TODO -> Link to the docs of the foreign key chapter use canyon_sql::crud::CrudOperations; -#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; +#[cfg(feature = "mssql")] +use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; use crate::tests_models::tournament::*; diff --git a/tests/crud/insert_operations.rs b/tests/crud/insert_operations.rs index 06ffbcbf..d52fa868 100644 --- a/tests/crud/insert_operations.rs +++ b/tests/crud/insert_operations.rs @@ -2,7 +2,8 @@ ///! generates and executes *INSERT* statements use canyon_sql::crud::CrudOperations; -#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; +#[cfg(feature = "mssql")] +use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; /// Inserts a new record on the database, given an entity that is diff --git a/tests/crud/mod.rs b/tests/crud/mod.rs index 82fdfd0b..407e727c 100644 --- a/tests/crud/mod.rs +++ b/tests/crud/mod.rs @@ -2,8 +2,9 @@ pub mod delete_operations; pub mod foreign_key_operations; +#[cfg(feature = "mssql")] +pub mod init_mssql; pub mod insert_operations; pub mod querybuilder_operations; pub mod select_operations; pub mod update_operations; -#[cfg(feature = "mssql")] pub mod init_mssql; diff --git a/tests/crud/querybuilder_operations.rs b/tests/crud/querybuilder_operations.rs index 8f9d1659..1c853161 100644 --- a/tests/crud/querybuilder_operations.rs +++ b/tests/crud/querybuilder_operations.rs @@ -9,10 +9,12 @@ use canyon_sql::{ query::{operators::Comp, ops::QueryBuilder}, }; +#[cfg(feature = "mssql")] +use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; +#[cfg(feature = "mssql")] +use crate::tests_models::player::*; use crate::tests_models::tournament::*; -#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; -#[cfg(feature = "mssql")] use crate::tests_models::player::*; /// Builds a new SQL statement for retrieves entities of the `T` type, filtered /// with the parameters that modifies the base SQL to SELECT * FROM diff --git a/tests/crud/select_operations.rs b/tests/crud/select_operations.rs index 5c20e958..9f9a6f5c 100644 --- a/tests/crud/select_operations.rs +++ b/tests/crud/select_operations.rs @@ -1,6 +1,7 @@ #![allow(clippy::nonminimal_bool)] -#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; +#[cfg(feature = "mssql")] +use crate::constants::SQL_SERVER_DS; ///! Integration tests for the CRUD operations available in `Canyon` that ///! generates and executes *SELECT* statements use crate::Error; diff --git a/tests/crud/update_operations.rs b/tests/crud/update_operations.rs index eee448cc..e4085560 100644 --- a/tests/crud/update_operations.rs +++ b/tests/crud/update_operations.rs @@ -2,7 +2,8 @@ ///! generates and executes *UPDATE* statements use canyon_sql::crud::CrudOperations; -#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; +#[cfg(feature = "mssql")] +use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; /// Update operation is a *CRUD* method defined for some entity `T`, that works by appliying From a6049a1ab144fcbe227dcda54827b8723bc077f9 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 20 Apr 2023 17:13:59 +0200 Subject: [PATCH 52/82] v0.3.0 --- .github/workflows/code-quality.yml | 2 +- CHANGELOG.md | 9 +++++++++ Cargo.toml | 14 +++++++++----- canyon_connection/Cargo.toml | 6 +++--- canyon_crud/Cargo.toml | 4 ++-- canyon_crud/src/crud.rs | 23 +++-------------------- canyon_macros/Cargo.toml | 10 +++++----- canyon_observer/Cargo.toml | 9 ++++----- tests/canyon.toml | 20 ++++++++++---------- 9 files changed, 46 insertions(+), 51 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 07ce16a2..9de14f14 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -55,7 +55,7 @@ jobs: strategy: fail-fast: false matrix: - crate: [canyon_connection, canyon_crud, canyon_macros, canyon_observer, canyon_sql_root] + crate: [canyon_connection, canyon_crud, canyon_macros, canyon_observer] steps: - uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index d79ce967..db434f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ Year format is defined as: `YYYY-m-d` ## [0.2.0] - 2023 - 04 - 13 +### Feature + +- Enabled conditional compilation for the database dependencies of the project. +This caused a major rework in the codebase, but none of the client APIs has been affected. +Now, Canyon-SQL comes with two features, ["postgres", "mssql"]. +There's no default features enabled for the project. + +## [0.2.0] - 2023 - 04 - 13 + ### Feature [BREAKING CHANGES] - The configuration file has been reworked, by providing a whole category dedicated diff --git a/Cargo.toml b/Cargo.toml index 005b8648..6f4da496 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,10 @@ tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } [workspace.dependencies] -canyon_crud = { version = "0.2.0", path = "canyon_crud" } -canyon_connection = { version = "0.2.0", path = "canyon_connection" } -canyon_observer = { version = "0.2.0", path = "canyon_observer" } -canyon_macros = { version = "0.2.0", path = "canyon_macros" } +canyon_crud = { version = "0.3.0", path = "canyon_crud" } +canyon_connection = { version = "0.3.0", path = "canyon_connection" } +canyon_observer = { version = "0.3.0", path = "canyon_observer" } +canyon_macros = { version = "0.3.0", path = "canyon_macros" } tokio = { version = "1.27.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["compat"] } @@ -45,9 +45,13 @@ lazy_static = "1.4.0" toml = "0.7.3" async-trait = "0.1.68" walkdir = "2.3.3" +regex = "1.5" + +quote = "1.0.9" +proc-macro2 = "1.0.27" [workspace.package] -version = "0.2.0" +version = "0.3.0" edition = "2021" authors = ["Alex Vergara, Gonzalo Busto"] documentation = "https://zerodaycode.github.io/canyon-book/" diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 886971bb..fd37fd4e 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -13,14 +13,14 @@ description.workspace = true tokio = { workspace = true } tokio-util = { workspace = true } -tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"], optional = true } -tiberius = { version = "0.12.1", features = ["tds73", "chrono", "integrated-auth-gssapi"], optional = true } +tokio-postgres = { workspace = true, optional = true } +tiberius = { workspace = true, optional = true } futures = { workspace = true } indexmap = { workspace = true } lazy_static = { workspace = true } toml = { workspace = true } -serde = { workspace = true, features = ["derive"] } +serde = { workspace = true } async-std = { workspace = true, optional = true } walkdir = { workspace = true } diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index eaefae18..123a44fe 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -12,10 +12,10 @@ description.workspace = true [dependencies] tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } -chrono = { workspace = true, features = ["serde"] } +chrono = { workspace = true } async-trait = { workspace = true } -canyon_connection = { workspace = true, path = "../canyon_connection" } +canyon_connection = { workspace = true } [features] postgres = ["tokio-postgres", "canyon_connection/postgres"] diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index 8509a91e..f5c6d37e 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -14,30 +14,13 @@ use crate::rows::CanyonRows; /// This traits defines and implements a query against a database given /// an statement `stmt` and the params to pass the to the client. /// -/// It returns a [`DatabaseResult`], which is the core Canyon type to wrap -/// the result of the query and, if the user desires, -/// automatically map it to an struct. +/// Returns [`std::result::Result`] of [`CanyonRows`], which is the core Canyon type to wrap +/// the result of the query provide automatic mappings and deserialization #[async_trait] pub trait Transaction { - // /// Performs a query against the targeted database by the selected or - // /// the defaulted datasource, returning a collection of instances of *T* - // async fn query<'a, S, Z>( - // stmt: S, - // params: Z, - // datasource_name: &'a str, - // ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> - // where - // S: AsRef + Display + Sync + Send + 'a, - // Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, - // { - // Self::query_for_rows(stmt, params, datasource_name) - // .await - // .map(|res| res.into_results()) - // } - /// Performs a query against the targeted database by the selected or /// the defaulted datasource, wrapping the resultant collection of entities - /// in [`super::rows::Rows`] + /// in [`super::rows::CanyonRows`] async fn query<'a, S, Z>( stmt: S, params: Z, diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index 40682671..82d336f5 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -13,11 +13,11 @@ description.workspace = true proc-macro = true [dependencies] -syn = { version = "1.0.109", features = ["full"] } -quote = "1.0.9" -proc-macro2 = "1.0.27" -futures = "0.3.21" -tokio = { version = "1.9.0", features = ["full"] } +syn = { version = "1.0.109", features = ["full"] } # TODO Pending to upgrade and refactor +quote = { workspace = true } +proc-macro2 = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true } canyon_observer = { workspace = true } canyon_crud = { workspace = true } diff --git a/canyon_observer/Cargo.toml b/canyon_observer/Cargo.toml index c1b090ab..0f939b2c 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_observer/Cargo.toml @@ -17,13 +17,12 @@ tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } async-trait = { workspace = true } -# transform to opts with migrations feature -regex = "1.5" # opt +regex = { workspace = true } walkdir = { workspace = true } -proc-macro2 = "1.0.27" -syn = { version = "1.0.86", features = ["full", "parsing"] } -quote = "1.0.9" partialdebug = "0.2.0" +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { version = "1.0.86", features = ["full", "parsing"] } # TODO Pending to refactor and upgrade [features] postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres"] diff --git a/tests/canyon.toml b/tests/canyon.toml index dfa4a666..0b0614a4 100644 --- a/tests/canyon.toml +++ b/tests/canyon.toml @@ -12,13 +12,13 @@ port = 5438 db_name = 'postgres' -#[[canyon_sql.datasources]] -#name = 'sqlserver_docker' -# -#[canyon_sql.datasources.auth] -#sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } -# -#[canyon_sql.datasources.properties] -#host = 'localhost' -#port = 1434 -#db_name = 'master' +[[canyon_sql.datasources]] +name = 'sqlserver_docker' + +[canyon_sql.datasources.auth] +sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } + +[canyon_sql.datasources.properties] +host = 'localhost' +port = 1434 +db_name = 'master' From 9735fd446319f5be9b7b62eb42379a1cb89b362f Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 20 Apr 2023 17:37:25 +0200 Subject: [PATCH 53/82] No IT for Windows and MacOS targets --- .github/workflows/continuous-integration.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 53f77132..3c26ce66 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -53,8 +53,12 @@ jobs: - name: Run only UNIT tests for Windows if: ${{ matrix.os == 'windows-latest' }} - run: cargo test --verbose --workspace --target=x86_64-pc-windows-msvc --exclude tests --all-features --no-fail-fast -- --show-output + run: | + cargo test --verbose --workspace --lib --target=x86_64-pc-windows-msvc --all-features --no-fail-fast -- --show-output + cargo test --verbose --workspace --doc --target=x86_64-pc-windows-msvc --all-features --no-fail-fast -- --show-output - name: Run only UNIT tests for MacOS if: ${{ matrix.os == 'MacOS-latest' }} - run: cargo test --verbose --workspace --exclude tests --all-features --no-fail-fast -- --show-output + run: | + cargo test --verbose --workspace --lib --all-features --no-fail-fast -- --show-output + cargo test --verbose --workspace --doc --all-features --no-fail-fast -- --show-output From f95839f14440eebbf8bf2450423f94d0d8248cd6 Mon Sep 17 00:00:00 2001 From: Alex Vergara <68871459+Pyzyryab@users.noreply.github.com> Date: Thu, 20 Apr 2023 17:47:31 +0200 Subject: [PATCH 54/82] Generating the cfg features for splitting up database crates dependencies (#40) v0.3.0 --- .github/workflows/code-coverage.yml | 1 - .github/workflows/code-quality.yml | 2 +- .github/workflows/continuous-integration.yml | 8 +- CHANGELOG.md | 26 +- Cargo.toml | 61 ++- bash_aliases.sh | 2 +- canyon_connection/Cargo.toml | 41 +- .../src/canyon_database_connector.rs | 114 ++++-- canyon_connection/src/datasources.rs | 114 +++--- canyon_connection/src/lib.rs | 51 ++- canyon_crud/Cargo.toml | 27 +- canyon_crud/src/bounds.rs | 297 +++++++++----- canyon_crud/src/crud.rs | 101 ++--- canyon_crud/src/lib.rs | 3 +- canyon_crud/src/mapper.rs | 8 +- .../src/query_elements/query_builder.rs | 50 +-- canyon_crud/src/result.rs | 108 ----- canyon_crud/src/rows.rs | 71 ++++ canyon_macros/Cargo.toml | 35 +- canyon_macros/src/lib.rs | 54 ++- canyon_macros/src/query_operations/insert.rs | 196 ++++++--- canyon_macros/src/query_operations/select.rs | 99 +++-- canyon_observer/Cargo.toml | 42 +- canyon_observer/src/constants.rs | 5 +- canyon_observer/src/lib.rs | 1 + canyon_observer/src/manager/entity.rs | 4 +- canyon_observer/src/migrations/handler.rs | 134 ++++-- .../src/migrations/information_schema.rs | 23 +- canyon_observer/src/migrations/memory.rs | 72 ++-- canyon_observer/src/migrations/processor.rs | 381 ++++++++++-------- .../src/migrations/register_types.rs | 56 +-- canyon_sql/Cargo.toml | 19 - {canyon_sql/src => src}/lib.rs | 20 +- tests/Cargo.toml | 12 +- tests/canyon_integration_tests.rs | 2 + tests/constants.rs | 6 + tests/crud/delete_operations.rs | 7 +- tests/crud/foreign_key_operations.rs | 7 +- tests/crud/init_mssql.rs | 62 +++ tests/crud/insert_operations.rs | 7 +- tests/crud/mod.rs | 67 +-- tests/crud/querybuilder_operations.rs | 10 +- tests/crud/select_operations.rs | 10 + tests/crud/update_operations.rs | 3 + tests/migrations/mod.rs | 6 +- 45 files changed, 1460 insertions(+), 965 deletions(-) delete mode 100644 canyon_crud/src/result.rs create mode 100644 canyon_crud/src/rows.rs delete mode 100755 canyon_sql/Cargo.toml rename {canyon_sql/src => src}/lib.rs (73%) mode change 100755 => 100644 create mode 100644 tests/crud/init_mssql.rs diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 144aa42e..cb3ecc98 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -27,7 +27,6 @@ jobs: rustup override set nightly - name: Make the USER own the working directory. Installing `gssapi` headers - if: ${{ matrix.os == 'ubuntu-latest' }} run: | sudo chown -R $USER:$USER ${{ github.workspace }} sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index c72c0e5b..9de14f14 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -55,7 +55,7 @@ jobs: strategy: fail-fast: false matrix: - crate: [canyon_connection, canyon_crud, canyon_macros, canyon_observer, canyon_sql] + crate: [canyon_connection, canyon_crud, canyon_macros, canyon_observer] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 53f77132..3c26ce66 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -53,8 +53,12 @@ jobs: - name: Run only UNIT tests for Windows if: ${{ matrix.os == 'windows-latest' }} - run: cargo test --verbose --workspace --target=x86_64-pc-windows-msvc --exclude tests --all-features --no-fail-fast -- --show-output + run: | + cargo test --verbose --workspace --lib --target=x86_64-pc-windows-msvc --all-features --no-fail-fast -- --show-output + cargo test --verbose --workspace --doc --target=x86_64-pc-windows-msvc --all-features --no-fail-fast -- --show-output - name: Run only UNIT tests for MacOS if: ${{ matrix.os == 'MacOS-latest' }} - run: cargo test --verbose --workspace --exclude tests --all-features --no-fail-fast -- --show-output + run: | + cargo test --verbose --workspace --lib --all-features --no-fail-fast -- --show-output + cargo test --verbose --workspace --doc --all-features --no-fail-fast -- --show-output diff --git a/CHANGELOG.md b/CHANGELOG.md index d4ef370f..db434f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,31 @@ Year format is defined as: `YYYY-m-d` ## [Unreleased] -- Solved a bug in the canyon_entity proc macro that was wiring the incorrect user table name in the migrations +## [0.2.0] - 2023 - 04 - 13 + +### Feature + +- Enabled conditional compilation for the database dependencies of the project. +This caused a major rework in the codebase, but none of the client APIs has been affected. +Now, Canyon-SQL comes with two features, ["postgres", "mssql"]. +There's no default features enabled for the project. + +## [0.2.0] - 2023 - 04 - 13 + +### Feature [BREAKING CHANGES] + +- The configuration file has been reworked, by providing a whole category dedicated +to the authentication against the database server. +- We removed the database type property, since the database type can be inferred by +the new mandatory auth property +- Included support for the `MSSQL` integrated authentication via the cfg feature `mssql-integrated-auth` + +## [0.1.2] - 2023 - 03 - 28 + +### Update + +- Implemented bool types for QueryParameters<'_>. +- Minimal performance improvements ## [0.1.1] - 2023 - 03 - 20 diff --git a/Cargo.toml b/Cargo.toml index 800ad578..6f4da496 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,65 @@ -# This is the root Cargo.toml file that serves as manager for the workspace of the project +[package] +name = "canyon_sql" +version.workspace = true +edition.workspace = true [workspace] members = [ - "canyon_sql", + "canyon_connection", + "canyon_crud", "canyon_observer", "canyon_macros", - "canyon_crud", - "canyon_connection", "tests" ] + +[dependencies] +# Project crates +canyon_connection = { workspace = true, path = "canyon_connection" } +canyon_crud = { workspace = true, path = "canyon_crud" } +canyon_observer = { workspace = true, path = "canyon_observer" } +canyon_macros = { workspace = true, path = "canyon_macros" } + +# To be marked as opt deps +tokio-postgres = { workspace = true, optional = true } +tiberius = { workspace = true, optional = true } + +[workspace.dependencies] +canyon_crud = { version = "0.3.0", path = "canyon_crud" } +canyon_connection = { version = "0.3.0", path = "canyon_connection" } +canyon_observer = { version = "0.3.0", path = "canyon_observer" } +canyon_macros = { version = "0.3.0", path = "canyon_macros" } + +tokio = { version = "1.27.0", features = ["full"] } +tokio-util = { version = "0.7.4", features = ["compat"] } +tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"] } +tiberius = { version = "0.12.1", features = ["tds73", "chrono", "integrated-auth-gssapi"] } + +chrono = { version = "0.4", features = ["serde"] } # Just from TP better? +serde = { version = "1.0.138", features = ["derive"] } + +futures = "0.3.25" +indexmap = "1.9.1" +async-std = "1.12.0" +lazy_static = "1.4.0" +toml = "0.7.3" +async-trait = "0.1.68" +walkdir = "2.3.3" +regex = "1.5" + +quote = "1.0.9" +proc-macro2 = "1.0.27" + +[workspace.package] +version = "0.3.0" +edition = "2021" +authors = ["Alex Vergara, Gonzalo Busto"] +documentation = "https://zerodaycode.github.io/canyon-book/" +homepage = "https://github.com/zerodaycode/Canyon-SQL" +readme = "README.md" +license = "MIT" +description = "A Rust ORM and QueryBuilder" + +[features] +postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres", "canyon_observer/postgres", "canyon_macros/postgres"] +mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql", "canyon_observer/mssql", "canyon_macros/mssql"] diff --git a/bash_aliases.sh b/bash_aliases.sh index a67da429..64e2d931 100644 --- a/bash_aliases.sh +++ b/bash_aliases.sh @@ -39,7 +39,7 @@ alias SqlServerInitializationLinux='cargo test initialize_sql_server_docker_inst # Publish Canyon-SQL to the registry with its dependencies -alias PublishCanyon='cargo publish -p canyon_connection && cargo publish -p canyon_crud && cargo publish -p canyon_observer && cargo publish -p canyon_macros && cargo publish -p canyon_sql' +alias PublishCanyon='cargo publish -p canyon_connection && cargo publish -p canyon_crud && cargo publish -p canyon_observer && cargo publish -p canyon_macros && cargo publish -p canyon_sql_root' # Collects the code coverage for the project (tests must run before this) alias CcEnvVars='export CARGO_INCREMENTAL=0 diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index 99058cf2..fd37fd4e 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -1,29 +1,30 @@ [package] name = "canyon_connection" -version = "0.2.0" -edition = "2021" -documentation = "https://zerodaycode.github.io/canyon-book/" -homepage = "https://github.com/zerodaycode/Canyon-SQL" -readme = "../README.md" -license = "MIT" -description = "A Rust ORM and QueryBuilder" - +version.workspace = true +edition.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true [dependencies] -tokio = { version = "1.21.2", features = ["full"] } -tokio-util = { version = "0.7.4", features = ["compat"] } -tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"] } -futures = "0.3.25" -indexmap = "1.9.1" +tokio = { workspace = true } +tokio-util = { workspace = true } -tiberius = { version = "0.12.1", features = ["tds73", "chrono", "integrated-auth-gssapi"] } -async-std = { version = "1.12.0" } +tokio-postgres = { workspace = true, optional = true } +tiberius = { workspace = true, optional = true } -lazy_static = "1.4.0" +futures = { workspace = true } +indexmap = { workspace = true } +lazy_static = { workspace = true } +toml = { workspace = true } +serde = { workspace = true } +async-std = { workspace = true, optional = true } +walkdir = { workspace = true } -serde = { version = "1.0.138", features = ["derive"] } -toml = "0.7.3" [features] -mssql-integrated-auth = [] - +postgres = ["tokio-postgres"] +mssql = ["tiberius", "async-std"] diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 71fd767e..7196e948 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -1,28 +1,34 @@ -use async_std::net::TcpStream; - use serde::Deserialize; + +#[cfg(feature = "mssql")] +use async_std::net::TcpStream; +#[cfg(feature = "mssql")] use tiberius::{AuthMethod, Config}; +#[cfg(feature = "postgres")] use tokio_postgres::{Client, NoTls}; use crate::datasources::DatasourceConfig; /// Represents the current supported databases by Canyon -#[derive(Deserialize, Debug, Eq, PartialEq, Clone, Copy, Default)] +#[derive(Deserialize, Debug, Eq, PartialEq, Clone, Copy)] pub enum DatabaseType { - #[default] #[serde(alias = "postgres", alias = "postgresql")] + #[cfg(feature = "postgres")] PostgreSql, #[serde(alias = "sqlserver", alias = "mssql")] + #[cfg(feature = "mssql")] SqlServer, } /// A connection with a `PostgreSQL` database +#[cfg(feature = "postgres")] pub struct PostgreSqlConnection { pub client: Client, // pub connection: Connection, // TODO Hold it, or not to hold it... that's the question! } /// A connection with a `SqlServer` database +#[cfg(feature = "mssql")] pub struct SqlServerConnection { pub client: &'static mut tiberius::Client, } @@ -32,7 +38,9 @@ pub struct SqlServerConnection { /// process them and generates a pool of 1 to 1 database connection for /// every datasource defined. pub enum DatabaseConnection { + #[cfg(feature = "postgres")] Postgres(PostgreSqlConnection), + #[cfg(feature = "mssql")] SqlServer(SqlServerConnection), } @@ -44,6 +52,7 @@ impl DatabaseConnection { datasource: &DatasourceConfig, ) -> Result> { match datasource.get_db_type() { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => { let (username, password) = match &datasource.auth { crate::datasources::Auth::Postgres(postgres_auth) => match postgres_auth { @@ -51,6 +60,7 @@ impl DatabaseConnection { (username.as_str(), password.as_str()) } }, + #[cfg(feature = "mssql")] crate::datasources::Auth::SqlServer(_) => { panic!("Found SqlServer auth configuration for a PostgreSQL datasource") } @@ -79,6 +89,7 @@ impl DatabaseConnection { // connection: new_connection, })) } + #[cfg(feature = "mssql")] DatabaseType::SqlServer => { let mut config = Config::new(); @@ -88,6 +99,7 @@ impl DatabaseConnection { // Using SQL Server authentication. config.authentication(match &datasource.auth { + #[cfg(feature = "postgres")] crate::datasources::Auth::Postgres(_) => { panic!("Found PostgreSQL auth configuration for a SqlServer database") } @@ -95,7 +107,6 @@ impl DatabaseConnection { crate::datasources::SqlServerAuth::Basic { username, password } => { AuthMethod::sql_server(username, password) } - #[cfg(feature = "mssql-integrated-auth")] crate::datasources::SqlServerAuth::Integrated => AuthMethod::Integrated, }, }); @@ -128,19 +139,21 @@ impl DatabaseConnection { } } - pub fn postgres_connection(&self) -> Option<&PostgreSqlConnection> { - if let DatabaseConnection::Postgres(conn) = self { - Some(conn) - } else { - None + #[cfg(feature = "postgres")] + pub fn postgres_connection(&self) -> &PostgreSqlConnection { + match self { + DatabaseConnection::Postgres(conn) => conn, + #[cfg(all(feature = "postgres", feature = "mssql"))] + _ => panic!(), } } - pub fn sqlserver_connection(&mut self) -> Option<&mut SqlServerConnection> { - if let DatabaseConnection::SqlServer(conn) = self { - Some(conn) - } else { - None + #[cfg(feature = "mssql")] + pub fn sqlserver_connection(&mut self) -> &mut SqlServerConnection { + match self { + DatabaseConnection::SqlServer(conn) => conn, + #[cfg(all(feature = "postgres", feature = "mssql"))] + _ => panic!(), } } } @@ -150,27 +163,60 @@ mod database_connection_handler { use super::*; use crate::CanyonSqlConfig; - const CONFIG_FILE_MOCK_ALT: &str = r#" - [canyon_sql] - datasources = [ - {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, - {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } - ] - "#; - /// Tests the behaviour of the `DatabaseType::from_datasource(...)` #[test] fn check_from_datasource() { - let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) - .expect("A failure happened retrieving the [canyon_sql] section"); - - assert_eq!( - config.canyon_sql.datasources[0].get_db_type(), - DatabaseType::PostgreSql - ); - assert_eq!( - config.canyon_sql.datasources[1].get_db_type(), - DatabaseType::SqlServer - ); + #[cfg(all(feature = "postgres", feature = "mssql"))] + { + const CONFIG_FILE_MOCK_ALT_ALL: &str = r#" + [canyon_sql] + datasources = [ + {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, + {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + ] + "#; + let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_ALL) + .expect("A failure happened retrieving the [canyon_sql] section"); + assert_eq!( + config.canyon_sql.datasources[0].get_db_type(), + DatabaseType::PostgreSql + ); + assert_eq!( + config.canyon_sql.datasources[1].get_db_type(), + DatabaseType::SqlServer + ); + } + + #[cfg(feature = "postgres")] + { + const CONFIG_FILE_MOCK_ALT_PG: &str = r#" + [canyon_sql] + datasources = [ + {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, + ] + "#; + let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_PG) + .expect("A failure happened retrieving the [canyon_sql] section"); + assert_eq!( + config.canyon_sql.datasources[0].get_db_type(), + DatabaseType::PostgreSql + ); + } + + #[cfg(feature = "mssql")] + { + const CONFIG_FILE_MOCK_ALT_MSSQL: &str = r#" + [canyon_sql] + datasources = [ + {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + ] + "#; + let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_MSSQL) + .expect("A failure happened retrieving the [canyon_sql] section"); + assert_eq!( + config.canyon_sql.datasources[0].get_db_type(), + DatabaseType::SqlServer + ); + } } } diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 81c4e611..9571c343 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -5,57 +5,72 @@ use crate::canyon_database_connector::DatabaseType; /// ``` #[test] fn load_ds_config_from_array() { - const CONFIG_FILE_MOCK_ALT: &str = r#" - [canyon_sql] - datasources = [ - {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, - {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' }, - {name = 'SqlServerDS', auth = { sqlserver = { integrated = {} } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } - ] - "#; - - let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT) - .expect("A failure happened retrieving the [canyon_sql] section"); - - let ds_0 = &config.canyon_sql.datasources[0]; - let ds_1 = &config.canyon_sql.datasources[1]; - let ds_2 = &config.canyon_sql.datasources[2]; - - assert_eq!(ds_0.name, "PostgresDS"); - assert_eq!(ds_0.get_db_type(), DatabaseType::PostgreSql); - assert_eq!( - ds_0.auth, - Auth::Postgres(PostgresAuth::Basic { - username: "postgres".to_string(), - password: "postgres".to_string() - }) - ); - assert_eq!(ds_0.properties.host, "localhost"); - assert_eq!(ds_0.properties.port, None); - assert_eq!(ds_0.properties.db_name, "triforce"); - assert_eq!(ds_0.properties.migrations, Some(Migrations::Enabled)); - - assert_eq!(ds_1.name, "SqlServerDS"); - assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); - assert_eq!( - ds_1.auth, - Auth::SqlServer(SqlServerAuth::Basic { - username: "sa".to_string(), - password: "SqlServer-10".to_string() - }) - ); - assert_eq!(ds_1.properties.host, "192.168.0.250.1"); - assert_eq!(ds_1.properties.port, Some(3340)); - assert_eq!(ds_1.properties.db_name, "triforce2"); - assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); - - assert_eq!(ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)) + #[cfg(feature = "postgres")] + { + const CONFIG_FILE_MOCK_ALT_PG: &str = r#" + [canyon_sql] + datasources = [ + {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, + ] + "#; + let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_PG) + .expect("A failure happened retrieving the [canyon_sql] section"); + + let ds_0 = &config.canyon_sql.datasources[0]; + + assert_eq!(ds_0.name, "PostgresDS"); + assert_eq!(ds_0.get_db_type(), DatabaseType::PostgreSql); + assert_eq!( + ds_0.auth, + Auth::Postgres(PostgresAuth::Basic { + username: "postgres".to_string(), + password: "postgres".to_string() + }) + ); + assert_eq!(ds_0.properties.host, "localhost"); + assert_eq!(ds_0.properties.port, None); + assert_eq!(ds_0.properties.db_name, "triforce"); + assert_eq!(ds_0.properties.migrations, Some(Migrations::Enabled)); + } + + #[cfg(feature = "mssql")] + { + const CONFIG_FILE_MOCK_ALT_MSSQL: &str = r#" + [canyon_sql] + datasources = [ + {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' }, + {name = 'SqlServerDS', auth = { sqlserver = { integrated = {} } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + ] + "#; + let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_MSSQL) + .expect("A failure happened retrieving the [canyon_sql] section"); + + let ds_1 = &config.canyon_sql.datasources[0]; + let ds_2 = &config.canyon_sql.datasources[1]; + + assert_eq!(ds_1.name, "SqlServerDS"); + assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); + assert_eq!( + ds_1.auth, + Auth::SqlServer(SqlServerAuth::Basic { + username: "sa".to_string(), + password: "SqlServer-10".to_string() + }) + ); + assert_eq!(ds_1.properties.host, "192.168.0.250.1"); + assert_eq!(ds_1.properties.port, Some(3340)); + assert_eq!(ds_1.properties.db_name, "triforce2"); + assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); + + assert_eq!(ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)); + } } /// #[derive(Deserialize, Debug, Clone)] pub struct CanyonSqlConfig { pub canyon_sql: Datasources, } + #[derive(Deserialize, Debug, Clone)] pub struct Datasources { pub datasources: Vec, @@ -71,7 +86,9 @@ pub struct DatasourceConfig { impl DatasourceConfig { pub fn get_db_type(&self) -> DatabaseType { match self.auth { + #[cfg(feature = "postgres")] Auth::Postgres(_) => DatabaseType::PostgreSql, + #[cfg(feature = "mssql")] Auth::SqlServer(_) => DatabaseType::SqlServer, } } @@ -79,23 +96,26 @@ impl DatasourceConfig { #[derive(Deserialize, Debug, Clone, PartialEq)] pub enum Auth { - #[serde(alias = "PostgreSQL", alias = "postgresql")] + #[serde(alias = "PostgreSQL", alias = "postgresql", alias = "postgres")] + #[cfg(feature = "postgres")] Postgres(PostgresAuth), #[serde(alias = "SqlServer", alias = "sqlserver", alias = "mssql")] + #[cfg(feature = "mssql")] SqlServer(SqlServerAuth), } #[derive(Deserialize, Debug, Clone, PartialEq)] +#[cfg(feature = "postgres")] pub enum PostgresAuth { #[serde(alias = "Basic", alias = "basic")] Basic { username: String, password: String }, } #[derive(Deserialize, Debug, Clone, PartialEq)] +#[cfg(feature = "mssql")] pub enum SqlServerAuth { #[serde(alias = "Basic", alias = "basic")] Basic { username: String, password: String }, - #[cfg(feature = "mssql-integrated-auth")] #[serde(alias = "Integrated", alias = "integrated")] Integrated, } diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index 1a8f7cab..fed9f31f 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -1,8 +1,11 @@ +#[cfg(feature = "mssql")] pub extern crate async_std; pub extern crate futures; pub extern crate lazy_static; +#[cfg(feature = "mssql")] pub extern crate tiberius; pub extern crate tokio; +#[cfg(feature = "postgres")] pub extern crate tokio_postgres; pub extern crate tokio_util; @@ -10,21 +13,21 @@ pub mod canyon_database_connector; pub mod datasources; use std::fs; +use std::path::PathBuf; use crate::datasources::{CanyonSqlConfig, DatasourceConfig}; use canyon_database_connector::DatabaseConnection; use indexmap::IndexMap; use lazy_static::lazy_static; -use tokio::sync::Mutex; - -const CONFIG_FILE_IDENTIFIER: &str = "canyon.toml"; +use tokio::sync::{Mutex, MutexGuard}; +use walkdir::WalkDir; lazy_static! { pub static ref CANYON_TOKIO_RUNTIME: tokio::runtime::Runtime = tokio::runtime::Runtime::new() // TODO Make the config with the builder .expect("Failed initializing the Canyon-SQL Tokio Runtime"); - static ref RAW_CONFIG_FILE: String = fs::read_to_string(CONFIG_FILE_IDENTIFIER) + static ref RAW_CONFIG_FILE: String = fs::read_to_string(find_canyon_config_file()) .expect("Error opening or reading the Canyon configuration file"); static ref CONFIG_FILE: CanyonSqlConfig = toml::from_str(RAW_CONFIG_FILE.as_str()) .expect("Error generating the configuration for Canyon-SQL"); @@ -36,6 +39,24 @@ lazy_static! { Mutex::new(IndexMap::new()); } +fn find_canyon_config_file() -> PathBuf { + for e in WalkDir::new(".") + .max_depth(2) + .into_iter() + .filter_map(|e| e.ok()) + { + let filename = e.file_name().to_str().unwrap(); + if e.metadata().unwrap().is_file() + && filename.starts_with("canyon") + && filename.ends_with(".toml") + { + return e.path().to_path_buf(); + } + } + + panic!() +} + /// Convenient free function to initialize a kind of connection pool based on the datasources present defined /// in the configuration file. /// @@ -61,3 +82,25 @@ pub async fn init_connections_cache() { ); } } + +/// +pub fn get_database_connection<'a>( + datasource_name: &str, + guarded_cache: &'a mut MutexGuard>, +) -> &'a mut DatabaseConnection { + if datasource_name.is_empty() { + guarded_cache + .get_mut( + DATASOURCES + .get(0) + .expect("We didn't found any valid datasource configuration. Check your `canyon.toml` file") + .name + .as_str() + ).unwrap_or_else(|| panic!("No default datasource found. Check your `canyon.toml` file")) + } else { + guarded_cache.get_mut(datasource_name) + .unwrap_or_else(|| + panic!("Canyon couldn't find a datasource in the pool with the argument provided: {datasource_name}") + ) + } +} diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index 4c30408f..123a44fe 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -1,15 +1,22 @@ [package] name = "canyon_crud" -version = "0.2.0" -edition = "2021" -documentation = "https://zerodaycode.github.io/canyon-book/" -homepage = "https://github.com/zerodaycode/Canyon-SQL" -readme = "../README.md" -license = "MIT" -description = "A Rust ORM and QueryBuilder" +version.workspace = true +edition.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true [dependencies] -chrono = { version = "0.4", features = ["serde"] } -async-trait = { version = "0.1.50" } +tokio-postgres = { workspace = true, optional = true } +tiberius = { workspace = true, optional = true } +chrono = { workspace = true } +async-trait = { workspace = true } -canyon_connection = { version = "0.2.0", path = "../canyon_connection" } +canyon_connection = { workspace = true } + +[features] +postgres = ["tokio-postgres", "canyon_connection/postgres"] +mssql = ["tiberius", "canyon_connection/mssql"] diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs index e484fe8c..d46bf863 100644 --- a/canyon_crud/src/bounds.rs +++ b/canyon_crud/src/bounds.rs @@ -1,18 +1,19 @@ -#![allow(clippy::extra_unused_lifetimes)] - use crate::{ crud::{CrudOperations, Transaction}, mapper::RowMapper, }; -use canyon_connection::{ - tiberius::{self, ColumnData, IntoSql}, - tokio_postgres::{self, types::ToSql}, -}; + +#[cfg(feature = "postgres")] +use canyon_connection::tokio_postgres::{self, types::ToSql}; + +#[cfg(feature = "mssql")] +use canyon_connection::tiberius::{self, ColumnData, IntoSql}; + use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use std::any::Any; /// Created for retrieve the field's name of a field of a struct, giving -/// the Canoyn's autogenerated enum with the variants that maps this +/// the Canyon's autogenerated enum with the variants that maps this /// fields. /// /// ``` @@ -79,26 +80,29 @@ pub trait ForeignKeyable { fn get_fk_column(&self, column: &str) -> Option<&dyn QueryParameter<'_>>; } -/// To define trait objects that helps to relates the necessary bounds in the 'IN` SQL clause -pub trait InClauseValues: ToSql + ToString {} - /// Generic abstraction to represent any of the Row types /// from the client crates pub trait Row { fn as_any(&self) -> &dyn Any; } + +#[cfg(feature = "postgres")] impl Row for tokio_postgres::Row { fn as_any(&self) -> &dyn Any { self } } +#[cfg(feature = "mssql")] impl Row for tiberius::Row { fn as_any(&self) -> &dyn Any { self } } +/// Generic abstraction for hold a Column type that will be one of the Column +/// types present in the dependent crates +// #[derive(Copy, Clone)] pub struct Column<'a> { name: &'a str, type_: ColumnType, @@ -110,116 +114,152 @@ impl<'a> Column<'a> { pub fn column_type(&self) -> &ColumnType { &self.type_ } - pub fn type_(&'a self) -> &'_ dyn Type { - match &self.type_ { - ColumnType::Postgres(v) => v as &'a dyn Type, - ColumnType::SqlServer(v) => v as &'a dyn Type, - } - } + // pub fn type_(&'a self) -> &'_ dyn Type { + // match (*self).type_ { + // #[cfg(feature = "postgres")] ColumnType::Postgres(v) => v as &'a dyn Type, + // #[cfg(feature = "mssql")] ColumnType::SqlServer(v) => v as &'a dyn Type, + // } + // } } pub trait Type { fn as_any(&self) -> &dyn Any; } +#[cfg(feature = "postgres")] impl Type for tokio_postgres::types::Type { fn as_any(&self) -> &dyn Any { self } } +#[cfg(feature = "mssql")] impl Type for tiberius::ColumnType { fn as_any(&self) -> &dyn Any { self } } +/// Wrapper over the dependencies Column's types pub enum ColumnType { + #[cfg(feature = "postgres")] Postgres(tokio_postgres::types::Type), + #[cfg(feature = "mssql")] SqlServer(tiberius::ColumnType), } pub trait RowOperations { - /// Abstracts the different forms of use the common `get` row - /// function or method dynamically no matter what are the origin - /// type from any database client provider - fn get<'a, Output>(&'a self, col_name: &str) -> Output + #[cfg(feature = "postgres")] + fn get_postgres<'a, Output>(&'a self, col_name: &'a str) -> Output + where + Output: tokio_postgres::types::FromSql<'a>; + #[cfg(feature = "mssql")] + fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output where - Output: tokio_postgres::types::FromSql<'a> + tiberius::FromSql<'a>; + Output: tiberius::FromSql<'a>; - fn get_opt<'a, Output>(&'a self, col_name: &str) -> Option + #[cfg(feature = "postgres")] + fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option where - Output: tokio_postgres::types::FromSql<'a> + tiberius::FromSql<'a>; + Output: tokio_postgres::types::FromSql<'a>; + #[cfg(feature = "mssql")] + fn get_mssql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option + where + Output: tiberius::FromSql<'a>; fn columns(&self) -> Vec; } impl RowOperations for &dyn Row { - fn get<'a, Output>(&'a self, col_name: &str) -> Output + #[cfg(feature = "postgres")] + fn get_postgres<'a, Output>(&'a self, col_name: &'a str) -> Output where - Output: tokio_postgres::types::FromSql<'a> + tiberius::FromSql<'a>, + Output: tokio_postgres::types::FromSql<'a>, { if let Some(row) = self.as_any().downcast_ref::() { return row.get::<&str, Output>(col_name); }; + panic!() // TODO into result and propagate + } + #[cfg(feature = "mssql")] + fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output + where + Output: tiberius::FromSql<'a>, + { if let Some(row) = self.as_any().downcast_ref::() { return row .get::(col_name) .expect("Failed to obtain a row in the MSSQL migrations"); }; - panic!() + panic!() // TODO into result and propagate } - fn columns(&self) -> Vec { - let mut cols = vec![]; - - if self.as_any().is::() { - self.as_any() - .downcast_ref::() - .expect("Not a tokio postgres Row for column") - .columns() - .iter() - .for_each(|c| { - cols.push(Column { - name: c.name(), - type_: ColumnType::Postgres(c.type_().to_owned()), - }) - }) - } else { - self.as_any() - .downcast_ref::() - .expect("Not a Tiberius Row for column") - .columns() - .iter() - .for_each(|c| { - cols.push(Column { - name: c.name(), - type_: ColumnType::SqlServer(c.column_type()), - }) - }) - }; - - cols - } - - fn get_opt<'a, Output>(&'a self, col_name: &str) -> Option + #[cfg(feature = "postgres")] + fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option where - Output: tokio_postgres::types::FromSql<'a> + tiberius::FromSql<'a>, + Output: tokio_postgres::types::FromSql<'a>, { if let Some(row) = self.as_any().downcast_ref::() { return row.get::<&str, Option>(col_name); }; + panic!() // TODO into result and propagate + } + + #[cfg(feature = "mssql")] + fn get_mssql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option + where + Output: tiberius::FromSql<'a>, + { if let Some(row) = self.as_any().downcast_ref::() { - return row - .try_get::(col_name) - .expect("Failed to obtain a row in the MSSQL migrations"); + return row.get::(col_name); }; - panic!() + panic!() // TODO into result and propagate + } + + fn columns(&self) -> Vec { + let mut cols = vec![]; + + #[cfg(feature = "postgres")] + { + if self.as_any().is::() { + self.as_any() + .downcast_ref::() + .expect("Not a tokio postgres Row for column") + .columns() + .iter() + .for_each(|c| { + cols.push(Column { + name: c.name(), + type_: ColumnType::Postgres(c.type_().to_owned()), + }) + }) + } + } + #[cfg(feature = "mssql")] + { + if self.as_any().is::() { + self.as_any() + .downcast_ref::() + .expect("Not a Tiberius Row for column") + .columns() + .iter() + .for_each(|c| { + cols.push(Column { + name: c.name(), + type_: ColumnType::SqlServer(c.column_type()), + }) + }) + }; + } + + cols } } /// Defines a trait for represent type bounds against the allowed -/// datatypes supported by Canyon to be used as query parameters. +/// data types supported by Canyon to be used as query parameters. pub trait QueryParameter<'a>: std::fmt::Debug + Sync + Send { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync); + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_>; } @@ -231,6 +271,7 @@ pub trait QueryParameter<'a>: std::fmt::Debug + Sync + Send { /// a collection of [`QueryParameter<'a>`], in order to allow a workflow /// that is not dependent of the specific type of the argument that holds /// the query parameters of the database connectors +#[cfg(feature = "mssql")] impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { fn into_sql(self) -> ColumnData<'a> { self.as_sqlserver_param() @@ -238,118 +279,131 @@ impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { } impl<'a> QueryParameter<'a> for bool { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::Bit(Some(*self)) } } impl<'a> QueryParameter<'a> for i16 { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(*self)) } } impl<'a> QueryParameter<'a> for &i16 { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(*self) } } impl<'a> QueryParameter<'a> for Option<&i16> { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for i32 { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self)) } } impl<'a> QueryParameter<'a> for &i32 { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(*self) } } impl<'a> QueryParameter<'a> for Option<&i32> { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for f32 { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some(*self)) } } impl<'a> QueryParameter<'a> for &f32 { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(*self) } } impl<'a> QueryParameter<'a> for Option<&f32> { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some( *self.expect("Error on an f32 value on QueryParameter<'_>"), @@ -357,37 +411,42 @@ impl<'a> QueryParameter<'a> for Option<&f32> { } } impl<'a> QueryParameter<'a> for f64 { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some(*self)) } } impl<'a> QueryParameter<'a> for &f64 { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(*self) } } impl<'a> QueryParameter<'a> for Option<&f64> { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some( *self.expect("Error on an f64 value on QueryParameter<'_>"), @@ -395,64 +454,71 @@ impl<'a> QueryParameter<'a> for Option<&f64> { } } impl<'a> QueryParameter<'a> for i64 { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self)) } } impl<'a> QueryParameter<'a> for &i64 { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(**self)) } } impl<'a> QueryParameter<'a> for Option { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(*self) } } impl<'a> QueryParameter<'a> for Option<&i64> { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self.unwrap())) } } impl<'a> QueryParameter<'a> for String { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Owned(self.to_owned()))) } } impl<'a> QueryParameter<'a> for &String { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Borrowed(self))) } } impl<'a> QueryParameter<'a> for Option { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { match self { Some(string) => ColumnData::String(Some(std::borrow::Cow::Owned(string.to_owned()))), @@ -461,10 +527,11 @@ impl<'a> QueryParameter<'a> for Option { } } impl<'a> QueryParameter<'a> for Option<&String> { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { match self { Some(string) => ColumnData::String(Some(std::borrow::Cow::Borrowed(string))), @@ -472,20 +539,22 @@ impl<'a> QueryParameter<'a> for Option<&String> { } } } -impl<'a> QueryParameter<'_> for &'_ str { +impl<'a> QueryParameter<'a> for &'_ str { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Borrowed(*self))) } } impl<'a> QueryParameter<'a> for Option<&'_ str> { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { match *self { Some(str) => ColumnData::String(Some(std::borrow::Cow::Borrowed(str))), @@ -493,92 +562,102 @@ impl<'a> QueryParameter<'a> for Option<&'_ str> { } } } -impl<'a> QueryParameter<'_> for NaiveDate { +impl<'a> QueryParameter<'a> for NaiveDate { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } -impl<'a> QueryParameter<'_> for NaiveTime { +impl<'a> QueryParameter<'a> for NaiveTime { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } -impl<'a> QueryParameter<'_> for NaiveDateTime { +impl<'a> QueryParameter<'a> for NaiveDateTime { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } -impl<'a> QueryParameter<'_> for DateTime { +impl<'a> QueryParameter<'a> for DateTime { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } impl<'a> QueryParameter<'a> for Option> { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } -impl<'a> QueryParameter<'_> for DateTime { +impl<'a> QueryParameter<'a> for DateTime { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } } -impl<'a> QueryParameter<'_> for Option> { +impl<'a> QueryParameter<'a> for Option> { + #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { self } - + #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index 8f587a02..f5c6d37e 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -2,54 +2,39 @@ use std::fmt::Display; use async_trait::async_trait; use canyon_connection::canyon_database_connector::DatabaseConnection; -use canyon_connection::{CACHED_DATABASE_CONN, DATASOURCES}; +use canyon_connection::{get_database_connection, CACHED_DATABASE_CONN}; use crate::bounds::QueryParameter; use crate::mapper::RowMapper; use crate::query_elements::query_builder::{ DeleteQueryBuilder, SelectQueryBuilder, UpdateQueryBuilder, }; -use crate::result::DatabaseResult; +use crate::rows::CanyonRows; /// This traits defines and implements a query against a database given -/// an statemt `stmt` and the params to pass the to the client. +/// an statement `stmt` and the params to pass the to the client. /// -/// It returns a [`DatabaseResult`], which is the core Canyon type to wrap -/// the result of the query and, if the user desires, -/// automatically map it to an struct. +/// Returns [`std::result::Result`] of [`CanyonRows`], which is the core Canyon type to wrap +/// the result of the query provide automatic mappings and deserialization #[async_trait] pub trait Transaction { - /// Performs a query against the targeted database by the selected datasource. - /// - /// No datasource means take the entry zero + /// Performs a query against the targeted database by the selected or + /// the defaulted datasource, wrapping the resultant collection of entities + /// in [`super::rows::CanyonRows`] async fn query<'a, S, Z>( stmt: S, params: Z, datasource_name: &'a str, - ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> + ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> where S: AsRef + Display + Sync + Send + 'a, Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, { let mut guarded_cache = CACHED_DATABASE_CONN.lock().await; + let database_conn = get_database_connection(datasource_name, &mut guarded_cache); - let database_conn = if datasource_name.is_empty() { - guarded_cache - .get_mut( - DATASOURCES - .get(0) - .expect("We didn't found any valid datasource configuration. Check your `canyon.toml` file") - .name - .as_str() - ).unwrap_or_else(|| panic!("No default datasource found. Check your `canyon.toml` file")) - } else { - guarded_cache.get_mut(datasource_name) - .unwrap_or_else(|| - panic!("Canyon couldn't find a datasource in the pool with the argument provided: {datasource_name}" - )) - }; - - match database_conn { + match *database_conn { + #[cfg(feature = "postgres")] DatabaseConnection::Postgres(_) => { postgres_query_launcher::launch::( database_conn, @@ -58,6 +43,7 @@ pub trait Transaction { ) .await } + #[cfg(feature = "mssql")] DatabaseConnection::SqlServer(_) => { sqlserver_query_launcher::launch::( database_conn, @@ -84,7 +70,7 @@ pub trait Transaction { /// /// See it's definition and docs to see the implementations. /// Also, you can find the written macro-code that performs the auto-mapping -/// in the *canyon_sql::canyon_macros* crates, on the root of this project. +/// in the *canyon_sql_root::canyon_macros* crates, on the root of this project. #[async_trait] pub trait CrudOperations: Transaction where @@ -119,14 +105,12 @@ where datasource_name: &'a str, ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>>; - async fn insert<'a>( - &mut self, - ) -> Result<(), Box>; + async fn insert<'a>(&mut self) -> Result<(), Box>; async fn insert_datasource<'a>( &mut self, datasource_name: &'a str, - ) -> Result<(), Box>; + ) -> Result<(), Box>; async fn multi_insert<'a>( instances: &'a mut [&'a mut T], @@ -137,69 +121,68 @@ where datasource_name: &'a str, ) -> Result<(), Box<(dyn std::error::Error + Send + Sync + 'static)>>; - async fn update(&self) -> Result<(), Box>; + async fn update(&self) -> Result<(), Box>; async fn update_datasource<'a>( &self, datasource_name: &'a str, - ) -> Result<(), Box>; + ) -> Result<(), Box>; fn update_query<'a>() -> UpdateQueryBuilder<'a, T>; fn update_query_datasource(datasource_name: &str) -> UpdateQueryBuilder<'_, T>; - async fn delete(&self) -> Result<(), Box>; + async fn delete(&self) -> Result<(), Box>; async fn delete_datasource<'a>( &self, datasource_name: &'a str, - ) -> Result<(), Box>; + ) -> Result<(), Box>; fn delete_query<'a>() -> DeleteQueryBuilder<'a, T>; fn delete_query_datasource(datasource_name: &str) -> DeleteQueryBuilder<'_, T>; } +#[cfg(feature = "postgres")] mod postgres_query_launcher { use crate::bounds::QueryParameter; - use crate::result::DatabaseResult; + use crate::rows::CanyonRows; use canyon_connection::canyon_database_connector::DatabaseConnection; pub async fn launch<'a, T>( db_conn: &DatabaseConnection, stmt: String, params: &'a [&'_ dyn QueryParameter<'_>], - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> { + ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> { let mut m_params = Vec::new(); for param in params { m_params.push(param.as_postgres_param()); } - Ok(DatabaseResult::new_postgresql( - db_conn - .postgres_connection() - .unwrap() - .client - .query(&stmt, m_params.as_slice()) - .await?, - )) + let r = db_conn + .postgres_connection() + .client + .query(&stmt, m_params.as_slice()) + .await?; + + Ok(CanyonRows::Postgres(r)) } } +#[cfg(feature = "mssql")] mod sqlserver_query_launcher { - use canyon_connection::tiberius::Row; - + use crate::rows::CanyonRows; use crate::{ bounds::QueryParameter, canyon_connection::{canyon_database_connector::DatabaseConnection, tiberius::Query}, - result::DatabaseResult, }; pub async fn launch<'a, T, Z>( db_conn: &mut DatabaseConnection, stmt: &mut String, params: Z, - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> + ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> where Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, { @@ -223,20 +206,14 @@ mod sqlserver_query_launcher { .iter() .for_each(|param| mssql_query.bind(*param)); - let _results: Vec = mssql_query - .query( - db_conn - .sqlserver_connection() - .expect("Error querying the MSSQL database") - .client, - ) + let _results = mssql_query + .query(db_conn.sqlserver_connection().client) .await? .into_results() - .await? - .into_iter() - .flatten() - .collect::>(); + .await?; - Ok(DatabaseResult::new_sqlserver(_results)) + Ok(CanyonRows::Tiberius( + _results.into_iter().flatten().collect(), + )) } } diff --git a/canyon_crud/src/lib.rs b/canyon_crud/src/lib.rs index 8a20b48e..cea474cb 100644 --- a/canyon_crud/src/lib.rs +++ b/canyon_crud/src/lib.rs @@ -1,10 +1,11 @@ +pub extern crate async_trait; extern crate canyon_connection; pub mod bounds; pub mod crud; pub mod mapper; pub mod query_elements; -pub mod result; +pub mod rows; pub use query_elements::operators::*; diff --git a/canyon_crud/src/mapper.rs b/canyon_crud/src/mapper.rs index 71303785..66cb91d2 100644 --- a/canyon_crud/src/mapper.rs +++ b/canyon_crud/src/mapper.rs @@ -1,4 +1,7 @@ -use canyon_connection::{tiberius, tokio_postgres}; +#[cfg(feature = "mssql")] +use canyon_connection::tiberius; +#[cfg(feature = "postgres")] +use canyon_connection::tokio_postgres; use crate::crud::Transaction; @@ -6,7 +9,8 @@ use crate::crud::Transaction; /// from some supported database in Canyon-SQL into a user's defined /// type `T` pub trait RowMapper>: Sized { + #[cfg(feature = "postgres")] fn deserialize_postgresql(row: &tokio_postgres::Row) -> T; - + #[cfg(feature = "mssql")] fn deserialize_sqlserver(row: &tiberius::Row) -> T; } diff --git a/canyon_crud/src/query_elements/query_builder.rs b/canyon_crud/src/query_elements/query_builder.rs index f0e68223..92146542 100644 --- a/canyon_crud/src/query_elements/query_builder.rs +++ b/canyon_crud/src/query_elements/query_builder.rs @@ -26,7 +26,7 @@ pub mod ops { /// hierarchy. /// /// For example, the [`super::QueryBuilder`] type holds the data - /// necessary for track the SQL sentece while it's being generated + /// necessary for track the SQL sentence while it's being generated /// thought the fluent builder, and provides the behaviour of /// the common elements defined in this trait. /// @@ -44,7 +44,7 @@ pub mod ops { /// just one type. pub trait QueryBuilder<'a, T> where - T: Debug + CrudOperations + Transaction + RowMapper, + T: CrudOperations + Transaction + RowMapper, { /// Returns a read-only reference to the underlying SQL sentence, /// with the same lifetime as self @@ -174,7 +174,7 @@ where self.datasource_name, ) .await? - .get_entities::()) + .into_results::()) } pub fn r#where>(&mut self, r#where: Z, op: impl Operator) { @@ -324,7 +324,7 @@ where } /// Adds a *LEFT JOIN* SQL statement to the underlying - /// [`Query`] holded by the [`QueryBuilder`], where: + /// [`Query`] held by the [`QueryBuilder`], where: /// /// * `join_table` - The table target of the join operation /// * `col1` - The left side of the ON operator for the join @@ -340,7 +340,7 @@ where } /// Adds a *RIGHT JOIN* SQL statement to the underlying - /// [`Query`] holded by the [`QueryBuilder`], where: + /// [`Query`] held by the [`QueryBuilder`], where: /// /// * `join_table` - The table target of the join operation /// * `col1` - The left side of the ON operator for the join @@ -356,7 +356,7 @@ where } /// Adds a *RIGHT JOIN* SQL statement to the underlying - /// [`Query`] holded by the [`QueryBuilder`], where: + /// [`Query`] held by the [`QueryBuilder`], where: /// /// * `join_table` - The table target of the join operation /// * `col1` - The left side of the ON operator for the join @@ -372,7 +372,7 @@ where } /// Adds a *FULL JOIN* SQL statement to the underlying - /// [`Query`] holded by the [`QueryBuilder`], where: + /// [`Query`] held by the [`QueryBuilder`], where: /// /// * `join_table` - The table target of the join operation /// * `col1` - The left side of the ON operator for the join @@ -428,12 +428,6 @@ where self } - #[inline] - fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { - self._inner.or(column, op); - self - } - #[inline] fn or_values_in(&mut self, r#and: Z, values: &'a [Q]) -> &mut Self where @@ -444,6 +438,12 @@ where self } + #[inline] + fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { + self._inner.or(column, op); + self + } + #[inline] fn order_by>(&mut self, order_by: Z, desc: bool) -> &mut Self { self._inner.order_by(order_by, desc); @@ -565,12 +565,6 @@ where self } - #[inline] - fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { - self._inner.or(column, op); - self - } - #[inline] fn or_values_in(&mut self, r#or: Z, values: &'a [Q]) -> &mut Self where @@ -581,6 +575,12 @@ where self } + #[inline] + fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { + self._inner.or(column, op); + self + } + #[inline] fn order_by>(&mut self, order_by: Z, desc: bool) -> &mut Self { self._inner.order_by(order_by, desc); @@ -665,12 +665,6 @@ where self } - #[inline] - fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { - self._inner.or(column, op); - self - } - #[inline] fn or_values_in(&mut self, r#or: Z, values: &'a [Q]) -> &mut Self where @@ -681,6 +675,12 @@ where self } + #[inline] + fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { + self._inner.or(column, op); + self + } + #[inline] fn order_by>(&mut self, order_by: Z, desc: bool) -> &mut Self { self._inner.order_by(order_by, desc); diff --git a/canyon_crud/src/result.rs b/canyon_crud/src/result.rs deleted file mode 100644 index 1a2cae29..00000000 --- a/canyon_crud/src/result.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::{bounds::Row, crud::Transaction, mapper::RowMapper}; -use canyon_connection::{canyon_database_connector::DatabaseType, tiberius, tokio_postgres}; -use std::{fmt::Debug, marker::PhantomData}; - -/// Represents a database result after a query, by wrapping the `Vec` types that comes with the -/// results after the query. -/// and providing methods to deserialize this result into a **user defined struct** -#[derive(Debug)] -pub struct DatabaseResult { - pub postgres: Vec, - pub sqlserver: Vec, - pub active_ds: DatabaseType, - _phantom_data: std::marker::PhantomData, -} - -impl DatabaseResult { - pub fn new_postgresql(result: Vec) -> Self { - Self { - postgres: result, - sqlserver: Vec::with_capacity(0), - active_ds: DatabaseType::PostgreSql, - _phantom_data: PhantomData, - } - } - - pub fn new_sqlserver(results: Vec) -> Self { - Self { - postgres: Vec::with_capacity(0), - sqlserver: results, - active_ds: DatabaseType::SqlServer, - _phantom_data: PhantomData, - } - } - - /// Returns a [`Vec`] filled with instances of the type T. - /// Z param it's used to constraint the types that can call this method. - /// - /// Also, provides a way to statically call `Z::deserialize_` method, - /// which it's the implementation used by the macros to automatically - /// map database columns into the fields for T. - pub fn get_entities>(&self) -> Vec - where - T: Transaction, - { - match self.active_ds { - DatabaseType::PostgreSql => self.map_from_postgresql::(), - DatabaseType::SqlServer => self.map_from_sql_server::(), - } - } - - fn map_from_postgresql>(&self) -> Vec - where - T: Transaction, - { - let mut results = Vec::new(); - - self.postgres - .iter() - .for_each(|row| results.push(Z::deserialize_postgresql(row))); - - results - } - - fn map_from_sql_server>(&self) -> Vec - where - T: Transaction, - { - let mut results = Vec::new(); - - self.sqlserver - .iter() - .for_each(|row| results.push(Z::deserialize_sqlserver(row))); - - results - } - - pub fn as_canyon_rows(&self) -> Vec<&dyn Row> { - let mut results = Vec::new(); - - match self.active_ds { - DatabaseType::PostgreSql => { - self.postgres - .iter() - .for_each(|row| results.push(row as &dyn Row)); - } - DatabaseType::SqlServer => { - self.sqlserver - .iter() - .for_each(|row| results.push(row as &dyn Row)); - } - }; - - results - } - - /// Returns the active datasource - pub fn get_active_ds(&self) -> &DatabaseType { - &self.active_ds - } - - /// Returns how many rows contains the result of the query - pub fn number_of_results(&self) -> usize { - match self.active_ds { - DatabaseType::PostgreSql => self.postgres.len(), - DatabaseType::SqlServer => self.sqlserver.len(), - } - } -} diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs new file mode 100644 index 00000000..d8d35070 --- /dev/null +++ b/canyon_crud/src/rows.rs @@ -0,0 +1,71 @@ +use crate::crud::Transaction; +use crate::mapper::RowMapper; +use std::marker::PhantomData; + +/// Lightweight wrapper over the collection of results of the different crates +/// supported by Canyon-SQL. +/// +/// Even tho the wrapping seems meaningless, this allows us to provide internal +/// operations that are too difficult or to ugly to implement in the macros that +/// will call the query method of Crud. +pub enum CanyonRows { + #[cfg(feature = "postgres")] + Postgres(Vec), + #[cfg(feature = "mssql")] + Tiberius(Vec), + UnusableTypeMarker(PhantomData), +} + +impl CanyonRows { + #[cfg(feature = "postgres")] + pub fn get_postgres_rows(&self) -> &Vec { + match self { + Self::Postgres(v) => v, + _ => panic!("This branch will never ever should be reachable"), + } + } + + #[cfg(feature = "mssql")] + pub fn get_tiberius_rows(&self) -> &Vec { + match self { + Self::Tiberius(v) => v, + _ => panic!("This branch will never ever should be reachable"), + } + } + + /// Consumes `self` and returns the wrapped [`std::vec::Vec`] with the instances of T + pub fn into_results>(self) -> Vec + where + T: Transaction, + { + match self { + #[cfg(feature = "postgres")] + Self::Postgres(v) => v.iter().map(|row| Z::deserialize_postgresql(row)).collect(), + #[cfg(feature = "mssql")] + Self::Tiberius(v) => v.iter().map(|row| Z::deserialize_sqlserver(row)).collect(), + _ => panic!("This branch will never ever should be reachable"), + } + } + + /// Returns the number of elements present on the wrapped collection + pub fn len(&self) -> usize { + match self { + #[cfg(feature = "postgres")] + Self::Postgres(v) => v.len(), + #[cfg(feature = "mssql")] + Self::Tiberius(v) => v.len(), + _ => panic!("This branch will never ever should be reachable"), + } + } + + /// Returns true whenever the wrapped collection of Rows does not contains any elements + pub fn is_empty(&self) -> bool { + match self { + #[cfg(feature = "postgres")] + Self::Postgres(v) => v.is_empty(), + #[cfg(feature = "mssql")] + Self::Tiberius(v) => v.is_empty(), + _ => panic!("This branch will never ever should be reachable"), + } + } +} diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index 93695087..82d336f5 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -1,23 +1,28 @@ [package] name = "canyon_macros" -version = "0.2.0" -edition = "2021" -documentation = "https://zerodaycode.github.io/canyon-book/" -homepage = "https://github.com/zerodaycode/Canyon-SQL" -readme = "../README.md" -license = "MIT" -description = "A Rust ORM and QueryBuilder" +version.workspace = true +edition.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true [lib] proc-macro = true [dependencies] -syn = { version = "1.0.109", features = ["full"] } -quote = "1.0.9" -proc-macro2 = "1.0.27" -futures = "0.3.21" -tokio = { version = "1.9.0", features = ["full"] } +syn = { version = "1.0.109", features = ["full"] } # TODO Pending to upgrade and refactor +quote = { workspace = true } +proc-macro2 = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true } -canyon_observer = { version = "0.2.0", path = "../canyon_observer" } -canyon_crud = { version = "0.2.0", path = "../canyon_crud" } -canyon_connection = { version = "0.2.0", path = "../canyon_connection" } +canyon_observer = { workspace = true } +canyon_crud = { workspace = true } +canyon_connection = { workspace = true } + +[features] +postgres = ["canyon_connection/postgres", "canyon_crud/postgres", "canyon_observer/postgres"] +mssql = ["canyon_connection/mssql", "canyon_crud/mssql", "canyon_observer/mssql"] diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index 34a166e8..ce03cc58 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -322,7 +322,7 @@ fn impl_crud_operations_trait_for_struct( _search_by_revese_fk_tokens.iter().map(|(_, m_impl)| m_impl); // The autogenerated name for the trait that holds the fk and rev fk searches - let fk_trait_ident = proc_macro2::Ident::new( + let fk_trait_ident = Ident::new( &format!("{}FkOperations", &ty.to_string()), proc_macro2::Span::call_site(), ); @@ -486,7 +486,6 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac } }); - // TODO rework this ugly piece of code in the upcoming versions let init_field_values_sqlserver = fields.iter().map(|(_vis, ident, ty)| { let ident_name = ident.to_string(); @@ -568,21 +567,52 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac // The type of the Struct let ty = ast.ident; - let tokens = quote! { - impl canyon_sql::crud::RowMapper for #ty - { - fn deserialize_postgresql(row: &canyon_sql::db_clients::tokio_postgres::Row) -> #ty { - Self { - #(#init_field_values),* + let postgres_enabled = cfg!(feature = "postgres"); + let mssql_enabled = cfg!(feature = "mssql"); + + let tokens = if postgres_enabled && mssql_enabled { + quote! { + impl canyon_sql::crud::RowMapper for #ty { + fn deserialize_postgresql(row: &canyon_sql::db_clients::tokio_postgres::Row) -> #ty { + Self { + #(#init_field_values),* + } + } + fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { + Self { + #(#init_field_values_sqlserver),* + } } } - - fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { - Self { - #(#init_field_values_sqlserver),* + } + } else if postgres_enabled { + quote! { + impl canyon_sql::crud::RowMapper for #ty { + fn deserialize_postgresql(row: &canyon_sql::db_clients::tokio_postgres::Row) -> #ty { + Self { + #(#init_field_values),* + } } } } + } else if mssql_enabled { + quote! { + impl canyon_sql::crud::RowMapper for #ty { + fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { + Self { + #(#init_field_values_sqlserver),* + } + } + } + } + } else { + quote! { + panic!( + "Reached a branch in the implementation of the Row Mapper macro that should never be reached.\ + This is a severe bug of Canyon-SQL. Please, open us an issue at \ + https://github.com/zerodaycode/Canyon-SQL/issues and let us know about that failure." + ) + } }; tokens.into() diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index 11890b31..329399f0 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -34,11 +34,62 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri ._fields_with_types() .into_iter() .find(|(i, _t)| Some(i.to_string()) == primary_key); - let insert_transaction = if let Some(pk_data) = &pk_ident_type { let pk_ident = &pk_data.0; let pk_type = &pk_data.1; + let postgres_enabled = cfg!(feature = "postgres"); + let mssql_enabled = cfg!(feature = "mssql"); + + let match_rows = if postgres_enabled && mssql_enabled { + quote! { + canyon_sql::crud::CanyonRows::Postgres(mut v) => { + self.#pk_ident = v + .get(0) + .ok_or("Failed getting the returned IDs for an insert")? + .get::<&str, #pk_type>(#primary_key); + Ok(()) + } + canyon_sql::crud::CanyonRows::Tiberius(mut v) => { + self.#pk_ident = v + .get(0) + .ok_or("Failed getting the returned IDs for a multi insert")? + .get::<#pk_type, &str>(#primary_key) + .ok_or("SQL Server primary key type failed to be set as value")?; + Ok(()) + } + } + } else if postgres_enabled { + quote! { + canyon_sql::crud::CanyonRows::Postgres(mut v) => { + self.#pk_ident = v + .get(0) + .ok_or("Failed getting the returned IDs for an insert")? + .get::<&str, #pk_type>(#primary_key); + Ok(()) + } + } + } else if mssql_enabled { + quote! { + canyon_sql::crud::CanyonRows::Tiberius(mut v) => { + self.#pk_ident = v + .get(0) + .ok_or("Failed getting the returned IDs for a multi insert")? + .get::<#pk_type, &str>(#primary_key) + .ok_or("SQL Server primary key type failed to be set as value")?; + Ok(()) + } + } + } else { + quote! { + panic!( + "Reached a branch in the implementation of the Row Mapper macro that should never be reached.\ + This is a severe bug of Canyon-SQL. Please, open us an issue at \ + https://github.com/zerodaycode/Canyon-SQL/issues and let us know about that failure." + ) + } + }; + quote! { #remove_pk_value_from_fn_entry; @@ -50,35 +101,15 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri #primary_key ); - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( + let rows = <#ty as canyon_sql::crud::Transaction<#ty>>::query( stmt, values, datasource_name - ).await; - - match result { - Ok(res) => { - match res.get_active_ds() { - canyon_sql::crud::DatabaseType::PostgreSql => { - self.#pk_ident = res.postgres.get(0) - .expect("No value found on the returning clause") - .get::<&str, #pk_type>(#primary_key) - .to_owned(); - - Ok(()) - }, - canyon_sql::crud::DatabaseType::SqlServer => { - self.#pk_ident = res.sqlserver.get(0) - .expect("No value found on the returning clause") - .get::<#pk_type, &str>(#primary_key) - .expect("SQL Server primary key type failed to be set as value") - .to_owned(); - - Ok(()) - } - } - }, - Err(e) => Err(e) + ).await?; + + match rows { + #match_rows + _ => panic!("Reached the panic match arm of insert for the DatabaseConnection type") // TODO remove when the generics will be refactored } } } else { @@ -228,6 +259,70 @@ pub fn generate_multiple_insert_tokens( let pk_ident = &pk_data.0; let pk_type = &pk_data.1; + let postgres_enabled = cfg!(feature = "postgres"); + let mssql_enabled = cfg!(feature = "mssql"); + + let match_multi_insert_rows = if postgres_enabled && mssql_enabled { + quote! { + canyon_sql::crud::CanyonRows::Postgres(mut v) => { + for (idx, instance) in instances.iter_mut().enumerate() { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") + .get::<&str, #pk_type>(#pk); + } + + Ok(()) + } + canyon_sql::crud::CanyonRows::Tiberius(mut v) => { + for (idx, instance) in instances.iter_mut().enumerate() { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") + .get::<#pk_type, &str>(#pk) + .expect("SQL Server primary key type failed to be set as value"); + } + + Ok(()) + } + } + } else if postgres_enabled { + quote! { + canyon_sql::crud::CanyonRows::Postgres(mut v) => { + for (idx, instance) in instances.iter_mut().enumerate() { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") + .get::<&str, #pk_type>(#pk); + } + + Ok(()) + } + } + } else if mssql_enabled { + quote! { + canyon_sql::crud::CanyonRows::Tiberius(mut v) => { + for (idx, instance) in instances.iter_mut().enumerate() { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") + .get::<#pk_type, &str>(#pk) + .expect("SQL Server primary key type failed to be set as value"); + } + + Ok(()) + } + } + } else { + quote! { + panic!( + "Reached a branch in the implementation of the Row Mapper macro that should never be reached.\ + This is a severe bug of Canyon-SQL. Please, open us an issue at \ + https://github.com/zerodaycode/Canyon-SQL/issues and let us know about that failure." + ) + } + }; + quote! { mapped_fields = #column_names .split(", ") @@ -290,41 +385,15 @@ pub fn generate_multiple_insert_tokens( } } - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( + let multi_insert_result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( stmt, v_arr, datasource_name - ).await; - - match result { - Ok(res) => { - match res.get_active_ds() { - canyon_sql::crud::DatabaseType::PostgreSql => { - for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = res - .postgres - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") - .get::<&str, #pk_type>(#pk); - } - - Ok(()) - }, - canyon_sql::crud::DatabaseType::SqlServer => { - for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = res - .sqlserver - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") - .get::<#pk_type, &str>(#pk) - .expect("SQL Server primary key type failed to be set as value"); - } - - Ok(()) - } - } - }, - Err(e) => Err(e) + ).await?; + + match multi_insert_result { + #match_multi_insert_rows + _ => panic!() // TODO remove when the generics will be refactored } } } else { @@ -382,16 +451,13 @@ pub fn generate_multiple_insert_tokens( } } - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( + <#ty as canyon_sql::crud::Transaction<#ty>>::query( stmt, v_arr, datasource_name - ).await; + ).await?; - match result { - Ok(res) => Ok(()), - Err(e) => Err(e) - } + Ok(()) } }; diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index 761451c1..0f70ab4d 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -16,7 +16,7 @@ pub fn generate_find_all_unchecked_tokens( let stmt = format!("SELECT * FROM {table_schema_data}"); quote! { - /// Performns a `SELECT * FROM table_name`, where `table_name` it's + /// Performs a `SELECT * FROM table_name`, where `table_name` it's /// the name of your entity but converted to the corresponding /// database convention. P.ej. PostgreSQL prefers table names declared /// with snake_case identifiers. @@ -27,7 +27,7 @@ pub fn generate_find_all_unchecked_tokens( "" ).await .unwrap() - .get_entities::<#ty>() + .into_results::<#ty>() } /// Performs a `SELECT * FROM table_name`, where `table_name` it's @@ -45,7 +45,7 @@ pub fn generate_find_all_unchecked_tokens( datasource_name ).await .unwrap() - .get_entities::<#ty>() + .into_results::<#ty>() } } } @@ -60,7 +60,7 @@ pub fn generate_find_all_tokens( let stmt = format!("SELECT * FROM {table_schema_data}"); quote! { - /// Performns a `SELECT * FROM table_name`, where `table_name` it's + /// Performs a `SELECT * FROM table_name`, where `table_name` it's /// the name of your entity but converted to the corresponding /// database convention. P.ej. PostgreSQL prefers table names declared /// with snake_case identifiers. @@ -73,11 +73,11 @@ pub fn generate_find_all_tokens( &[], "" ).await? - .get_entities::<#ty>() + .into_results::<#ty>() ) } - /// Performns a `SELECT * FROM table_name`, where `table_name` it's + /// Performs a `SELECT * FROM table_name`, where `table_name` it's /// the name of your entity but converted to the corresponding /// database convention. P.ej. PostgreSQL prefers table names declared /// with snake_case identifiers. @@ -98,7 +98,7 @@ pub fn generate_find_all_tokens( &[], datasource_name ).await? - .get_entities::<#ty>() + .into_results::<#ty>() ) } } @@ -150,25 +150,46 @@ pub fn generate_count_tokens( let ty_str = &ty.to_string(); let stmt = format!("SELECT COUNT (*) FROM {table_schema_data}"); - let result_handling = quote! { - match count.get_active_ds() { - canyon_sql::crud::DatabaseType::PostgreSql => { - Ok( - count.postgres.get(0) - .expect(&format!("Count operation failed for {:?}", #ty_str)) - .get::<&str, i64>("count") - .to_owned() - ) - }, - canyon_sql::crud::DatabaseType::SqlServer => { - Ok( - count.sqlserver.get(0) - .expect(&format!("Count operation failed for {:?}", #ty_str)) - .get::(0) - .expect(&format!("SQL Server failed to return the count values for {:?}", #ty_str)) - .into() - ) - } + let postgres_enabled = cfg!(feature = "postgres"); + let mssql_enabled = cfg!(feature = "mssql"); + + let result_handling = if postgres_enabled && mssql_enabled { + quote! { + canyon_sql::crud::CanyonRows::Postgres(mut v) => Ok( + v.remove(0).get::<&str, i64>("count") + ), + canyon_sql::crud::CanyonRows::Tiberius(mut v) => + v.remove(0) + .get::(0) + .map(|c| c as i64) + .ok_or(format!("Failure in the COUNT query for MSSQL for: {}", #ty_str).into()) + .into(), + _ => panic!() // TODO remove when the generics will be refactored + } + } else if postgres_enabled { + quote! { + canyon_sql::crud::CanyonRows::Postgres(mut v) => Ok( + v.remove(0).get::<&str, i64>("count") + ), + _ => panic!() // TODO remove when the generics will be refactored + } + } else if mssql_enabled { + quote! { + canyon_sql::crud::CanyonRows::Tiberius(mut v) => + v.remove(0) + .get::(0) + .map(|c| c as i64) + .ok_or(format!("Failure in the COUNT query for MSSQL for: {}", #ty_str).into()) + .into(), + _ => panic!() // TODO remove when the generics will be refactored + } + } else { + quote! { + panic!( + "Reached a branch in the implementation of the Row Mapper macro that should never be reached.\ + This is a severe bug of Canyon-SQL. Please, open us an issue at \ + https://github.com/zerodaycode/Canyon-SQL/issues and let us know about that failure." + ) } }; @@ -182,7 +203,9 @@ pub fn generate_count_tokens( "" ).await?; - #result_handling + match count { + #result_handling + } } /// Performs a COUNT(*) query over some table, returning a [`Result`] rather than panicking, @@ -194,7 +217,9 @@ pub fn generate_count_tokens( datasource_name ).await?; - #result_handling + match count { + #result_handling + } } } } @@ -242,9 +267,9 @@ pub fn generate_find_by_pk_tokens( let result_handling = quote! { match result { - n if n.number_of_results() == 0 => Ok(None), + n if n.len() == 0 => Ok(None), _ => Ok( - Some(result.get_entities::<#ty>().remove(0)) + Some(result.into_results::<#ty>().remove(0)) ) } }; @@ -347,9 +372,9 @@ pub fn generate_find_by_foreign_key_tokens( ); let result_handler = quote! { match result { - n if n.number_of_results() == 0 => Ok(None), + n if n.len() == 0 => Ok(None), _ => Ok(Some( - result.get_entities::<#fk_ty>().remove(0) + result.into_results::<#fk_ty>().remove(0) )) } }; @@ -434,8 +459,8 @@ pub fn generate_find_by_reverse_foreign_key_tokens( #quoted_method_signature { let lookage_value = value.get_fk_column(#column) - .expect(format!( - "Column: {:?} not found in type: {:?}", #column, #table + .expect(format!( + "Column: {:?} not found in type: {:?}", #column, #table ).as_str()); let stmt = format!( @@ -448,8 +473,7 @@ pub fn generate_find_by_reverse_foreign_key_tokens( stmt, &[lookage_value], "" - ).await? - .get_entities::<#ty>()) + ).await?.into_results::<#ty>()) } }, )); @@ -477,8 +501,7 @@ pub fn generate_find_by_reverse_foreign_key_tokens( stmt, &[lookage_value], datasource_name - ).await? - .get_entities::<#ty>()) + ).await?.into_results::<#ty>()) } }, )); diff --git a/canyon_observer/Cargo.toml b/canyon_observer/Cargo.toml index cb4bd353..0f939b2c 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_observer/Cargo.toml @@ -1,27 +1,29 @@ [package] name = "canyon_observer" -version = "0.2.0" -edition = "2021" -documentation = "https://zerodaycode.github.io/canyon-book/" -homepage = "https://github.com/zerodaycode/Canyon-SQL" -readme = "../README.md" -license = "MIT" -description = "A Rust ORM and QueryBuilder" +version.workspace = true +edition.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true [dependencies] -tokio = { version = "1.9.0", features = ["full"] } -tokio-postgres = { version = "0.7.2" , features=["with-chrono-0_4"] } -async-trait = { version = "0.1.50" } -regex = "1.5" -walkdir = "2" +canyon_crud = { workspace = true } +canyon_connection = { workspace = true } +tokio = { workspace = true } +tokio-postgres = { workspace = true, optional = true } +tiberius = { workspace = true, optional = true } +async-trait = { workspace = true } -proc-macro2 = "1.0.27" -syn = { version = "1.0.86", features = ["full", "parsing"] } -quote = "1.0.9" - -# Debug +regex = { workspace = true } +walkdir = { workspace = true } partialdebug = "0.2.0" +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { version = "1.0.86", features = ["full", "parsing"] } # TODO Pending to refactor and upgrade -# Internal dependencies -canyon_crud = { version = "0.2.0", path = "../canyon_crud" } -canyon_connection = { version = "0.2.0", path = "../canyon_connection" } +[features] +postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres"] +mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql"] diff --git a/canyon_observer/src/constants.rs b/canyon_observer/src/constants.rs index c9db74e8..3928da4f 100644 --- a/canyon_observer/src/constants.rs +++ b/canyon_observer/src/constants.rs @@ -1,5 +1,6 @@ pub const NUMERIC_PK_DATATYPE: [&str; 6] = ["i16", "u16", "i32", "u32", "i64", "u64"]; +#[cfg(feature = "postgres")] pub mod postgresql_queries { pub static CANYON_MEMORY_TABLE: &str = "CREATE TABLE IF NOT EXISTS canyon_memory ( id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, @@ -35,6 +36,7 @@ pub mod postgresql_queries { table_schema = 'public';"; } +#[cfg(feature = "mssql")] pub mod mssql_queries { pub static CANYON_MEMORY_TABLE: &str = "IF OBJECT_ID(N'[dbo].[canyon_memory]', N'U') IS NULL BEGIN @@ -142,7 +144,7 @@ pub mod rust_type { pub const OPT_NAIVE_DATE_TIME: &str = "Option"; } -/// TODO +#[cfg(feature = "postgres")] pub mod postgresql_type { pub const INT_8: &str = "int8"; pub const SMALL_INT: &str = "smallint"; @@ -155,6 +157,7 @@ pub mod postgresql_type { pub const DATETIME: &str = "timestamp without time zone"; } +#[cfg(feature = "mssql")] pub mod sqlserver_type { pub const TINY_INT: &str = "TINY INT"; pub const SMALL_INT: &str = "SMALL INT"; diff --git a/canyon_observer/src/lib.rs b/canyon_observer/src/lib.rs index 1a0766e5..41e0dd42 100644 --- a/canyon_observer/src/lib.rs +++ b/canyon_observer/src/lib.rs @@ -11,6 +11,7 @@ /// in order to perform the migrations pub mod migrations; +extern crate canyon_connection; extern crate canyon_crud; mod constants; diff --git a/canyon_observer/src/manager/entity.rs b/canyon_observer/src/manager/entity.rs index 78e2f157..7aaeb38e 100644 --- a/canyon_observer/src/manager/entity.rs +++ b/canyon_observer/src/manager/entity.rs @@ -71,7 +71,7 @@ impl CanyonEntity { /// Generates an implementation of the match pattern to find whatever variant /// is being requested when the method `.field_name_as_str(self)` it's invoked over some - /// instance that implements the `canyon_sql::crud::bounds::FieldIdentifier` trait + /// instance that implements the `canyon_sql_root::crud::bounds::FieldIdentifier` trait pub fn create_match_arm_for_get_variant_as_string( &self, enum_name: &Ident, @@ -91,7 +91,7 @@ impl CanyonEntity { /// Generates an implementation of the match pattern to find whatever variant /// is being requested when the method `.value()` it's invoked over some - /// instance that implements the `canyon_sql::crud::bounds::FieldValueIdentifier` trait + /// instance that implements the `canyon_sql_root::crud::bounds::FieldValueIdentifier` trait pub fn create_match_arm_for_relate_fields_with_values( &self, enum_name: &Ident, diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_observer/src/migrations/handler.rs index d454128a..9ce3c4e8 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_observer/src/migrations/handler.rs @@ -1,11 +1,11 @@ use canyon_connection::{datasources::Migrations as MigrationsStatus, DATASOURCES}; +use canyon_crud::rows::CanyonRows; use partialdebug::placeholder::PartialDebug; use crate::{ canyon_crud::{ bounds::{Column, Row, RowOperations}, crud::Transaction, - result::DatabaseResult, DatabaseType, }, constants, @@ -53,7 +53,8 @@ impl Migrations { // Tracked entities that must be migrated whenever Canyon starts let schema_status = Self::fetch_database(&datasource.name, datasource.get_db_type()).await; - let database_tables_schema_info = Self::map_rows(schema_status); + let database_tables_schema_info = + Self::map_rows(schema_status, datasource.get_db_type()); // We filter the tables from the schema that aren't Canyon entities let mut user_database_tables = vec![]; @@ -87,9 +88,11 @@ impl Migrations { async fn fetch_database( datasource_name: &str, db_type: DatabaseType, - ) -> DatabaseResult { + ) -> CanyonRows { let query = match db_type { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => constants::postgresql_queries::FETCH_PUBLIC_SCHEMA, + #[cfg(feature = "mssql")] DatabaseType::SqlServer => constants::mssql_queries::FETCH_PUBLIC_SCHEMA, }; @@ -105,34 +108,14 @@ impl Migrations { /// Handler for parse the result of query the information of some database schema, /// and extract the content of the returned rows into custom structures with /// the data well organized for every entity present on that schema - fn map_rows(db_results: DatabaseResult) -> Vec { - let mut schema_info: Vec = Vec::new(); - - for res_row in db_results.as_canyon_rows().into_iter() { - let unique_table = schema_info - .iter_mut() - .find(|table| table.table_name == *res_row.get::<&str>("table_name").to_owned()); - match unique_table { - Some(table) => { - /* If a table entity it's already present on the collection, we add it - the founded columns related to the table */ - Self::get_columns_metadata(res_row, table); - } - None => { - /* If there's no table for a given "table_name" property on the - collection yet, we must create a new instance and attach it - the founded columns data in this iteration */ - let mut new_table = TableMetadata { - table_name: res_row.get::<&str>("table_name").to_owned(), - columns: Vec::new(), - }; - Self::get_columns_metadata(res_row, &mut new_table); - schema_info.push(new_table); - } - }; + fn map_rows(db_results: CanyonRows, db_type: DatabaseType) -> Vec { + match db_results { + #[cfg(feature = "postgres")] + CanyonRows::Postgres(v) => Self::process_tp_rows(v, db_type), + #[cfg(feature = "mssql")] + CanyonRows::Tiberius(v) => Self::process_tib_rows(v, db_type), + _ => panic!(), } - - schema_info } /// Parses all the [`Row`] after query the information of the targeted schema, @@ -218,4 +201,95 @@ impl Migrations { } }; } + + #[cfg(feature = "postgres")] + fn process_tp_rows( + db_results: Vec, + db_type: DatabaseType, + ) -> Vec { + let mut schema_info: Vec = Vec::new(); + for res_row in db_results.iter() { + let unique_table = schema_info + .iter_mut() + .find(|table| check_for_table_name(table, db_type, res_row as &dyn Row)); + match unique_table { + Some(table) => { + /* If a table entity it's already present on the collection, we add it + the founded columns related to the table */ + Self::get_columns_metadata(res_row as &dyn Row, table); + } + None => { + /* If there's no table for a given "table_name" property on the + collection yet, we must create a new instance and attach it + the founded columns data in this iteration */ + let mut new_table = TableMetadata { + table_name: get_table_name_from_tp_row(res_row), + columns: Vec::new(), + }; + Self::get_columns_metadata(res_row as &dyn Row, &mut new_table); + schema_info.push(new_table); + } + }; + } + + schema_info + } + + #[cfg(feature = "mssql")] + fn process_tib_rows( + db_results: Vec, + db_type: DatabaseType, + ) -> Vec { + let mut schema_info: Vec = Vec::new(); + for res_row in db_results.iter() { + let unique_table = schema_info + .iter_mut() + .find(|table| check_for_table_name(table, db_type, res_row as &dyn Row)); + match unique_table { + Some(table) => { + /* If a table entity it's already present on the collection, we add it + the founded columns related to the table */ + Self::get_columns_metadata(res_row as &dyn Row, table); + } + None => { + /* If there's no table for a given "table_name" property on the + collection yet, we must create a new instance and attach it + the founded columns data in this iteration */ + let mut new_table = TableMetadata { + table_name: get_table_name_from_tib_row(res_row), + columns: Vec::new(), + }; + Self::get_columns_metadata(res_row as &dyn Row, &mut new_table); + schema_info.push(new_table); + } + }; + } + + schema_info + } +} + +#[cfg(feature = "postgres")] +fn get_table_name_from_tp_row(res_row: &tokio_postgres::Row) -> String { + res_row.get::<&str, String>("table_name") +} +#[cfg(feature = "mssql")] +fn get_table_name_from_tib_row(res_row: &tiberius::Row) -> String { + res_row + .get::<&str, &str>("table_name") + .unwrap_or_default() + .to_string() +} + +fn check_for_table_name( + table: &&mut TableMetadata, + db_type: DatabaseType, + res_row: &dyn Row, +) -> bool { + match db_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => table.table_name == res_row.get_postgres::<&str>("table_name"), + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => table.table_name == res_row.get_mssql::<&str>("table_name"), + } } diff --git a/canyon_observer/src/migrations/information_schema.rs b/canyon_observer/src/migrations/information_schema.rs index bdf9f48e..74709619 100644 --- a/canyon_observer/src/migrations/information_schema.rs +++ b/canyon_observer/src/migrations/information_schema.rs @@ -1,4 +1,7 @@ -use canyon_connection::{tiberius::ColumnType as TIB_TY, tokio_postgres::types::Type as TP_TYP}; +#[cfg(feature = "mssql")] +use canyon_connection::tiberius::ColumnType as TIB_TY; +#[cfg(feature = "postgres")] +use canyon_connection::tokio_postgres::types::Type as TP_TYP; use canyon_crud::bounds::{Column, ColumnType, Row, RowOperations}; /// Model that represents the database entities that belongs to the current schema. @@ -40,21 +43,27 @@ impl ColumnMetadataTypeValue { /// Retrieves the value stored in a [`Column`] for a passed [`Row`] pub fn get_value(row: &dyn Row, col: &Column) -> Self { match col.column_type() { + #[cfg(feature = "postgres")] ColumnType::Postgres(v) => { match *v { - TP_TYP::NAME | TP_TYP::VARCHAR | TP_TYP::TEXT => { - Self::StringValue(row.get_opt::<&str>(col.name()).map(|opt| opt.to_owned())) - } - TP_TYP::INT4 => Self::IntValue(row.get_opt::(col.name())), + TP_TYP::NAME | TP_TYP::VARCHAR | TP_TYP::TEXT => Self::StringValue( + row.get_postgres_opt::<&str>(col.name()) + .map(|opt| opt.to_owned()), + ), + TP_TYP::INT4 => Self::IntValue(row.get_postgres_opt::(col.name())), _ => Self::NoneValue, // TODO watchout this one } } + #[cfg(feature = "mssql")] ColumnType::SqlServer(v) => match v { TIB_TY::NChar | TIB_TY::NVarchar | TIB_TY::BigChar | TIB_TY::BigVarChar => { - Self::StringValue(row.get_opt::<&str>(col.name()).map(|opt| opt.to_owned())) + Self::StringValue( + row.get_mssql_opt::<&str>(col.name()) + .map(|opt| opt.to_owned()), + ) } TIB_TY::Int2 | TIB_TY::Int4 | TIB_TY::Int8 | TIB_TY::Intn => { - Self::IntValue(row.get_opt::(col.name())) + Self::IntValue(row.get_mssql_opt::(col.name())) } _ => Self::NoneValue, }, diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_observer/src/migrations/memory.rs index 0a4080c0..18f6eb31 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_observer/src/migrations/memory.rs @@ -1,5 +1,5 @@ use crate::constants; -use canyon_crud::{bounds::RowOperations, crud::Transaction, DatabaseType, DatasourceConfig}; +use canyon_crud::{crud::Transaction, DatabaseType, DatasourceConfig}; use regex::Regex; use std::collections::HashMap; use std::fs; @@ -70,21 +70,47 @@ impl CanyonMemory { let res = Self::query("SELECT * FROM canyon_memory", [], &datasource.name) .await .expect("Error querying Canyon Memory"); - let mem_results = res.as_canyon_rows(); // Manually maps the results let mut db_rows = Vec::new(); - for row in mem_results.iter() { - let db_row = CanyonMemoryRow { - id: row.get::("id"), - filepath: row.get::<&str>("filepath"), - struct_name: row.get::<&str>("struct_name"), - declared_table_name: row.get::<&str>("declared_table_name"), - }; - db_rows.push(db_row); + #[cfg(feature = "postgres")] + { + let mem_results: &Vec = res.get_postgres_rows(); + for row in mem_results { + let db_row = CanyonMemoryRow { + id: row.get::<&str, i32>("id"), + filepath: row.get::<&str, String>("filepath"), + struct_name: row.get::<&str, String>("struct_name").to_owned(), + declared_table_name: row.get::<&str, String>("declared_table_name").to_owned(), + }; + db_rows.push(db_row); + } + } + #[cfg(feature = "mssql")] + { + let mem_results: &Vec = res.get_tiberius_rows(); + for row in mem_results { + let db_row = CanyonMemoryRow { + id: row.get::("id").unwrap(), + filepath: row.get::<&str, &str>("filepath").unwrap().to_string(), + struct_name: row.get::<&str, &str>("struct_name").unwrap().to_string(), + declared_table_name: row + .get::<&str, &str>("declared_table_name") + .unwrap() + .to_string(), + }; + db_rows.push(db_row); + } } - // Parses the source code files looking for the #[canyon_entity] annotated classes + Self::populate_memory(datasource, canyon_entities, db_rows).await + } + + async fn populate_memory( + datasource: &DatasourceConfig, + canyon_entities: &[CanyonRegisterEntity<'_>], + db_rows: Vec, + ) -> CanyonMemory { let mut mem = Self { memory: Vec::new(), renamed_entities: HashMap::new(), @@ -106,7 +132,7 @@ impl CanyonMemory { && old.struct_name == _struct.struct_name && old.declared_table_name == _struct.declared_table_name) { - updates.push(old.struct_name); + updates.push(&old.struct_name); let stmt = format!( "UPDATE canyon_memory SET filepath = '{}', struct_name = '{}', declared_table_name = '{}' \ WHERE id = {}", @@ -137,12 +163,12 @@ impl CanyonMemory { } // Deletes the records from canyon_memory, because they stopped to be tracked by Canyon - for db_row in db_rows.into_iter() { + for db_row in db_rows.iter() { if !mem .memory .iter() .any(|entity| entity.struct_name == db_row.struct_name) - && !updates.contains(&db_row.struct_name) + && !updates.contains(&&(db_row.struct_name)) { save_canyon_memory_query( format!( @@ -216,12 +242,12 @@ impl CanyonMemory { } /// Generates, if not exists the `canyon_memory` table - #[cfg(not(cargo_check))] async fn create_memory(datasource_name: &str, database_type: &DatabaseType) { - let query = if database_type == &DatabaseType::PostgreSql { - constants::postgresql_queries::CANYON_MEMORY_TABLE - } else { - constants::mssql_queries::CANYON_MEMORY_TABLE + let query = match database_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => constants::postgresql_queries::CANYON_MEMORY_TABLE, + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => constants::mssql_queries::CANYON_MEMORY_TABLE, }; Self::query(query, [], datasource_name) @@ -250,11 +276,11 @@ fn save_canyon_memory_query(stmt: String, ds_name: &str) { /// Represents a single row from the `canyon_memory` table #[derive(Debug)] -struct CanyonMemoryRow<'a> { +struct CanyonMemoryRow { id: i32, - filepath: &'a str, - struct_name: &'a str, - declared_table_name: &'a str, + filepath: String, + struct_name: String, + declared_table_name: String, } /// Represents the data that will be serialized in the `canyon_memory` table diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_observer/src/migrations/processor.rs index c3995bbf..b096b828 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_observer/src/migrations/processor.rs @@ -169,7 +169,7 @@ impl MigrationsProcessor { entity_name: &'a str, entity_fields: Vec, current_table_metadata: Option<&'a TableMetadata>, - db_type: DatabaseType, + _db_type: DatabaseType, ) { if current_table_metadata.is_none() { return; @@ -188,12 +188,15 @@ impl MigrationsProcessor { .collect(); for column_metadata in columns_name_to_delete { - if db_type == DatabaseType::SqlServer && !column_metadata.is_nullable { - self.drop_column_not_null( - entity_name, - column_metadata.column_name.clone(), - MigrationsHelper::get_datatype_from_column_metadata(column_metadata), - ) + #[cfg(feature = "mssql")] + { + if _db_type == DatabaseType::SqlServer && !column_metadata.is_nullable { + self.drop_column_not_null( + entity_name, + column_metadata.column_name.clone(), + MigrationsHelper::get_datatype_from_column_metadata(column_metadata), + ) + } } self.delete_column(entity_name, column_metadata.column_name.clone()); } @@ -243,6 +246,7 @@ impl MigrationsProcessor { ))); } + #[cfg(feature = "mssql")] fn drop_column_not_null( &mut self, table_name: &str, @@ -314,8 +318,11 @@ impl MigrationsProcessor { if attr.starts_with("Annotation: PrimaryKey") { Self::add_primary_key(self, entity_name, canyon_register_entity_field.clone()); - if canyon_register_entity_field.is_autoincremental() { - Self::add_identity(self, entity_name, canyon_register_entity_field.clone()); + #[cfg(feature = "postgres")] + { + if canyon_register_entity_field.is_autoincremental() { + Self::add_identity(self, entity_name, canyon_register_entity_field.clone()); + } } } } @@ -351,6 +358,7 @@ impl MigrationsProcessor { ))); } + #[cfg(feature = "postgres")] fn add_identity(&mut self, entity_name: &str, field: CanyonRegisterEntityField) { self.constraints_operations .push(Box::new(ColumnOperation::AlterColumnAddIdentity( @@ -386,19 +394,24 @@ impl MigrationsProcessor { if field_is_primary_key && current_column_metadata.primary_key_info.is_none() { Self::add_primary_key(self, entity_name, canyon_register_entity_field.clone()); - if canyon_register_entity_field.is_autoincremental() { - Self::add_identity(self, entity_name, canyon_register_entity_field.clone()); + #[cfg(feature = "postgres")] + { + if canyon_register_entity_field.is_autoincremental() { + Self::add_identity(self, entity_name, canyon_register_entity_field.clone()); + } } } // Case when the field contains a primary key annotation, and it's already on the database else if field_is_primary_key && current_column_metadata.primary_key_info.is_some() { - let is_autoincr_rust = canyon_register_entity_field.is_autoincremental(); - let is_autoincr_in_db = current_column_metadata.is_identity; - - if !is_autoincr_rust && is_autoincr_in_db { - Self::drop_identity(self, entity_name, canyon_register_entity_field.clone()) - } else if is_autoincr_rust && !is_autoincr_in_db { - Self::add_identity(self, entity_name, canyon_register_entity_field.clone()) + #[cfg(feature = "postgres")] + { + let is_autoincr_rust = canyon_register_entity_field.is_autoincremental(); + let is_autoincr_in_db = current_column_metadata.is_identity; + if !is_autoincr_rust && is_autoincr_in_db { + Self::drop_identity(self, entity_name, canyon_register_entity_field.clone()) + } else if is_autoincr_rust && !is_autoincr_in_db { + Self::add_identity(self, entity_name, canyon_register_entity_field.clone()) + } } } // Case when field doesn't contains a primary key annotation, but there is one in the database column @@ -413,8 +426,11 @@ impl MigrationsProcessor { .to_string(), ); - if current_column_metadata.is_identity { - Self::drop_identity(self, entity_name, canyon_register_entity_field.clone()); + #[cfg(feature = "postgres")] + { + if current_column_metadata.is_identity { + Self::drop_identity(self, entity_name, canyon_register_entity_field.clone()); + } } } @@ -527,6 +543,7 @@ impl MigrationsProcessor { ))); } + #[cfg(feature = "postgres")] fn drop_identity( &mut self, entity_name: &str, @@ -619,6 +636,7 @@ impl MigrationsHelper { } } + #[cfg(feature = "mssql")] fn get_datatype_from_column_metadata(current_column_metadata: &ColumnMetadata) -> String { // TODO Add all SQL Server text datatypes if vec!["nvarchar", "varchar"] @@ -640,20 +658,27 @@ impl MigrationsHelper { canyon_register_entity_field: &CanyonRegisterEntityField, current_column_metadata: &ColumnMetadata, ) -> bool { - if db_type == DatabaseType::PostgreSql { - canyon_register_entity_field - .to_postgres_alter_syntax() - .to_lowercase() - == current_column_metadata.datatype - } else if db_type == DatabaseType::SqlServer { - // TODO Search a better way to get the datatype without useless info (like "VARCHAR(MAX)") - canyon_register_entity_field - .to_sqlserver_alter_syntax() - .to_lowercase() - == current_column_metadata.datatype - } else { - todo!() + #[cfg(feature = "postgres")] + { + if db_type == DatabaseType::PostgreSql { + return canyon_register_entity_field + .to_postgres_alter_syntax() + .to_lowercase() + == current_column_metadata.datatype; + } + } + #[cfg(feature = "mssql")] + { + if db_type == DatabaseType::SqlServer { + // TODO Search a better way to get the datatype without useless info (like "VARCHAR(MAX)") + return canyon_register_entity_field + .to_sqlserver_alter_syntax() + .to_lowercase() + == current_column_metadata.datatype; + } } + + false } fn extract_foreign_key_annotation(field_annotations: &[String]) -> (String, String) { @@ -752,112 +777,110 @@ impl DatabaseOperation for TableOperation { let stmt = match self { TableOperation::CreateTable(table_name, table_fields) => { - if db_type == DatabaseType::PostgreSql { - format!( - "CREATE TABLE \"{table_name}\" ({});", - table_fields - .iter() - .map(|entity_field| format!( - "\"{}\" {}", - entity_field.field_name, - entity_field.to_postgres_syntax() - )) - .collect::>() - .join(", ") - ) - } else if db_type == DatabaseType::SqlServer { - format!( - "CREATE TABLE {:?} ({:?});", - table_name, - table_fields - .iter() - .map(|entity_field| format!( - "{} {}", - entity_field.field_name, - entity_field.to_sqlserver_syntax() - )) - .collect::>() - .join(", ") - ) - .replace('"', "") - } else { - todo!("There's no other databases supported in Canyon-SQL right now") + match db_type { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => { + format!( + "CREATE TABLE \"{table_name}\" ({});", + table_fields + .iter() + .map(|entity_field| format!( + "\"{}\" {}", + entity_field.field_name, + entity_field.to_postgres_syntax() + )) + .collect::>() + .join(", ") + ) + } + #[cfg(feature = "mssql")] DatabaseType::SqlServer => { + format!( + "CREATE TABLE {:?} ({:?});", + table_name, + table_fields + .iter() + .map(|entity_field| format!( + "{} {}", + entity_field.field_name, + entity_field.to_sqlserver_syntax() + )) + .collect::>() + .join(", ") + ) + .replace('"', "") + } } } TableOperation::AlterTableName(old_table_name, new_table_name) => { - if db_type == DatabaseType::PostgreSql { - format!("ALTER TABLE {old_table_name} RENAME TO {new_table_name};") - } else if db_type == DatabaseType::SqlServer { - /* - Notes: Brackets around `old_table_name`, p.e. - exec sp_rename ['league'], 'leagues' // NOT VALID! - is only allowed for compound names split by a dot. - exec sp_rename ['random.league'], 'leagues' // OK - - CARE! This doesn't mean that we are including the schema. - exec sp_rename ['dbo.random.league'], 'leagues' // OK - exec sp_rename 'dbo.league', 'leagues' // OK - Schema doesn't need brackets - - Due to the automatic mapped name from Rust to DB and vice-versa, this won't - be an allowed behaviour for now, only with the table_name parameter on the - CanyonEntity annotation. - */ - format!("exec sp_rename '{old_table_name}', '{new_table_name}';") - } else { - todo!() + match db_type { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => + format!("ALTER TABLE {old_table_name} RENAME TO {new_table_name};"), + #[cfg(feature = "mssql")] DatabaseType::SqlServer => + /* + Notes: Brackets around `old_table_name`, p.e. + exec sp_rename ['league'], 'leagues' // NOT VALID! + is only allowed for compound names split by a dot. + exec sp_rename ['random.league'], 'leagues' // OK + + CARE! This doesn't mean that we are including the schema. + exec sp_rename ['dbo.random.league'], 'leagues' // OK + exec sp_rename 'dbo.league', 'leagues' // OK - Schema doesn't need brackets + + Due to the automatic mapped name from Rust to DB and vice-versa, this won't + be an allowed behaviour for now, only with the table_name parameter on the + CanyonEntity annotation. + */ + format!("exec sp_rename '{old_table_name}', '{new_table_name}';") } } TableOperation::AddTableForeignKey( - table_name, - foreign_key_name, - column_foreign_key, - table_to_reference, - column_to_reference, + _table_name, + _foreign_key_name, + _column_foreign_key, + _table_to_reference, + _column_to_reference, ) => { - if db_type == DatabaseType::PostgreSql { - format!( - "ALTER TABLE {table_name} ADD CONSTRAINT {foreign_key_name} \ - FOREIGN KEY ({column_foreign_key}) REFERENCES {table_to_reference} ({column_to_reference});" - ) - } else if db_type == DatabaseType::SqlServer { - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } else { - todo!() + match db_type { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => + format!( + "ALTER TABLE {_table_name} ADD CONSTRAINT {_foreign_key_name} \ + FOREIGN KEY ({_column_foreign_key}) REFERENCES {_table_to_reference} ({_column_to_reference});" + ), + #[cfg(feature = "mssql")] DatabaseType::SqlServer => + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } - TableOperation::DeleteTableForeignKey(table_with_foreign_key, constraint_name) => { - if db_type == DatabaseType::PostgreSql { - format!( - "ALTER TABLE {table_with_foreign_key} DROP CONSTRAINT {constraint_name};", - ) - } else if db_type == DatabaseType::SqlServer { - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } else { - todo!() + TableOperation::DeleteTableForeignKey(_table_with_foreign_key, _constraint_name) => { + match db_type { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => + format!( + "ALTER TABLE {_table_with_foreign_key} DROP CONSTRAINT {_constraint_name};", + ), + #[cfg(feature = "mssql")] DatabaseType::SqlServer => + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } - TableOperation::AddTablePrimaryKey(table_name, entity_field) => { - if db_type == DatabaseType::PostgreSql { - format!( - "ALTER TABLE \"{table_name}\" ADD PRIMARY KEY (\"{}\");", - entity_field.field_name - ) - } else if db_type == DatabaseType::SqlServer { - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } else { - todo!() + TableOperation::AddTablePrimaryKey(_table_name, _entity_field) => { + match db_type { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => + format!( + "ALTER TABLE \"{_table_name}\" ADD PRIMARY KEY (\"{}\");", + _entity_field.field_name + ), + #[cfg(feature = "mssql")] DatabaseType::SqlServer => + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } } TableOperation::DeleteTablePrimaryKey(table_name, primary_key_name) => { - if db_type == DatabaseType::PostgreSql || db_type == DatabaseType::SqlServer { - format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;") - } else { - todo!() + match db_type { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => + format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;"), + #[cfg(feature = "mssql")] DatabaseType::SqlServer => + format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;") } } }; @@ -875,11 +898,14 @@ enum ColumnOperation { // AlterColumnName, AlterColumnType(String, CanyonRegisterEntityField), AlterColumnDropNotNull(String, CanyonRegisterEntityField), + AlterColumnSetNotNull(String, CanyonRegisterEntityField), + + #[cfg(feature = "mssql")] // SQL server specific operation - SQL server can't drop a NOT NULL column DropNotNullBeforeDropColumn(String, String, String), - AlterColumnSetNotNull(String, CanyonRegisterEntityField), - // TODO if implement through annotations, modify for both GENERATED {ALWAYS, BY DEFAULT} + #[cfg(feature = "postgres")] AlterColumnAddIdentity(String, CanyonRegisterEntityField), + #[cfg(feature = "postgres")] AlterColumnDropIdentity(String, CanyonRegisterEntityField), } @@ -892,51 +918,47 @@ impl DatabaseOperation for ColumnOperation { let stmt = match self { ColumnOperation::CreateColumn(table_name, entity_field) => - if db_type == DatabaseType::PostgreSql { - format!( - "ALTER TABLE \"{}\" ADD COLUMN \"{}\" {};", - table_name, - entity_field.field_name, - entity_field.to_postgres_syntax()) - } else if db_type == DatabaseType::SqlServer { - format!( - "ALTER TABLE {} ADD \"{}\" {};", - table_name, - entity_field.field_name, - entity_field.to_sqlserver_syntax() - ) - } else { - todo!() - }, + match db_type { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => + format!( + "ALTER TABLE \"{}\" ADD COLUMN \"{}\" {};", + table_name, + entity_field.field_name, + entity_field.to_postgres_syntax() + ), + #[cfg(feature = "mssql")] DatabaseType::SqlServer => + format!( + "ALTER TABLE {} ADD \"{}\" {};", + table_name, + entity_field.field_name, + entity_field.to_sqlserver_syntax() + ) + } ColumnOperation::DeleteColumn(table_name, column_name) => { // TODO Check if operation for SQL server is different format!("ALTER TABLE \"{table_name}\" DROP COLUMN \"{column_name}\";") }, - ColumnOperation::AlterColumnType(table_name, entity_field) => - if db_type == DatabaseType::PostgreSql { - format!( - "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" TYPE {};", - entity_field.field_name, entity_field.to_postgres_alter_syntax() - ) - } else if db_type == DatabaseType::SqlServer { - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } else { - todo!() - } - , + ColumnOperation::AlterColumnType(_table_name, _entity_field) => + match db_type { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => + format!( + "ALTER TABLE \"{_table_name}\" ALTER COLUMN \"{}\" TYPE {};", + _entity_field.field_name, _entity_field.to_postgres_alter_syntax() + ), + #[cfg(feature = "mssql")] DatabaseType::SqlServer => + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") + } ColumnOperation::AlterColumnDropNotNull(table_name, entity_field) => - if db_type == DatabaseType::PostgreSql { - format!("ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" DROP NOT NULL;", entity_field.field_name) - } else if db_type == DatabaseType::SqlServer { - format!( - "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NULL", - entity_field.field_name, entity_field.to_sqlserver_alter_syntax() - ) - } else { - todo!() - } - - ColumnOperation::DropNotNullBeforeDropColumn(table_name, column_name, column_datatype) => + match db_type { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => + format!("ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" DROP NOT NULL;", entity_field.field_name), + #[cfg(feature = "mssql")] DatabaseType::SqlServer => + format!( + "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NULL", + entity_field.field_name, entity_field.to_sqlserver_alter_syntax() + ) + } + #[cfg(feature = "mssql")] ColumnOperation::DropNotNullBeforeDropColumn(table_name, column_name, column_datatype) => format!( "ALTER TABLE {table_name} ALTER COLUMN {column_name} {column_datatype} NULL; DECLARE @tableName VARCHAR(MAX) = '{table_name}' DECLARE @columnName VARCHAR(MAX) = '{column_name}' @@ -951,15 +973,24 @@ impl DatabaseOperation for ColumnOperation { EXEC('ALTER TABLE '+@tableName+' DROP CONSTRAINT ' + @ConstraintName);" ), - ColumnOperation::AlterColumnSetNotNull(table_name, entity_field) => format!( - "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" SET NOT NULL;", entity_field.field_name - ), + ColumnOperation::AlterColumnSetNotNull(table_name, entity_field) => { + match db_type { + #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( + "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" SET NOT NULL;", entity_field.field_name + ), + #[cfg(feature = "mssql")] DatabaseType::SqlServer => format!( + "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NOT NULL", + entity_field.field_name, + entity_field.to_sqlserver_alter_syntax() + ) + } + } - ColumnOperation::AlterColumnAddIdentity(table_name, entity_field) => format!( + #[cfg(feature = "postgres")] ColumnOperation::AlterColumnAddIdentity(table_name, entity_field) => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" ADD GENERATED ALWAYS AS IDENTITY;", entity_field.field_name ), - ColumnOperation::AlterColumnDropIdentity(table_name, entity_field) => format!( + #[cfg(feature = "postgres")] ColumnOperation::AlterColumnDropIdentity(table_name, entity_field) => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" DROP IDENTITY;", entity_field.field_name ), }; @@ -969,34 +1000,26 @@ impl DatabaseOperation for ColumnOperation { } /// Helper for operations involving sequences +#[cfg(feature = "postgres")] #[derive(Debug)] -#[allow(dead_code)] enum SequenceOperation { ModifySequence(String, CanyonRegisterEntityField), } - +#[cfg(feature = "postgres")] impl Transaction for SequenceOperation {} +#[cfg(feature = "postgres")] #[async_trait] impl DatabaseOperation for SequenceOperation { async fn generate_sql(&self, datasource: &DatasourceConfig) { - let db_type = datasource.get_db_type(); - let stmt = match self { SequenceOperation::ModifySequence(table_name, entity_field) => { - if db_type == DatabaseType::PostgreSql { - format!( + format!( "SELECT setval(pg_get_serial_sequence('\"{table_name}\"', '{}'), max(\"{}\")) from \"{table_name}\";", entity_field.field_name, entity_field.field_name ) - } else if db_type == DatabaseType::SqlServer { - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } else { - todo!() - } } }; - save_migrations_query_to_execute(stmt, &datasource.name); } } diff --git a/canyon_observer/src/migrations/register_types.rs b/canyon_observer/src/migrations/register_types.rs index 470944db..14481c13 100644 --- a/canyon_observer/src/migrations/register_types.rs +++ b/canyon_observer/src/migrations/register_types.rs @@ -1,8 +1,10 @@ use regex::Regex; -use crate::constants::{ - postgresql_type, regex_patterns, rust_type, sqlserver_type, NUMERIC_PK_DATATYPE, -}; +#[cfg(feature = "postgres")] +use crate::constants::postgresql_type; +#[cfg(feature = "mssql")] +use crate::constants::sqlserver_type; +use crate::constants::{regex_patterns, rust_type, NUMERIC_PK_DATATYPE}; /// This file contains `Rust` types that represents an entry on the `CanyonRegister` /// where `Canyon` tracks the user types that has to manage @@ -28,6 +30,7 @@ pub struct CanyonRegisterEntityField { impl CanyonRegisterEntityField { /// Return the postgres datatype and parameters to create a column for a given rust type + #[cfg(feature = "postgres")] pub fn to_postgres_syntax(&self) -> String { let rust_type_clean = self.field_type.replace(' ', ""); @@ -74,6 +77,7 @@ impl CanyonRegisterEntityField { /// Return the postgres datatype and parameters to create a column for a given rust type /// for Microsoft SQL Server + #[cfg(feature = "mssql")] pub fn to_sqlserver_syntax(&self) -> String { let rust_type_clean = self.field_type.replace(' ', ""); @@ -120,6 +124,7 @@ impl CanyonRegisterEntityField { } } + #[cfg(feature = "postgres")] pub fn to_postgres_alter_syntax(&self) -> String { let mut rust_type_clean = self.field_type.replace(' ', ""); let rs_type_is_optional = self.field_type.to_uppercase().starts_with("OPTION"); @@ -162,6 +167,7 @@ impl CanyonRegisterEntityField { } } + #[cfg(feature = "mssql")] pub fn to_sqlserver_alter_syntax(&self) -> String { let mut rust_type_clean = self.field_type.replace(' ', ""); let rs_type_is_optional = self.field_type.to_uppercase().starts_with("OPTION"); @@ -200,50 +206,6 @@ impl CanyonRegisterEntityField { } } - /// Return the datatype and parameters to create an id column, given the corresponding "CanyonRegisterEntityField" - /// with the correct format for PostgreSQL - fn _to_postgres_id_syntax(&self) -> String { - let has_pk_annotation = self - .annotations - .iter() - .find(|a| a.starts_with("Annotation: PrimaryKey")); - - let pk_is_autoincremental = match has_pk_annotation { - Some(annotation) => annotation.contains("true"), - None => false, - }; - - let postgres_datatype_syntax = Self::to_postgres_syntax(self); - - if NUMERIC_PK_DATATYPE.contains(&self.field_type.as_str()) && pk_is_autoincremental { - format!("{postgres_datatype_syntax} PRIMARY KEY GENERATED ALWAYS AS IDENTITY") - } else { - format!("{postgres_datatype_syntax} PRIMARY KEY") - } - } - - /// Return the datatype and parameters to create an id column, given the corresponding "CanyonRegisterEntityField" - /// with the correct format for Microsoft SQL Server - fn _to_sqlserver_id_syntax(&self) -> String { - let has_pk_annotation = self - .annotations - .iter() - .find(|a| a.starts_with("Annotation: PrimaryKey")); - - let pk_is_autoincremental = match has_pk_annotation { - Some(annotation) => annotation.contains("true"), - None => false, - }; - - let sqlserver_datatype_syntax = Self::to_sqlserver_syntax(self); - - if NUMERIC_PK_DATATYPE.contains(&self.field_type.as_str()) && pk_is_autoincremental { - format!("{sqlserver_datatype_syntax} IDENTITY PRIMARY") - } else { - format!("{sqlserver_datatype_syntax} PRIMARY KEY") - } - } - /// Return if the field is autoincremental pub fn is_autoincremental(&self) -> bool { let has_pk_annotation = self diff --git a/canyon_sql/Cargo.toml b/canyon_sql/Cargo.toml deleted file mode 100755 index 0a13a101..00000000 --- a/canyon_sql/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "canyon_sql" -version = "0.2.0" -edition = "2021" -authors = ["Alex Vergara, Gonzalo Busto"] -documentation = "https://zerodaycode.github.io/canyon-book/" -homepage = "https://github.com/zerodaycode/Canyon-SQL" -readme = "../README.md" -license = "MIT" -description = "A Rust ORM and QueryBuilder" - -[dependencies] -async-trait = { version = "0.1.50" } - -# Project crates -canyon_macros = { version = "0.2.0", path = "../canyon_macros" } -canyon_observer = { version = "0.2.0", path = "../canyon_observer" } -canyon_crud = { version = "0.2.0", path = "../canyon_crud" } -canyon_connection = { version = "0.2.0", path = "../canyon_connection" } diff --git a/canyon_sql/src/lib.rs b/src/lib.rs old mode 100755 new mode 100644 similarity index 73% rename from canyon_sql/src/lib.rs rename to src/lib.rs index 330b8ed4..33a2c82b --- a/canyon_sql/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,10 @@ /// Here it's where all the available functionalities and features /// reaches the top most level, grouping them and making them visible /// through this crate, building the *public API* of the library +extern crate canyon_connection; +extern crate canyon_crud; +extern crate canyon_macros; +extern crate canyon_observer; /// Reexported elements to the root of the public API pub mod migrations { @@ -15,17 +19,27 @@ pub use canyon_macros::main; /// Public API for the `Canyon-SQL` proc-macros, and for the external ones pub mod macros { - pub use async_trait::*; + pub use canyon_crud::async_trait::*; pub use canyon_macros::*; } +/// connection module serves to reexport the public elements of the `canyon_connection` crate, +/// exposing them through the public API +pub mod connection { + #[cfg(feature = "postgres")] + pub use canyon_connection::canyon_database_connector::DatabaseConnection::Postgres; + + #[cfg(feature = "mssql")] + pub use canyon_connection::canyon_database_connector::DatabaseConnection::SqlServer; +} + /// Crud module serves to reexport the public elements of the `canyon_crud` crate, /// exposing them through the public API pub mod crud { pub use canyon_crud::bounds; pub use canyon_crud::crud::*; pub use canyon_crud::mapper::*; - pub use canyon_crud::result::*; + pub use canyon_crud::rows::CanyonRows; pub use canyon_crud::DatabaseType; } @@ -37,7 +51,9 @@ pub mod query { /// Reexport the available database clients within Canyon pub mod db_clients { + #[cfg(feature = "mssql")] pub use canyon_connection::tiberius; + #[cfg(feature = "postgres")] pub use canyon_connection::tokio_postgres; } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index f2e83953..da6b0dfc 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,12 +1,16 @@ [package] name = "tests" -version = "0.2.0" -edition = "2021" +version.workspace = true +edition.workspace = true publish = false [dev-dependencies] -canyon_sql = { path = "../canyon_sql" } +canyon_sql = { path = ".." } [[test]] name = "canyon_integration_tests" -path = "canyon_integration_tests.rs" \ No newline at end of file +path = "canyon_integration_tests.rs" + +[features] +postgres = ["canyon_sql/postgres"] +mssql = ["canyon_sql/mssql"] diff --git a/tests/canyon_integration_tests.rs b/tests/canyon_integration_tests.rs index 8120ee8f..30687987 100644 --- a/tests/canyon_integration_tests.rs +++ b/tests/canyon_integration_tests.rs @@ -1,3 +1,5 @@ +extern crate canyon_sql; + use std::error::Error; ///! Integration tests for the heart of a Canyon-SQL application, the CRUD operations. diff --git a/tests/constants.rs b/tests/constants.rs index f7804e43..1c9c8044 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -1,7 +1,11 @@ ///! Constant values to share across the integration tests + +#[cfg(feature = "postgres")] pub const PSQL_DS: &str = "postgres_docker"; +#[cfg(feature = "mssql")] pub const SQL_SERVER_DS: &str = "sqlserver_docker"; +#[cfg(feature = "postgres")] pub static FETCH_PUBLIC_SCHEMA: &str = "SELECT gi.table_name, @@ -33,6 +37,7 @@ LEFT JOIN pg_catalog.pg_constraint AS con on WHERE table_schema = 'public';"; +#[cfg(feature = "mssql")] pub const SQL_SERVER_CREATE_TABLES: &str = " IF OBJECT_ID(N'[dbo].[league]', N'U') IS NULL BEGIN @@ -87,6 +92,7 @@ BEGIN END; "; +#[cfg(feature = "mssql")] pub const SQL_SERVER_FILL_TABLE_VALUES: &str = " -- Values for league table -- Values for league table diff --git a/tests/crud/delete_operations.rs b/tests/crud/delete_operations.rs index 46d1bcaf..6420e553 100644 --- a/tests/crud/delete_operations.rs +++ b/tests/crud/delete_operations.rs @@ -2,7 +2,10 @@ ///! generates and executes *INSERT* statements use canyon_sql::crud::CrudOperations; -use crate::constants::{PSQL_DS, SQL_SERVER_DS}; +#[cfg(feature = "postgres")] +use crate::constants::PSQL_DS; +#[cfg(feature = "mssql")] +use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; /// Deletes a row from the database that is mapped into some instance of a `T` entity. @@ -14,6 +17,7 @@ use crate::tests_models::league::*; /// /// Attempt of usage the `t.delete(&self)` method on an entity without `#[primary_key]` /// will raise a runtime error. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_delete_method_operation() { // For test the delete, we will insert a new instance of the database, and then, @@ -58,6 +62,7 @@ fn test_crud_delete_method_operation() { } /// Same as the delete test, but performing the operations with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_delete_datasource_method_operation() { // For test the delete, we will insert a new instance of the database, and then, diff --git a/tests/crud/foreign_key_operations.rs b/tests/crud/foreign_key_operations.rs index b58df802..471dd639 100644 --- a/tests/crud/foreign_key_operations.rs +++ b/tests/crud/foreign_key_operations.rs @@ -10,13 +10,15 @@ ///! For more info: TODO -> Link to the docs of the foreign key chapter use canyon_sql::crud::CrudOperations; +#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; use crate::tests_models::tournament::*; /// Given an entity `T` which has some field declaring a foreign key relation -/// with some another entity `U`, for example, performns a search to find +/// with some another entity `U`, for example, performs a search to find /// what is the parent type `U` of `T` +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_search_by_foreign_key() { let some_tournament: Tournament = Tournament::find_by_pk(&1) @@ -38,6 +40,7 @@ fn test_crud_search_by_foreign_key() { } /// Same as the search by foreign key, but with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_search_by_foreign_key_datasource() { let some_tournament: Tournament = Tournament::find_by_pk_datasource(&10, SQL_SERVER_DS) @@ -67,6 +70,7 @@ fn test_crud_search_by_foreign_key_datasource() { /// to `U`. /// /// For this to work, `U`, the parent, must have derived the `ForeignKeyable` proc macro +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_search_reverse_side_foreign_key() { let some_league: League = League::find_by_pk(&1) @@ -87,6 +91,7 @@ fn test_crud_search_reverse_side_foreign_key() { /// Same as the search by the reverse side of a foreign key relation /// but with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_search_reverse_side_foreign_key_datasource() { let some_league: League = League::find_by_pk_datasource(&1, SQL_SERVER_DS) diff --git a/tests/crud/init_mssql.rs b/tests/crud/init_mssql.rs new file mode 100644 index 00000000..19b08549 --- /dev/null +++ b/tests/crud/init_mssql.rs @@ -0,0 +1,62 @@ +use crate::constants::SQL_SERVER_CREATE_TABLES; +use crate::constants::SQL_SERVER_DS; +use crate::constants::SQL_SERVER_FILL_TABLE_VALUES; +use crate::tests_models::league::League; + +use canyon_sql::crud::CrudOperations; +use canyon_sql::db_clients::tiberius::{Client, Config}; +use canyon_sql::runtime::tokio::net::TcpStream; +use canyon_sql::runtime::tokio_util::compat::TokioAsyncWriteCompatExt; + +/// In order to initialize data on `SqlServer`. we must manually insert it +/// when the docker starts. SqlServer official docker from Microsoft does +/// not allow you to run `.sql` files against the database (not at least, without) +/// using a workaround. So, we are going to query the `SqlServer` to check if already +/// has some data (other processes, persistence or multi-threading envs), af if not, +/// we are going to retrieve the inserted data on the `postgreSQL` at start-up and +/// inserting into the `SqlServer` instance. +/// +/// This will be marked as `#[ignore]`, so we can force to run first the marked as +/// ignored, check the data available, perform the necessary init operations and +/// then *cargo test * the real integration tests +#[canyon_sql::macros::canyon_tokio_test] +#[ignore] +fn initialize_sql_server_docker_instance() { + static CONN_STR: &str = + "server=tcp:localhost,1434;User Id=SA;Password=SqlServer-10;TrustServerCertificate=true"; + + canyon_sql::runtime::futures::executor::block_on(async { + let config = Config::from_ado_string(CONN_STR).unwrap(); + + let tcp = TcpStream::connect(config.get_addr()).await.unwrap(); + let tcp2 = TcpStream::connect(config.get_addr()).await.unwrap(); + tcp.set_nodelay(true).ok(); + + let mut client = Client::connect(config.clone(), tcp.compat_write()) + .await + .unwrap(); + + // Create the tables + let query_result = client.query(SQL_SERVER_CREATE_TABLES, &[]).await; + assert!(query_result.is_ok()); + + let leagues_sql = League::find_all_datasource(SQL_SERVER_DS).await; + println!("LSQL ERR: {leagues_sql:?}"); + assert!(leagues_sql.is_ok()); + + match leagues_sql { + Ok(ref leagues) => { + let leagues_len = leagues.len(); + println!("Leagues already inserted on SQLSERVER: {:?}", &leagues_len); + if leagues.len() < 10 { + let mut client2 = Client::connect(config, tcp2.compat_write()) + .await + .expect("Can't connect to MSSQL"); + let result = client2.query(SQL_SERVER_FILL_TABLE_VALUES, &[]).await; + assert!(result.is_ok()); + } + } + Err(e) => eprintln!("Error retrieving the leagues: {e}"), + } + }); +} diff --git a/tests/crud/insert_operations.rs b/tests/crud/insert_operations.rs index 29c0c9fa..d52fa868 100644 --- a/tests/crud/insert_operations.rs +++ b/tests/crud/insert_operations.rs @@ -2,6 +2,7 @@ ///! generates and executes *INSERT* statements use canyon_sql::crud::CrudOperations; +#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; @@ -25,7 +26,8 @@ use crate::tests_models::league::*; /// /// If the type hasn't a `#[primary_key]` annotation, or the annotation contains /// an argument specifying not autoincremental behaviour, all the fields will be -/// inserted on the database and no returning value will be placed in any field. +/// inserted on the database and no returning value will be placed in any field. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_insert_operation() { let mut new_league: League = League { @@ -54,6 +56,7 @@ fn test_crud_insert_operation() { /// Same as the insert operation above, but targeting the database defined in /// the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_insert_datasource_operation() { let mut new_league: League = League { @@ -93,6 +96,7 @@ fn test_crud_insert_datasource_operation() { /// /// The instances without `#[primary_key]` inserts all the values on the instaqce fields /// on the database. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_multi_insert_operation() { let mut new_league_mi: League = League { @@ -154,6 +158,7 @@ fn test_crud_multi_insert_operation() { } /// Same as the multi insert above, but with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_multi_insert_datasource_operation() { let mut new_league_mi: League = League { diff --git a/tests/crud/mod.rs b/tests/crud/mod.rs index 7526c8f6..407e727c 100644 --- a/tests/crud/mod.rs +++ b/tests/crud/mod.rs @@ -1,69 +1,10 @@ +#![allow(unused_imports)] + pub mod delete_operations; pub mod foreign_key_operations; +#[cfg(feature = "mssql")] +pub mod init_mssql; pub mod insert_operations; pub mod querybuilder_operations; pub mod select_operations; pub mod update_operations; - -use crate::constants::SQL_SERVER_CREATE_TABLES; -use crate::constants::SQL_SERVER_DS; -use crate::constants::SQL_SERVER_FILL_TABLE_VALUES; -use crate::tests_models::league::League; - -use canyon_sql::crud::CrudOperations; -use canyon_sql::db_clients::tiberius::{Client, Config}; -use canyon_sql::runtime::tokio::net::TcpStream; -use canyon_sql::runtime::tokio_util::compat::TokioAsyncWriteCompatExt; - -/// In order to initialize data on `SqlServer`. we must manually insert it -/// when the docker starts. SqlServer official docker from Microsoft does -/// not allow you to run `.sql` files against the database (not at least, without) -/// using a workaround. So, we are going to query the `SqlServer` to check if already -/// has some data (other processes, persistence or multi-threading envs), af if not, -/// we are going to retrieve the inserted data on the `postgreSQL` at start-up and -/// inserting into the `SqlServer` instance. -/// -/// This will be marked as `#[ignore]`, so we can force to run first the marked as -/// ignored, check the data available, perform the necessary init operations and -/// then *cargo test * the real integration tests -#[canyon_sql::macros::canyon_tokio_test] -#[ignore] -fn initialize_sql_server_docker_instance() { - canyon_sql::runtime::futures::executor::block_on(async { - static CONN_STR: &str = - "server=tcp:localhost,1434;User Id=SA;Password=SqlServer-10;TrustServerCertificate=true"; - - let config = Config::from_ado_string(CONN_STR).unwrap(); - - let tcp = TcpStream::connect(config.get_addr()).await.unwrap(); - let tcp2 = TcpStream::connect(config.get_addr()).await.unwrap(); - tcp.set_nodelay(true).ok(); - - let mut client = Client::connect(config.clone(), tcp.compat_write()) - .await - .unwrap(); - - // Create the tables - let query_result = client.query(SQL_SERVER_CREATE_TABLES, &[]).await; - assert!(query_result.is_ok()); - - let leagues_sql = League::find_all_datasource(SQL_SERVER_DS).await; - println!("LSQL ERR: {leagues_sql:?}"); - assert!(leagues_sql.is_ok()); - - match leagues_sql { - Ok(ref leagues) => { - let leagues_len = leagues.len(); - println!("Leagues already inserted on SQLSERVER: {:?}", &leagues_len); - if leagues.len() < 10 { - let mut client2 = Client::connect(config, tcp2.compat_write()) - .await - .expect("Can't connect to MSSQL"); - let result = client2.query(SQL_SERVER_FILL_TABLE_VALUES, &[]).await; - assert!(result.is_ok()); - } - } - Err(e) => eprintln!("Error retrieving the leagues: {e}"), - } - }); -} diff --git a/tests/crud/querybuilder_operations.rs b/tests/crud/querybuilder_operations.rs index 4700f598..1c853161 100644 --- a/tests/crud/querybuilder_operations.rs +++ b/tests/crud/querybuilder_operations.rs @@ -9,8 +9,10 @@ use canyon_sql::{ query::{operators::Comp, ops::QueryBuilder}, }; +#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; +#[cfg(feature = "mssql")] use crate::tests_models::player::*; use crate::tests_models::tournament::*; @@ -38,6 +40,7 @@ fn test_generated_sql_by_the_select_querybuilder() { /// Builds a new SQL statement for retrieves entities of the `T` type, filtered /// with the parameters that modifies the base SQL to SELECT * FROM +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_with_querybuilder() { // Find all the leagues with ID less or equals that 7 @@ -57,6 +60,7 @@ fn test_crud_find_with_querybuilder() { } /// Same than the above but with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_with_querybuilder_datasource() { // Find all the players where its ID column value is greater that 50 @@ -70,6 +74,7 @@ fn test_crud_find_with_querybuilder_datasource() { /// Updates the values of the range on entries defined by the constraint parameters /// in the database entity +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_update_with_querybuilder() { // Find all the leagues with ID less or equals that 7 @@ -82,7 +87,7 @@ fn test_crud_update_with_querybuilder() { .r#where(LeagueFieldValue::id(&1), Comp::Gt) .and(LeagueFieldValue::id(&8), Comp::Lt); - /* Family of QueryBuilders are clone, useful in case of need to read the generated SQL + /* NOTE: Family of QueryBuilders are clone, useful in case of need to read the generated SQL let qpr = q.clone(); println!("PSQL: {:?}", qpr.read_sql()); */ @@ -105,6 +110,7 @@ fn test_crud_update_with_querybuilder() { } /// Same as above, but with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_update_with_querybuilder_datasource() { // Find all the leagues with ID less or equals that 7 @@ -139,6 +145,7 @@ fn test_crud_update_with_querybuilder_datasource() { /// Note if the database is persisted (not created and destroyed on every docker or /// GitHub Action wake up), it won't delete things that already have been deleted, /// but this isn't an error. They just don't exists. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_delete_with_querybuilder() { Tournament::delete_query() @@ -152,6 +159,7 @@ fn test_crud_delete_with_querybuilder() { } /// Same as the above delete, but with the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_delete_with_querybuilder_datasource() { Player::delete_query_datasource(SQL_SERVER_DS) diff --git a/tests/crud/select_operations.rs b/tests/crud/select_operations.rs index 26e0e5f2..9f9a6f5c 100644 --- a/tests/crud/select_operations.rs +++ b/tests/crud/select_operations.rs @@ -1,5 +1,6 @@ #![allow(clippy::nonminimal_bool)] +#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; ///! Integration tests for the CRUD operations available in `Canyon` that ///! generates and executes *SELECT* statements @@ -12,6 +13,7 @@ use crate::tests_models::player::*; /// Tests the behaviour of a SELECT * FROM {table_name} within Canyon, through the /// `::find_all()` associated function derived with the `CanyonCrud` derive proc-macro /// and using the *default datasource* +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_all() { let find_all_result: Result, Box> = @@ -28,6 +30,7 @@ fn test_crud_find_all() { /// Same as the `find_all()`, but with the unchecked variant, which directly returns `Vec` not /// `Result` wrapped +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_all_unchecked() { let find_all_result: Vec = League::find_all_unchecked().await; @@ -37,6 +40,7 @@ fn test_crud_find_all_unchecked() { /// Tests the behaviour of a SELECT * FROM {table_name} within Canyon, through the /// `::find_all()` associated function derived with the `CanyonCrud` derive proc-macro /// and using the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_all_datasource() { let find_all_result: Result, Box> = @@ -48,6 +52,7 @@ fn test_crud_find_all_datasource() { /// Same as the `find_all_datasource()`, but with the unchecked variant and the specified dataosource, /// returning directly `Vec` and not `Result, Err>` +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_all_unchecked_datasource() { let find_all_result: Vec = League::find_all_unchecked_datasource(SQL_SERVER_DS).await; @@ -58,6 +63,7 @@ fn test_crud_find_all_unchecked_datasource() { /// defined with the #[primary_key] attribute over some field of the type. /// /// Uses the *default datasource*. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_by_pk() { let find_by_pk_result: Result, Box> = @@ -80,6 +86,8 @@ fn test_crud_find_by_pk() { /// defined with the #[primary_key] attribute over some field of the type. /// /// Uses the *specified datasource* in the second parameter of the function call. +#[cfg(feature = "postgres")] +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_by_pk_datasource() { let find_by_pk_result: Result, Box> = @@ -99,6 +107,7 @@ fn test_crud_find_by_pk_datasource() { } /// Counts how many rows contains an entity on the target database. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_count_operation() { assert_eq!( @@ -109,6 +118,7 @@ fn test_crud_count_operation() { /// Counts how many rows contains an entity on the target database using /// the specified datasource +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_count_datasource_operation() { assert_eq!( diff --git a/tests/crud/update_operations.rs b/tests/crud/update_operations.rs index fc7ae733..e4085560 100644 --- a/tests/crud/update_operations.rs +++ b/tests/crud/update_operations.rs @@ -2,6 +2,7 @@ ///! generates and executes *UPDATE* statements use canyon_sql::crud::CrudOperations; +#[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; @@ -15,6 +16,7 @@ use crate::tests_models::league::*; /// /// Attempt of usage the `t.update(&self)` method on an entity without `#[primary_key]` /// will raise a runtime error. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_update_method_operation() { // We first retrieve some entity from the database. Note that we must make @@ -55,6 +57,7 @@ fn test_crud_update_method_operation() { } /// Same as the above test, but with the specified datasource. +#[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_update_datasource_method_operation() { // We first retrieve some entity from the database. Note that we must make diff --git a/tests/migrations/mod.rs b/tests/migrations/mod.rs index 17b19c35..47f82566 100644 --- a/tests/migrations/mod.rs +++ b/tests/migrations/mod.rs @@ -1,16 +1,18 @@ +#![allow(unused_imports)] ///! Integration tests for the migrations feature of `Canyon-SQL` use canyon_sql::{crud::Transaction, migrations::handler::Migrations}; use crate::constants; /// Brings the information of the `PostgreSQL` requested schema +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_migrations_postgresql_status_query() { let results = Migrations::query(constants::FETCH_PUBLIC_SCHEMA, [], constants::PSQL_DS).await; assert!(results.is_ok()); - let public_schema_info = results.ok().unwrap().postgres; - + let res = results.unwrap(); + let public_schema_info = res.get_postgres_rows(); let first_result = public_schema_info.get(0).unwrap(); assert_eq!(first_result.columns().get(0).unwrap().name(), "table_name"); From 3b2120a37b87a0ab51c689d0df5fe72f11bbc73b Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 20 Apr 2023 18:02:28 +0200 Subject: [PATCH 55/82] Setting up the v0.3.1 due to the publish GH action failed because the missing --all-features flag --- .github/workflows/release.yml | 1 + Cargo.toml | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c3342da..1b2060c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,6 +28,7 @@ jobs: with: registry-token: ${{ secrets.CRATES_IO_TOKEN }} publish-delay: 15000 + args: --all-features release-publisher: needs: 'publish' diff --git a/Cargo.toml b/Cargo.toml index 6f4da496..e7399690 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,10 @@ tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } [workspace.dependencies] -canyon_crud = { version = "0.3.0", path = "canyon_crud" } -canyon_connection = { version = "0.3.0", path = "canyon_connection" } -canyon_observer = { version = "0.3.0", path = "canyon_observer" } -canyon_macros = { version = "0.3.0", path = "canyon_macros" } +canyon_crud = { version = "0.3.1", path = "canyon_crud" } +canyon_connection = { version = "0.3.1", path = "canyon_connection" } +canyon_observer = { version = "0.3.1", path = "canyon_observer" } +canyon_macros = { version = "0.3.1", path = "canyon_macros" } tokio = { version = "1.27.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["compat"] } @@ -51,7 +51,7 @@ quote = "1.0.9" proc-macro2 = "1.0.27" [workspace.package] -version = "0.3.0" +version = "0.3.1" edition = "2021" authors = ["Alex Vergara, Gonzalo Busto"] documentation = "https://zerodaycode.github.io/canyon-book/" From f470c2aa612969bacf4f426aaf7df9573ed9ca9b Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 20 Apr 2023 18:24:48 +0200 Subject: [PATCH 56/82] Added missing Cargo metadata to the new set up with a non virtual workspace --- Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e7399690..a0bba641 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,12 @@ name = "canyon_sql" version.workspace = true edition.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true [workspace] members = [ From af89640e80300ce646efbeaf064a988f635709fd Mon Sep 17 00:00:00 2001 From: Gonzalo Busto Musi Date: Sat, 22 Apr 2023 14:31:10 +0200 Subject: [PATCH 57/82] Creating feature "migrations". Changed "canyon_observer" to "canyon_migrations" --- .github/workflows/code-quality.yml | 2 +- Cargo.toml | 13 +++++++------ bash_aliases.sh | 2 +- canyon_macros/Cargo.toml | 7 ++++--- canyon_macros/src/canyon_macro.rs | 2 +- canyon_macros/src/lib.rs | 4 ++-- canyon_macros/src/query_operations/select.rs | 2 +- canyon_macros/src/utils/macro_tokens.rs | 2 +- {canyon_observer => canyon_migrations}/Cargo.toml | 2 +- .../src/constants.rs | 0 {canyon_observer => canyon_migrations}/src/lib.rs | 0 .../src/manager/entity.rs | 0 .../src/manager/entity_fields.rs | 0 .../src/manager/field_annotation.rs | 0 .../src/manager/manager_builder.rs | 0 .../src/manager/mod.rs | 0 .../src/migrations/handler.rs | 0 .../src/migrations/information_schema.rs | 0 .../src/migrations/memory.rs | 0 .../src/migrations/mod.rs | 0 .../src/migrations/processor.rs | 0 .../src/migrations/register_types.rs | 0 src/lib.rs | 4 ++-- 23 files changed, 21 insertions(+), 19 deletions(-) rename {canyon_observer => canyon_migrations}/Cargo.toml (97%) rename {canyon_observer => canyon_migrations}/src/constants.rs (100%) rename {canyon_observer => canyon_migrations}/src/lib.rs (100%) rename {canyon_observer => canyon_migrations}/src/manager/entity.rs (100%) rename {canyon_observer => canyon_migrations}/src/manager/entity_fields.rs (100%) rename {canyon_observer => canyon_migrations}/src/manager/field_annotation.rs (100%) rename {canyon_observer => canyon_migrations}/src/manager/manager_builder.rs (100%) rename {canyon_observer => canyon_migrations}/src/manager/mod.rs (100%) rename {canyon_observer => canyon_migrations}/src/migrations/handler.rs (100%) rename {canyon_observer => canyon_migrations}/src/migrations/information_schema.rs (100%) rename {canyon_observer => canyon_migrations}/src/migrations/memory.rs (100%) rename {canyon_observer => canyon_migrations}/src/migrations/mod.rs (100%) rename {canyon_observer => canyon_migrations}/src/migrations/processor.rs (100%) rename {canyon_observer => canyon_migrations}/src/migrations/register_types.rs (100%) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 9de14f14..b955295c 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -55,7 +55,7 @@ jobs: strategy: fail-fast: false matrix: - crate: [canyon_connection, canyon_crud, canyon_macros, canyon_observer] + crate: [canyon_connection, canyon_crud, canyon_macros, canyon_migrations] steps: - uses: actions/checkout@v3 diff --git a/Cargo.toml b/Cargo.toml index a0bba641..944082f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ description.workspace = true members = [ "canyon_connection", "canyon_crud", - "canyon_observer", + "canyon_migrations", "canyon_macros", "tests" @@ -23,7 +23,7 @@ members = [ # Project crates canyon_connection = { workspace = true, path = "canyon_connection" } canyon_crud = { workspace = true, path = "canyon_crud" } -canyon_observer = { workspace = true, path = "canyon_observer" } +canyon_migrations = { workspace = true, path = "canyon_migrations", optional = true } canyon_macros = { workspace = true, path = "canyon_macros" } # To be marked as opt deps @@ -33,7 +33,7 @@ tiberius = { workspace = true, optional = true } [workspace.dependencies] canyon_crud = { version = "0.3.1", path = "canyon_crud" } canyon_connection = { version = "0.3.1", path = "canyon_connection" } -canyon_observer = { version = "0.3.1", path = "canyon_observer" } +canyon_migrations = { version = "0.3.1", path = "canyon_migrations"} canyon_macros = { version = "0.3.1", path = "canyon_macros" } tokio = { version = "1.27.0", features = ["full"] } @@ -59,7 +59,7 @@ proc-macro2 = "1.0.27" [workspace.package] version = "0.3.1" edition = "2021" -authors = ["Alex Vergara, Gonzalo Busto"] +authors = ["Alex Vergara, Gonzalo Busto Musi"] documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" readme = "README.md" @@ -67,5 +67,6 @@ license = "MIT" description = "A Rust ORM and QueryBuilder" [features] -postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres", "canyon_observer/postgres", "canyon_macros/postgres"] -mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql", "canyon_observer/mssql", "canyon_macros/mssql"] +postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres", "canyon_migrations/postgres", "canyon_macros/postgres"] +mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql", "canyon_migrations/mssql", "canyon_macros/mssql"] +migrations = ["canyon_migrations"] diff --git a/bash_aliases.sh b/bash_aliases.sh index 64e2d931..aee09cd7 100644 --- a/bash_aliases.sh +++ b/bash_aliases.sh @@ -39,7 +39,7 @@ alias SqlServerInitializationLinux='cargo test initialize_sql_server_docker_inst # Publish Canyon-SQL to the registry with its dependencies -alias PublishCanyon='cargo publish -p canyon_connection && cargo publish -p canyon_crud && cargo publish -p canyon_observer && cargo publish -p canyon_macros && cargo publish -p canyon_sql_root' +alias PublishCanyon='cargo publish -p canyon_connection && cargo publish -p canyon_crud && cargo publish -p canyon_migrations && cargo publish -p canyon_macros && cargo publish -p canyon_sql_root' # Collects the code coverage for the project (tests must run before this) alias CcEnvVars='export CARGO_INCREMENTAL=0 diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index 82d336f5..c8523914 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -19,10 +19,11 @@ proc-macro2 = { workspace = true } futures = { workspace = true } tokio = { workspace = true } -canyon_observer = { workspace = true } +canyon_migrations = { workspace = true, optional = true } canyon_crud = { workspace = true } canyon_connection = { workspace = true } [features] -postgres = ["canyon_connection/postgres", "canyon_crud/postgres", "canyon_observer/postgres"] -mssql = ["canyon_connection/mssql", "canyon_crud/mssql", "canyon_observer/mssql"] +postgres = ["canyon_connection/postgres", "canyon_crud/postgres", "canyon_migrations/postgres"] +mssql = ["canyon_connection/mssql", "canyon_crud/mssql", "canyon_migrations/mssql"] +migrations = ["canyon_migrations"] diff --git a/canyon_macros/src/canyon_macro.rs b/canyon_macros/src/canyon_macro.rs index 1424de92..57b46764 100644 --- a/canyon_macros/src/canyon_macro.rs +++ b/canyon_macros/src/canyon_macro.rs @@ -5,7 +5,7 @@ use proc_macro2::{Ident, TokenStream}; use quote::quote; -use canyon_observer::{CM_QUERIES_TO_EXECUTE, QUERIES_TO_EXECUTE}; +use canyon_migrations::{CM_QUERIES_TO_EXECUTE, QUERIES_TO_EXECUTE}; use syn::{Lit, NestedMeta}; #[derive(Debug)] diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index ce03cc58..b9968191 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -26,7 +26,7 @@ use query_operations::{ use canyon_macro::{parse_canyon_macro_attributes, wire_queries_to_execute}; use utils::{function_parser::FunctionParser, helpers, macro_tokens::MacroTokens}; -use canyon_observer::{ +use canyon_migrations::{ manager::{ entity::CanyonEntity, manager_builder::{ @@ -36,7 +36,7 @@ use canyon_observer::{ migrations::handler::Migrations, }; -use canyon_observer::{ +use canyon_migrations::{ migrations::register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}, CANYON_REGISTER_ENTITIES, }; diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index 0f70ab4d..b756db68 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -1,4 +1,4 @@ -use canyon_observer::manager::field_annotation::EntityFieldAnnotation; +use canyon_migrations::manager::field_annotation::EntityFieldAnnotation; use proc_macro2::TokenStream; use quote::quote; diff --git a/canyon_macros/src/utils/macro_tokens.rs b/canyon_macros/src/utils/macro_tokens.rs index 370fbeea..3145f494 100644 --- a/canyon_macros/src/utils/macro_tokens.rs +++ b/canyon_macros/src/utils/macro_tokens.rs @@ -1,6 +1,6 @@ use std::convert::TryFrom; -use canyon_observer::manager::field_annotation::EntityFieldAnnotation; +use canyon_migrations::manager::field_annotation::EntityFieldAnnotation; use proc_macro2::Ident; use syn::{Attribute, DeriveInput, Fields, Generics, Type, Visibility}; diff --git a/canyon_observer/Cargo.toml b/canyon_migrations/Cargo.toml similarity index 97% rename from canyon_observer/Cargo.toml rename to canyon_migrations/Cargo.toml index 0f939b2c..8b508827 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_migrations/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "canyon_observer" +name = "canyon_migrations" version.workspace = true edition.workspace = true authors.workspace = true diff --git a/canyon_observer/src/constants.rs b/canyon_migrations/src/constants.rs similarity index 100% rename from canyon_observer/src/constants.rs rename to canyon_migrations/src/constants.rs diff --git a/canyon_observer/src/lib.rs b/canyon_migrations/src/lib.rs similarity index 100% rename from canyon_observer/src/lib.rs rename to canyon_migrations/src/lib.rs diff --git a/canyon_observer/src/manager/entity.rs b/canyon_migrations/src/manager/entity.rs similarity index 100% rename from canyon_observer/src/manager/entity.rs rename to canyon_migrations/src/manager/entity.rs diff --git a/canyon_observer/src/manager/entity_fields.rs b/canyon_migrations/src/manager/entity_fields.rs similarity index 100% rename from canyon_observer/src/manager/entity_fields.rs rename to canyon_migrations/src/manager/entity_fields.rs diff --git a/canyon_observer/src/manager/field_annotation.rs b/canyon_migrations/src/manager/field_annotation.rs similarity index 100% rename from canyon_observer/src/manager/field_annotation.rs rename to canyon_migrations/src/manager/field_annotation.rs diff --git a/canyon_observer/src/manager/manager_builder.rs b/canyon_migrations/src/manager/manager_builder.rs similarity index 100% rename from canyon_observer/src/manager/manager_builder.rs rename to canyon_migrations/src/manager/manager_builder.rs diff --git a/canyon_observer/src/manager/mod.rs b/canyon_migrations/src/manager/mod.rs similarity index 100% rename from canyon_observer/src/manager/mod.rs rename to canyon_migrations/src/manager/mod.rs diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_migrations/src/migrations/handler.rs similarity index 100% rename from canyon_observer/src/migrations/handler.rs rename to canyon_migrations/src/migrations/handler.rs diff --git a/canyon_observer/src/migrations/information_schema.rs b/canyon_migrations/src/migrations/information_schema.rs similarity index 100% rename from canyon_observer/src/migrations/information_schema.rs rename to canyon_migrations/src/migrations/information_schema.rs diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_migrations/src/migrations/memory.rs similarity index 100% rename from canyon_observer/src/migrations/memory.rs rename to canyon_migrations/src/migrations/memory.rs diff --git a/canyon_observer/src/migrations/mod.rs b/canyon_migrations/src/migrations/mod.rs similarity index 100% rename from canyon_observer/src/migrations/mod.rs rename to canyon_migrations/src/migrations/mod.rs diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_migrations/src/migrations/processor.rs similarity index 100% rename from canyon_observer/src/migrations/processor.rs rename to canyon_migrations/src/migrations/processor.rs diff --git a/canyon_observer/src/migrations/register_types.rs b/canyon_migrations/src/migrations/register_types.rs similarity index 100% rename from canyon_observer/src/migrations/register_types.rs rename to canyon_migrations/src/migrations/register_types.rs diff --git a/src/lib.rs b/src/lib.rs index 33a2c82b..1d2e3375 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,11 +6,11 @@ extern crate canyon_connection; extern crate canyon_crud; extern crate canyon_macros; -extern crate canyon_observer; +extern crate canyon_migrations; /// Reexported elements to the root of the public API pub mod migrations { - pub use canyon_observer::migrations::{handler, processor}; + pub use canyon_migrations::migrations::{handler, processor}; } /// The top level reexport. Here we define the path to some really important From 1bf7ba1a5b3e00ac465fcff3a56801aa9177322c Mon Sep 17 00:00:00 2001 From: Gonzalo Busto Musi Date: Sat, 22 Apr 2023 16:15:53 +0200 Subject: [PATCH 58/82] WIP - Bringing back canyon_manager as canyon_entities --- Cargo.toml | 6 +- .../manager => canyon_entities/src}/entity.rs | 2 +- .../src}/entity_fields.rs | 0 .../src}/field_annotation.rs | 0 .../src}/manager_builder.rs | 0 canyon_entities/src/register_types.rs | 45 ++++ canyon_macros/Cargo.toml | 5 +- canyon_macros/src/canyon_macro.rs | 114 ++------- canyon_macros/src/lib.rs | 87 ++----- canyon_macros/src/query_operations/select.rs | 2 +- canyon_macros/src/utils/macro_tokens.rs | 2 +- canyon_migrations/Cargo.toml | 5 +- canyon_migrations/src/constants.rs | 2 - canyon_migrations/src/lib.rs | 6 +- canyon_migrations/src/manager/mod.rs | 4 - canyon_migrations/src/migrations/handler.rs | 4 +- canyon_migrations/src/migrations/memory.rs | 2 +- canyon_migrations/src/migrations/mod.rs | 186 +++++++++++++- canyon_migrations/src/migrations/processor.rs | 26 +- .../src/migrations/register_types.rs | 228 ------------------ .../src/migrations/transforms.rs | 0 tests/canyon.toml | 22 +- 22 files changed, 318 insertions(+), 430 deletions(-) rename {canyon_migrations/src/manager => canyon_entities/src}/entity.rs (98%) rename {canyon_migrations/src/manager => canyon_entities/src}/entity_fields.rs (100%) rename {canyon_migrations/src/manager => canyon_entities/src}/field_annotation.rs (100%) rename {canyon_migrations/src/manager => canyon_entities/src}/manager_builder.rs (100%) create mode 100644 canyon_entities/src/register_types.rs delete mode 100644 canyon_migrations/src/manager/mod.rs delete mode 100644 canyon_migrations/src/migrations/register_types.rs create mode 100644 canyon_migrations/src/migrations/transforms.rs diff --git a/Cargo.toml b/Cargo.toml index 944082f6..dcfb553f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ description.workspace = true members = [ "canyon_connection", "canyon_crud", + "canyon_entities", "canyon_migrations", "canyon_macros", @@ -23,6 +24,7 @@ members = [ # Project crates canyon_connection = { workspace = true, path = "canyon_connection" } canyon_crud = { workspace = true, path = "canyon_crud" } +canyon_entities = { workspace = true, path = "canyon_entities" } canyon_migrations = { workspace = true, path = "canyon_migrations", optional = true } canyon_macros = { workspace = true, path = "canyon_macros" } @@ -33,6 +35,7 @@ tiberius = { workspace = true, optional = true } [workspace.dependencies] canyon_crud = { version = "0.3.1", path = "canyon_crud" } canyon_connection = { version = "0.3.1", path = "canyon_connection" } +canyon_entities = { version = "0.3.1", path = "canyon_entities" } canyon_migrations = { version = "0.3.1", path = "canyon_migrations"} canyon_macros = { version = "0.3.1", path = "canyon_macros" } @@ -52,6 +55,7 @@ toml = "0.7.3" async-trait = "0.1.68" walkdir = "2.3.3" regex = "1.5" +partialdebug = "0.2.0" quote = "1.0.9" proc-macro2 = "1.0.27" @@ -69,4 +73,4 @@ description = "A Rust ORM and QueryBuilder" [features] postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres", "canyon_migrations/postgres", "canyon_macros/postgres"] mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql", "canyon_migrations/mssql", "canyon_macros/mssql"] -migrations = ["canyon_migrations"] +migrations = ["canyon_migrations", "canyon_macros/migrations"] diff --git a/canyon_migrations/src/manager/entity.rs b/canyon_entities/src/entity.rs similarity index 98% rename from canyon_migrations/src/manager/entity.rs rename to canyon_entities/src/entity.rs index 7aaeb38e..8604d0e8 100644 --- a/canyon_migrations/src/manager/entity.rs +++ b/canyon_entities/src/entity.rs @@ -10,7 +10,7 @@ use syn::{ use super::entity_fields::EntityField; /// Provides a convenient way of handling the data on any -/// `CanyonEntity` struct anntotaded with the macro `#[canyon_entity]` +/// `CanyonEntity` struct annotated with the macro `#[canyon_entity]` #[derive(PartialDebug, Clone)] pub struct CanyonEntity { pub struct_name: Ident, diff --git a/canyon_migrations/src/manager/entity_fields.rs b/canyon_entities/src/entity_fields.rs similarity index 100% rename from canyon_migrations/src/manager/entity_fields.rs rename to canyon_entities/src/entity_fields.rs diff --git a/canyon_migrations/src/manager/field_annotation.rs b/canyon_entities/src/field_annotation.rs similarity index 100% rename from canyon_migrations/src/manager/field_annotation.rs rename to canyon_entities/src/field_annotation.rs diff --git a/canyon_migrations/src/manager/manager_builder.rs b/canyon_entities/src/manager_builder.rs similarity index 100% rename from canyon_migrations/src/manager/manager_builder.rs rename to canyon_entities/src/manager_builder.rs diff --git a/canyon_entities/src/register_types.rs b/canyon_entities/src/register_types.rs new file mode 100644 index 00000000..45cd1b8d --- /dev/null +++ b/canyon_entities/src/register_types.rs @@ -0,0 +1,45 @@ +/// This file contains `Rust` types that represents an entry on the `CanyonRegister` +/// where `Canyon` tracks the user types that has to manage + +pub const NUMERIC_PK_DATATYPE: [&str; 6] = ["i16", "u16", "i32", "u32", "i64", "u64"]; + +/// Gets the necessary identifiers of a CanyonEntity to make it the comparative +/// against the database schemas +#[derive(Debug, Clone, Default)] +pub struct CanyonRegisterEntity<'a> { + pub entity_name: &'a str, + pub entity_db_table_name: &'a str, + pub user_schema_name: Option<&'a str>, + pub entity_fields: Vec, +} + +/// Complementary type for a field that represents a struct field that maps +/// some real database column data +#[derive(Debug, Clone, Default)] +pub struct CanyonRegisterEntityField { + pub field_name: String, + pub field_type: String, + pub annotations: Vec, +} + +impl CanyonRegisterEntityField { + /// Return if the field is autoincremental + pub fn is_autoincremental(&self) -> bool { + let has_pk_annotation = self + .annotations + .iter() + .find(|a| a.starts_with("Annotation: PrimaryKey")); + + let pk_is_autoincremental = match has_pk_annotation { + Some(annotation) => annotation.contains("true"), + None => false, + }; + + NUMERIC_PK_DATATYPE.contains(&self.field_type.as_str()) && pk_is_autoincremental + } + + /// Return the nullability of a the field + pub fn is_nullable(&self) -> bool { + self.field_type.to_uppercase().starts_with("OPTION") + } +} diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index c8523914..763fde8d 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -19,9 +19,10 @@ proc-macro2 = { workspace = true } futures = { workspace = true } tokio = { workspace = true } -canyon_migrations = { workspace = true, optional = true } -canyon_crud = { workspace = true } canyon_connection = { workspace = true } +canyon_crud = { workspace = true } +canyon_entities = { workspace = true } +canyon_migrations = { workspace = true, optional = true } [features] postgres = ["canyon_connection/postgres", "canyon_crud/postgres", "canyon_migrations/postgres"] diff --git a/canyon_macros/src/canyon_macro.rs b/canyon_macros/src/canyon_macro.rs index 57b46764..cc4aa61e 100644 --- a/canyon_macros/src/canyon_macro.rs +++ b/canyon_macros/src/canyon_macro.rs @@ -1,112 +1,32 @@ //! Provides helpers to build the `#[canyon_macros::canyon]` procedural like attribute macro -use proc_macro::TokenStream as TokenStream1; -use proc_macro2::{Ident, TokenStream}; - +use proc_macro2::TokenStream; use quote::quote; - +use canyon_connection::CANYON_TOKIO_RUNTIME; use canyon_migrations::{CM_QUERIES_TO_EXECUTE, QUERIES_TO_EXECUTE}; -use syn::{Lit, NestedMeta}; - -#[derive(Debug)] -/// Utilery struct for wrapping the content and result of parsing the attributes on the `canyon` macro -pub struct CanyonMacroAttributes { - pub allowed_migrations: bool, - pub error: Option, -} - -/// Parses the [`syn::NestedMeta::Meta`] or [`syn::NestedMeta::Lit`] attached to the `canyon` macro -pub fn parse_canyon_macro_attributes(_meta: &Vec) -> CanyonMacroAttributes { - let mut res = CanyonMacroAttributes { - allowed_migrations: false, - error: None, - }; - - for nested_meta in _meta { - match nested_meta { - syn::NestedMeta::Meta(m) => determine_allowed_attributes(m, &mut res), - syn::NestedMeta::Lit(lit) => match lit { - syn::Lit::Str(ref l) => { - res.error = Some(report_literals_not_allowed(&l.value(), lit)) - } - syn::Lit::ByteStr(ref l) => { - res.error = Some(report_literals_not_allowed( - &String::from_utf8_lossy(&l.value()), - lit, - )) - } - syn::Lit::Byte(ref l) => { - res.error = Some(report_literals_not_allowed(&l.value().to_string(), lit)) - } - syn::Lit::Char(ref l) => { - res.error = Some(report_literals_not_allowed(&l.value().to_string(), lit)) - } - syn::Lit::Int(ref l) => { - res.error = Some(report_literals_not_allowed(&l.to_string(), lit)) - } - syn::Lit::Float(ref l) => { - res.error = Some(report_literals_not_allowed(&l.to_string(), lit)) - } - syn::Lit::Bool(ref l) => { - res.error = Some(report_literals_not_allowed(&l.value().to_string(), lit)) - } - syn::Lit::Verbatim(ref l) => { - res.error = Some(report_literals_not_allowed(&l.to_string(), lit)) - } - }, - } - } +use canyon_migrations::migrations::handler::Migrations; - res -} - -/// Determines whenever a [`syn::NestedMeta::Meta`] it's classified as a valid argument of the `canyon` macro -fn determine_allowed_attributes(meta: &syn::Meta, cma: &mut CanyonMacroAttributes) { - const ALLOWED_ATTRS: [&str; 1] = ["enable_migrations"]; - - let attr_ident = meta.path().get_ident().unwrap(); - let attr_ident_str = attr_ident.to_string(); - - if attr_ident_str.as_str() == "enable_migrations" { - cma.allowed_migrations = true; - } else { - let error = syn::Error::new_spanned( - Ident::new(&attr_ident_str, attr_ident.span()), - format!( - "No `{attr_ident_str}` arguments allowed in the `Canyon` macro attributes.\n\ - Allowed ones are: {ALLOWED_ATTRS:?}" - ), - ) - .into_compile_error(); - cma.error = Some( - quote! { - #error - fn main() {} - } - .into(), - ) - } -} - -/// Creates a custom error for report not allowed literals on the attribute -/// args of the `canyon` proc macro -fn report_literals_not_allowed(ident: &str, s: &Lit) -> TokenStream1 { - let error = syn::Error::new_spanned( - Ident::new(ident, s.span()), - "No literals allowed in the `Canyon` macro", - ) - .into_compile_error(); +#[cfg(feature = "migrations")] +pub fn main_with_queries() -> TokenStream { + CANYON_TOKIO_RUNTIME.block_on(async { + canyon_connection::init_connections_cache().await; + Migrations::migrate().await; + }); + // The queries to execute at runtime in the managed state + let mut queries_tokens: Vec = Vec::new(); + wire_queries_to_execute(&mut queries_tokens); quote! { - #error - fn main() {} + { + #(#queries_tokens)* + } } - .into() } /// Creates a TokenScream that is used to load the data generated at compile-time /// by the `CanyonManaged` macros again on the queries register -pub fn wire_queries_to_execute(canyon_manager_tokens: &mut Vec) { +#[cfg(feature = "migrations")] +fn wire_queries_to_execute(canyon_manager_tokens: &mut Vec) { let cm_data = CM_QUERIES_TO_EXECUTE.lock().unwrap(); let data = QUERIES_TO_EXECUTE.lock().unwrap(); diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index b9968191..f35612ef 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -1,17 +1,18 @@ extern crate proc_macro; -mod canyon_entity_macro; -mod canyon_macro; -mod query_operations; mod utils; +mod query_operations; +mod canyon_entity_macro; +#[cfg(feature = "migrations")] mod canyon_macro; -use canyon_connection::CANYON_TOKIO_RUNTIME; use canyon_entity_macro::parse_canyon_entity_proc_macro_attr; use proc_macro::TokenStream as CompilerTokenStream; use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use syn::{DeriveInput, Fields, Type, Visibility}; +#[cfg(feature = "migrations")] use canyon_macro::main_with_queries; + use query_operations::{ delete::{generate_delete_query_tokens, generate_delete_tokens}, insert::{generate_insert_tokens, generate_multiple_insert_tokens}, @@ -22,24 +23,17 @@ use query_operations::{ }, update::{generate_update_query_tokens, generate_update_tokens}, }; - -use canyon_macro::{parse_canyon_macro_attributes, wire_queries_to_execute}; use utils::{function_parser::FunctionParser, helpers, macro_tokens::MacroTokens}; -use canyon_migrations::{ - manager::{ - entity::CanyonEntity, - manager_builder::{ - generate_enum_with_fields, generate_enum_with_fields_values, generate_user_struct, - }, +use canyon_entities::{ + CANYON_REGISTER_ENTITIES, + entity::CanyonEntity, + manager_builder::{ + generate_enum_with_fields, generate_enum_with_fields_values, generate_user_struct, }, - migrations::handler::Migrations, + register_types::{CanyonRegisterEntity, CanyonRegisterEntityField} }; -use canyon_migrations::{ - migrations::register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}, - CANYON_REGISTER_ENTITIES, -}; /// Macro for handling the entry point to the program. /// @@ -51,15 +45,6 @@ use canyon_migrations::{ /// the necessary operations for the migrations #[proc_macro_attribute] pub fn main(_meta: CompilerTokenStream, input: CompilerTokenStream) -> CompilerTokenStream { - let attrs = syn::parse_macro_input!(_meta as syn::AttributeArgs); - - // Parses the attributes declared in the arguments of this proc macro - let attrs_parse_result = parse_canyon_macro_attributes(&attrs); - if attrs_parse_result.error.is_some() { - return attrs_parse_result.error.unwrap(); - } - - // Parses the function items that this attribute is attached to let func_res = syn::parse::(input); if func_res.is_err() { return quote! { fn main() {} }.into(); @@ -70,46 +55,26 @@ pub fn main(_meta: CompilerTokenStream, input: CompilerTokenStream) -> CompilerT let sign = func.sig; let body = func.block.stmts; - if attrs_parse_result.allowed_migrations { - CANYON_TOKIO_RUNTIME.block_on(async { - canyon_connection::init_connections_cache().await; - Migrations::migrate().await; - }); - - // The queries to execute at runtime in the managed state - let mut queries_tokens: Vec = Vec::new(); - wire_queries_to_execute(&mut queries_tokens); + #[allow(unused_mut, unused_assignments)] + let mut migrations_tokens = quote! {}; + #[cfg(feature = "migrations")] { + migrations_tokens = main_with_queries(); + } - // The final code wired in main() - quote! { - #sign { - canyon_sql::runtime::CANYON_TOKIO_RUNTIME - .handle() - .block_on( async { - canyon_sql::runtime::init_connections_cache().await; - { - #(#queries_tokens)* - } - #(#body)* - } - ) - } - } - .into() - } else { - quote! { - #sign { - canyon_sql::runtime::CANYON_TOKIO_RUNTIME + // The final code wired in main() + quote! { + #sign { + canyon_sql::runtime::CANYON_TOKIO_RUNTIME .handle() .block_on( async { - canyon_sql::runtime::init_connections_cache().await; - #(#body)* - } - ) - } + canyon_sql::runtime::init_connections_cache().await; + #migrations_tokens + #(#body)* + } + ) } - .into() } + .into() } #[proc_macro_attribute] diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index b756db68..5a5a4e15 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -1,4 +1,4 @@ -use canyon_migrations::manager::field_annotation::EntityFieldAnnotation; +use canyon_entities::field_annotation::EntityFieldAnnotation; use proc_macro2::TokenStream; use quote::quote; diff --git a/canyon_macros/src/utils/macro_tokens.rs b/canyon_macros/src/utils/macro_tokens.rs index 3145f494..29de0467 100644 --- a/canyon_macros/src/utils/macro_tokens.rs +++ b/canyon_macros/src/utils/macro_tokens.rs @@ -1,6 +1,6 @@ use std::convert::TryFrom; -use canyon_migrations::manager::field_annotation::EntityFieldAnnotation; +use canyon_entities::field_annotation::EntityFieldAnnotation; use proc_macro2::Ident; use syn::{Attribute, DeriveInput, Fields, Generics, Type, Visibility}; diff --git a/canyon_migrations/Cargo.toml b/canyon_migrations/Cargo.toml index 8b508827..ba353b76 100644 --- a/canyon_migrations/Cargo.toml +++ b/canyon_migrations/Cargo.toml @@ -12,14 +12,17 @@ description.workspace = true [dependencies] canyon_crud = { workspace = true } canyon_connection = { workspace = true } +canyon_entities = { workspace = true } + tokio = { workspace = true } tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } + async-trait = { workspace = true } regex = { workspace = true } +partialdebug = { workspace = true } walkdir = { workspace = true } -partialdebug = "0.2.0" proc-macro2 = { workspace = true } quote = { workspace = true } syn = { version = "1.0.86", features = ["full", "parsing"] } # TODO Pending to refactor and upgrade diff --git a/canyon_migrations/src/constants.rs b/canyon_migrations/src/constants.rs index 3928da4f..9f025762 100644 --- a/canyon_migrations/src/constants.rs +++ b/canyon_migrations/src/constants.rs @@ -1,5 +1,3 @@ -pub const NUMERIC_PK_DATATYPE: [&str; 6] = ["i16", "u16", "i32", "u32", "i64", "u64"]; - #[cfg(feature = "postgres")] pub mod postgresql_queries { pub static CANYON_MEMORY_TABLE: &str = "CREATE TABLE IF NOT EXISTS canyon_memory ( diff --git a/canyon_migrations/src/lib.rs b/canyon_migrations/src/lib.rs index 41e0dd42..ec939e12 100644 --- a/canyon_migrations/src/lib.rs +++ b/canyon_migrations/src/lib.rs @@ -13,16 +13,14 @@ pub mod migrations; extern crate canyon_connection; extern crate canyon_crud; +extern crate canyon_entities; mod constants; -pub mod manager; -use crate::migrations::register_types::CanyonRegisterEntity; use canyon_connection::lazy_static::lazy_static; use std::{collections::HashMap, sync::Mutex}; -pub static CANYON_REGISTER_ENTITIES: Mutex>> = - Mutex::new(Vec::new()); + lazy_static! { pub static ref QUERIES_TO_EXECUTE: Mutex>> = Mutex::new(HashMap::new()); diff --git a/canyon_migrations/src/manager/mod.rs b/canyon_migrations/src/manager/mod.rs deleted file mode 100644 index eca614b8..00000000 --- a/canyon_migrations/src/manager/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod entity; -pub mod entity_fields; -pub mod field_annotation; -pub mod manager_builder; diff --git a/canyon_migrations/src/migrations/handler.rs b/canyon_migrations/src/migrations/handler.rs index 9ce3c4e8..33e9b5bb 100644 --- a/canyon_migrations/src/migrations/handler.rs +++ b/canyon_migrations/src/migrations/handler.rs @@ -1,6 +1,7 @@ use canyon_connection::{datasources::Migrations as MigrationsStatus, DATASOURCES}; use canyon_crud::rows::CanyonRows; use partialdebug::placeholder::PartialDebug; +use canyon_entities::CANYON_REGISTER_ENTITIES; use crate::{ canyon_crud::{ @@ -13,8 +14,7 @@ use crate::{ information_schema::{ColumnMetadata, ColumnMetadataTypeValue, TableMetadata}, memory::CanyonMemory, processor::MigrationsProcessor, - }, - CANYON_REGISTER_ENTITIES, + } }; #[derive(PartialDebug)] diff --git a/canyon_migrations/src/migrations/memory.rs b/canyon_migrations/src/migrations/memory.rs index 18f6eb31..1d822fd1 100644 --- a/canyon_migrations/src/migrations/memory.rs +++ b/canyon_migrations/src/migrations/memory.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use std::fs; use walkdir::WalkDir; -use super::register_types::CanyonRegisterEntity; +use canyon_entities::register_types::CanyonRegisterEntity; /// Convenient struct that contains the necessary data and operations to implement /// the `Canyon Memory`. diff --git a/canyon_migrations/src/migrations/mod.rs b/canyon_migrations/src/migrations/mod.rs index 525cbc10..ec717c77 100644 --- a/canyon_migrations/src/migrations/mod.rs +++ b/canyon_migrations/src/migrations/mod.rs @@ -1,5 +1,189 @@ +#[cfg(feature = "postgres")] use crate::constants::postgresql_type; +#[cfg(feature = "mssql")] use crate::constants::sqlserver_type; +use crate::constants::{regex_patterns, rust_type}; + pub mod handler; pub mod information_schema; pub mod memory; pub mod processor; -pub mod register_types; +pub mod transforms; + +use canyon_entities::register_types::CanyonRegisterEntityField; +use regex::Regex; + +/// Return the postgres datatype and parameters to create a column for a given rust type +#[cfg(feature = "postgres")] +pub fn to_postgres_syntax(field: &CanyonRegisterEntityField) -> String { + let rust_type_clean = field.field_type.replace(' ', ""); + + match rust_type_clean.as_str() { + rust_type::I8 | rust_type::U8 => { + String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) + } + rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(postgresql_type::INTEGER), + + rust_type::I16 | rust_type::U16 => { + String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) + } + rust_type::OPT_I16 | rust_type::OPT_U16 => String::from(postgresql_type::INTEGER), + + rust_type::I32 | rust_type::U32 => { + String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) + } + rust_type::OPT_I32 | rust_type::OPT_U32 => String::from(postgresql_type::INTEGER), + + rust_type::I64 | rust_type::U64 => { + String::from(&format!("{} NOT NULL", postgresql_type::BIGINT)) + } + rust_type::OPT_I64 | rust_type::OPT_U64 => String::from(postgresql_type::BIGINT), + + rust_type::STRING => String::from(&format!("{} NOT NULL", postgresql_type::TEXT)), + rust_type::OPT_STRING => String::from(postgresql_type::TEXT), + + rust_type::BOOL => String::from(&format!("{} NOT NULL", postgresql_type::BOOLEAN)), + rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), + + rust_type::NAIVE_DATE => String::from(&format!("{} NOT NULL", postgresql_type::DATE)), + rust_type::OPT_NAIVE_DATE => String::from(postgresql_type::DATE), + + rust_type::NAIVE_TIME => String::from(&format!("{} NOT NULL", postgresql_type::TIME)), + rust_type::OPT_NAIVE_TIME => String::from(postgresql_type::TIME), + + rust_type::NAIVE_DATE_TIME => { + String::from(&format!("{} NOT NULL", postgresql_type::DATETIME)) + } + rust_type::OPT_NAIVE_DATE_TIME => String::from(postgresql_type::DATETIME), + &_ => todo!("Not supported datatype for this migrations version"), + } +} + +/// Return the postgres datatype and parameters to create a column for a given rust type +/// for Microsoft SQL Server +#[cfg(feature = "mssql")] +pub fn to_sqlserver_syntax(field: &CanyonRegisterEntityField) -> String { + let rust_type_clean = field.field_type.replace(' ', ""); + + match rust_type_clean.as_str() { + rust_type::I8 | rust_type::U8 => { + String::from(&format!("{} NOT NULL", sqlserver_type::INT)) + } + rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(sqlserver_type::INT), + + rust_type::I16 | rust_type::U16 => { + String::from(&format!("{} NOT NULL", sqlserver_type::INT)) + } + rust_type::OPT_I16 | rust_type::OPT_U16 => String::from(sqlserver_type::INT), + + rust_type::I32 | rust_type::U32 => { + String::from(&format!("{} NOT NULL", sqlserver_type::INT)) + } + rust_type::OPT_I32 | rust_type::OPT_U32 => String::from(sqlserver_type::INT), + + rust_type::I64 | rust_type::U64 => { + String::from(&format!("{} NOT NULL", sqlserver_type::BIGINT)) + } + rust_type::OPT_I64 | rust_type::OPT_U64 => String::from(sqlserver_type::BIGINT), + + rust_type::STRING => { + String::from(&format!("{} NOT NULL DEFAULT ''", sqlserver_type::NVARCHAR)) + } + rust_type::OPT_STRING => String::from(sqlserver_type::NVARCHAR), + + rust_type::BOOL => String::from(&format!("{} NOT NULL", sqlserver_type::BIT)), + rust_type::OPT_BOOL => String::from(sqlserver_type::BIT), + + rust_type::NAIVE_DATE => String::from(&format!("{} NOT NULL", sqlserver_type::DATE)), + rust_type::OPT_NAIVE_DATE => String::from(sqlserver_type::DATE), + + rust_type::NAIVE_TIME => String::from(&format!("{} NOT NULL", sqlserver_type::TIME)), + rust_type::OPT_NAIVE_TIME => String::from(sqlserver_type::TIME), + + rust_type::NAIVE_DATE_TIME => { + String::from(&format!("{} NOT NULL", sqlserver_type::DATETIME)) + } + rust_type::OPT_NAIVE_DATE_TIME => String::from(sqlserver_type::DATETIME), + &_ => todo!("Not supported datatype for this migrations version"), + } +} + +#[cfg(feature = "postgres")] +pub fn to_postgres_alter_syntax(field: &CanyonRegisterEntityField) -> String { + let mut rust_type_clean = field.field_type.replace(' ', ""); + let rs_type_is_optional = field.field_type.to_uppercase().starts_with("OPTION"); + + if rs_type_is_optional { + let type_regex = Regex::new(regex_patterns::EXTRACT_RUST_OPT_REGEX).unwrap(); + let capture_rust_type = type_regex.captures(rust_type_clean.as_str()).unwrap(); + rust_type_clean = capture_rust_type + .name("rust_type") + .unwrap() + .as_str() + .to_string(); + } + + match rust_type_clean.as_str() { + rust_type::I8 | rust_type::U8 | rust_type::OPT_I8 | rust_type::OPT_U8 => { + String::from(postgresql_type::INT_8) + } + rust_type::I16 | rust_type::U16 | rust_type::OPT_I16 | rust_type::OPT_U16 => { + String::from(postgresql_type::SMALL_INT) + } + rust_type::I32 | rust_type::U32 | rust_type::OPT_I32 | rust_type::OPT_U32 => { + String::from(postgresql_type::INTEGER) + } + rust_type::I64 | rust_type::U64 | rust_type::OPT_I64 | rust_type::OPT_U64 => { + String::from(postgresql_type::BIGINT) + } + rust_type::STRING | rust_type::OPT_STRING => String::from(postgresql_type::TEXT), + rust_type::BOOL | rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), + rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => { + String::from(postgresql_type::DATE) + } + rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => { + String::from(postgresql_type::TIME) + } + rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { + String::from(postgresql_type::DATETIME) + } + &_ => todo!("Not supported datatype for this migrations version"), + } +} + +#[cfg(feature = "mssql")] +pub fn to_sqlserver_alter_syntax(field: &CanyonRegisterEntityField) -> String { + let mut rust_type_clean = field.field_type.replace(' ', ""); + let rs_type_is_optional = field.field_type.to_uppercase().starts_with("OPTION"); + + if rs_type_is_optional { + let type_regex = Regex::new(regex_patterns::EXTRACT_RUST_OPT_REGEX).unwrap(); + let capture_rust_type = type_regex.captures(rust_type_clean.as_str()).unwrap(); + rust_type_clean = capture_rust_type + .name("rust_type") + .unwrap() + .as_str() + .to_string(); + } + + match rust_type_clean.as_str() { + rust_type::I8 | rust_type::U8 | rust_type::OPT_I8 | rust_type::OPT_U8 => { + String::from(sqlserver_type::TINY_INT) + } + rust_type::I16 | rust_type::U16 | rust_type::OPT_I16 | rust_type::OPT_U16 => { + String::from(sqlserver_type::SMALL_INT) + } + rust_type::I32 | rust_type::U32 | rust_type::OPT_I32 | rust_type::OPT_U32 => { + String::from(sqlserver_type::INT) + } + rust_type::I64 | rust_type::U64 | rust_type::OPT_I64 | rust_type::OPT_U64 => { + String::from(sqlserver_type::BIGINT) + } + rust_type::STRING | rust_type::OPT_STRING => String::from(sqlserver_type::NVARCHAR), + rust_type::BOOL | rust_type::OPT_BOOL => String::from(sqlserver_type::BIT), + rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => String::from(sqlserver_type::DATE), + rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => String::from(sqlserver_type::TIME), + rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { + String::from(sqlserver_type::DATETIME) + } + &_ => todo!("Not supported datatype for this migrations version"), + } +} \ No newline at end of file diff --git a/canyon_migrations/src/migrations/processor.rs b/canyon_migrations/src/migrations/processor.rs index b096b828..5391a494 100644 --- a/canyon_migrations/src/migrations/processor.rs +++ b/canyon_migrations/src/migrations/processor.rs @@ -13,7 +13,9 @@ use crate::save_migrations_query_to_execute; use super::information_schema::{ColumnMetadata, TableMetadata}; use super::memory::CanyonMemory; -use super::register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}; +use canyon_entities::register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}; +#[cfg(feature = "postgres")] use crate::migrations::{to_postgres_alter_syntax, to_postgres_syntax}; +#[cfg(feature = "mssql")] use crate::migrations::{to_sqlserver_alter_syntax, to_sqlserver_syntax}; /// Responsible of generating the queries to sync the database status with the /// Rust source code managed by Canyon, for successfully make the migrations @@ -661,8 +663,8 @@ impl MigrationsHelper { #[cfg(feature = "postgres")] { if db_type == DatabaseType::PostgreSql { - return canyon_register_entity_field - .to_postgres_alter_syntax() + return + to_postgres_alter_syntax(canyon_register_entity_field) .to_lowercase() == current_column_metadata.datatype; } @@ -671,8 +673,8 @@ impl MigrationsHelper { { if db_type == DatabaseType::SqlServer { // TODO Search a better way to get the datatype without useless info (like "VARCHAR(MAX)") - return canyon_register_entity_field - .to_sqlserver_alter_syntax() + return + to_sqlserver_alter_syntax(canyon_register_entity_field) .to_lowercase() == current_column_metadata.datatype; } @@ -786,7 +788,7 @@ impl DatabaseOperation for TableOperation { .map(|entity_field| format!( "\"{}\" {}", entity_field.field_name, - entity_field.to_postgres_syntax() + to_postgres_syntax(entity_field) )) .collect::>() .join(", ") @@ -801,7 +803,7 @@ impl DatabaseOperation for TableOperation { .map(|entity_field| format!( "{} {}", entity_field.field_name, - entity_field.to_sqlserver_syntax() + to_sqlserver_syntax(entity_field) )) .collect::>() .join(", ") @@ -924,14 +926,14 @@ impl DatabaseOperation for ColumnOperation { "ALTER TABLE \"{}\" ADD COLUMN \"{}\" {};", table_name, entity_field.field_name, - entity_field.to_postgres_syntax() + to_postgres_syntax(entity_field) ), #[cfg(feature = "mssql")] DatabaseType::SqlServer => format!( "ALTER TABLE {} ADD \"{}\" {};", table_name, entity_field.field_name, - entity_field.to_sqlserver_syntax() + to_sqlserver_syntax(entity_field) ) } ColumnOperation::DeleteColumn(table_name, column_name) => { @@ -943,7 +945,7 @@ impl DatabaseOperation for ColumnOperation { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( "ALTER TABLE \"{_table_name}\" ALTER COLUMN \"{}\" TYPE {};", - _entity_field.field_name, _entity_field.to_postgres_alter_syntax() + _entity_field.field_name, to_postgres_alter_syntax(_entity_field) ), #[cfg(feature = "mssql")] DatabaseType::SqlServer => todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") @@ -955,7 +957,7 @@ impl DatabaseOperation for ColumnOperation { #[cfg(feature = "mssql")] DatabaseType::SqlServer => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NULL", - entity_field.field_name, entity_field.to_sqlserver_alter_syntax() + entity_field.field_name, to_sqlserver_alter_syntax(entity_field) ) } #[cfg(feature = "mssql")] ColumnOperation::DropNotNullBeforeDropColumn(table_name, column_name, column_datatype) => @@ -981,7 +983,7 @@ impl DatabaseOperation for ColumnOperation { #[cfg(feature = "mssql")] DatabaseType::SqlServer => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NOT NULL", entity_field.field_name, - entity_field.to_sqlserver_alter_syntax() + to_sqlserver_alter_syntax(entity_field) ) } } diff --git a/canyon_migrations/src/migrations/register_types.rs b/canyon_migrations/src/migrations/register_types.rs deleted file mode 100644 index 14481c13..00000000 --- a/canyon_migrations/src/migrations/register_types.rs +++ /dev/null @@ -1,228 +0,0 @@ -use regex::Regex; - -#[cfg(feature = "postgres")] -use crate::constants::postgresql_type; -#[cfg(feature = "mssql")] -use crate::constants::sqlserver_type; -use crate::constants::{regex_patterns, rust_type, NUMERIC_PK_DATATYPE}; - -/// This file contains `Rust` types that represents an entry on the `CanyonRegister` -/// where `Canyon` tracks the user types that has to manage - -/// Gets the necessary identifiers of a CanyonEntity to make it the comparative -/// against the database schemas -#[derive(Debug, Clone, Default)] -pub struct CanyonRegisterEntity<'a> { - pub entity_name: &'a str, - pub entity_db_table_name: &'a str, - pub user_schema_name: Option<&'a str>, - pub entity_fields: Vec, -} - -/// Complementary type for a field that represents a struct field that maps -/// some real database column data -#[derive(Debug, Clone, Default)] -pub struct CanyonRegisterEntityField { - pub field_name: String, - pub field_type: String, - pub annotations: Vec, -} - -impl CanyonRegisterEntityField { - /// Return the postgres datatype and parameters to create a column for a given rust type - #[cfg(feature = "postgres")] - pub fn to_postgres_syntax(&self) -> String { - let rust_type_clean = self.field_type.replace(' ', ""); - - match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 => { - String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) - } - rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(postgresql_type::INTEGER), - - rust_type::I16 | rust_type::U16 => { - String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) - } - rust_type::OPT_I16 | rust_type::OPT_U16 => String::from(postgresql_type::INTEGER), - - rust_type::I32 | rust_type::U32 => { - String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) - } - rust_type::OPT_I32 | rust_type::OPT_U32 => String::from(postgresql_type::INTEGER), - - rust_type::I64 | rust_type::U64 => { - String::from(&format!("{} NOT NULL", postgresql_type::BIGINT)) - } - rust_type::OPT_I64 | rust_type::OPT_U64 => String::from(postgresql_type::BIGINT), - - rust_type::STRING => String::from(&format!("{} NOT NULL", postgresql_type::TEXT)), - rust_type::OPT_STRING => String::from(postgresql_type::TEXT), - - rust_type::BOOL => String::from(&format!("{} NOT NULL", postgresql_type::BOOLEAN)), - rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), - - rust_type::NAIVE_DATE => String::from(&format!("{} NOT NULL", postgresql_type::DATE)), - rust_type::OPT_NAIVE_DATE => String::from(postgresql_type::DATE), - - rust_type::NAIVE_TIME => String::from(&format!("{} NOT NULL", postgresql_type::TIME)), - rust_type::OPT_NAIVE_TIME => String::from(postgresql_type::TIME), - - rust_type::NAIVE_DATE_TIME => { - String::from(&format!("{} NOT NULL", postgresql_type::DATETIME)) - } - rust_type::OPT_NAIVE_DATE_TIME => String::from(postgresql_type::DATETIME), - &_ => todo!("Not supported datatype for this migrations version"), - } - } - - /// Return the postgres datatype and parameters to create a column for a given rust type - /// for Microsoft SQL Server - #[cfg(feature = "mssql")] - pub fn to_sqlserver_syntax(&self) -> String { - let rust_type_clean = self.field_type.replace(' ', ""); - - match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 => { - String::from(&format!("{} NOT NULL", sqlserver_type::INT)) - } - rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(sqlserver_type::INT), - - rust_type::I16 | rust_type::U16 => { - String::from(&format!("{} NOT NULL", sqlserver_type::INT)) - } - rust_type::OPT_I16 | rust_type::OPT_U16 => String::from(sqlserver_type::INT), - - rust_type::I32 | rust_type::U32 => { - String::from(&format!("{} NOT NULL", sqlserver_type::INT)) - } - rust_type::OPT_I32 | rust_type::OPT_U32 => String::from(sqlserver_type::INT), - - rust_type::I64 | rust_type::U64 => { - String::from(&format!("{} NOT NULL", sqlserver_type::BIGINT)) - } - rust_type::OPT_I64 | rust_type::OPT_U64 => String::from(sqlserver_type::BIGINT), - - rust_type::STRING => { - String::from(&format!("{} NOT NULL DEFAULT ''", sqlserver_type::NVARCHAR)) - } - rust_type::OPT_STRING => String::from(sqlserver_type::NVARCHAR), - - rust_type::BOOL => String::from(&format!("{} NOT NULL", sqlserver_type::BIT)), - rust_type::OPT_BOOL => String::from(sqlserver_type::BIT), - - rust_type::NAIVE_DATE => String::from(&format!("{} NOT NULL", sqlserver_type::DATE)), - rust_type::OPT_NAIVE_DATE => String::from(sqlserver_type::DATE), - - rust_type::NAIVE_TIME => String::from(&format!("{} NOT NULL", sqlserver_type::TIME)), - rust_type::OPT_NAIVE_TIME => String::from(sqlserver_type::TIME), - - rust_type::NAIVE_DATE_TIME => { - String::from(&format!("{} NOT NULL", sqlserver_type::DATETIME)) - } - rust_type::OPT_NAIVE_DATE_TIME => String::from(sqlserver_type::DATETIME), - &_ => todo!("Not supported datatype for this migrations version"), - } - } - - #[cfg(feature = "postgres")] - pub fn to_postgres_alter_syntax(&self) -> String { - let mut rust_type_clean = self.field_type.replace(' ', ""); - let rs_type_is_optional = self.field_type.to_uppercase().starts_with("OPTION"); - - if rs_type_is_optional { - let type_regex = Regex::new(regex_patterns::EXTRACT_RUST_OPT_REGEX).unwrap(); - let capture_rust_type = type_regex.captures(rust_type_clean.as_str()).unwrap(); - rust_type_clean = capture_rust_type - .name("rust_type") - .unwrap() - .as_str() - .to_string(); - } - - match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 | rust_type::OPT_I8 | rust_type::OPT_U8 => { - String::from(postgresql_type::INT_8) - } - rust_type::I16 | rust_type::U16 | rust_type::OPT_I16 | rust_type::OPT_U16 => { - String::from(postgresql_type::SMALL_INT) - } - rust_type::I32 | rust_type::U32 | rust_type::OPT_I32 | rust_type::OPT_U32 => { - String::from(postgresql_type::INTEGER) - } - rust_type::I64 | rust_type::U64 | rust_type::OPT_I64 | rust_type::OPT_U64 => { - String::from(postgresql_type::BIGINT) - } - rust_type::STRING | rust_type::OPT_STRING => String::from(postgresql_type::TEXT), - rust_type::BOOL | rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), - rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => { - String::from(postgresql_type::DATE) - } - rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => { - String::from(postgresql_type::TIME) - } - rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { - String::from(postgresql_type::DATETIME) - } - &_ => todo!("Not supported datatype for this migrations version"), - } - } - - #[cfg(feature = "mssql")] - pub fn to_sqlserver_alter_syntax(&self) -> String { - let mut rust_type_clean = self.field_type.replace(' ', ""); - let rs_type_is_optional = self.field_type.to_uppercase().starts_with("OPTION"); - - if rs_type_is_optional { - let type_regex = Regex::new(regex_patterns::EXTRACT_RUST_OPT_REGEX).unwrap(); - let capture_rust_type = type_regex.captures(rust_type_clean.as_str()).unwrap(); - rust_type_clean = capture_rust_type - .name("rust_type") - .unwrap() - .as_str() - .to_string(); - } - - match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 | rust_type::OPT_I8 | rust_type::OPT_U8 => { - String::from(sqlserver_type::TINY_INT) - } - rust_type::I16 | rust_type::U16 | rust_type::OPT_I16 | rust_type::OPT_U16 => { - String::from(sqlserver_type::SMALL_INT) - } - rust_type::I32 | rust_type::U32 | rust_type::OPT_I32 | rust_type::OPT_U32 => { - String::from(sqlserver_type::INT) - } - rust_type::I64 | rust_type::U64 | rust_type::OPT_I64 | rust_type::OPT_U64 => { - String::from(sqlserver_type::BIGINT) - } - rust_type::STRING | rust_type::OPT_STRING => String::from(sqlserver_type::NVARCHAR), - rust_type::BOOL | rust_type::OPT_BOOL => String::from(sqlserver_type::BIT), - rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => String::from(sqlserver_type::DATE), - rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => String::from(sqlserver_type::TIME), - rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { - String::from(sqlserver_type::DATETIME) - } - &_ => todo!("Not supported datatype for this migrations version"), - } - } - - /// Return if the field is autoincremental - pub fn is_autoincremental(&self) -> bool { - let has_pk_annotation = self - .annotations - .iter() - .find(|a| a.starts_with("Annotation: PrimaryKey")); - - let pk_is_autoincremental = match has_pk_annotation { - Some(annotation) => annotation.contains("true"), - None => false, - }; - - NUMERIC_PK_DATATYPE.contains(&self.field_type.as_str()) && pk_is_autoincremental - } - - /// Return the nullability of a the field - pub fn is_nullable(&self) -> bool { - self.field_type.to_uppercase().starts_with("OPTION") - } -} diff --git a/canyon_migrations/src/migrations/transforms.rs b/canyon_migrations/src/migrations/transforms.rs new file mode 100644 index 00000000..e69de29b diff --git a/tests/canyon.toml b/tests/canyon.toml index 0b0614a4..417a6c82 100644 --- a/tests/canyon.toml +++ b/tests/canyon.toml @@ -11,14 +11,14 @@ host = 'localhost' port = 5438 db_name = 'postgres' - -[[canyon_sql.datasources]] -name = 'sqlserver_docker' - -[canyon_sql.datasources.auth] -sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } - -[canyon_sql.datasources.properties] -host = 'localhost' -port = 1434 -db_name = 'master' +# +#[[canyon_sql.datasources]] +#name = 'sqlserver_docker' +# +#[canyon_sql.datasources.auth] +#sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } +# +#[canyon_sql.datasources.properties] +#host = 'localhost' +#port = 1434 +#db_name = 'master' From 11604da586b28000a6a6a992c0eae311da01a2e6 Mon Sep 17 00:00:00 2001 From: Gonzalo Busto Musi Date: Sun, 23 Apr 2023 11:46:49 +0200 Subject: [PATCH 59/82] Refactoring functions rust datatype to database datatype to be standalone --- canyon_migrations/src/migrations/mod.rs | 184 ------------------ canyon_migrations/src/migrations/processor.rs | 4 +- .../src/migrations/transforms.rs | 184 ++++++++++++++++++ tests/canyon.toml | 22 +-- 4 files changed, 197 insertions(+), 197 deletions(-) diff --git a/canyon_migrations/src/migrations/mod.rs b/canyon_migrations/src/migrations/mod.rs index ec717c77..1b139fdd 100644 --- a/canyon_migrations/src/migrations/mod.rs +++ b/canyon_migrations/src/migrations/mod.rs @@ -1,189 +1,5 @@ -#[cfg(feature = "postgres")] use crate::constants::postgresql_type; -#[cfg(feature = "mssql")] use crate::constants::sqlserver_type; -use crate::constants::{regex_patterns, rust_type}; - pub mod handler; pub mod information_schema; pub mod memory; pub mod processor; pub mod transforms; - -use canyon_entities::register_types::CanyonRegisterEntityField; -use regex::Regex; - -/// Return the postgres datatype and parameters to create a column for a given rust type -#[cfg(feature = "postgres")] -pub fn to_postgres_syntax(field: &CanyonRegisterEntityField) -> String { - let rust_type_clean = field.field_type.replace(' ', ""); - - match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 => { - String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) - } - rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(postgresql_type::INTEGER), - - rust_type::I16 | rust_type::U16 => { - String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) - } - rust_type::OPT_I16 | rust_type::OPT_U16 => String::from(postgresql_type::INTEGER), - - rust_type::I32 | rust_type::U32 => { - String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) - } - rust_type::OPT_I32 | rust_type::OPT_U32 => String::from(postgresql_type::INTEGER), - - rust_type::I64 | rust_type::U64 => { - String::from(&format!("{} NOT NULL", postgresql_type::BIGINT)) - } - rust_type::OPT_I64 | rust_type::OPT_U64 => String::from(postgresql_type::BIGINT), - - rust_type::STRING => String::from(&format!("{} NOT NULL", postgresql_type::TEXT)), - rust_type::OPT_STRING => String::from(postgresql_type::TEXT), - - rust_type::BOOL => String::from(&format!("{} NOT NULL", postgresql_type::BOOLEAN)), - rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), - - rust_type::NAIVE_DATE => String::from(&format!("{} NOT NULL", postgresql_type::DATE)), - rust_type::OPT_NAIVE_DATE => String::from(postgresql_type::DATE), - - rust_type::NAIVE_TIME => String::from(&format!("{} NOT NULL", postgresql_type::TIME)), - rust_type::OPT_NAIVE_TIME => String::from(postgresql_type::TIME), - - rust_type::NAIVE_DATE_TIME => { - String::from(&format!("{} NOT NULL", postgresql_type::DATETIME)) - } - rust_type::OPT_NAIVE_DATE_TIME => String::from(postgresql_type::DATETIME), - &_ => todo!("Not supported datatype for this migrations version"), - } -} - -/// Return the postgres datatype and parameters to create a column for a given rust type -/// for Microsoft SQL Server -#[cfg(feature = "mssql")] -pub fn to_sqlserver_syntax(field: &CanyonRegisterEntityField) -> String { - let rust_type_clean = field.field_type.replace(' ', ""); - - match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 => { - String::from(&format!("{} NOT NULL", sqlserver_type::INT)) - } - rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(sqlserver_type::INT), - - rust_type::I16 | rust_type::U16 => { - String::from(&format!("{} NOT NULL", sqlserver_type::INT)) - } - rust_type::OPT_I16 | rust_type::OPT_U16 => String::from(sqlserver_type::INT), - - rust_type::I32 | rust_type::U32 => { - String::from(&format!("{} NOT NULL", sqlserver_type::INT)) - } - rust_type::OPT_I32 | rust_type::OPT_U32 => String::from(sqlserver_type::INT), - - rust_type::I64 | rust_type::U64 => { - String::from(&format!("{} NOT NULL", sqlserver_type::BIGINT)) - } - rust_type::OPT_I64 | rust_type::OPT_U64 => String::from(sqlserver_type::BIGINT), - - rust_type::STRING => { - String::from(&format!("{} NOT NULL DEFAULT ''", sqlserver_type::NVARCHAR)) - } - rust_type::OPT_STRING => String::from(sqlserver_type::NVARCHAR), - - rust_type::BOOL => String::from(&format!("{} NOT NULL", sqlserver_type::BIT)), - rust_type::OPT_BOOL => String::from(sqlserver_type::BIT), - - rust_type::NAIVE_DATE => String::from(&format!("{} NOT NULL", sqlserver_type::DATE)), - rust_type::OPT_NAIVE_DATE => String::from(sqlserver_type::DATE), - - rust_type::NAIVE_TIME => String::from(&format!("{} NOT NULL", sqlserver_type::TIME)), - rust_type::OPT_NAIVE_TIME => String::from(sqlserver_type::TIME), - - rust_type::NAIVE_DATE_TIME => { - String::from(&format!("{} NOT NULL", sqlserver_type::DATETIME)) - } - rust_type::OPT_NAIVE_DATE_TIME => String::from(sqlserver_type::DATETIME), - &_ => todo!("Not supported datatype for this migrations version"), - } -} - -#[cfg(feature = "postgres")] -pub fn to_postgres_alter_syntax(field: &CanyonRegisterEntityField) -> String { - let mut rust_type_clean = field.field_type.replace(' ', ""); - let rs_type_is_optional = field.field_type.to_uppercase().starts_with("OPTION"); - - if rs_type_is_optional { - let type_regex = Regex::new(regex_patterns::EXTRACT_RUST_OPT_REGEX).unwrap(); - let capture_rust_type = type_regex.captures(rust_type_clean.as_str()).unwrap(); - rust_type_clean = capture_rust_type - .name("rust_type") - .unwrap() - .as_str() - .to_string(); - } - - match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 | rust_type::OPT_I8 | rust_type::OPT_U8 => { - String::from(postgresql_type::INT_8) - } - rust_type::I16 | rust_type::U16 | rust_type::OPT_I16 | rust_type::OPT_U16 => { - String::from(postgresql_type::SMALL_INT) - } - rust_type::I32 | rust_type::U32 | rust_type::OPT_I32 | rust_type::OPT_U32 => { - String::from(postgresql_type::INTEGER) - } - rust_type::I64 | rust_type::U64 | rust_type::OPT_I64 | rust_type::OPT_U64 => { - String::from(postgresql_type::BIGINT) - } - rust_type::STRING | rust_type::OPT_STRING => String::from(postgresql_type::TEXT), - rust_type::BOOL | rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), - rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => { - String::from(postgresql_type::DATE) - } - rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => { - String::from(postgresql_type::TIME) - } - rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { - String::from(postgresql_type::DATETIME) - } - &_ => todo!("Not supported datatype for this migrations version"), - } -} - -#[cfg(feature = "mssql")] -pub fn to_sqlserver_alter_syntax(field: &CanyonRegisterEntityField) -> String { - let mut rust_type_clean = field.field_type.replace(' ', ""); - let rs_type_is_optional = field.field_type.to_uppercase().starts_with("OPTION"); - - if rs_type_is_optional { - let type_regex = Regex::new(regex_patterns::EXTRACT_RUST_OPT_REGEX).unwrap(); - let capture_rust_type = type_regex.captures(rust_type_clean.as_str()).unwrap(); - rust_type_clean = capture_rust_type - .name("rust_type") - .unwrap() - .as_str() - .to_string(); - } - - match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 | rust_type::OPT_I8 | rust_type::OPT_U8 => { - String::from(sqlserver_type::TINY_INT) - } - rust_type::I16 | rust_type::U16 | rust_type::OPT_I16 | rust_type::OPT_U16 => { - String::from(sqlserver_type::SMALL_INT) - } - rust_type::I32 | rust_type::U32 | rust_type::OPT_I32 | rust_type::OPT_U32 => { - String::from(sqlserver_type::INT) - } - rust_type::I64 | rust_type::U64 | rust_type::OPT_I64 | rust_type::OPT_U64 => { - String::from(sqlserver_type::BIGINT) - } - rust_type::STRING | rust_type::OPT_STRING => String::from(sqlserver_type::NVARCHAR), - rust_type::BOOL | rust_type::OPT_BOOL => String::from(sqlserver_type::BIT), - rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => String::from(sqlserver_type::DATE), - rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => String::from(sqlserver_type::TIME), - rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { - String::from(sqlserver_type::DATETIME) - } - &_ => todo!("Not supported datatype for this migrations version"), - } -} \ No newline at end of file diff --git a/canyon_migrations/src/migrations/processor.rs b/canyon_migrations/src/migrations/processor.rs index 5391a494..2e4f1769 100644 --- a/canyon_migrations/src/migrations/processor.rs +++ b/canyon_migrations/src/migrations/processor.rs @@ -14,8 +14,8 @@ use crate::save_migrations_query_to_execute; use super::information_schema::{ColumnMetadata, TableMetadata}; use super::memory::CanyonMemory; use canyon_entities::register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}; -#[cfg(feature = "postgres")] use crate::migrations::{to_postgres_alter_syntax, to_postgres_syntax}; -#[cfg(feature = "mssql")] use crate::migrations::{to_sqlserver_alter_syntax, to_sqlserver_syntax}; +#[cfg(feature = "postgres")] use crate::migrations::transforms::{to_postgres_alter_syntax, to_postgres_syntax}; +#[cfg(feature = "mssql")] use crate::migrations::transforms::{to_sqlserver_alter_syntax, to_sqlserver_syntax}; /// Responsible of generating the queries to sync the database status with the /// Rust source code managed by Canyon, for successfully make the migrations diff --git a/canyon_migrations/src/migrations/transforms.rs b/canyon_migrations/src/migrations/transforms.rs index e69de29b..2c153a48 100644 --- a/canyon_migrations/src/migrations/transforms.rs +++ b/canyon_migrations/src/migrations/transforms.rs @@ -0,0 +1,184 @@ +#[cfg(feature = "postgres")] use crate::constants::postgresql_type; +#[cfg(feature = "mssql")] use crate::constants::sqlserver_type; +use crate::constants::{regex_patterns, rust_type}; + + +use canyon_entities::register_types::CanyonRegisterEntityField; +use regex::Regex; + +/// Return the postgres datatype and parameters to create a column for a given rust type +#[cfg(feature = "postgres")] +pub fn to_postgres_syntax(field: &CanyonRegisterEntityField) -> String { + let rust_type_clean = field.field_type.replace(' ', ""); + + match rust_type_clean.as_str() { + rust_type::I8 | rust_type::U8 => { + String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) + } + rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(postgresql_type::INTEGER), + + rust_type::I16 | rust_type::U16 => { + String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) + } + rust_type::OPT_I16 | rust_type::OPT_U16 => String::from(postgresql_type::INTEGER), + + rust_type::I32 | rust_type::U32 => { + String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) + } + rust_type::OPT_I32 | rust_type::OPT_U32 => String::from(postgresql_type::INTEGER), + + rust_type::I64 | rust_type::U64 => { + String::from(&format!("{} NOT NULL", postgresql_type::BIGINT)) + } + rust_type::OPT_I64 | rust_type::OPT_U64 => String::from(postgresql_type::BIGINT), + + rust_type::STRING => String::from(&format!("{} NOT NULL", postgresql_type::TEXT)), + rust_type::OPT_STRING => String::from(postgresql_type::TEXT), + + rust_type::BOOL => String::from(&format!("{} NOT NULL", postgresql_type::BOOLEAN)), + rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), + + rust_type::NAIVE_DATE => String::from(&format!("{} NOT NULL", postgresql_type::DATE)), + rust_type::OPT_NAIVE_DATE => String::from(postgresql_type::DATE), + + rust_type::NAIVE_TIME => String::from(&format!("{} NOT NULL", postgresql_type::TIME)), + rust_type::OPT_NAIVE_TIME => String::from(postgresql_type::TIME), + + rust_type::NAIVE_DATE_TIME => { + String::from(&format!("{} NOT NULL", postgresql_type::DATETIME)) + } + rust_type::OPT_NAIVE_DATE_TIME => String::from(postgresql_type::DATETIME), + &_ => todo!("Not supported datatype for this migrations version"), + } +} + +/// Return the postgres datatype and parameters to create a column for a given rust type +/// for Microsoft SQL Server +#[cfg(feature = "mssql")] +pub fn to_sqlserver_syntax(field: &CanyonRegisterEntityField) -> String { + let rust_type_clean = field.field_type.replace(' ', ""); + + match rust_type_clean.as_str() { + rust_type::I8 | rust_type::U8 => { + String::from(&format!("{} NOT NULL", sqlserver_type::INT)) + } + rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(sqlserver_type::INT), + + rust_type::I16 | rust_type::U16 => { + String::from(&format!("{} NOT NULL", sqlserver_type::INT)) + } + rust_type::OPT_I16 | rust_type::OPT_U16 => String::from(sqlserver_type::INT), + + rust_type::I32 | rust_type::U32 => { + String::from(&format!("{} NOT NULL", sqlserver_type::INT)) + } + rust_type::OPT_I32 | rust_type::OPT_U32 => String::from(sqlserver_type::INT), + + rust_type::I64 | rust_type::U64 => { + String::from(&format!("{} NOT NULL", sqlserver_type::BIGINT)) + } + rust_type::OPT_I64 | rust_type::OPT_U64 => String::from(sqlserver_type::BIGINT), + + rust_type::STRING => { + String::from(&format!("{} NOT NULL DEFAULT ''", sqlserver_type::NVARCHAR)) + } + rust_type::OPT_STRING => String::from(sqlserver_type::NVARCHAR), + + rust_type::BOOL => String::from(&format!("{} NOT NULL", sqlserver_type::BIT)), + rust_type::OPT_BOOL => String::from(sqlserver_type::BIT), + + rust_type::NAIVE_DATE => String::from(&format!("{} NOT NULL", sqlserver_type::DATE)), + rust_type::OPT_NAIVE_DATE => String::from(sqlserver_type::DATE), + + rust_type::NAIVE_TIME => String::from(&format!("{} NOT NULL", sqlserver_type::TIME)), + rust_type::OPT_NAIVE_TIME => String::from(sqlserver_type::TIME), + + rust_type::NAIVE_DATE_TIME => { + String::from(&format!("{} NOT NULL", sqlserver_type::DATETIME)) + } + rust_type::OPT_NAIVE_DATE_TIME => String::from(sqlserver_type::DATETIME), + &_ => todo!("Not supported datatype for this migrations version"), + } +} + +#[cfg(feature = "postgres")] +pub fn to_postgres_alter_syntax(field: &CanyonRegisterEntityField) -> String { + let mut rust_type_clean = field.field_type.replace(' ', ""); + let rs_type_is_optional = field.field_type.to_uppercase().starts_with("OPTION"); + + if rs_type_is_optional { + let type_regex = Regex::new(regex_patterns::EXTRACT_RUST_OPT_REGEX).unwrap(); + let capture_rust_type = type_regex.captures(rust_type_clean.as_str()).unwrap(); + rust_type_clean = capture_rust_type + .name("rust_type") + .unwrap() + .as_str() + .to_string(); + } + + match rust_type_clean.as_str() { + rust_type::I8 | rust_type::U8 | rust_type::OPT_I8 | rust_type::OPT_U8 => { + String::from(postgresql_type::INT_8) + } + rust_type::I16 | rust_type::U16 | rust_type::OPT_I16 | rust_type::OPT_U16 => { + String::from(postgresql_type::SMALL_INT) + } + rust_type::I32 | rust_type::U32 | rust_type::OPT_I32 | rust_type::OPT_U32 => { + String::from(postgresql_type::INTEGER) + } + rust_type::I64 | rust_type::U64 | rust_type::OPT_I64 | rust_type::OPT_U64 => { + String::from(postgresql_type::BIGINT) + } + rust_type::STRING | rust_type::OPT_STRING => String::from(postgresql_type::TEXT), + rust_type::BOOL | rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), + rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => { + String::from(postgresql_type::DATE) + } + rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => { + String::from(postgresql_type::TIME) + } + rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { + String::from(postgresql_type::DATETIME) + } + &_ => todo!("Not supported datatype for this migrations version"), + } +} + +#[cfg(feature = "mssql")] +pub fn to_sqlserver_alter_syntax(field: &CanyonRegisterEntityField) -> String { + let mut rust_type_clean = field.field_type.replace(' ', ""); + let rs_type_is_optional = field.field_type.to_uppercase().starts_with("OPTION"); + + if rs_type_is_optional { + let type_regex = Regex::new(regex_patterns::EXTRACT_RUST_OPT_REGEX).unwrap(); + let capture_rust_type = type_regex.captures(rust_type_clean.as_str()).unwrap(); + rust_type_clean = capture_rust_type + .name("rust_type") + .unwrap() + .as_str() + .to_string(); + } + + match rust_type_clean.as_str() { + rust_type::I8 | rust_type::U8 | rust_type::OPT_I8 | rust_type::OPT_U8 => { + String::from(sqlserver_type::TINY_INT) + } + rust_type::I16 | rust_type::U16 | rust_type::OPT_I16 | rust_type::OPT_U16 => { + String::from(sqlserver_type::SMALL_INT) + } + rust_type::I32 | rust_type::U32 | rust_type::OPT_I32 | rust_type::OPT_U32 => { + String::from(sqlserver_type::INT) + } + rust_type::I64 | rust_type::U64 | rust_type::OPT_I64 | rust_type::OPT_U64 => { + String::from(sqlserver_type::BIGINT) + } + rust_type::STRING | rust_type::OPT_STRING => String::from(sqlserver_type::NVARCHAR), + rust_type::BOOL | rust_type::OPT_BOOL => String::from(sqlserver_type::BIT), + rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => String::from(sqlserver_type::DATE), + rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => String::from(sqlserver_type::TIME), + rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { + String::from(sqlserver_type::DATETIME) + } + &_ => todo!("Not supported datatype for this migrations version"), + } +} \ No newline at end of file diff --git a/tests/canyon.toml b/tests/canyon.toml index 417a6c82..0b0614a4 100644 --- a/tests/canyon.toml +++ b/tests/canyon.toml @@ -11,14 +11,14 @@ host = 'localhost' port = 5438 db_name = 'postgres' -# -#[[canyon_sql.datasources]] -#name = 'sqlserver_docker' -# -#[canyon_sql.datasources.auth] -#sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } -# -#[canyon_sql.datasources.properties] -#host = 'localhost' -#port = 1434 -#db_name = 'master' + +[[canyon_sql.datasources]] +name = 'sqlserver_docker' + +[canyon_sql.datasources.auth] +sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } + +[canyon_sql.datasources.properties] +host = 'localhost' +port = 1434 +db_name = 'master' From d81c9e47d1670ff125255c7fb38afb2e09bb453f Mon Sep 17 00:00:00 2001 From: Gonzalo Busto Musi Date: Sun, 23 Apr 2023 11:48:22 +0200 Subject: [PATCH 60/82] Cargo fmt --- canyon_macros/src/canyon_macro.rs | 6 +++--- canyon_macros/src/lib.rs | 18 +++++++++------- canyon_migrations/src/lib.rs | 1 - canyon_migrations/src/migrations/handler.rs | 4 ++-- canyon_migrations/src/migrations/processor.rs | 14 ++++++------- .../src/migrations/transforms.rs | 21 +++++++------------ 6 files changed, 29 insertions(+), 35 deletions(-) diff --git a/canyon_macros/src/canyon_macro.rs b/canyon_macros/src/canyon_macro.rs index cc4aa61e..48c89fcc 100644 --- a/canyon_macros/src/canyon_macro.rs +++ b/canyon_macros/src/canyon_macro.rs @@ -1,10 +1,10 @@ //! Provides helpers to build the `#[canyon_macros::canyon]` procedural like attribute macro -use proc_macro2::TokenStream; -use quote::quote; use canyon_connection::CANYON_TOKIO_RUNTIME; -use canyon_migrations::{CM_QUERIES_TO_EXECUTE, QUERIES_TO_EXECUTE}; use canyon_migrations::migrations::handler::Migrations; +use canyon_migrations::{CM_QUERIES_TO_EXECUTE, QUERIES_TO_EXECUTE}; +use proc_macro2::TokenStream; +use quote::quote; #[cfg(feature = "migrations")] pub fn main_with_queries() -> TokenStream { diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index f35612ef..160f6ece 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -1,9 +1,10 @@ extern crate proc_macro; -mod utils; -mod query_operations; mod canyon_entity_macro; -#[cfg(feature = "migrations")] mod canyon_macro; +#[cfg(feature = "migrations")] +mod canyon_macro; +mod query_operations; +mod utils; use canyon_entity_macro::parse_canyon_entity_proc_macro_attr; use proc_macro::TokenStream as CompilerTokenStream; @@ -11,7 +12,8 @@ use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use syn::{DeriveInput, Fields, Type, Visibility}; -#[cfg(feature = "migrations")] use canyon_macro::main_with_queries; +#[cfg(feature = "migrations")] +use canyon_macro::main_with_queries; use query_operations::{ delete::{generate_delete_query_tokens, generate_delete_tokens}, @@ -26,15 +28,14 @@ use query_operations::{ use utils::{function_parser::FunctionParser, helpers, macro_tokens::MacroTokens}; use canyon_entities::{ - CANYON_REGISTER_ENTITIES, entity::CanyonEntity, manager_builder::{ generate_enum_with_fields, generate_enum_with_fields_values, generate_user_struct, }, - register_types::{CanyonRegisterEntity, CanyonRegisterEntityField} + register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}, + CANYON_REGISTER_ENTITIES, }; - /// Macro for handling the entry point to the program. /// /// Avoids the user to write the tokio proc_attribute and @@ -57,7 +58,8 @@ pub fn main(_meta: CompilerTokenStream, input: CompilerTokenStream) -> CompilerT #[allow(unused_mut, unused_assignments)] let mut migrations_tokens = quote! {}; - #[cfg(feature = "migrations")] { + #[cfg(feature = "migrations")] + { migrations_tokens = main_with_queries(); } diff --git a/canyon_migrations/src/lib.rs b/canyon_migrations/src/lib.rs index ec939e12..5743cc8b 100644 --- a/canyon_migrations/src/lib.rs +++ b/canyon_migrations/src/lib.rs @@ -20,7 +20,6 @@ mod constants; use canyon_connection::lazy_static::lazy_static; use std::{collections::HashMap, sync::Mutex}; - lazy_static! { pub static ref QUERIES_TO_EXECUTE: Mutex>> = Mutex::new(HashMap::new()); diff --git a/canyon_migrations/src/migrations/handler.rs b/canyon_migrations/src/migrations/handler.rs index 33e9b5bb..24dfb1c4 100644 --- a/canyon_migrations/src/migrations/handler.rs +++ b/canyon_migrations/src/migrations/handler.rs @@ -1,7 +1,7 @@ use canyon_connection::{datasources::Migrations as MigrationsStatus, DATASOURCES}; use canyon_crud::rows::CanyonRows; -use partialdebug::placeholder::PartialDebug; use canyon_entities::CANYON_REGISTER_ENTITIES; +use partialdebug::placeholder::PartialDebug; use crate::{ canyon_crud::{ @@ -14,7 +14,7 @@ use crate::{ information_schema::{ColumnMetadata, ColumnMetadataTypeValue, TableMetadata}, memory::CanyonMemory, processor::MigrationsProcessor, - } + }, }; #[derive(PartialDebug)] diff --git a/canyon_migrations/src/migrations/processor.rs b/canyon_migrations/src/migrations/processor.rs index 2e4f1769..425c1b0d 100644 --- a/canyon_migrations/src/migrations/processor.rs +++ b/canyon_migrations/src/migrations/processor.rs @@ -13,9 +13,11 @@ use crate::save_migrations_query_to_execute; use super::information_schema::{ColumnMetadata, TableMetadata}; use super::memory::CanyonMemory; +#[cfg(feature = "postgres")] +use crate::migrations::transforms::{to_postgres_alter_syntax, to_postgres_syntax}; +#[cfg(feature = "mssql")] +use crate::migrations::transforms::{to_sqlserver_alter_syntax, to_sqlserver_syntax}; use canyon_entities::register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}; -#[cfg(feature = "postgres")] use crate::migrations::transforms::{to_postgres_alter_syntax, to_postgres_syntax}; -#[cfg(feature = "mssql")] use crate::migrations::transforms::{to_sqlserver_alter_syntax, to_sqlserver_syntax}; /// Responsible of generating the queries to sync the database status with the /// Rust source code managed by Canyon, for successfully make the migrations @@ -663,9 +665,7 @@ impl MigrationsHelper { #[cfg(feature = "postgres")] { if db_type == DatabaseType::PostgreSql { - return - to_postgres_alter_syntax(canyon_register_entity_field) - .to_lowercase() + return to_postgres_alter_syntax(canyon_register_entity_field).to_lowercase() == current_column_metadata.datatype; } } @@ -673,9 +673,7 @@ impl MigrationsHelper { { if db_type == DatabaseType::SqlServer { // TODO Search a better way to get the datatype without useless info (like "VARCHAR(MAX)") - return - to_sqlserver_alter_syntax(canyon_register_entity_field) - .to_lowercase() + return to_sqlserver_alter_syntax(canyon_register_entity_field).to_lowercase() == current_column_metadata.datatype; } } diff --git a/canyon_migrations/src/migrations/transforms.rs b/canyon_migrations/src/migrations/transforms.rs index 2c153a48..6d14e478 100644 --- a/canyon_migrations/src/migrations/transforms.rs +++ b/canyon_migrations/src/migrations/transforms.rs @@ -1,8 +1,9 @@ -#[cfg(feature = "postgres")] use crate::constants::postgresql_type; -#[cfg(feature = "mssql")] use crate::constants::sqlserver_type; +#[cfg(feature = "postgres")] +use crate::constants::postgresql_type; +#[cfg(feature = "mssql")] +use crate::constants::sqlserver_type; use crate::constants::{regex_patterns, rust_type}; - use canyon_entities::register_types::CanyonRegisterEntityField; use regex::Regex; @@ -59,9 +60,7 @@ pub fn to_sqlserver_syntax(field: &CanyonRegisterEntityField) -> String { let rust_type_clean = field.field_type.replace(' ', ""); match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 => { - String::from(&format!("{} NOT NULL", sqlserver_type::INT)) - } + rust_type::I8 | rust_type::U8 => String::from(&format!("{} NOT NULL", sqlserver_type::INT)), rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(sqlserver_type::INT), rust_type::I16 | rust_type::U16 => { @@ -131,12 +130,8 @@ pub fn to_postgres_alter_syntax(field: &CanyonRegisterEntityField) -> String { } rust_type::STRING | rust_type::OPT_STRING => String::from(postgresql_type::TEXT), rust_type::BOOL | rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), - rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => { - String::from(postgresql_type::DATE) - } - rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => { - String::from(postgresql_type::TIME) - } + rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => String::from(postgresql_type::DATE), + rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => String::from(postgresql_type::TIME), rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { String::from(postgresql_type::DATETIME) } @@ -181,4 +176,4 @@ pub fn to_sqlserver_alter_syntax(field: &CanyonRegisterEntityField) -> String { } &_ => todo!("Not supported datatype for this migrations version"), } -} \ No newline at end of file +} From 4cbb9d15b7f21f4d71c6a018243738d4be451aa7 Mon Sep 17 00:00:00 2001 From: Gonzalo Busto Musi Date: Sun, 23 Apr 2023 11:49:35 +0200 Subject: [PATCH 61/82] Added lib and Cargo.toml for canyon_entities --- canyon_entities/Cargo.toml | 17 +++++++++++++++++ canyon_entities/src/lib.rs | 11 +++++++++++ 2 files changed, 28 insertions(+) create mode 100644 canyon_entities/Cargo.toml create mode 100644 canyon_entities/src/lib.rs diff --git a/canyon_entities/Cargo.toml b/canyon_entities/Cargo.toml new file mode 100644 index 00000000..374e2e98 --- /dev/null +++ b/canyon_entities/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "canyon_entities" +version.workspace = true +edition.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true + +[dependencies] +regex = { workspace = true } +partialdebug = { workspace = true } +quote = { workspace = true } +proc-macro2 = { workspace = true } +syn = { version = "1.0.86", features = ["full", "parsing"] } # TODO Pending to refactor and upgrade diff --git a/canyon_entities/src/lib.rs b/canyon_entities/src/lib.rs new file mode 100644 index 00000000..8b3abd6c --- /dev/null +++ b/canyon_entities/src/lib.rs @@ -0,0 +1,11 @@ +use crate::register_types::CanyonRegisterEntity; +use std::sync::Mutex; + +pub mod entity; +pub mod entity_fields; +pub mod field_annotation; +pub mod manager_builder; +pub mod register_types; + +pub static CANYON_REGISTER_ENTITIES: Mutex>> = + Mutex::new(Vec::new()); From f8bd873ed81c1ee2b1302d64e4131e3a02031b39 Mon Sep 17 00:00:00 2001 From: Alex Vergara <68871459+Pyzyryab@users.noreply.github.com> Date: Sun, 23 Apr 2023 12:00:20 +0200 Subject: [PATCH 62/82] Added "migrations" feature (#41) * Creating feature "migrations". Changed "canyon_observer" to "canyon_migrations" * WIP - Bringing back canyon_manager as canyon_entities * Refactoring functions rust datatype to database datatype to be standalone * Cargo fmt * Added lib and Cargo.toml for canyon_entities --------- Co-authored-by: Gonzalo Busto Musi --- .github/workflows/code-quality.yml | 2 +- Cargo.toml | 17 +- bash_aliases.sh | 2 +- canyon_entities/Cargo.toml | 17 ++ .../manager => canyon_entities/src}/entity.rs | 2 +- .../src}/entity_fields.rs | 0 .../src}/field_annotation.rs | 0 canyon_entities/src/lib.rs | 11 + .../src}/manager_builder.rs | 0 canyon_entities/src/register_types.rs | 45 ++++ canyon_macros/Cargo.toml | 10 +- canyon_macros/src/canyon_macro.rs | 116 ++------- canyon_macros/src/lib.rs | 83 ++----- canyon_macros/src/query_operations/select.rs | 2 +- canyon_macros/src/utils/macro_tokens.rs | 2 +- .../Cargo.toml | 7 +- .../src/constants.rs | 2 - .../src/lib.rs | 5 +- .../src/migrations/handler.rs | 2 +- .../src/migrations/information_schema.rs | 0 .../src/migrations/memory.rs | 2 +- .../src/migrations/mod.rs | 2 +- .../src/migrations/processor.rs | 28 +-- .../src/migrations/transforms.rs | 179 ++++++++++++++ canyon_observer/src/manager/mod.rs | 4 - .../src/migrations/register_types.rs | 228 ------------------ src/lib.rs | 4 +- 27 files changed, 342 insertions(+), 430 deletions(-) create mode 100644 canyon_entities/Cargo.toml rename {canyon_observer/src/manager => canyon_entities/src}/entity.rs (98%) rename {canyon_observer/src/manager => canyon_entities/src}/entity_fields.rs (100%) rename {canyon_observer/src/manager => canyon_entities/src}/field_annotation.rs (100%) create mode 100644 canyon_entities/src/lib.rs rename {canyon_observer/src/manager => canyon_entities/src}/manager_builder.rs (100%) create mode 100644 canyon_entities/src/register_types.rs rename {canyon_observer => canyon_migrations}/Cargo.toml (89%) rename {canyon_observer => canyon_migrations}/src/constants.rs (99%) rename {canyon_observer => canyon_migrations}/src/lib.rs (87%) rename {canyon_observer => canyon_migrations}/src/migrations/handler.rs (99%) rename {canyon_observer => canyon_migrations}/src/migrations/information_schema.rs (100%) rename {canyon_observer => canyon_migrations}/src/migrations/memory.rs (99%) rename {canyon_observer => canyon_migrations}/src/migrations/mod.rs (76%) rename {canyon_observer => canyon_migrations}/src/migrations/processor.rs (97%) create mode 100644 canyon_migrations/src/migrations/transforms.rs delete mode 100644 canyon_observer/src/manager/mod.rs delete mode 100644 canyon_observer/src/migrations/register_types.rs diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 9de14f14..b955295c 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -55,7 +55,7 @@ jobs: strategy: fail-fast: false matrix: - crate: [canyon_connection, canyon_crud, canyon_macros, canyon_observer] + crate: [canyon_connection, canyon_crud, canyon_macros, canyon_migrations] steps: - uses: actions/checkout@v3 diff --git a/Cargo.toml b/Cargo.toml index a0bba641..dcfb553f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,8 @@ description.workspace = true members = [ "canyon_connection", "canyon_crud", - "canyon_observer", + "canyon_entities", + "canyon_migrations", "canyon_macros", "tests" @@ -23,7 +24,8 @@ members = [ # Project crates canyon_connection = { workspace = true, path = "canyon_connection" } canyon_crud = { workspace = true, path = "canyon_crud" } -canyon_observer = { workspace = true, path = "canyon_observer" } +canyon_entities = { workspace = true, path = "canyon_entities" } +canyon_migrations = { workspace = true, path = "canyon_migrations", optional = true } canyon_macros = { workspace = true, path = "canyon_macros" } # To be marked as opt deps @@ -33,7 +35,8 @@ tiberius = { workspace = true, optional = true } [workspace.dependencies] canyon_crud = { version = "0.3.1", path = "canyon_crud" } canyon_connection = { version = "0.3.1", path = "canyon_connection" } -canyon_observer = { version = "0.3.1", path = "canyon_observer" } +canyon_entities = { version = "0.3.1", path = "canyon_entities" } +canyon_migrations = { version = "0.3.1", path = "canyon_migrations"} canyon_macros = { version = "0.3.1", path = "canyon_macros" } tokio = { version = "1.27.0", features = ["full"] } @@ -52,6 +55,7 @@ toml = "0.7.3" async-trait = "0.1.68" walkdir = "2.3.3" regex = "1.5" +partialdebug = "0.2.0" quote = "1.0.9" proc-macro2 = "1.0.27" @@ -59,7 +63,7 @@ proc-macro2 = "1.0.27" [workspace.package] version = "0.3.1" edition = "2021" -authors = ["Alex Vergara, Gonzalo Busto"] +authors = ["Alex Vergara, Gonzalo Busto Musi"] documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" readme = "README.md" @@ -67,5 +71,6 @@ license = "MIT" description = "A Rust ORM and QueryBuilder" [features] -postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres", "canyon_observer/postgres", "canyon_macros/postgres"] -mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql", "canyon_observer/mssql", "canyon_macros/mssql"] +postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres", "canyon_migrations/postgres", "canyon_macros/postgres"] +mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql", "canyon_migrations/mssql", "canyon_macros/mssql"] +migrations = ["canyon_migrations", "canyon_macros/migrations"] diff --git a/bash_aliases.sh b/bash_aliases.sh index 64e2d931..aee09cd7 100644 --- a/bash_aliases.sh +++ b/bash_aliases.sh @@ -39,7 +39,7 @@ alias SqlServerInitializationLinux='cargo test initialize_sql_server_docker_inst # Publish Canyon-SQL to the registry with its dependencies -alias PublishCanyon='cargo publish -p canyon_connection && cargo publish -p canyon_crud && cargo publish -p canyon_observer && cargo publish -p canyon_macros && cargo publish -p canyon_sql_root' +alias PublishCanyon='cargo publish -p canyon_connection && cargo publish -p canyon_crud && cargo publish -p canyon_migrations && cargo publish -p canyon_macros && cargo publish -p canyon_sql_root' # Collects the code coverage for the project (tests must run before this) alias CcEnvVars='export CARGO_INCREMENTAL=0 diff --git a/canyon_entities/Cargo.toml b/canyon_entities/Cargo.toml new file mode 100644 index 00000000..374e2e98 --- /dev/null +++ b/canyon_entities/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "canyon_entities" +version.workspace = true +edition.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true + +[dependencies] +regex = { workspace = true } +partialdebug = { workspace = true } +quote = { workspace = true } +proc-macro2 = { workspace = true } +syn = { version = "1.0.86", features = ["full", "parsing"] } # TODO Pending to refactor and upgrade diff --git a/canyon_observer/src/manager/entity.rs b/canyon_entities/src/entity.rs similarity index 98% rename from canyon_observer/src/manager/entity.rs rename to canyon_entities/src/entity.rs index 7aaeb38e..8604d0e8 100644 --- a/canyon_observer/src/manager/entity.rs +++ b/canyon_entities/src/entity.rs @@ -10,7 +10,7 @@ use syn::{ use super::entity_fields::EntityField; /// Provides a convenient way of handling the data on any -/// `CanyonEntity` struct anntotaded with the macro `#[canyon_entity]` +/// `CanyonEntity` struct annotated with the macro `#[canyon_entity]` #[derive(PartialDebug, Clone)] pub struct CanyonEntity { pub struct_name: Ident, diff --git a/canyon_observer/src/manager/entity_fields.rs b/canyon_entities/src/entity_fields.rs similarity index 100% rename from canyon_observer/src/manager/entity_fields.rs rename to canyon_entities/src/entity_fields.rs diff --git a/canyon_observer/src/manager/field_annotation.rs b/canyon_entities/src/field_annotation.rs similarity index 100% rename from canyon_observer/src/manager/field_annotation.rs rename to canyon_entities/src/field_annotation.rs diff --git a/canyon_entities/src/lib.rs b/canyon_entities/src/lib.rs new file mode 100644 index 00000000..8b3abd6c --- /dev/null +++ b/canyon_entities/src/lib.rs @@ -0,0 +1,11 @@ +use crate::register_types::CanyonRegisterEntity; +use std::sync::Mutex; + +pub mod entity; +pub mod entity_fields; +pub mod field_annotation; +pub mod manager_builder; +pub mod register_types; + +pub static CANYON_REGISTER_ENTITIES: Mutex>> = + Mutex::new(Vec::new()); diff --git a/canyon_observer/src/manager/manager_builder.rs b/canyon_entities/src/manager_builder.rs similarity index 100% rename from canyon_observer/src/manager/manager_builder.rs rename to canyon_entities/src/manager_builder.rs diff --git a/canyon_entities/src/register_types.rs b/canyon_entities/src/register_types.rs new file mode 100644 index 00000000..45cd1b8d --- /dev/null +++ b/canyon_entities/src/register_types.rs @@ -0,0 +1,45 @@ +/// This file contains `Rust` types that represents an entry on the `CanyonRegister` +/// where `Canyon` tracks the user types that has to manage + +pub const NUMERIC_PK_DATATYPE: [&str; 6] = ["i16", "u16", "i32", "u32", "i64", "u64"]; + +/// Gets the necessary identifiers of a CanyonEntity to make it the comparative +/// against the database schemas +#[derive(Debug, Clone, Default)] +pub struct CanyonRegisterEntity<'a> { + pub entity_name: &'a str, + pub entity_db_table_name: &'a str, + pub user_schema_name: Option<&'a str>, + pub entity_fields: Vec, +} + +/// Complementary type for a field that represents a struct field that maps +/// some real database column data +#[derive(Debug, Clone, Default)] +pub struct CanyonRegisterEntityField { + pub field_name: String, + pub field_type: String, + pub annotations: Vec, +} + +impl CanyonRegisterEntityField { + /// Return if the field is autoincremental + pub fn is_autoincremental(&self) -> bool { + let has_pk_annotation = self + .annotations + .iter() + .find(|a| a.starts_with("Annotation: PrimaryKey")); + + let pk_is_autoincremental = match has_pk_annotation { + Some(annotation) => annotation.contains("true"), + None => false, + }; + + NUMERIC_PK_DATATYPE.contains(&self.field_type.as_str()) && pk_is_autoincremental + } + + /// Return the nullability of a the field + pub fn is_nullable(&self) -> bool { + self.field_type.to_uppercase().starts_with("OPTION") + } +} diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index 82d336f5..763fde8d 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -19,10 +19,12 @@ proc-macro2 = { workspace = true } futures = { workspace = true } tokio = { workspace = true } -canyon_observer = { workspace = true } -canyon_crud = { workspace = true } canyon_connection = { workspace = true } +canyon_crud = { workspace = true } +canyon_entities = { workspace = true } +canyon_migrations = { workspace = true, optional = true } [features] -postgres = ["canyon_connection/postgres", "canyon_crud/postgres", "canyon_observer/postgres"] -mssql = ["canyon_connection/mssql", "canyon_crud/mssql", "canyon_observer/mssql"] +postgres = ["canyon_connection/postgres", "canyon_crud/postgres", "canyon_migrations/postgres"] +mssql = ["canyon_connection/mssql", "canyon_crud/mssql", "canyon_migrations/mssql"] +migrations = ["canyon_migrations"] diff --git a/canyon_macros/src/canyon_macro.rs b/canyon_macros/src/canyon_macro.rs index 1424de92..48c89fcc 100644 --- a/canyon_macros/src/canyon_macro.rs +++ b/canyon_macros/src/canyon_macro.rs @@ -1,112 +1,32 @@ //! Provides helpers to build the `#[canyon_macros::canyon]` procedural like attribute macro -use proc_macro::TokenStream as TokenStream1; -use proc_macro2::{Ident, TokenStream}; - +use canyon_connection::CANYON_TOKIO_RUNTIME; +use canyon_migrations::migrations::handler::Migrations; +use canyon_migrations::{CM_QUERIES_TO_EXECUTE, QUERIES_TO_EXECUTE}; +use proc_macro2::TokenStream; use quote::quote; -use canyon_observer::{CM_QUERIES_TO_EXECUTE, QUERIES_TO_EXECUTE}; -use syn::{Lit, NestedMeta}; - -#[derive(Debug)] -/// Utilery struct for wrapping the content and result of parsing the attributes on the `canyon` macro -pub struct CanyonMacroAttributes { - pub allowed_migrations: bool, - pub error: Option, -} - -/// Parses the [`syn::NestedMeta::Meta`] or [`syn::NestedMeta::Lit`] attached to the `canyon` macro -pub fn parse_canyon_macro_attributes(_meta: &Vec) -> CanyonMacroAttributes { - let mut res = CanyonMacroAttributes { - allowed_migrations: false, - error: None, - }; - - for nested_meta in _meta { - match nested_meta { - syn::NestedMeta::Meta(m) => determine_allowed_attributes(m, &mut res), - syn::NestedMeta::Lit(lit) => match lit { - syn::Lit::Str(ref l) => { - res.error = Some(report_literals_not_allowed(&l.value(), lit)) - } - syn::Lit::ByteStr(ref l) => { - res.error = Some(report_literals_not_allowed( - &String::from_utf8_lossy(&l.value()), - lit, - )) - } - syn::Lit::Byte(ref l) => { - res.error = Some(report_literals_not_allowed(&l.value().to_string(), lit)) - } - syn::Lit::Char(ref l) => { - res.error = Some(report_literals_not_allowed(&l.value().to_string(), lit)) - } - syn::Lit::Int(ref l) => { - res.error = Some(report_literals_not_allowed(&l.to_string(), lit)) - } - syn::Lit::Float(ref l) => { - res.error = Some(report_literals_not_allowed(&l.to_string(), lit)) - } - syn::Lit::Bool(ref l) => { - res.error = Some(report_literals_not_allowed(&l.value().to_string(), lit)) - } - syn::Lit::Verbatim(ref l) => { - res.error = Some(report_literals_not_allowed(&l.to_string(), lit)) - } - }, - } - } - - res -} - -/// Determines whenever a [`syn::NestedMeta::Meta`] it's classified as a valid argument of the `canyon` macro -fn determine_allowed_attributes(meta: &syn::Meta, cma: &mut CanyonMacroAttributes) { - const ALLOWED_ATTRS: [&str; 1] = ["enable_migrations"]; - - let attr_ident = meta.path().get_ident().unwrap(); - let attr_ident_str = attr_ident.to_string(); - - if attr_ident_str.as_str() == "enable_migrations" { - cma.allowed_migrations = true; - } else { - let error = syn::Error::new_spanned( - Ident::new(&attr_ident_str, attr_ident.span()), - format!( - "No `{attr_ident_str}` arguments allowed in the `Canyon` macro attributes.\n\ - Allowed ones are: {ALLOWED_ATTRS:?}" - ), - ) - .into_compile_error(); - cma.error = Some( - quote! { - #error - fn main() {} - } - .into(), - ) - } -} - -/// Creates a custom error for report not allowed literals on the attribute -/// args of the `canyon` proc macro -fn report_literals_not_allowed(ident: &str, s: &Lit) -> TokenStream1 { - let error = syn::Error::new_spanned( - Ident::new(ident, s.span()), - "No literals allowed in the `Canyon` macro", - ) - .into_compile_error(); +#[cfg(feature = "migrations")] +pub fn main_with_queries() -> TokenStream { + CANYON_TOKIO_RUNTIME.block_on(async { + canyon_connection::init_connections_cache().await; + Migrations::migrate().await; + }); + // The queries to execute at runtime in the managed state + let mut queries_tokens: Vec = Vec::new(); + wire_queries_to_execute(&mut queries_tokens); quote! { - #error - fn main() {} + { + #(#queries_tokens)* + } } - .into() } /// Creates a TokenScream that is used to load the data generated at compile-time /// by the `CanyonManaged` macros again on the queries register -pub fn wire_queries_to_execute(canyon_manager_tokens: &mut Vec) { +#[cfg(feature = "migrations")] +fn wire_queries_to_execute(canyon_manager_tokens: &mut Vec) { let cm_data = CM_QUERIES_TO_EXECUTE.lock().unwrap(); let data = QUERIES_TO_EXECUTE.lock().unwrap(); diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index ce03cc58..160f6ece 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -1,17 +1,20 @@ extern crate proc_macro; mod canyon_entity_macro; +#[cfg(feature = "migrations")] mod canyon_macro; mod query_operations; mod utils; -use canyon_connection::CANYON_TOKIO_RUNTIME; use canyon_entity_macro::parse_canyon_entity_proc_macro_attr; use proc_macro::TokenStream as CompilerTokenStream; use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use syn::{DeriveInput, Fields, Type, Visibility}; +#[cfg(feature = "migrations")] +use canyon_macro::main_with_queries; + use query_operations::{ delete::{generate_delete_query_tokens, generate_delete_tokens}, insert::{generate_insert_tokens, generate_multiple_insert_tokens}, @@ -22,22 +25,14 @@ use query_operations::{ }, update::{generate_update_query_tokens, generate_update_tokens}, }; - -use canyon_macro::{parse_canyon_macro_attributes, wire_queries_to_execute}; use utils::{function_parser::FunctionParser, helpers, macro_tokens::MacroTokens}; -use canyon_observer::{ - manager::{ - entity::CanyonEntity, - manager_builder::{ - generate_enum_with_fields, generate_enum_with_fields_values, generate_user_struct, - }, +use canyon_entities::{ + entity::CanyonEntity, + manager_builder::{ + generate_enum_with_fields, generate_enum_with_fields_values, generate_user_struct, }, - migrations::handler::Migrations, -}; - -use canyon_observer::{ - migrations::register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}, + register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}, CANYON_REGISTER_ENTITIES, }; @@ -51,15 +46,6 @@ use canyon_observer::{ /// the necessary operations for the migrations #[proc_macro_attribute] pub fn main(_meta: CompilerTokenStream, input: CompilerTokenStream) -> CompilerTokenStream { - let attrs = syn::parse_macro_input!(_meta as syn::AttributeArgs); - - // Parses the attributes declared in the arguments of this proc macro - let attrs_parse_result = parse_canyon_macro_attributes(&attrs); - if attrs_parse_result.error.is_some() { - return attrs_parse_result.error.unwrap(); - } - - // Parses the function items that this attribute is attached to let func_res = syn::parse::(input); if func_res.is_err() { return quote! { fn main() {} }.into(); @@ -70,46 +56,27 @@ pub fn main(_meta: CompilerTokenStream, input: CompilerTokenStream) -> CompilerT let sign = func.sig; let body = func.block.stmts; - if attrs_parse_result.allowed_migrations { - CANYON_TOKIO_RUNTIME.block_on(async { - canyon_connection::init_connections_cache().await; - Migrations::migrate().await; - }); - - // The queries to execute at runtime in the managed state - let mut queries_tokens: Vec = Vec::new(); - wire_queries_to_execute(&mut queries_tokens); + #[allow(unused_mut, unused_assignments)] + let mut migrations_tokens = quote! {}; + #[cfg(feature = "migrations")] + { + migrations_tokens = main_with_queries(); + } - // The final code wired in main() - quote! { - #sign { - canyon_sql::runtime::CANYON_TOKIO_RUNTIME - .handle() - .block_on( async { - canyon_sql::runtime::init_connections_cache().await; - { - #(#queries_tokens)* - } - #(#body)* - } - ) - } - } - .into() - } else { - quote! { - #sign { - canyon_sql::runtime::CANYON_TOKIO_RUNTIME + // The final code wired in main() + quote! { + #sign { + canyon_sql::runtime::CANYON_TOKIO_RUNTIME .handle() .block_on( async { - canyon_sql::runtime::init_connections_cache().await; - #(#body)* - } - ) - } + canyon_sql::runtime::init_connections_cache().await; + #migrations_tokens + #(#body)* + } + ) } - .into() } + .into() } #[proc_macro_attribute] diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index 0f70ab4d..5a5a4e15 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -1,4 +1,4 @@ -use canyon_observer::manager::field_annotation::EntityFieldAnnotation; +use canyon_entities::field_annotation::EntityFieldAnnotation; use proc_macro2::TokenStream; use quote::quote; diff --git a/canyon_macros/src/utils/macro_tokens.rs b/canyon_macros/src/utils/macro_tokens.rs index 370fbeea..29de0467 100644 --- a/canyon_macros/src/utils/macro_tokens.rs +++ b/canyon_macros/src/utils/macro_tokens.rs @@ -1,6 +1,6 @@ use std::convert::TryFrom; -use canyon_observer::manager::field_annotation::EntityFieldAnnotation; +use canyon_entities::field_annotation::EntityFieldAnnotation; use proc_macro2::Ident; use syn::{Attribute, DeriveInput, Fields, Generics, Type, Visibility}; diff --git a/canyon_observer/Cargo.toml b/canyon_migrations/Cargo.toml similarity index 89% rename from canyon_observer/Cargo.toml rename to canyon_migrations/Cargo.toml index 0f939b2c..ba353b76 100644 --- a/canyon_observer/Cargo.toml +++ b/canyon_migrations/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "canyon_observer" +name = "canyon_migrations" version.workspace = true edition.workspace = true authors.workspace = true @@ -12,14 +12,17 @@ description.workspace = true [dependencies] canyon_crud = { workspace = true } canyon_connection = { workspace = true } +canyon_entities = { workspace = true } + tokio = { workspace = true } tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } + async-trait = { workspace = true } regex = { workspace = true } +partialdebug = { workspace = true } walkdir = { workspace = true } -partialdebug = "0.2.0" proc-macro2 = { workspace = true } quote = { workspace = true } syn = { version = "1.0.86", features = ["full", "parsing"] } # TODO Pending to refactor and upgrade diff --git a/canyon_observer/src/constants.rs b/canyon_migrations/src/constants.rs similarity index 99% rename from canyon_observer/src/constants.rs rename to canyon_migrations/src/constants.rs index 3928da4f..9f025762 100644 --- a/canyon_observer/src/constants.rs +++ b/canyon_migrations/src/constants.rs @@ -1,5 +1,3 @@ -pub const NUMERIC_PK_DATATYPE: [&str; 6] = ["i16", "u16", "i32", "u32", "i64", "u64"]; - #[cfg(feature = "postgres")] pub mod postgresql_queries { pub static CANYON_MEMORY_TABLE: &str = "CREATE TABLE IF NOT EXISTS canyon_memory ( diff --git a/canyon_observer/src/lib.rs b/canyon_migrations/src/lib.rs similarity index 87% rename from canyon_observer/src/lib.rs rename to canyon_migrations/src/lib.rs index 41e0dd42..5743cc8b 100644 --- a/canyon_observer/src/lib.rs +++ b/canyon_migrations/src/lib.rs @@ -13,16 +13,13 @@ pub mod migrations; extern crate canyon_connection; extern crate canyon_crud; +extern crate canyon_entities; mod constants; -pub mod manager; -use crate::migrations::register_types::CanyonRegisterEntity; use canyon_connection::lazy_static::lazy_static; use std::{collections::HashMap, sync::Mutex}; -pub static CANYON_REGISTER_ENTITIES: Mutex>> = - Mutex::new(Vec::new()); lazy_static! { pub static ref QUERIES_TO_EXECUTE: Mutex>> = Mutex::new(HashMap::new()); diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_migrations/src/migrations/handler.rs similarity index 99% rename from canyon_observer/src/migrations/handler.rs rename to canyon_migrations/src/migrations/handler.rs index 9ce3c4e8..24dfb1c4 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_migrations/src/migrations/handler.rs @@ -1,5 +1,6 @@ use canyon_connection::{datasources::Migrations as MigrationsStatus, DATASOURCES}; use canyon_crud::rows::CanyonRows; +use canyon_entities::CANYON_REGISTER_ENTITIES; use partialdebug::placeholder::PartialDebug; use crate::{ @@ -14,7 +15,6 @@ use crate::{ memory::CanyonMemory, processor::MigrationsProcessor, }, - CANYON_REGISTER_ENTITIES, }; #[derive(PartialDebug)] diff --git a/canyon_observer/src/migrations/information_schema.rs b/canyon_migrations/src/migrations/information_schema.rs similarity index 100% rename from canyon_observer/src/migrations/information_schema.rs rename to canyon_migrations/src/migrations/information_schema.rs diff --git a/canyon_observer/src/migrations/memory.rs b/canyon_migrations/src/migrations/memory.rs similarity index 99% rename from canyon_observer/src/migrations/memory.rs rename to canyon_migrations/src/migrations/memory.rs index 18f6eb31..1d822fd1 100644 --- a/canyon_observer/src/migrations/memory.rs +++ b/canyon_migrations/src/migrations/memory.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use std::fs; use walkdir::WalkDir; -use super::register_types::CanyonRegisterEntity; +use canyon_entities::register_types::CanyonRegisterEntity; /// Convenient struct that contains the necessary data and operations to implement /// the `Canyon Memory`. diff --git a/canyon_observer/src/migrations/mod.rs b/canyon_migrations/src/migrations/mod.rs similarity index 76% rename from canyon_observer/src/migrations/mod.rs rename to canyon_migrations/src/migrations/mod.rs index 525cbc10..1b139fdd 100644 --- a/canyon_observer/src/migrations/mod.rs +++ b/canyon_migrations/src/migrations/mod.rs @@ -2,4 +2,4 @@ pub mod handler; pub mod information_schema; pub mod memory; pub mod processor; -pub mod register_types; +pub mod transforms; diff --git a/canyon_observer/src/migrations/processor.rs b/canyon_migrations/src/migrations/processor.rs similarity index 97% rename from canyon_observer/src/migrations/processor.rs rename to canyon_migrations/src/migrations/processor.rs index b096b828..425c1b0d 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_migrations/src/migrations/processor.rs @@ -13,7 +13,11 @@ use crate::save_migrations_query_to_execute; use super::information_schema::{ColumnMetadata, TableMetadata}; use super::memory::CanyonMemory; -use super::register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}; +#[cfg(feature = "postgres")] +use crate::migrations::transforms::{to_postgres_alter_syntax, to_postgres_syntax}; +#[cfg(feature = "mssql")] +use crate::migrations::transforms::{to_sqlserver_alter_syntax, to_sqlserver_syntax}; +use canyon_entities::register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}; /// Responsible of generating the queries to sync the database status with the /// Rust source code managed by Canyon, for successfully make the migrations @@ -661,9 +665,7 @@ impl MigrationsHelper { #[cfg(feature = "postgres")] { if db_type == DatabaseType::PostgreSql { - return canyon_register_entity_field - .to_postgres_alter_syntax() - .to_lowercase() + return to_postgres_alter_syntax(canyon_register_entity_field).to_lowercase() == current_column_metadata.datatype; } } @@ -671,9 +673,7 @@ impl MigrationsHelper { { if db_type == DatabaseType::SqlServer { // TODO Search a better way to get the datatype without useless info (like "VARCHAR(MAX)") - return canyon_register_entity_field - .to_sqlserver_alter_syntax() - .to_lowercase() + return to_sqlserver_alter_syntax(canyon_register_entity_field).to_lowercase() == current_column_metadata.datatype; } } @@ -786,7 +786,7 @@ impl DatabaseOperation for TableOperation { .map(|entity_field| format!( "\"{}\" {}", entity_field.field_name, - entity_field.to_postgres_syntax() + to_postgres_syntax(entity_field) )) .collect::>() .join(", ") @@ -801,7 +801,7 @@ impl DatabaseOperation for TableOperation { .map(|entity_field| format!( "{} {}", entity_field.field_name, - entity_field.to_sqlserver_syntax() + to_sqlserver_syntax(entity_field) )) .collect::>() .join(", ") @@ -924,14 +924,14 @@ impl DatabaseOperation for ColumnOperation { "ALTER TABLE \"{}\" ADD COLUMN \"{}\" {};", table_name, entity_field.field_name, - entity_field.to_postgres_syntax() + to_postgres_syntax(entity_field) ), #[cfg(feature = "mssql")] DatabaseType::SqlServer => format!( "ALTER TABLE {} ADD \"{}\" {};", table_name, entity_field.field_name, - entity_field.to_sqlserver_syntax() + to_sqlserver_syntax(entity_field) ) } ColumnOperation::DeleteColumn(table_name, column_name) => { @@ -943,7 +943,7 @@ impl DatabaseOperation for ColumnOperation { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!( "ALTER TABLE \"{_table_name}\" ALTER COLUMN \"{}\" TYPE {};", - _entity_field.field_name, _entity_field.to_postgres_alter_syntax() + _entity_field.field_name, to_postgres_alter_syntax(_entity_field) ), #[cfg(feature = "mssql")] DatabaseType::SqlServer => todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") @@ -955,7 +955,7 @@ impl DatabaseOperation for ColumnOperation { #[cfg(feature = "mssql")] DatabaseType::SqlServer => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NULL", - entity_field.field_name, entity_field.to_sqlserver_alter_syntax() + entity_field.field_name, to_sqlserver_alter_syntax(entity_field) ) } #[cfg(feature = "mssql")] ColumnOperation::DropNotNullBeforeDropColumn(table_name, column_name, column_datatype) => @@ -981,7 +981,7 @@ impl DatabaseOperation for ColumnOperation { #[cfg(feature = "mssql")] DatabaseType::SqlServer => format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NOT NULL", entity_field.field_name, - entity_field.to_sqlserver_alter_syntax() + to_sqlserver_alter_syntax(entity_field) ) } } diff --git a/canyon_migrations/src/migrations/transforms.rs b/canyon_migrations/src/migrations/transforms.rs new file mode 100644 index 00000000..6d14e478 --- /dev/null +++ b/canyon_migrations/src/migrations/transforms.rs @@ -0,0 +1,179 @@ +#[cfg(feature = "postgres")] +use crate::constants::postgresql_type; +#[cfg(feature = "mssql")] +use crate::constants::sqlserver_type; +use crate::constants::{regex_patterns, rust_type}; + +use canyon_entities::register_types::CanyonRegisterEntityField; +use regex::Regex; + +/// Return the postgres datatype and parameters to create a column for a given rust type +#[cfg(feature = "postgres")] +pub fn to_postgres_syntax(field: &CanyonRegisterEntityField) -> String { + let rust_type_clean = field.field_type.replace(' ', ""); + + match rust_type_clean.as_str() { + rust_type::I8 | rust_type::U8 => { + String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) + } + rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(postgresql_type::INTEGER), + + rust_type::I16 | rust_type::U16 => { + String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) + } + rust_type::OPT_I16 | rust_type::OPT_U16 => String::from(postgresql_type::INTEGER), + + rust_type::I32 | rust_type::U32 => { + String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) + } + rust_type::OPT_I32 | rust_type::OPT_U32 => String::from(postgresql_type::INTEGER), + + rust_type::I64 | rust_type::U64 => { + String::from(&format!("{} NOT NULL", postgresql_type::BIGINT)) + } + rust_type::OPT_I64 | rust_type::OPT_U64 => String::from(postgresql_type::BIGINT), + + rust_type::STRING => String::from(&format!("{} NOT NULL", postgresql_type::TEXT)), + rust_type::OPT_STRING => String::from(postgresql_type::TEXT), + + rust_type::BOOL => String::from(&format!("{} NOT NULL", postgresql_type::BOOLEAN)), + rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), + + rust_type::NAIVE_DATE => String::from(&format!("{} NOT NULL", postgresql_type::DATE)), + rust_type::OPT_NAIVE_DATE => String::from(postgresql_type::DATE), + + rust_type::NAIVE_TIME => String::from(&format!("{} NOT NULL", postgresql_type::TIME)), + rust_type::OPT_NAIVE_TIME => String::from(postgresql_type::TIME), + + rust_type::NAIVE_DATE_TIME => { + String::from(&format!("{} NOT NULL", postgresql_type::DATETIME)) + } + rust_type::OPT_NAIVE_DATE_TIME => String::from(postgresql_type::DATETIME), + &_ => todo!("Not supported datatype for this migrations version"), + } +} + +/// Return the postgres datatype and parameters to create a column for a given rust type +/// for Microsoft SQL Server +#[cfg(feature = "mssql")] +pub fn to_sqlserver_syntax(field: &CanyonRegisterEntityField) -> String { + let rust_type_clean = field.field_type.replace(' ', ""); + + match rust_type_clean.as_str() { + rust_type::I8 | rust_type::U8 => String::from(&format!("{} NOT NULL", sqlserver_type::INT)), + rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(sqlserver_type::INT), + + rust_type::I16 | rust_type::U16 => { + String::from(&format!("{} NOT NULL", sqlserver_type::INT)) + } + rust_type::OPT_I16 | rust_type::OPT_U16 => String::from(sqlserver_type::INT), + + rust_type::I32 | rust_type::U32 => { + String::from(&format!("{} NOT NULL", sqlserver_type::INT)) + } + rust_type::OPT_I32 | rust_type::OPT_U32 => String::from(sqlserver_type::INT), + + rust_type::I64 | rust_type::U64 => { + String::from(&format!("{} NOT NULL", sqlserver_type::BIGINT)) + } + rust_type::OPT_I64 | rust_type::OPT_U64 => String::from(sqlserver_type::BIGINT), + + rust_type::STRING => { + String::from(&format!("{} NOT NULL DEFAULT ''", sqlserver_type::NVARCHAR)) + } + rust_type::OPT_STRING => String::from(sqlserver_type::NVARCHAR), + + rust_type::BOOL => String::from(&format!("{} NOT NULL", sqlserver_type::BIT)), + rust_type::OPT_BOOL => String::from(sqlserver_type::BIT), + + rust_type::NAIVE_DATE => String::from(&format!("{} NOT NULL", sqlserver_type::DATE)), + rust_type::OPT_NAIVE_DATE => String::from(sqlserver_type::DATE), + + rust_type::NAIVE_TIME => String::from(&format!("{} NOT NULL", sqlserver_type::TIME)), + rust_type::OPT_NAIVE_TIME => String::from(sqlserver_type::TIME), + + rust_type::NAIVE_DATE_TIME => { + String::from(&format!("{} NOT NULL", sqlserver_type::DATETIME)) + } + rust_type::OPT_NAIVE_DATE_TIME => String::from(sqlserver_type::DATETIME), + &_ => todo!("Not supported datatype for this migrations version"), + } +} + +#[cfg(feature = "postgres")] +pub fn to_postgres_alter_syntax(field: &CanyonRegisterEntityField) -> String { + let mut rust_type_clean = field.field_type.replace(' ', ""); + let rs_type_is_optional = field.field_type.to_uppercase().starts_with("OPTION"); + + if rs_type_is_optional { + let type_regex = Regex::new(regex_patterns::EXTRACT_RUST_OPT_REGEX).unwrap(); + let capture_rust_type = type_regex.captures(rust_type_clean.as_str()).unwrap(); + rust_type_clean = capture_rust_type + .name("rust_type") + .unwrap() + .as_str() + .to_string(); + } + + match rust_type_clean.as_str() { + rust_type::I8 | rust_type::U8 | rust_type::OPT_I8 | rust_type::OPT_U8 => { + String::from(postgresql_type::INT_8) + } + rust_type::I16 | rust_type::U16 | rust_type::OPT_I16 | rust_type::OPT_U16 => { + String::from(postgresql_type::SMALL_INT) + } + rust_type::I32 | rust_type::U32 | rust_type::OPT_I32 | rust_type::OPT_U32 => { + String::from(postgresql_type::INTEGER) + } + rust_type::I64 | rust_type::U64 | rust_type::OPT_I64 | rust_type::OPT_U64 => { + String::from(postgresql_type::BIGINT) + } + rust_type::STRING | rust_type::OPT_STRING => String::from(postgresql_type::TEXT), + rust_type::BOOL | rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), + rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => String::from(postgresql_type::DATE), + rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => String::from(postgresql_type::TIME), + rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { + String::from(postgresql_type::DATETIME) + } + &_ => todo!("Not supported datatype for this migrations version"), + } +} + +#[cfg(feature = "mssql")] +pub fn to_sqlserver_alter_syntax(field: &CanyonRegisterEntityField) -> String { + let mut rust_type_clean = field.field_type.replace(' ', ""); + let rs_type_is_optional = field.field_type.to_uppercase().starts_with("OPTION"); + + if rs_type_is_optional { + let type_regex = Regex::new(regex_patterns::EXTRACT_RUST_OPT_REGEX).unwrap(); + let capture_rust_type = type_regex.captures(rust_type_clean.as_str()).unwrap(); + rust_type_clean = capture_rust_type + .name("rust_type") + .unwrap() + .as_str() + .to_string(); + } + + match rust_type_clean.as_str() { + rust_type::I8 | rust_type::U8 | rust_type::OPT_I8 | rust_type::OPT_U8 => { + String::from(sqlserver_type::TINY_INT) + } + rust_type::I16 | rust_type::U16 | rust_type::OPT_I16 | rust_type::OPT_U16 => { + String::from(sqlserver_type::SMALL_INT) + } + rust_type::I32 | rust_type::U32 | rust_type::OPT_I32 | rust_type::OPT_U32 => { + String::from(sqlserver_type::INT) + } + rust_type::I64 | rust_type::U64 | rust_type::OPT_I64 | rust_type::OPT_U64 => { + String::from(sqlserver_type::BIGINT) + } + rust_type::STRING | rust_type::OPT_STRING => String::from(sqlserver_type::NVARCHAR), + rust_type::BOOL | rust_type::OPT_BOOL => String::from(sqlserver_type::BIT), + rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => String::from(sqlserver_type::DATE), + rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => String::from(sqlserver_type::TIME), + rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { + String::from(sqlserver_type::DATETIME) + } + &_ => todo!("Not supported datatype for this migrations version"), + } +} diff --git a/canyon_observer/src/manager/mod.rs b/canyon_observer/src/manager/mod.rs deleted file mode 100644 index eca614b8..00000000 --- a/canyon_observer/src/manager/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod entity; -pub mod entity_fields; -pub mod field_annotation; -pub mod manager_builder; diff --git a/canyon_observer/src/migrations/register_types.rs b/canyon_observer/src/migrations/register_types.rs deleted file mode 100644 index 14481c13..00000000 --- a/canyon_observer/src/migrations/register_types.rs +++ /dev/null @@ -1,228 +0,0 @@ -use regex::Regex; - -#[cfg(feature = "postgres")] -use crate::constants::postgresql_type; -#[cfg(feature = "mssql")] -use crate::constants::sqlserver_type; -use crate::constants::{regex_patterns, rust_type, NUMERIC_PK_DATATYPE}; - -/// This file contains `Rust` types that represents an entry on the `CanyonRegister` -/// where `Canyon` tracks the user types that has to manage - -/// Gets the necessary identifiers of a CanyonEntity to make it the comparative -/// against the database schemas -#[derive(Debug, Clone, Default)] -pub struct CanyonRegisterEntity<'a> { - pub entity_name: &'a str, - pub entity_db_table_name: &'a str, - pub user_schema_name: Option<&'a str>, - pub entity_fields: Vec, -} - -/// Complementary type for a field that represents a struct field that maps -/// some real database column data -#[derive(Debug, Clone, Default)] -pub struct CanyonRegisterEntityField { - pub field_name: String, - pub field_type: String, - pub annotations: Vec, -} - -impl CanyonRegisterEntityField { - /// Return the postgres datatype and parameters to create a column for a given rust type - #[cfg(feature = "postgres")] - pub fn to_postgres_syntax(&self) -> String { - let rust_type_clean = self.field_type.replace(' ', ""); - - match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 => { - String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) - } - rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(postgresql_type::INTEGER), - - rust_type::I16 | rust_type::U16 => { - String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) - } - rust_type::OPT_I16 | rust_type::OPT_U16 => String::from(postgresql_type::INTEGER), - - rust_type::I32 | rust_type::U32 => { - String::from(&format!("{} NOT NULL", postgresql_type::INTEGER)) - } - rust_type::OPT_I32 | rust_type::OPT_U32 => String::from(postgresql_type::INTEGER), - - rust_type::I64 | rust_type::U64 => { - String::from(&format!("{} NOT NULL", postgresql_type::BIGINT)) - } - rust_type::OPT_I64 | rust_type::OPT_U64 => String::from(postgresql_type::BIGINT), - - rust_type::STRING => String::from(&format!("{} NOT NULL", postgresql_type::TEXT)), - rust_type::OPT_STRING => String::from(postgresql_type::TEXT), - - rust_type::BOOL => String::from(&format!("{} NOT NULL", postgresql_type::BOOLEAN)), - rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), - - rust_type::NAIVE_DATE => String::from(&format!("{} NOT NULL", postgresql_type::DATE)), - rust_type::OPT_NAIVE_DATE => String::from(postgresql_type::DATE), - - rust_type::NAIVE_TIME => String::from(&format!("{} NOT NULL", postgresql_type::TIME)), - rust_type::OPT_NAIVE_TIME => String::from(postgresql_type::TIME), - - rust_type::NAIVE_DATE_TIME => { - String::from(&format!("{} NOT NULL", postgresql_type::DATETIME)) - } - rust_type::OPT_NAIVE_DATE_TIME => String::from(postgresql_type::DATETIME), - &_ => todo!("Not supported datatype for this migrations version"), - } - } - - /// Return the postgres datatype and parameters to create a column for a given rust type - /// for Microsoft SQL Server - #[cfg(feature = "mssql")] - pub fn to_sqlserver_syntax(&self) -> String { - let rust_type_clean = self.field_type.replace(' ', ""); - - match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 => { - String::from(&format!("{} NOT NULL", sqlserver_type::INT)) - } - rust_type::OPT_I8 | rust_type::OPT_U8 => String::from(sqlserver_type::INT), - - rust_type::I16 | rust_type::U16 => { - String::from(&format!("{} NOT NULL", sqlserver_type::INT)) - } - rust_type::OPT_I16 | rust_type::OPT_U16 => String::from(sqlserver_type::INT), - - rust_type::I32 | rust_type::U32 => { - String::from(&format!("{} NOT NULL", sqlserver_type::INT)) - } - rust_type::OPT_I32 | rust_type::OPT_U32 => String::from(sqlserver_type::INT), - - rust_type::I64 | rust_type::U64 => { - String::from(&format!("{} NOT NULL", sqlserver_type::BIGINT)) - } - rust_type::OPT_I64 | rust_type::OPT_U64 => String::from(sqlserver_type::BIGINT), - - rust_type::STRING => { - String::from(&format!("{} NOT NULL DEFAULT ''", sqlserver_type::NVARCHAR)) - } - rust_type::OPT_STRING => String::from(sqlserver_type::NVARCHAR), - - rust_type::BOOL => String::from(&format!("{} NOT NULL", sqlserver_type::BIT)), - rust_type::OPT_BOOL => String::from(sqlserver_type::BIT), - - rust_type::NAIVE_DATE => String::from(&format!("{} NOT NULL", sqlserver_type::DATE)), - rust_type::OPT_NAIVE_DATE => String::from(sqlserver_type::DATE), - - rust_type::NAIVE_TIME => String::from(&format!("{} NOT NULL", sqlserver_type::TIME)), - rust_type::OPT_NAIVE_TIME => String::from(sqlserver_type::TIME), - - rust_type::NAIVE_DATE_TIME => { - String::from(&format!("{} NOT NULL", sqlserver_type::DATETIME)) - } - rust_type::OPT_NAIVE_DATE_TIME => String::from(sqlserver_type::DATETIME), - &_ => todo!("Not supported datatype for this migrations version"), - } - } - - #[cfg(feature = "postgres")] - pub fn to_postgres_alter_syntax(&self) -> String { - let mut rust_type_clean = self.field_type.replace(' ', ""); - let rs_type_is_optional = self.field_type.to_uppercase().starts_with("OPTION"); - - if rs_type_is_optional { - let type_regex = Regex::new(regex_patterns::EXTRACT_RUST_OPT_REGEX).unwrap(); - let capture_rust_type = type_regex.captures(rust_type_clean.as_str()).unwrap(); - rust_type_clean = capture_rust_type - .name("rust_type") - .unwrap() - .as_str() - .to_string(); - } - - match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 | rust_type::OPT_I8 | rust_type::OPT_U8 => { - String::from(postgresql_type::INT_8) - } - rust_type::I16 | rust_type::U16 | rust_type::OPT_I16 | rust_type::OPT_U16 => { - String::from(postgresql_type::SMALL_INT) - } - rust_type::I32 | rust_type::U32 | rust_type::OPT_I32 | rust_type::OPT_U32 => { - String::from(postgresql_type::INTEGER) - } - rust_type::I64 | rust_type::U64 | rust_type::OPT_I64 | rust_type::OPT_U64 => { - String::from(postgresql_type::BIGINT) - } - rust_type::STRING | rust_type::OPT_STRING => String::from(postgresql_type::TEXT), - rust_type::BOOL | rust_type::OPT_BOOL => String::from(postgresql_type::BOOLEAN), - rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => { - String::from(postgresql_type::DATE) - } - rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => { - String::from(postgresql_type::TIME) - } - rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { - String::from(postgresql_type::DATETIME) - } - &_ => todo!("Not supported datatype for this migrations version"), - } - } - - #[cfg(feature = "mssql")] - pub fn to_sqlserver_alter_syntax(&self) -> String { - let mut rust_type_clean = self.field_type.replace(' ', ""); - let rs_type_is_optional = self.field_type.to_uppercase().starts_with("OPTION"); - - if rs_type_is_optional { - let type_regex = Regex::new(regex_patterns::EXTRACT_RUST_OPT_REGEX).unwrap(); - let capture_rust_type = type_regex.captures(rust_type_clean.as_str()).unwrap(); - rust_type_clean = capture_rust_type - .name("rust_type") - .unwrap() - .as_str() - .to_string(); - } - - match rust_type_clean.as_str() { - rust_type::I8 | rust_type::U8 | rust_type::OPT_I8 | rust_type::OPT_U8 => { - String::from(sqlserver_type::TINY_INT) - } - rust_type::I16 | rust_type::U16 | rust_type::OPT_I16 | rust_type::OPT_U16 => { - String::from(sqlserver_type::SMALL_INT) - } - rust_type::I32 | rust_type::U32 | rust_type::OPT_I32 | rust_type::OPT_U32 => { - String::from(sqlserver_type::INT) - } - rust_type::I64 | rust_type::U64 | rust_type::OPT_I64 | rust_type::OPT_U64 => { - String::from(sqlserver_type::BIGINT) - } - rust_type::STRING | rust_type::OPT_STRING => String::from(sqlserver_type::NVARCHAR), - rust_type::BOOL | rust_type::OPT_BOOL => String::from(sqlserver_type::BIT), - rust_type::NAIVE_DATE | rust_type::OPT_NAIVE_DATE => String::from(sqlserver_type::DATE), - rust_type::NAIVE_TIME | rust_type::OPT_NAIVE_TIME => String::from(sqlserver_type::TIME), - rust_type::NAIVE_DATE_TIME | rust_type::OPT_NAIVE_DATE_TIME => { - String::from(sqlserver_type::DATETIME) - } - &_ => todo!("Not supported datatype for this migrations version"), - } - } - - /// Return if the field is autoincremental - pub fn is_autoincremental(&self) -> bool { - let has_pk_annotation = self - .annotations - .iter() - .find(|a| a.starts_with("Annotation: PrimaryKey")); - - let pk_is_autoincremental = match has_pk_annotation { - Some(annotation) => annotation.contains("true"), - None => false, - }; - - NUMERIC_PK_DATATYPE.contains(&self.field_type.as_str()) && pk_is_autoincremental - } - - /// Return the nullability of a the field - pub fn is_nullable(&self) -> bool { - self.field_type.to_uppercase().starts_with("OPTION") - } -} diff --git a/src/lib.rs b/src/lib.rs index 33a2c82b..1d2e3375 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,11 +6,11 @@ extern crate canyon_connection; extern crate canyon_crud; extern crate canyon_macros; -extern crate canyon_observer; +extern crate canyon_migrations; /// Reexported elements to the root of the public API pub mod migrations { - pub use canyon_observer::migrations::{handler, processor}; + pub use canyon_migrations::migrations::{handler, processor}; } /// The top level reexport. Here we define the path to some really important From cca1c8f65ff58e38b8e376e6debd53863768ce19 Mon Sep 17 00:00:00 2001 From: Gonzalo Busto Musi Date: Sun, 23 Apr 2023 12:08:38 +0200 Subject: [PATCH 63/82] v0.4.0 --- CHANGELOG.md | 13 ++++++++++++- Cargo.toml | 12 ++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db434f8a..51ac262f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,18 @@ Year format is defined as: `YYYY-m-d` ## [Unreleased] -## [0.2.0] - 2023 - 04 - 13 +## [0.4.0] - 2023 - 04 - 23 + +### Feature + +- Added the migrations cfg feature. Removed the arguments of the Canyon main macro for enabling +migrations. Now, the way to enable them is this new cfg feature. + +## [0.3.1] - 2023 - 04 - 20 + +- No changes + +## [0.3.0] - 2023 - 04 - 20 ### Feature diff --git a/Cargo.toml b/Cargo.toml index dcfb553f..17322b31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,11 +33,11 @@ tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } [workspace.dependencies] -canyon_crud = { version = "0.3.1", path = "canyon_crud" } -canyon_connection = { version = "0.3.1", path = "canyon_connection" } -canyon_entities = { version = "0.3.1", path = "canyon_entities" } -canyon_migrations = { version = "0.3.1", path = "canyon_migrations"} -canyon_macros = { version = "0.3.1", path = "canyon_macros" } +canyon_crud = { version = "0.4.0", path = "canyon_crud" } +canyon_connection = { version = "0.4.0", path = "canyon_connection" } +canyon_entities = { version = "0.4.0", path = "canyon_entities" } +canyon_migrations = { version = "0.4.0", path = "canyon_migrations"} +canyon_macros = { version = "0.4.0", path = "canyon_macros" } tokio = { version = "1.27.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["compat"] } @@ -61,7 +61,7 @@ quote = "1.0.9" proc-macro2 = "1.0.27" [workspace.package] -version = "0.3.1" +version = "0.4.0" edition = "2021" authors = ["Alex Vergara, Gonzalo Busto Musi"] documentation = "https://zerodaycode.github.io/canyon-book/" From 6f30744b74fb8905b13f8d859db041f8507cb5b6 Mon Sep 17 00:00:00 2001 From: Gonzalo Busto Musi <35508741+gbm25@users.noreply.github.com> Date: Sun, 23 Apr 2023 20:45:24 +0200 Subject: [PATCH 64/82] Added like operator (#42) * Added 'like' operator. Removed unneeded whitespaces on queries. * Fixes for like operator. Added tests for like operators. * Upgrade to v0.4.1 --- Cargo.toml | 12 +-- canyon_crud/src/query_elements/operators.rs | 38 ++++++-- .../src/query_elements/query_builder.rs | 25 ++--- tests/crud/querybuilder_operations.rs | 96 ++++++++++++++++++- 4 files changed, 135 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 17322b31..402a49a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,11 +33,11 @@ tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } [workspace.dependencies] -canyon_crud = { version = "0.4.0", path = "canyon_crud" } -canyon_connection = { version = "0.4.0", path = "canyon_connection" } -canyon_entities = { version = "0.4.0", path = "canyon_entities" } -canyon_migrations = { version = "0.4.0", path = "canyon_migrations"} -canyon_macros = { version = "0.4.0", path = "canyon_macros" } +canyon_crud = { version = "0.4.1", path = "canyon_crud" } +canyon_connection = { version = "0.4.1", path = "canyon_connection" } +canyon_entities = { version = "0.4.1", path = "canyon_entities" } +canyon_migrations = { version = "0.4.1", path = "canyon_migrations"} +canyon_macros = { version = "0.4.1", path = "canyon_macros" } tokio = { version = "1.27.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["compat"] } @@ -61,7 +61,7 @@ quote = "1.0.9" proc-macro2 = "1.0.27" [workspace.package] -version = "0.4.0" +version = "0.4.1" edition = "2021" authors = ["Alex Vergara, Gonzalo Busto Musi"] documentation = "https://zerodaycode.github.io/canyon-book/" diff --git a/canyon_crud/src/query_elements/operators.rs b/canyon_crud/src/query_elements/operators.rs index 7a91e7ef..30637bad 100644 --- a/canyon_crud/src/query_elements/operators.rs +++ b/canyon_crud/src/query_elements/operators.rs @@ -1,5 +1,5 @@ pub trait Operator { - fn as_str(&self) -> &'static str; + fn as_str(&self, placeholder_counter: usize) -> String; } /// Enumerated type for represent the comparison operations @@ -18,15 +18,37 @@ pub enum Comp { /// Operator "=<" less or equals than value LtEq, } + impl Operator for Comp { - fn as_str(&self) -> &'static str { + fn as_str(&self, placeholder_counter: usize) -> String { + match *self { + Self::Eq => format!(" = ${placeholder_counter}"), + Self::Neq => format!(" <> ${placeholder_counter}"), + Self::Gt => format!(" > ${placeholder_counter}"), + Self::GtEq => format!(" >= ${placeholder_counter}"), + Self::Lt => format!(" < ${placeholder_counter}"), + Self::LtEq => format!(" <= ${placeholder_counter}"), + } + } +} + +pub enum Like { + /// Operator "LIKE" as '%pattern%' + Full, + /// Operator "LIKE" as '%pattern' + Left, + /// Operator "LIKE" as 'pattern%' + Right, +} + +impl Operator for Like { + fn as_str(&self, placeholder_counter: usize) -> String { match *self { - Self::Eq => " = ", - Self::Neq => " <> ", - Self::Gt => " > ", - Self::GtEq => " >= ", - Self::Lt => " < ", - Self::LtEq => " <= ", + Like::Full => { + format!(" LIKE CONCAT('%', CAST(${placeholder_counter} AS VARCHAR) ,'%')") + } + Like::Left => format!(" LIKE CONCAT('%', CAST(${placeholder_counter} AS VARCHAR))"), + Like::Right => format!(" LIKE CONCAT(CAST(${placeholder_counter} AS VARCHAR) ,'%')"), } } } diff --git a/canyon_crud/src/query_elements/query_builder.rs b/canyon_crud/src/query_elements/query_builder.rs index 92146542..ddcde0fd 100644 --- a/canyon_crud/src/query_elements/query_builder.rs +++ b/canyon_crud/src/query_elements/query_builder.rs @@ -180,11 +180,8 @@ where pub fn r#where>(&mut self, r#where: Z, op: impl Operator) { let (column_name, value) = r#where.value(); - let where_ = String::from(" WHERE ") - + column_name - + op.as_str() - + "$" - + &(self.query.params.len() + 1).to_string(); + let where_ = + String::from(" WHERE ") + column_name + &op.as_str(self.query.params.len() + 1); self.query.sql.push_str(&where_); self.query.params.push(value); @@ -193,12 +190,7 @@ where pub fn and>(&mut self, r#and: Z, op: impl Operator) { let (column_name, value) = r#and.value(); - let and_ = String::from(" AND ") - + column_name - + op.as_str() - + "$" - + &(self.query.params.len() + 1).to_string() - + " "; + let and_ = String::from(" AND ") + column_name + &op.as_str(self.query.params.len() + 1); self.query.sql.push_str(&and_); self.query.params.push(value); @@ -207,12 +199,7 @@ where pub fn or>(&mut self, r#and: Z, op: impl Operator) { let (column_name, value) = r#and.value(); - let and_ = String::from(" OR ") - + column_name - + op.as_str() - + "$" - + &(self.query.params.len() + 1).to_string() - + " "; + let and_ = String::from(" OR ") + column_name + &op.as_str(self.query.params.len() + 1); self.query.sql.push_str(&and_); self.query.params.push(value); @@ -246,7 +233,7 @@ where self.query.params.push(qp) }); - self.query.sql.push_str(") "); + self.query.sql.push_str(")"); } fn or_values_in(&mut self, r#or: Z, values: &'a [Q]) @@ -277,7 +264,7 @@ where self.query.params.push(qp) }); - self.query.sql.push_str(") "); + self.query.sql.push_str(")"); } #[inline] diff --git a/tests/crud/querybuilder_operations.rs b/tests/crud/querybuilder_operations.rs index 1c853161..4bc205f6 100644 --- a/tests/crud/querybuilder_operations.rs +++ b/tests/crud/querybuilder_operations.rs @@ -6,7 +6,7 @@ /// use canyon_sql::{ crud::CrudOperations, - query::{operators::Comp, ops::QueryBuilder}, + query::{operators::Comp, operators::Like, ops::QueryBuilder}, }; #[cfg(feature = "mssql")] @@ -34,7 +34,7 @@ fn test_generated_sql_by_the_select_querybuilder() { // generated SQL by the SelectQueryBuilder is the spected assert_eq!( select_with_joins.read_sql(), - "SELECT * FROM league INNER JOIN tournament ON league.id = tournament.league_id LEFT JOIN team ON tournament.id = player.tournament_id WHERE id > $1 AND name = $2 AND name IN ($2, $3) " + "SELECT * FROM league INNER JOIN tournament ON league.id = tournament.league_id LEFT JOIN team ON tournament.id = player.tournament_id WHERE id > $1 AND name = $2 AND name IN ($2, $3)" ) } @@ -59,6 +59,96 @@ fn test_crud_find_with_querybuilder() { assert_eq!(league_idx_0.region, "KOREA"); } +/// Builds a new SQL statement for retrieves entities of the `T` type, filtered +/// with the parameters that modifies the base SQL to SELECT * FROM +#[cfg(feature = "postgres")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_with_querybuilder_and_fulllike() { + // Find all the leagues with "LC" in their name + let mut filtered_leagues_result = League::select_query(); + filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Full); + + assert_eq!( + filtered_leagues_result.read_sql(), + "SELECT * FROM league WHERE name LIKE CONCAT('%', CAST($1 AS VARCHAR) ,'%')" + ) +} + +/// Builds a new SQL statement for retrieves entities of the `T` type, filtered +/// with the parameters that modifies the base SQL to SELECT * FROM +#[cfg(feature = "mssql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_with_querybuilder_and_fulllike_datasource() { + // Find all the leagues with "LC" in their name + let mut filtered_leagues_result = League::select_query_datasource(SQL_SERVER_DS); + filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Full); + + assert_eq!( + filtered_leagues_result.read_sql(), + "SELECT * FROM league WHERE name LIKE CONCAT('%', CAST($1 AS VARCHAR) ,'%')" + ) +} + +/// Builds a new SQL statement for retrieves entities of the `T` type, filtered +/// with the parameters that modifies the base SQL to SELECT * FROM +#[cfg(feature = "postgres")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_with_querybuilder_and_leftlike() { + // Find all the leagues whose name ends with "CK" + let mut filtered_leagues_result = League::select_query(); + filtered_leagues_result.r#where(LeagueFieldValue::name(&"CK"), Like::Left); + + assert_eq!( + filtered_leagues_result.read_sql(), + "SELECT * FROM league WHERE name LIKE CONCAT('%', CAST($1 AS VARCHAR))" + ) +} + +/// Builds a new SQL statement for retrieves entities of the `T` type, filtered +/// with the parameters that modifies the base SQL to SELECT * FROM +#[cfg(feature = "mssql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_with_querybuilder_and_leftlike_datasource() { + // Find all the leagues whose name ends with "CK" + let mut filtered_leagues_result = League::select_query(); + filtered_leagues_result.r#where(LeagueFieldValue::name(&"CK"), Like::Left); + + assert_eq!( + filtered_leagues_result.read_sql(), + "SELECT * FROM league WHERE name LIKE CONCAT('%', CAST($1 AS VARCHAR))" + ) +} + +/// Builds a new SQL statement for retrieves entities of the `T` type, filtered +/// with the parameters that modifies the base SQL to SELECT * FROM +#[cfg(feature = "postgres")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_with_querybuilder_and_rightlike() { + // Find all the leagues whose name starts with "LC" + let mut filtered_leagues_result = League::select_query(); + filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Right); + + assert_eq!( + filtered_leagues_result.read_sql(), + "SELECT * FROM league WHERE name LIKE CONCAT(CAST($1 AS VARCHAR) ,'%')" + ) +} + +/// Builds a new SQL statement for retrieves entities of the `T` type, filtered +/// with the parameters that modifies the base SQL to SELECT * FROM +#[cfg(feature = "mssql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_with_querybuilder_and_rightlike_datasource() { + // Find all the leagues whose name starts with "LC" + let mut filtered_leagues_result = League::select_query_datasource(SQL_SERVER_DS); + filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Right); + + assert_eq!( + filtered_leagues_result.read_sql(), + "SELECT * FROM league WHERE name LIKE CONCAT(CAST($1 AS VARCHAR) ,'%')" + ) +} + /// Same than the above but with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] @@ -239,7 +329,7 @@ fn test_or_clause_with_in_constraint() { assert_eq!( l.read_sql(), - "SELECT * FROM league WHERE name = $1 OR id IN ($1, $2, $3) " + "SELECT * FROM league WHERE name = $1 OR id IN ($1, $2, $3)" ) } From d4e150292ea8d9a2fc4dbc6a3b6ed68485b5069f Mon Sep 17 00:00:00 2001 From: Gonzalo Busto Musi <35508741+gbm25@users.noreply.github.com> Date: Sun, 23 Apr 2023 21:01:21 +0200 Subject: [PATCH 65/82] updated changelog (#43) * Added 'like' operator. Removed unneeded whitespaces on queries. * Fixes for like operator. Added tests for like operators. * Upgrade to v0.4.1 * Fix clippy warnings --- CHANGELOG.md | 15 +++++++++++++++ canyon_crud/src/query_elements/query_builder.rs | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51ac262f..85a96308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,21 @@ Year format is defined as: `YYYY-m-d` ## [Unreleased] +## [0.4.1 - 2023 - 04 - 23 + +### Feature + +-The "Like" operator has been added with 3 options: + + Full: allows a search filtering by the field provided and the value contains the String provided. + Left: allows you to perform a filtered search by the field provided and the value ends with the String provided. + Right: allows a search filtering by the provided field and the value starts with the provided String. + +The logic of the operators has been changed a bit. + +The corresponding tests have been added to validate that the queries with "Like" are generated correctly. + + ## [0.4.0] - 2023 - 04 - 23 ### Feature diff --git a/canyon_crud/src/query_elements/query_builder.rs b/canyon_crud/src/query_elements/query_builder.rs index ddcde0fd..e6987d47 100644 --- a/canyon_crud/src/query_elements/query_builder.rs +++ b/canyon_crud/src/query_elements/query_builder.rs @@ -233,7 +233,7 @@ where self.query.params.push(qp) }); - self.query.sql.push_str(")"); + self.query.sql.push(')') } fn or_values_in(&mut self, r#or: Z, values: &'a [Q]) @@ -264,7 +264,7 @@ where self.query.params.push(qp) }); - self.query.sql.push_str(")"); + self.query.sql.push(')') } #[inline] From c990b33cba262d9b762ee56bcaf453079c59171c Mon Sep 17 00:00:00 2001 From: Gonzalo Busto Musi <35508741+gbm25@users.noreply.github.com> Date: Wed, 3 May 2023 11:26:44 +0200 Subject: [PATCH 66/82] Fix compilation without features (#44) * Fix for unspecified features * cargo fmt and clippy * Upgrade to v0.4.2 and added entry for the version in changelog * Correction of a comment * Test fix * Cargo fmt and clippy * Fix unused import clippy --- CHANGELOG.md | 8 +++++++- Cargo.toml | 12 ++++++------ bash_aliases.sh | 2 +- src/lib.rs | 2 ++ tests/constants.rs | 2 +- tests/migrations/mod.rs | 9 +++++---- 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85a96308..9bdad1dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,13 @@ Year format is defined as: `YYYY-m-d` ## [Unreleased] -## [0.4.1 - 2023 - 04 - 23 +## [0.4.2 - 2023 - 05 - 02] + +### Bugfix + +Fixed a bug related to migrations that prevented compiling if features were not specified. + +## [0.4.1 - 2023 - 04 - 23] ### Feature diff --git a/Cargo.toml b/Cargo.toml index 402a49a7..341a28dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,11 +33,11 @@ tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } [workspace.dependencies] -canyon_crud = { version = "0.4.1", path = "canyon_crud" } -canyon_connection = { version = "0.4.1", path = "canyon_connection" } -canyon_entities = { version = "0.4.1", path = "canyon_entities" } -canyon_migrations = { version = "0.4.1", path = "canyon_migrations"} -canyon_macros = { version = "0.4.1", path = "canyon_macros" } +canyon_crud = { version = "0.4.2", path = "canyon_crud" } +canyon_connection = { version = "0.4.2", path = "canyon_connection" } +canyon_entities = { version = "0.4.2", path = "canyon_entities" } +canyon_migrations = { version = "0.4.2", path = "canyon_migrations"} +canyon_macros = { version = "0.4.2", path = "canyon_macros" } tokio = { version = "1.27.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["compat"] } @@ -61,7 +61,7 @@ quote = "1.0.9" proc-macro2 = "1.0.27" [workspace.package] -version = "0.4.1" +version = "0.4.2" edition = "2021" authors = ["Alex Vergara, Gonzalo Busto Musi"] documentation = "https://zerodaycode.github.io/canyon-book/" diff --git a/bash_aliases.sh b/bash_aliases.sh index aee09cd7..64b40415 100644 --- a/bash_aliases.sh +++ b/bash_aliases.sh @@ -7,7 +7,7 @@ # In order to run the script, simply type `$ . ./bash_aliases.sh` from the root of the project. # (refreshing the current terminal session could be required) -# Executes the docker compose script to wake up the postgres container +# Executes the docker compose script to wake up the containers alias DockerUp='docker-compose -f ./docker/docker-compose.yml up' # Shutdown the postgres container alias DockerDown='docker-compose -f ./docker/docker-compose.yml down' diff --git a/src/lib.rs b/src/lib.rs index 1d2e3375..d89f79b2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,9 +6,11 @@ extern crate canyon_connection; extern crate canyon_crud; extern crate canyon_macros; +#[cfg(feature = "migrations")] extern crate canyon_migrations; /// Reexported elements to the root of the public API +#[cfg(feature = "migrations")] pub mod migrations { pub use canyon_migrations::migrations::{handler, processor}; } diff --git a/tests/constants.rs b/tests/constants.rs index 1c9c8044..dd3a268b 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -5,7 +5,7 @@ pub const PSQL_DS: &str = "postgres_docker"; #[cfg(feature = "mssql")] pub const SQL_SERVER_DS: &str = "sqlserver_docker"; -#[cfg(feature = "postgres")] +#[cfg(all(feature = "postgres", feature = "migrations"))] pub static FETCH_PUBLIC_SCHEMA: &str = "SELECT gi.table_name, diff --git a/tests/migrations/mod.rs b/tests/migrations/mod.rs index 47f82566..01260fb3 100644 --- a/tests/migrations/mod.rs +++ b/tests/migrations/mod.rs @@ -1,11 +1,12 @@ #![allow(unused_imports)] -///! Integration tests for the migrations feature of `Canyon-SQL` -use canyon_sql::{crud::Transaction, migrations::handler::Migrations}; - use crate::constants; +///! Integration tests for the migrations feature of `Canyon-SQL` +use canyon_sql::crud::Transaction; +#[cfg(feature = "migrations")] +use canyon_sql::migrations::handler::Migrations; /// Brings the information of the `PostgreSQL` requested schema -#[cfg(feature = "postgres")] +#[cfg(all(feature = "postgres", feature = "migrations"))] #[canyon_sql::macros::canyon_tokio_test] fn test_migrations_postgresql_status_query() { let results = Migrations::query(constants::FETCH_PUBLIC_SCHEMA, [], constants::PSQL_DS).await; From 13b2a980fb2beb23172a2c0a427bbb11e2615867 Mon Sep 17 00:00:00 2001 From: OnSystem <48860619+0nSystem@users.noreply.github.com> Date: Sun, 10 Dec 2023 10:40:11 +0100 Subject: [PATCH 67/82] Mysql integration in Canyon (#45) * mysql-support start with docker configuration to up container and load data * mysql-support - first implementation workspace canyon_connection * mysql - change module bounds pending DateTime look problem and implement solution * mysql-support canyon macros, pending trait implement FromValue in &str and chrono:Date * mysql-support count table implemented * mysql-support - pending implement select test * mysql-support craete reorder array params to use with mysql params * mysql-support resolve quotes in queries and innecesary loop in reorder map * mysql-support select, update , and query_builder * mysql-support correction test insert and delete mysql implementation and investigate get last insert in mysql_async * develop remove imports not use * mysql-support added integration to returning primary key * mysql-support correction cfg features * mysql-support remove comment to mysql implementation in canyon_database_connector.rs * mysql-support canyon_macro.rs added in feature migrations * mysql-support replace error with feature in canyon_macro and separate pattern in crud.rs * changes: remove CanyonRowsMysql and implement mysql_async, repair initialization mssql * changes: corrections other test * changes: insert and multiinsert match_rows * changes: lib cargo_macros RowMapper * changes: select macro remove else if to generate tokenstream with action count * changes: resolve clippy error format! in expect to create Regex * changes: resolve clippy error format! in expect to create Regex * changes: implement Operator by datasource type * changes: added default datasources * changes: correction error feature specify in datasources.rs enum Auth * changes: correction cargo fmt --------- Co-authored-by: OnSystem --- .gitignore | 3 +- Cargo.toml | 11 +- bash_aliases.sh | 0 canyon_connection/Cargo.toml | 6 + .../src/canyon_database_connector.rs | 107 ++++++- canyon_connection/src/datasources.rs | 43 ++- canyon_connection/src/lib.rs | 18 ++ canyon_crud/Cargo.toml | 6 + canyon_crud/src/bounds.rs | 233 ++++++++++++++- canyon_crud/src/crud.rs | 116 +++++++- canyon_crud/src/mapper.rs | 4 + canyon_crud/src/query_elements/operators.rs | 27 +- .../src/query_elements/query_builder.rs | 21 +- canyon_crud/src/rows.rs | 17 ++ canyon_macros/Cargo.toml | 2 + canyon_macros/src/lib.rs | 66 ++--- canyon_macros/src/query_operations/insert.rs | 178 ++++-------- canyon_macros/src/query_operations/select.rs | 46 +-- canyon_migrations/Cargo.toml | 5 + canyon_migrations/src/migrations/handler.rs | 4 + .../src/migrations/information_schema.rs | 2 + canyon_migrations/src/migrations/memory.rs | 2 + canyon_migrations/src/migrations/processor.rs | 41 ++- docker/docker-compose.yml | 11 + docker/mysql/create_tables.sql | 44 +++ docker/mysql/fill_tables.sql | 275 ++++++++++++++++++ src/lib.rs | 5 + tests/Cargo.toml | 1 + tests/canyon.toml | 12 + tests/constants.rs | 2 + tests/crud/delete_operations.rs | 52 +++- tests/crud/foreign_key_operations.rs | 55 +++- tests/crud/insert_operations.rs | 101 ++++++- tests/crud/querybuilder_operations.rs | 127 +++++++- tests/crud/select_operations.rs | 58 +++- tests/crud/update_operations.rs | 48 ++- 36 files changed, 1489 insertions(+), 260 deletions(-) mode change 100644 => 100755 bash_aliases.sh create mode 100644 docker/mysql/create_tables.sql create mode 100644 docker/mysql/fill_tables.sql diff --git a/.gitignore b/.gitignore index a38bca38..056b3728 100755 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ Cargo.lock canyon_tester/ macro_utils.rs .vscode/ -postgres-data/ \ No newline at end of file +postgres-data/ +mysql-data/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 341a28dd..c5063ad6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ members = [ "canyon_entities", "canyon_migrations", "canyon_macros", - "tests" ] @@ -31,6 +30,9 @@ canyon_macros = { workspace = true, path = "canyon_macros" } # To be marked as opt deps tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } +mysql_async = { workspace = true, optional = true } +mysql_common = { workspace = true, optional = true } + [workspace.dependencies] canyon_crud = { version = "0.4.2", path = "canyon_crud" } @@ -43,6 +45,8 @@ tokio = { version = "1.27.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["compat"] } tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"] } tiberius = { version = "0.12.1", features = ["tds73", "chrono", "integrated-auth-gssapi"] } +mysql_async = { version = "0.32.2" } +mysql_common = { version = "0.30.6", features = [ "chrono" ]} chrono = { version = "0.4", features = ["serde"] } # Just from TP better? serde = { version = "1.0.138", features = ["derive"] } @@ -54,12 +58,14 @@ lazy_static = "1.4.0" toml = "0.7.3" async-trait = "0.1.68" walkdir = "2.3.3" -regex = "1.5" +regex = "1.9.3" partialdebug = "0.2.0" quote = "1.0.9" proc-macro2 = "1.0.27" + + [workspace.package] version = "0.4.2" edition = "2021" @@ -73,4 +79,5 @@ description = "A Rust ORM and QueryBuilder" [features] postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres", "canyon_migrations/postgres", "canyon_macros/postgres"] mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql", "canyon_migrations/mssql", "canyon_macros/mssql"] +mysql = ["mysql_async", "mysql_common", "canyon_connection/mysql", "canyon_crud/mysql", "canyon_migrations/mysql", "canyon_macros/mysql"] migrations = ["canyon_migrations", "canyon_macros/migrations"] diff --git a/bash_aliases.sh b/bash_aliases.sh old mode 100644 new mode 100755 diff --git a/canyon_connection/Cargo.toml b/canyon_connection/Cargo.toml index fd37fd4e..fac88ef5 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_connection/Cargo.toml @@ -15,6 +15,9 @@ tokio-util = { workspace = true } tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } +mysql_async = { workspace = true, optional = true } +mysql_common = { workspace = true, optional = true } + futures = { workspace = true } indexmap = { workspace = true } @@ -28,3 +31,6 @@ walkdir = { workspace = true } [features] postgres = ["tokio-postgres"] mssql = ["tiberius", "async-std"] +mysql = ["mysql_async","mysql_common"] + + diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 7196e948..438f3548 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -2,12 +2,14 @@ use serde::Deserialize; #[cfg(feature = "mssql")] use async_std::net::TcpStream; +#[cfg(feature = "mysql")] +use mysql_async::Pool; #[cfg(feature = "mssql")] use tiberius::{AuthMethod, Config}; #[cfg(feature = "postgres")] use tokio_postgres::{Client, NoTls}; -use crate::datasources::DatasourceConfig; +use crate::datasources::{Auth, DatasourceConfig}; /// Represents the current supported databases by Canyon #[derive(Deserialize, Debug, Eq, PartialEq, Clone, Copy)] @@ -18,6 +20,19 @@ pub enum DatabaseType { #[serde(alias = "sqlserver", alias = "mssql")] #[cfg(feature = "mssql")] SqlServer, + #[serde(alias = "mysql")] + #[cfg(feature = "mysql")] + MySQL, +} + +impl From<&Auth> for DatabaseType { + fn from(value: &Auth) -> Self { + match value { + crate::datasources::Auth::Postgres(_) => DatabaseType::PostgreSql, + crate::datasources::Auth::SqlServer(_) => DatabaseType::SqlServer, + crate::datasources::Auth::MySQL(_) => DatabaseType::MySQL, + } + } } /// A connection with a `PostgreSQL` database @@ -33,6 +48,12 @@ pub struct SqlServerConnection { pub client: &'static mut tiberius::Client, } +/// A connection with a `Mysql` database +#[cfg(feature = "mysql")] +pub struct MysqlConnection { + pub client: Pool, +} + /// The Canyon database connection handler. When the client's program /// starts, Canyon gets the information about the desired datasources, /// process them and generates a pool of 1 to 1 database connection for @@ -42,6 +63,8 @@ pub enum DatabaseConnection { Postgres(PostgreSqlConnection), #[cfg(feature = "mssql")] SqlServer(SqlServerConnection), + #[cfg(feature = "mysql")] + MySQL(MysqlConnection), } unsafe impl Send for DatabaseConnection {} @@ -64,6 +87,10 @@ impl DatabaseConnection { crate::datasources::Auth::SqlServer(_) => { panic!("Found SqlServer auth configuration for a PostgreSQL datasource") } + #[cfg(feature = "mysql")] + crate::datasources::Auth::MySQL(_) => { + panic!("Found MySql auth configuration for a PostgreSQL datasource") + } }; let (new_client, new_connection) = tokio_postgres::connect( &format!( @@ -109,6 +136,10 @@ impl DatabaseConnection { } crate::datasources::SqlServerAuth::Integrated => AuthMethod::Integrated, }, + #[cfg(feature = "mysql")] + crate::datasources::Auth::MySQL(_) => { + panic!("Found PostgreSQL auth configuration for a SqlServer database") + } }); // on production, it is not a good idea to do this. We should upgrade @@ -136,6 +167,41 @@ impl DatabaseConnection { )), })) } + #[cfg(feature = "mysql")] + DatabaseType::MySQL => { + let (user, password) = match &datasource.auth { + #[cfg(feature = "mssql")] + crate::datasources::Auth::SqlServer(_) => { + panic!("Found SqlServer auth configuration for a PostgreSQL datasource") + } + #[cfg(feature = "postgres")] + crate::datasources::Auth::Postgres(_) => { + panic!("Found MySql auth configuration for a PostgreSQL datasource") + } + #[cfg(feature = "mysql")] + crate::datasources::Auth::MySQL(mysql_auth) => match mysql_auth { + crate::datasources::MySQLAuth::Basic { username, password } => { + (username, password) + } + }, + }; + + //TODO add options to optionals params in url + + let url = format!( + "mysql://{}:{}@{}:{}/{}", + user, + password, + datasource.properties.host, + datasource.properties.port.unwrap_or_default(), + datasource.properties.db_name + ); + let mysql_connection = Pool::from_url(url)?; + + Ok(DatabaseConnection::MySQL(MysqlConnection { + client: { mysql_connection }, + })) + } } } @@ -143,7 +209,7 @@ impl DatabaseConnection { pub fn postgres_connection(&self) -> &PostgreSqlConnection { match self { DatabaseConnection::Postgres(conn) => conn, - #[cfg(all(feature = "postgres", feature = "mssql"))] + #[cfg(all(feature = "postgres", feature = "mssql", feature = "mysql"))] _ => panic!(), } } @@ -152,7 +218,16 @@ impl DatabaseConnection { pub fn sqlserver_connection(&mut self) -> &mut SqlServerConnection { match self { DatabaseConnection::SqlServer(conn) => conn, - #[cfg(all(feature = "postgres", feature = "mssql"))] + #[cfg(all(feature = "postgres", feature = "mssql", feature = "mysql"))] + _ => panic!(), + } + } + + #[cfg(feature = "mysql")] + pub fn mysql_connection(&self) -> &MysqlConnection { + match self { + DatabaseConnection::MySQL(conn) => conn, + #[cfg(all(feature = "postgres", feature = "mssql", feature = "mysql"))] _ => panic!(), } } @@ -166,13 +241,14 @@ mod database_connection_handler { /// Tests the behaviour of the `DatabaseType::from_datasource(...)` #[test] fn check_from_datasource() { - #[cfg(all(feature = "postgres", feature = "mssql"))] + #[cfg(all(feature = "postgres", feature = "mssql", feature = "mysql"))] { const CONFIG_FILE_MOCK_ALT_ALL: &str = r#" [canyon_sql] datasources = [ {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, - {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' }, + {name = 'MysqlDS', auth = { mysql = { basic = { username = "root", password = "root" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } ] "#; let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_ALL) @@ -185,6 +261,10 @@ mod database_connection_handler { config.canyon_sql.datasources[1].get_db_type(), DatabaseType::SqlServer ); + assert_eq!( + config.canyon_sql.datasources[2].get_db_type(), + DatabaseType::MySQL + ); } #[cfg(feature = "postgres")] @@ -218,5 +298,22 @@ mod database_connection_handler { DatabaseType::SqlServer ); } + + #[cfg(feature = "mysql")] + { + const CONFIG_FILE_MOCK_ALT_MYSQL: &str = r#" + [canyon_sql] + datasources = [ + {name = 'MysqlDS', auth = { mysql = { basic = { username = "root", password = "root" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + ] + "#; + + let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_MYSQL) + .expect("A failure happened retrieving the [canyon_sql] section"); + assert_eq!( + config.canyon_sql.datasources[0].get_db_type(), + DatabaseType::MySQL + ); + } } } diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index 9571c343..ccfd3694 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -11,7 +11,7 @@ fn load_ds_config_from_array() { [canyon_sql] datasources = [ {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, - ] + ] "#; let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_PG) .expect("A failure happened retrieving the [canyon_sql] section"); @@ -64,6 +64,33 @@ fn load_ds_config_from_array() { assert_eq!(ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)); } + #[cfg(feature = "mysql")] + { + const CONFIG_FILE_MOCK_ALT_MYSQL: &str = r#" + [canyon_sql] + datasources = [ + {name = 'MysqlDS', auth = { mysql = { basic = { username = "root", password = "root" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } + ] + "#; + let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_MYSQL) + .expect("A failure happened retrieving the [canyon_sql] section"); + + let ds_1 = &config.canyon_sql.datasources[0]; + + assert_eq!(ds_1.name, "MysqlDS"); + assert_eq!(ds_1.get_db_type(), DatabaseType::MySQL); + assert_eq!( + ds_1.auth, + Auth::MySQL(MySQLAuth::Basic { + username: "root".to_string(), + password: "root".to_string() + }) + ); + assert_eq!(ds_1.properties.host, "192.168.0.250.1"); + assert_eq!(ds_1.properties.port, Some(3340)); + assert_eq!(ds_1.properties.db_name, "triforce2"); + assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); + } } /// #[derive(Deserialize, Debug, Clone)] @@ -90,18 +117,23 @@ impl DatasourceConfig { Auth::Postgres(_) => DatabaseType::PostgreSql, #[cfg(feature = "mssql")] Auth::SqlServer(_) => DatabaseType::SqlServer, + #[cfg(feature = "mysql")] + Auth::MySQL(_) => DatabaseType::MySQL, } } } #[derive(Deserialize, Debug, Clone, PartialEq)] pub enum Auth { - #[serde(alias = "PostgreSQL", alias = "postgresql", alias = "postgres")] + #[serde(alias = "PostgresSQL", alias = "postgresql", alias = "postgres")] #[cfg(feature = "postgres")] Postgres(PostgresAuth), #[serde(alias = "SqlServer", alias = "sqlserver", alias = "mssql")] #[cfg(feature = "mssql")] SqlServer(SqlServerAuth), + #[serde(alias = "MYSQL", alias = "mysql", alias = "MySQL")] + #[cfg(feature = "mysql")] + MySQL(MySQLAuth), } #[derive(Deserialize, Debug, Clone, PartialEq)] @@ -120,6 +152,13 @@ pub enum SqlServerAuth { Integrated, } +#[derive(Deserialize, Debug, Clone, PartialEq)] +#[cfg(feature = "mysql")] +pub enum MySQLAuth { + #[serde(alias = "Basic", alias = "basic")] + Basic { username: String, password: String }, +} + #[derive(Deserialize, Debug, Clone)] pub struct DatasourceProperties { pub host: String, diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index fed9f31f..fd5d009e 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -2,6 +2,8 @@ pub extern crate async_std; pub extern crate futures; pub extern crate lazy_static; +#[cfg(feature = "mysql")] +pub extern crate mysql_async; #[cfg(feature = "mssql")] pub extern crate tiberius; pub extern crate tokio; @@ -104,3 +106,19 @@ pub fn get_database_connection<'a>( ) } } + +pub fn get_database_config<'a>( + datasource_name: &str, + datasources_config: &'a [DatasourceConfig], +) -> &'a DatasourceConfig { + if datasource_name.is_empty() { + datasources_config + .get(0) + .unwrap_or_else(|| panic!("Not exist datasource")) + } else { + datasources_config + .iter() + .find(|dc| dc.name == datasource_name) + .unwrap_or_else(|| panic!("Not found datasource expected {datasource_name}")) + } +} diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index 123a44fe..dfdd3ddb 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -12,11 +12,17 @@ description.workspace = true [dependencies] tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } +mysql_async = { workspace = true, optional = true } +mysql_common = { workspace = true, optional = true } + chrono = { workspace = true } async-trait = { workspace = true } canyon_connection = { workspace = true } +regex = { workspace = true } + [features] postgres = ["tokio-postgres", "canyon_connection/postgres"] mssql = ["tiberius", "canyon_connection/mssql"] +mysql = ["mysql_async","mysql_common", "canyon_connection/mysql"] diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs index d46bf863..27ffb97f 100644 --- a/canyon_crud/src/bounds.rs +++ b/canyon_crud/src/bounds.rs @@ -2,15 +2,16 @@ use crate::{ crud::{CrudOperations, Transaction}, mapper::RowMapper, }; - -#[cfg(feature = "postgres")] -use canyon_connection::tokio_postgres::{self, types::ToSql}; - +#[cfg(feature = "mysql")] +use canyon_connection::mysql_async::{self, prelude::ToValue}; #[cfg(feature = "mssql")] use canyon_connection::tiberius::{self, ColumnData, IntoSql}; +#[cfg(feature = "postgres")] +use canyon_connection::tokio_postgres::{self, types::ToSql}; use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc}; -use std::any::Any; + +use std::{any::Any, borrow::Cow}; /// Created for retrieve the field's name of a field of a struct, giving /// the Canyon's autogenerated enum with the variants that maps this @@ -100,16 +101,23 @@ impl Row for tiberius::Row { } } +#[cfg(feature = "mysql")] +impl Row for mysql_async::Row { + fn as_any(&self) -> &dyn Any { + self + } +} + /// Generic abstraction for hold a Column type that will be one of the Column /// types present in the dependent crates // #[derive(Copy, Clone)] pub struct Column<'a> { - name: &'a str, + name: Cow<'a, str>, type_: ColumnType, } impl<'a> Column<'a> { - pub fn name(&self) -> &'_ str { - self.name + pub fn name(&self) -> &str { + &self.name } pub fn column_type(&self) -> &ColumnType { &self.type_ @@ -137,6 +145,12 @@ impl Type for tiberius::ColumnType { self } } +#[cfg(feature = "mysql")] +impl Type for mysql_async::consts::ColumnType { + fn as_any(&self) -> &dyn Any { + self + } +} /// Wrapper over the dependencies Column's types pub enum ColumnType { @@ -144,6 +158,8 @@ pub enum ColumnType { Postgres(tokio_postgres::types::Type), #[cfg(feature = "mssql")] SqlServer(tiberius::ColumnType), + #[cfg(feature = "mysql")] + MySQL(mysql_async::consts::ColumnType), } pub trait RowOperations { @@ -155,6 +171,10 @@ pub trait RowOperations { fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output where Output: tiberius::FromSql<'a>; + #[cfg(feature = "mysql")] + fn get_mysql<'a, Output>(&'a self, col_name: &'a str) -> Output + where + Output: mysql_async::prelude::FromValue; #[cfg(feature = "postgres")] fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option @@ -165,6 +185,11 @@ pub trait RowOperations { where Output: tiberius::FromSql<'a>; + #[cfg(feature = "mysql")] + fn get_mysql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option + where + Output: mysql_async::prelude::FromValue; + fn columns(&self) -> Vec; } @@ -192,6 +217,15 @@ impl RowOperations for &dyn Row { panic!() // TODO into result and propagate } + #[cfg(feature = "mysql")] + fn get_mysql<'a, Output>(&'a self, col_name: &'a str) -> Output + where + Output: mysql_async::prelude::FromValue, + { + self.get_mysql_opt(col_name) + .expect("Failed to obtain a column in the MySql") + } + #[cfg(feature = "postgres")] fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option where @@ -213,6 +247,16 @@ impl RowOperations for &dyn Row { }; panic!() // TODO into result and propagate } + #[cfg(feature = "mysql")] + fn get_mysql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option + where + Output: mysql_async::prelude::FromValue, + { + if let Some(row) = self.as_any().downcast_ref::() { + return row.get::(col_name); + }; + panic!() // TODO into result and propagate + } fn columns(&self) -> Vec { let mut cols = vec![]; @@ -227,7 +271,7 @@ impl RowOperations for &dyn Row { .iter() .for_each(|c| { cols.push(Column { - name: c.name(), + name: Cow::from(c.name()), type_: ColumnType::Postgres(c.type_().to_owned()), }) }) @@ -243,12 +287,23 @@ impl RowOperations for &dyn Row { .iter() .for_each(|c| { cols.push(Column { - name: c.name(), + name: Cow::from(c.name()), type_: ColumnType::SqlServer(c.column_type()), }) }) }; } + #[cfg(feature = "mysql")] + { + if let Some(mysql_row) = self.as_any().downcast_ref::() { + mysql_row.columns_ref().iter().for_each(|c| { + cols.push(Column { + name: c.name_str(), + type_: ColumnType::MySQL(c.column_type()), + }) + }) + } + } cols } @@ -261,6 +316,8 @@ pub trait QueryParameter<'a>: std::fmt::Debug + Sync + Send { fn as_postgres_param(&self) -> &(dyn ToSql + Sync); #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_>; + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue; } /// The implementation of the [`canyon_connection::tiberius`] [`IntoSql`] for the @@ -278,6 +335,8 @@ impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { } } +//TODO Pending to review and see if it is necessary to apply something similar to the previous implementation. + impl<'a> QueryParameter<'a> for bool { #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { @@ -287,6 +346,10 @@ impl<'a> QueryParameter<'a> for bool { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::Bit(Some(*self)) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } } impl<'a> QueryParameter<'a> for i16 { #[cfg(feature = "postgres")] @@ -297,6 +360,10 @@ impl<'a> QueryParameter<'a> for i16 { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(*self)) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for &i16 { #[cfg(feature = "postgres")] @@ -307,6 +374,10 @@ impl<'a> QueryParameter<'a> for &i16 { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(**self)) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option { #[cfg(feature = "postgres")] @@ -317,6 +388,10 @@ impl<'a> QueryParameter<'a> for Option { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(*self) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option<&i16> { #[cfg(feature = "postgres")] @@ -327,6 +402,10 @@ impl<'a> QueryParameter<'a> for Option<&i16> { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I16(Some(*self.unwrap())) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for i32 { #[cfg(feature = "postgres")] @@ -337,6 +416,10 @@ impl<'a> QueryParameter<'a> for i32 { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self)) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for &i32 { #[cfg(feature = "postgres")] @@ -347,6 +430,10 @@ impl<'a> QueryParameter<'a> for &i32 { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(**self)) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option { #[cfg(feature = "postgres")] @@ -357,6 +444,10 @@ impl<'a> QueryParameter<'a> for Option { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(*self) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option<&i32> { #[cfg(feature = "postgres")] @@ -367,6 +458,10 @@ impl<'a> QueryParameter<'a> for Option<&i32> { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self.unwrap())) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for f32 { #[cfg(feature = "postgres")] @@ -377,6 +472,10 @@ impl<'a> QueryParameter<'a> for f32 { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some(*self)) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for &f32 { #[cfg(feature = "postgres")] @@ -387,6 +486,10 @@ impl<'a> QueryParameter<'a> for &f32 { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(Some(**self)) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option { #[cfg(feature = "postgres")] @@ -397,6 +500,10 @@ impl<'a> QueryParameter<'a> for Option { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F32(*self) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option<&f32> { #[cfg(feature = "postgres")] @@ -409,6 +516,10 @@ impl<'a> QueryParameter<'a> for Option<&f32> { *self.expect("Error on an f32 value on QueryParameter<'_>"), )) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for f64 { #[cfg(feature = "postgres")] @@ -419,6 +530,10 @@ impl<'a> QueryParameter<'a> for f64 { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some(*self)) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for &f64 { #[cfg(feature = "postgres")] @@ -426,10 +541,13 @@ impl<'a> QueryParameter<'a> for &f64 { self } #[cfg(feature = "mssql")] - #[cfg(feature = "mssql")] fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(Some(**self)) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option { #[cfg(feature = "postgres")] @@ -440,6 +558,10 @@ impl<'a> QueryParameter<'a> for Option { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::F64(*self) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option<&f64> { #[cfg(feature = "postgres")] @@ -452,6 +574,10 @@ impl<'a> QueryParameter<'a> for Option<&f64> { *self.expect("Error on an f64 value on QueryParameter<'_>"), )) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for i64 { #[cfg(feature = "postgres")] @@ -462,6 +588,10 @@ impl<'a> QueryParameter<'a> for i64 { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self)) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for &i64 { #[cfg(feature = "postgres")] @@ -472,6 +602,10 @@ impl<'a> QueryParameter<'a> for &i64 { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(**self)) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option { #[cfg(feature = "postgres")] @@ -482,6 +616,10 @@ impl<'a> QueryParameter<'a> for Option { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(*self) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option<&i64> { #[cfg(feature = "postgres")] @@ -492,6 +630,10 @@ impl<'a> QueryParameter<'a> for Option<&i64> { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self.unwrap())) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for String { #[cfg(feature = "postgres")] @@ -502,6 +644,10 @@ impl<'a> QueryParameter<'a> for String { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Owned(self.to_owned()))) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for &String { #[cfg(feature = "postgres")] @@ -512,6 +658,10 @@ impl<'a> QueryParameter<'a> for &String { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Borrowed(self))) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option { #[cfg(feature = "postgres")] @@ -525,6 +675,10 @@ impl<'a> QueryParameter<'a> for Option { None => ColumnData::String(None), } } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option<&String> { #[cfg(feature = "postgres")] @@ -538,6 +692,10 @@ impl<'a> QueryParameter<'a> for Option<&String> { None => ColumnData::String(None), } } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for &'_ str { #[cfg(feature = "postgres")] @@ -548,6 +706,10 @@ impl<'a> QueryParameter<'a> for &'_ str { fn as_sqlserver_param(&self) -> ColumnData<'_> { ColumnData::String(Some(std::borrow::Cow::Borrowed(*self))) } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option<&'_ str> { #[cfg(feature = "postgres")] @@ -561,6 +723,10 @@ impl<'a> QueryParameter<'a> for Option<&'_ str> { None => ColumnData::String(None), } } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for NaiveDate { #[cfg(feature = "postgres")] @@ -571,6 +737,10 @@ impl<'a> QueryParameter<'a> for NaiveDate { fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option { #[cfg(feature = "postgres")] @@ -581,6 +751,10 @@ impl<'a> QueryParameter<'a> for Option { fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for NaiveTime { #[cfg(feature = "postgres")] @@ -591,6 +765,10 @@ impl<'a> QueryParameter<'a> for NaiveTime { fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option { #[cfg(feature = "postgres")] @@ -601,6 +779,10 @@ impl<'a> QueryParameter<'a> for Option { fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for NaiveDateTime { #[cfg(feature = "postgres")] @@ -611,6 +793,10 @@ impl<'a> QueryParameter<'a> for NaiveDateTime { fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } impl<'a> QueryParameter<'a> for Option { #[cfg(feature = "postgres")] @@ -621,7 +807,13 @@ impl<'a> QueryParameter<'a> for Option { fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } } + +//TODO pending impl<'a> QueryParameter<'a> for DateTime { #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { @@ -631,7 +823,12 @@ impl<'a> QueryParameter<'a> for DateTime { fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + todo!() + } } + impl<'a> QueryParameter<'a> for Option> { #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { @@ -641,7 +838,12 @@ impl<'a> QueryParameter<'a> for Option> { fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + todo!() + } } + impl<'a> QueryParameter<'a> for DateTime { #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { @@ -651,7 +853,12 @@ impl<'a> QueryParameter<'a> for DateTime { fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + todo!() + } } + impl<'a> QueryParameter<'a> for Option> { #[cfg(feature = "postgres")] fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { @@ -661,4 +868,8 @@ impl<'a> QueryParameter<'a> for Option> { fn as_sqlserver_param(&self) -> ColumnData<'_> { self.into_sql() } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + todo!() + } } diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index f5c6d37e..981c24f1 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -1,6 +1,6 @@ +use async_trait::async_trait; use std::fmt::Display; -use async_trait::async_trait; use canyon_connection::canyon_database_connector::DatabaseConnection; use canyon_connection::{get_database_connection, CACHED_DATABASE_CONN}; @@ -11,6 +11,11 @@ use crate::query_elements::query_builder::{ }; use crate::rows::CanyonRows; +#[cfg(feature = "mysql")] +pub const DETECT_PARAMS_IN_QUERY: &str = r"\$([\d])+"; +#[cfg(feature = "mysql")] +pub const DETECT_QUOTE_IN_QUERY: &str = r#"\"|\\"#; + /// This traits defines and implements a query against a database given /// an statement `stmt` and the params to pass the to the client. /// @@ -52,6 +57,11 @@ pub trait Transaction { ) .await } + #[cfg(feature = "mysql")] + DatabaseConnection::MySQL(_) => { + mysql_query_launcher::launch::(database_conn, stmt.to_string(), params.as_ref()) + .await + } } } } @@ -146,9 +156,10 @@ where #[cfg(feature = "postgres")] mod postgres_query_launcher { + use canyon_connection::canyon_database_connector::DatabaseConnection; + use crate::bounds::QueryParameter; use crate::rows::CanyonRows; - use canyon_connection::canyon_database_connector::DatabaseConnection; pub async fn launch<'a, T>( db_conn: &DatabaseConnection, @@ -217,3 +228,104 @@ mod sqlserver_query_launcher { )) } } + +#[cfg(feature = "mysql")] +mod mysql_query_launcher { + use std::sync::Arc; + + use mysql_async::prelude::Query; + use mysql_async::QueryWithParams; + use mysql_async::Value; + + use canyon_connection::canyon_database_connector::DatabaseConnection; + + use crate::bounds::QueryParameter; + use crate::rows::CanyonRows; + use mysql_async::Row; + use mysql_common::constants::ColumnType; + use mysql_common::row; + + use super::reorder_params; + use crate::crud::{DETECT_PARAMS_IN_QUERY, DETECT_QUOTE_IN_QUERY}; + use regex::Regex; + + pub async fn launch<'a, T>( + db_conn: &DatabaseConnection, + stmt: String, + params: &'a [&'_ dyn QueryParameter<'_>], + ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> { + let mysql_connection = db_conn.mysql_connection().client.get_conn().await?; + + let stmt_with_escape_characters = regex::escape(&stmt); + let query_string = + Regex::new(DETECT_PARAMS_IN_QUERY)?.replace_all(&stmt_with_escape_characters, "?"); + + let mut query_string = Regex::new(DETECT_QUOTE_IN_QUERY)? + .replace_all(&query_string, "") + .to_string(); + + let mut is_insert = false; + if let Some(index_start_clausule_returning) = query_string.find(" RETURNING") { + query_string.truncate(index_start_clausule_returning); + is_insert = true; + } + + let params_query: Vec = + reorder_params(&stmt, params, |f| f.as_mysql_param().to_value()); + + let query_with_params = QueryWithParams { + query: query_string, + params: params_query, + }; + + let mut query_result = query_with_params + .run(mysql_connection) + .await + .expect("Error executing query in mysql"); + + let result_rows = if is_insert { + let last_insert = query_result + .last_insert_id() + .map(Value::UInt) + .expect("Error getting pk id in insert"); + + vec![row::new_row( + vec![last_insert], + Arc::new([mysql_async::Column::new(ColumnType::MYSQL_TYPE_UNKNOWN)]), + )] + } else { + query_result + .collect::() + .await + .expect("Error resolved trait FromRow in mysql") + }; + + Ok(CanyonRows::MySQL(result_rows)) + } +} + +#[cfg(feature = "mysql")] +fn reorder_params( + stmt: &str, + params: &[&'_ dyn QueryParameter<'_>], + fn_parser: impl Fn(&&dyn QueryParameter<'_>) -> T, +) -> Vec { + let mut ordered_params = vec![]; + let rg = regex::Regex::new(DETECT_PARAMS_IN_QUERY) + .expect("Error create regex with detect params pattern expression"); + + for positional_param in rg.find_iter(stmt) { + let pp: &str = positional_param.as_str(); + let pp_index = pp[1..] // param $1 -> get 1 + .parse::() + .expect("Error parse mapped parameter to usized.") + - 1; + + let element = params + .get(pp_index) + .expect("Error obtaining the element of the mapping against parameters."); + ordered_params.push(fn_parser(element)); + } + + ordered_params +} diff --git a/canyon_crud/src/mapper.rs b/canyon_crud/src/mapper.rs index 66cb91d2..252df1ce 100644 --- a/canyon_crud/src/mapper.rs +++ b/canyon_crud/src/mapper.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "mysql")] +use canyon_connection::mysql_async; #[cfg(feature = "mssql")] use canyon_connection::tiberius; #[cfg(feature = "postgres")] @@ -13,4 +15,6 @@ pub trait RowMapper>: Sized { fn deserialize_postgresql(row: &tokio_postgres::Row) -> T; #[cfg(feature = "mssql")] fn deserialize_sqlserver(row: &tiberius::Row) -> T; + #[cfg(feature = "mysql")] + fn deserialize_mysql(row: &mysql_async::Row) -> T; } diff --git a/canyon_crud/src/query_elements/operators.rs b/canyon_crud/src/query_elements/operators.rs index 30637bad..015ced03 100644 --- a/canyon_crud/src/query_elements/operators.rs +++ b/canyon_crud/src/query_elements/operators.rs @@ -1,5 +1,7 @@ +use canyon_connection::canyon_database_connector::DatabaseType; + pub trait Operator { - fn as_str(&self, placeholder_counter: usize) -> String; + fn as_str(&self, placeholder_counter: usize, datasource_type: &DatabaseType) -> String; } /// Enumerated type for represent the comparison operations @@ -20,7 +22,7 @@ pub enum Comp { } impl Operator for Comp { - fn as_str(&self, placeholder_counter: usize) -> String { + fn as_str(&self, placeholder_counter: usize, _datasource_type: &DatabaseType) -> String { match *self { Self::Eq => format!(" = ${placeholder_counter}"), Self::Neq => format!(" <> ${placeholder_counter}"), @@ -42,13 +44,26 @@ pub enum Like { } impl Operator for Like { - fn as_str(&self, placeholder_counter: usize) -> String { + fn as_str(&self, placeholder_counter: usize, datasource_type: &DatabaseType) -> String { + let type_data_to_cast_str = match datasource_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => "VARCHAR", + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => "VARCHAR", + #[cfg(feature = "mysql")] + DatabaseType::MySQL => "CHAR", + }; + match *self { Like::Full => { - format!(" LIKE CONCAT('%', CAST(${placeholder_counter} AS VARCHAR) ,'%')") + format!(" LIKE CONCAT('%', CAST(${placeholder_counter} AS {type_data_to_cast_str}) ,'%')") } - Like::Left => format!(" LIKE CONCAT('%', CAST(${placeholder_counter} AS VARCHAR))"), - Like::Right => format!(" LIKE CONCAT(CAST(${placeholder_counter} AS VARCHAR) ,'%')"), + Like::Left => format!( + " LIKE CONCAT('%', CAST(${placeholder_counter} AS {type_data_to_cast_str}))" + ), + Like::Right => format!( + " LIKE CONCAT(CAST(${placeholder_counter} AS {type_data_to_cast_str}) ,'%')" + ), } } } diff --git a/canyon_crud/src/query_elements/query_builder.rs b/canyon_crud/src/query_elements/query_builder.rs index e6987d47..e25ff9fe 100644 --- a/canyon_crud/src/query_elements/query_builder.rs +++ b/canyon_crud/src/query_elements/query_builder.rs @@ -1,5 +1,9 @@ use std::fmt::Debug; +use canyon_connection::{ + canyon_database_connector::DatabaseType, get_database_config, DATASOURCES, +}; + use crate::{ bounds::{FieldIdentifier, FieldValueIdentifier, QueryParameter}, crud::{CrudOperations, Transaction}, @@ -138,6 +142,7 @@ where { query: Query<'a, T>, datasource_name: &'a str, + datasource_type: DatabaseType, } unsafe impl<'a, T> Send for QueryBuilder<'a, T> where @@ -158,6 +163,9 @@ where Self { query, datasource_name, + datasource_type: DatabaseType::from( + &get_database_config(datasource_name, &DATASOURCES).auth, + ), } } @@ -180,8 +188,9 @@ where pub fn r#where>(&mut self, r#where: Z, op: impl Operator) { let (column_name, value) = r#where.value(); - let where_ = - String::from(" WHERE ") + column_name + &op.as_str(self.query.params.len() + 1); + let where_ = String::from(" WHERE ") + + column_name + + &op.as_str(self.query.params.len() + 1, &self.datasource_type); self.query.sql.push_str(&where_); self.query.params.push(value); @@ -190,7 +199,9 @@ where pub fn and>(&mut self, r#and: Z, op: impl Operator) { let (column_name, value) = r#and.value(); - let and_ = String::from(" AND ") + column_name + &op.as_str(self.query.params.len() + 1); + let and_ = String::from(" AND ") + + column_name + + &op.as_str(self.query.params.len() + 1, &self.datasource_type); self.query.sql.push_str(&and_); self.query.params.push(value); @@ -199,7 +210,9 @@ where pub fn or>(&mut self, r#and: Z, op: impl Operator) { let (column_name, value) = r#and.value(); - let and_ = String::from(" OR ") + column_name + &op.as_str(self.query.params.len() + 1); + let and_ = String::from(" OR ") + + column_name + + &op.as_str(self.query.params.len() + 1, &self.datasource_type); self.query.sql.push_str(&and_); self.query.params.push(value); diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs index d8d35070..517592a6 100644 --- a/canyon_crud/src/rows.rs +++ b/canyon_crud/src/rows.rs @@ -13,6 +13,9 @@ pub enum CanyonRows { Postgres(Vec), #[cfg(feature = "mssql")] Tiberius(Vec), + #[cfg(feature = "mysql")] + MySQL(Vec), + UnusableTypeMarker(PhantomData), } @@ -33,6 +36,14 @@ impl CanyonRows { } } + #[cfg(feature = "mysql")] + pub fn get_mysql_rows(&self) -> &Vec { + match self { + Self::MySQL(v) => v, + _ => panic!("This branch will never ever should be reachable"), + } + } + /// Consumes `self` and returns the wrapped [`std::vec::Vec`] with the instances of T pub fn into_results>(self) -> Vec where @@ -43,6 +54,8 @@ impl CanyonRows { Self::Postgres(v) => v.iter().map(|row| Z::deserialize_postgresql(row)).collect(), #[cfg(feature = "mssql")] Self::Tiberius(v) => v.iter().map(|row| Z::deserialize_sqlserver(row)).collect(), + #[cfg(feature = "mysql")] + Self::MySQL(v) => v.iter().map(|row| Z::deserialize_mysql(row)).collect(), _ => panic!("This branch will never ever should be reachable"), } } @@ -54,6 +67,8 @@ impl CanyonRows { Self::Postgres(v) => v.len(), #[cfg(feature = "mssql")] Self::Tiberius(v) => v.len(), + #[cfg(feature = "mysql")] + Self::MySQL(v) => v.len(), _ => panic!("This branch will never ever should be reachable"), } } @@ -65,6 +80,8 @@ impl CanyonRows { Self::Postgres(v) => v.is_empty(), #[cfg(feature = "mssql")] Self::Tiberius(v) => v.is_empty(), + #[cfg(feature = "mysql")] + Self::MySQL(v) => v.is_empty(), _ => panic!("This branch will never ever should be reachable"), } } diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index 763fde8d..8b8a2852 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -27,4 +27,6 @@ canyon_migrations = { workspace = true, optional = true } [features] postgres = ["canyon_connection/postgres", "canyon_crud/postgres", "canyon_migrations/postgres"] mssql = ["canyon_connection/mssql", "canyon_crud/mssql", "canyon_migrations/mssql"] +mysql = ["canyon_connection/mysql", "canyon_crud/mysql", "canyon_migrations/mysql"] + migrations = ["canyon_migrations"] diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index 160f6ece..6f094fff 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -2,6 +2,8 @@ extern crate proc_macro; mod canyon_entity_macro; #[cfg(feature = "migrations")] +use canyon_macro::main_with_queries; + mod canyon_macro; mod query_operations; mod utils; @@ -12,9 +14,6 @@ use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use syn::{DeriveInput, Fields, Type, Visibility}; -#[cfg(feature = "migrations")] -use canyon_macro::main_with_queries; - use query_operations::{ delete::{generate_delete_query_tokens, generate_delete_tokens}, insert::{generate_insert_tokens, generate_multiple_insert_tokens}, @@ -531,55 +530,38 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac } }); + let init_field_values_mysql = fields.iter().map(|(_vis, ident, _ty)| { + let ident_name = ident.to_string(); + quote! { + #ident: row.get(#ident_name) + .expect(format!("Failed to retrieve the {} field", #ident_name).as_ref()) + } + }); + // The type of the Struct let ty = ast.ident; - let postgres_enabled = cfg!(feature = "postgres"); - let mssql_enabled = cfg!(feature = "mssql"); - - let tokens = if postgres_enabled && mssql_enabled { - quote! { - impl canyon_sql::crud::RowMapper for #ty { - fn deserialize_postgresql(row: &canyon_sql::db_clients::tokio_postgres::Row) -> #ty { - Self { - #(#init_field_values),* - } - } - fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { - Self { - #(#init_field_values_sqlserver),* - } + let tokens = quote! { + impl canyon_sql::crud::RowMapper for #ty { + #[cfg(feature="postgres")] + fn deserialize_postgresql(row: &canyon_sql::db_clients::tokio_postgres::Row) -> #ty { + Self { + #(#init_field_values),* } } - } - } else if postgres_enabled { - quote! { - impl canyon_sql::crud::RowMapper for #ty { - fn deserialize_postgresql(row: &canyon_sql::db_clients::tokio_postgres::Row) -> #ty { - Self { - #(#init_field_values),* - } + #[cfg(feature="mssql")] + fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { + Self { + #(#init_field_values_sqlserver),* } } - } - } else if mssql_enabled { - quote! { - impl canyon_sql::crud::RowMapper for #ty { - fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { - Self { - #(#init_field_values_sqlserver),* - } + #[cfg(feature="mysql")] + fn deserialize_mysql(row: &canyon_sql::db_clients::mysql_async::Row) -> #ty { + Self { + #(#init_field_values_mysql),* } } } - } else { - quote! { - panic!( - "Reached a branch in the implementation of the Row Mapper macro that should never be reached.\ - This is a severe bug of Canyon-SQL. Please, open us an issue at \ - https://github.com/zerodaycode/Canyon-SQL/issues and let us know about that failure." - ) - } }; tokens.into() diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs index 329399f0..c6e5e205 100644 --- a/canyon_macros/src/query_operations/insert.rs +++ b/canyon_macros/src/query_operations/insert.rs @@ -38,58 +38,6 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri let pk_ident = &pk_data.0; let pk_type = &pk_data.1; - let postgres_enabled = cfg!(feature = "postgres"); - let mssql_enabled = cfg!(feature = "mssql"); - - let match_rows = if postgres_enabled && mssql_enabled { - quote! { - canyon_sql::crud::CanyonRows::Postgres(mut v) => { - self.#pk_ident = v - .get(0) - .ok_or("Failed getting the returned IDs for an insert")? - .get::<&str, #pk_type>(#primary_key); - Ok(()) - } - canyon_sql::crud::CanyonRows::Tiberius(mut v) => { - self.#pk_ident = v - .get(0) - .ok_or("Failed getting the returned IDs for a multi insert")? - .get::<#pk_type, &str>(#primary_key) - .ok_or("SQL Server primary key type failed to be set as value")?; - Ok(()) - } - } - } else if postgres_enabled { - quote! { - canyon_sql::crud::CanyonRows::Postgres(mut v) => { - self.#pk_ident = v - .get(0) - .ok_or("Failed getting the returned IDs for an insert")? - .get::<&str, #pk_type>(#primary_key); - Ok(()) - } - } - } else if mssql_enabled { - quote! { - canyon_sql::crud::CanyonRows::Tiberius(mut v) => { - self.#pk_ident = v - .get(0) - .ok_or("Failed getting the returned IDs for a multi insert")? - .get::<#pk_type, &str>(#primary_key) - .ok_or("SQL Server primary key type failed to be set as value")?; - Ok(()) - } - } - } else { - quote! { - panic!( - "Reached a branch in the implementation of the Row Mapper macro that should never be reached.\ - This is a severe bug of Canyon-SQL. Please, open us an issue at \ - https://github.com/zerodaycode/Canyon-SQL/issues and let us know about that failure." - ) - } - }; - quote! { #remove_pk_value_from_fn_entry; @@ -108,7 +56,32 @@ pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &Stri ).await?; match rows { - #match_rows + #[cfg(feature = "postgres")] + canyon_sql::crud::CanyonRows::Postgres(mut v) => { + self.#pk_ident = v + .get(0) + .ok_or("Failed getting the returned IDs for an insert")? + .get::<&str, #pk_type>(#primary_key); + Ok(()) + }, + #[cfg(feature = "mssql")] + canyon_sql::crud::CanyonRows::Tiberius(mut v) => { + self.#pk_ident = v + .get(0) + .ok_or("Failed getting the returned IDs for a multi insert")? + .get::<#pk_type, &str>(#primary_key) + .ok_or("SQL Server primary key type failed to be set as value")?; + Ok(()) + }, + #[cfg(feature = "mysql")] + canyon_sql::crud::CanyonRows::MySQL(mut v) => { + self.#pk_ident = v + .get(0) + .ok_or("Failed getting the returned IDs for a multi insert")? + .get::<#pk_type,usize>(0) + .ok_or("MYSQL primary key type failed to be set as value")?; + Ok(()) + }, _ => panic!("Reached the panic match arm of insert for the DatabaseConnection type") // TODO remove when the generics will be refactored } } @@ -259,70 +232,6 @@ pub fn generate_multiple_insert_tokens( let pk_ident = &pk_data.0; let pk_type = &pk_data.1; - let postgres_enabled = cfg!(feature = "postgres"); - let mssql_enabled = cfg!(feature = "mssql"); - - let match_multi_insert_rows = if postgres_enabled && mssql_enabled { - quote! { - canyon_sql::crud::CanyonRows::Postgres(mut v) => { - for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = v - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") - .get::<&str, #pk_type>(#pk); - } - - Ok(()) - } - canyon_sql::crud::CanyonRows::Tiberius(mut v) => { - for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = v - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") - .get::<#pk_type, &str>(#pk) - .expect("SQL Server primary key type failed to be set as value"); - } - - Ok(()) - } - } - } else if postgres_enabled { - quote! { - canyon_sql::crud::CanyonRows::Postgres(mut v) => { - for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = v - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") - .get::<&str, #pk_type>(#pk); - } - - Ok(()) - } - } - } else if mssql_enabled { - quote! { - canyon_sql::crud::CanyonRows::Tiberius(mut v) => { - for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = v - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") - .get::<#pk_type, &str>(#pk) - .expect("SQL Server primary key type failed to be set as value"); - } - - Ok(()) - } - } - } else { - quote! { - panic!( - "Reached a branch in the implementation of the Row Mapper macro that should never be reached.\ - This is a severe bug of Canyon-SQL. Please, open us an issue at \ - https://github.com/zerodaycode/Canyon-SQL/issues and let us know about that failure." - ) - } - }; - quote! { mapped_fields = #column_names .split(", ") @@ -392,7 +301,40 @@ pub fn generate_multiple_insert_tokens( ).await?; match multi_insert_result { - #match_multi_insert_rows + #[cfg(feature="postgres")] + canyon_sql::crud::CanyonRows::Postgres(mut v) => { + for (idx, instance) in instances.iter_mut().enumerate() { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") + .get::<&str, #pk_type>(#pk); + } + + Ok(()) + }, + #[cfg(feature="mssql")] + canyon_sql::crud::CanyonRows::Tiberius(mut v) => { + for (idx, instance) in instances.iter_mut().enumerate() { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") + .get::<#pk_type, &str>(#pk) + .expect("SQL Server primary key type failed to be set as value"); + } + + Ok(()) + }, + #[cfg(feature="mysql")] + canyon_sql::crud::CanyonRows::MySQL(mut v) => { + for (idx, instance) in instances.iter_mut().enumerate() { + instance.#pk_ident = v + .get(idx) + .expect("Failed getting the returned IDs for a multi insert") + .get::<#pk_type,usize>(0) + .expect("MYSQL primary key type failed to be set as value"); + } + Ok(()) + }, _ => panic!() // TODO remove when the generics will be refactored } } diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs index 5a5a4e15..82a1a5b5 100644 --- a/canyon_macros/src/query_operations/select.rs +++ b/canyon_macros/src/query_operations/select.rs @@ -148,49 +148,25 @@ pub fn generate_count_tokens( ) -> TokenStream { let ty = macro_data.ty; let ty_str = &ty.to_string(); - let stmt = format!("SELECT COUNT (*) FROM {table_schema_data}"); + let stmt = format!("SELECT COUNT(*) FROM {table_schema_data}"); - let postgres_enabled = cfg!(feature = "postgres"); - let mssql_enabled = cfg!(feature = "mssql"); - - let result_handling = if postgres_enabled && mssql_enabled { - quote! { - canyon_sql::crud::CanyonRows::Postgres(mut v) => Ok( - v.remove(0).get::<&str, i64>("count") - ), - canyon_sql::crud::CanyonRows::Tiberius(mut v) => - v.remove(0) - .get::(0) - .map(|c| c as i64) - .ok_or(format!("Failure in the COUNT query for MSSQL for: {}", #ty_str).into()) - .into(), - _ => panic!() // TODO remove when the generics will be refactored - } - } else if postgres_enabled { - quote! { - canyon_sql::crud::CanyonRows::Postgres(mut v) => Ok( + let result_handling = quote! { + #[cfg(feature="postgres")] + canyon_sql::crud::CanyonRows::Postgres(mut v) => Ok( v.remove(0).get::<&str, i64>("count") ), - _ => panic!() // TODO remove when the generics will be refactored - } - } else if mssql_enabled { - quote! { - canyon_sql::crud::CanyonRows::Tiberius(mut v) => + #[cfg(feature="mssql")] + canyon_sql::crud::CanyonRows::Tiberius(mut v) => v.remove(0) .get::(0) .map(|c| c as i64) .ok_or(format!("Failure in the COUNT query for MSSQL for: {}", #ty_str).into()) .into(), + #[cfg(feature="mysql")] + canyon_sql::crud::CanyonRows::MySQL(mut v) => v.remove(0) + .get::(0) + .ok_or(format!("Failure in the COUNT query for MYSQL for: {}", #ty_str).into()), _ => panic!() // TODO remove when the generics will be refactored - } - } else { - quote! { - panic!( - "Reached a branch in the implementation of the Row Mapper macro that should never be reached.\ - This is a severe bug of Canyon-SQL. Please, open us an issue at \ - https://github.com/zerodaycode/Canyon-SQL/issues and let us know about that failure." - ) - } }; quote! { @@ -380,7 +356,7 @@ pub fn generate_find_by_foreign_key_tokens( }; fk_quotes.push(( - quote!{ #quoted_method_signature; }, + quote! { #quoted_method_signature; }, quote! { /// Searches the parent entity (if exists) for this type #quoted_method_signature { diff --git a/canyon_migrations/Cargo.toml b/canyon_migrations/Cargo.toml index ba353b76..ec9a31db 100644 --- a/canyon_migrations/Cargo.toml +++ b/canyon_migrations/Cargo.toml @@ -17,6 +17,9 @@ canyon_entities = { workspace = true } tokio = { workspace = true } tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } +mysql_async = { workspace = true, optional = true } +mysql_common = { workspace = true, optional = true } + async-trait = { workspace = true } @@ -30,3 +33,5 @@ syn = { version = "1.0.86", features = ["full", "parsing"] } # TODO Pending to r [features] postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres"] mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql"] +mysql = ["mysql_async","mysql_common", "canyon_connection/mysql", "canyon_crud/mysql"] + diff --git a/canyon_migrations/src/migrations/handler.rs b/canyon_migrations/src/migrations/handler.rs index 24dfb1c4..3d00da8b 100644 --- a/canyon_migrations/src/migrations/handler.rs +++ b/canyon_migrations/src/migrations/handler.rs @@ -94,6 +94,8 @@ impl Migrations { DatabaseType::PostgreSql => constants::postgresql_queries::FETCH_PUBLIC_SCHEMA, #[cfg(feature = "mssql")] DatabaseType::SqlServer => constants::mssql_queries::FETCH_PUBLIC_SCHEMA, + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!("Not implemented fetch database in mysql"), }; Self::query(query, [], datasource_name) @@ -291,5 +293,7 @@ fn check_for_table_name( DatabaseType::PostgreSql => table.table_name == res_row.get_postgres::<&str>("table_name"), #[cfg(feature = "mssql")] DatabaseType::SqlServer => table.table_name == res_row.get_mssql::<&str>("table_name"), + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!(), } } diff --git a/canyon_migrations/src/migrations/information_schema.rs b/canyon_migrations/src/migrations/information_schema.rs index 74709619..9e165eee 100644 --- a/canyon_migrations/src/migrations/information_schema.rs +++ b/canyon_migrations/src/migrations/information_schema.rs @@ -67,6 +67,8 @@ impl ColumnMetadataTypeValue { } _ => Self::NoneValue, }, + #[cfg(feature = "mysql")] + ColumnType::MySQL(_) => todo!(), } } } diff --git a/canyon_migrations/src/migrations/memory.rs b/canyon_migrations/src/migrations/memory.rs index 1d822fd1..80fbe3ad 100644 --- a/canyon_migrations/src/migrations/memory.rs +++ b/canyon_migrations/src/migrations/memory.rs @@ -248,6 +248,8 @@ impl CanyonMemory { DatabaseType::PostgreSql => constants::postgresql_queries::CANYON_MEMORY_TABLE, #[cfg(feature = "mssql")] DatabaseType::SqlServer => constants::mssql_queries::CANYON_MEMORY_TABLE, + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!("Memory table in mysql not implemented"), }; Self::query(query, [], datasource_name) diff --git a/canyon_migrations/src/migrations/processor.rs b/canyon_migrations/src/migrations/processor.rs index 425c1b0d..aee9a89e 100644 --- a/canyon_migrations/src/migrations/processor.rs +++ b/canyon_migrations/src/migrations/processor.rs @@ -807,7 +807,9 @@ impl DatabaseOperation for TableOperation { .join(", ") ) .replace('"', "") - } + }, + #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() + } } @@ -830,7 +832,9 @@ impl DatabaseOperation for TableOperation { be an allowed behaviour for now, only with the table_name parameter on the CanyonEntity annotation. */ - format!("exec sp_rename '{old_table_name}', '{new_table_name}';") + format!("exec sp_rename '{old_table_name}', '{new_table_name}';"), + #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() + } } @@ -848,7 +852,9 @@ impl DatabaseOperation for TableOperation { FOREIGN KEY ({_column_foreign_key}) REFERENCES {_table_to_reference} ({_column_to_reference});" ), #[cfg(feature = "mssql")] DatabaseType::SqlServer => - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]"), + #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() + } } @@ -859,7 +865,9 @@ impl DatabaseOperation for TableOperation { "ALTER TABLE {_table_with_foreign_key} DROP CONSTRAINT {_constraint_name};", ), #[cfg(feature = "mssql")] DatabaseType::SqlServer => - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]"), + #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() + } } @@ -871,7 +879,9 @@ impl DatabaseOperation for TableOperation { _entity_field.field_name ), #[cfg(feature = "mssql")] DatabaseType::SqlServer => - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]"), + #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() + } } @@ -880,7 +890,9 @@ impl DatabaseOperation for TableOperation { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;"), #[cfg(feature = "mssql")] DatabaseType::SqlServer => - format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;") + format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;"), + #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() + } } }; @@ -932,7 +944,9 @@ impl DatabaseOperation for ColumnOperation { table_name, entity_field.field_name, to_sqlserver_syntax(entity_field) - ) + ), + #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() + } ColumnOperation::DeleteColumn(table_name, column_name) => { // TODO Check if operation for SQL server is different @@ -946,7 +960,10 @@ impl DatabaseOperation for ColumnOperation { _entity_field.field_name, to_postgres_alter_syntax(_entity_field) ), #[cfg(feature = "mssql")] DatabaseType::SqlServer => - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]"), + #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() + + } ColumnOperation::AlterColumnDropNotNull(table_name, entity_field) => match db_type { @@ -956,7 +973,9 @@ impl DatabaseOperation for ColumnOperation { format!( "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NULL", entity_field.field_name, to_sqlserver_alter_syntax(entity_field) - ) + ), + #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() + } #[cfg(feature = "mssql")] ColumnOperation::DropNotNullBeforeDropColumn(table_name, column_name, column_datatype) => format!( @@ -982,7 +1001,9 @@ impl DatabaseOperation for ColumnOperation { "ALTER TABLE \"{table_name}\" ALTER COLUMN {} {} NOT NULL", entity_field.field_name, to_sqlserver_alter_syntax(entity_field) - ) + ), + #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() + } } diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 04c21b89..d24de91c 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -27,3 +27,14 @@ services: environment: MSSQL_SA_PASSWORD: "SqlServer-10" ACCEPT_EULA: "Y" + mysql: + image: mysql:latest + container_name: mysql + environment: + MYSQL_ROOT_PASSWORD: root + ports: + - '3307:3306' + volumes: + - ./mysql-data:/var/lib/mysql + - ./mysql/create_tables.sql:/docker-entrypoint-initdb.d/create_tables.sql + - ./mysql/fill_tables.sql:/docker-entrypoint-initdb.d/fill_tables.sql \ No newline at end of file diff --git a/docker/mysql/create_tables.sql b/docker/mysql/create_tables.sql new file mode 100644 index 00000000..8963767c --- /dev/null +++ b/docker/mysql/create_tables.sql @@ -0,0 +1,44 @@ +CREATE DATABASE public; + +CREATE TABLE public.league ( + id INT AUTO_INCREMENT PRIMARY KEY, + ext_id BIGINT NOT NULL, + slug TEXT NOT NULL, + name TEXT NOT NULL, + region TEXT NOT NULL, + image_url TEXT NOT NULL +); + +CREATE TABLE public.tournament ( + id INT AUTO_INCREMENT PRIMARY KEY, + ext_id BIGINT NOT NULL, + slug TEXT NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + league INT, + FOREIGN KEY (league) REFERENCES league(id) + +); + +CREATE TABLE public.player ( + id INT AUTO_INCREMENT PRIMARY KEY, + ext_id BIGINT NOT NULL, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + summoner_name TEXT NOT NULL, + image_url TEXT, + role TEXT NOT NULL +); + +CREATE TABLE public.team ( + id INT AUTO_INCREMENT PRIMARY KEY, + ext_id BIGINT NOT NULL, + slug TEXT NOT NULL, + name TEXT NOT NULL, + code TEXT NOT NULL, + image_url TEXT NOT NULL, + alt_image_url TEXT, + bg_image_url TEXT, + home_league INT, + FOREIGN KEY (home_league) REFERENCES league(id) +); diff --git a/docker/mysql/fill_tables.sql b/docker/mysql/fill_tables.sql new file mode 100644 index 00000000..84eff356 --- /dev/null +++ b/docker/mysql/fill_tables.sql @@ -0,0 +1,275 @@ +-- Values for league table +INSERT INTO public.league VALUES (1, 100695891328981122, 'european-masters', 'European Masters', 'EUROPE', 'http://static.lolesports.com/leagues/EM_Bug_Outline1.png'); +INSERT INTO public.league VALUES (2, 101097443346691685, 'turkey-academy-league', 'TAL', 'TURKEY', 'http://static.lolesports.com/leagues/1592516072459_TAL-01-FullonDark.png'); +INSERT INTO public.league VALUES (3, 101382741235120470, 'lla', 'LLA', 'LATIN AMERICA', 'http://static.lolesports.com/leagues/1592516315279_LLA-01-FullonDark.png'); +INSERT INTO public.league VALUES (4, 104366947889790212, 'pcs', 'PCS', 'HONG KONG, MACAU, TAIWAN', 'http://static.lolesports.com/leagues/1592515942679_PCS-01-FullonDark.png'); +INSERT INTO public.league VALUES (5, 105266074488398661, 'superliga', 'SuperLiga', 'EUROPE', 'http://static.lolesports.com/leagues/SL21-V-white.png'); +INSERT INTO public.league VALUES (6, 105266088231437431, 'ultraliga', 'Ultraliga', 'EUROPE', 'http://static.lolesports.com/leagues/1639390623717_ULTRALIGA_logo_sq_cyan.png'); +INSERT INTO public.league VALUES (7, 105266091639104326, 'primeleague', 'Prime League', 'EUROPE', 'http://static.lolesports.com/leagues/PrimeLeagueResized.png'); +INSERT INTO public.league VALUES (8, 105266094998946936, 'pg_nationals', 'PG Nationals', 'EUROPE', 'http://static.lolesports.com/leagues/PG_Nationals_Logo_White.png'); +INSERT INTO public.league VALUES (9, 105266098308571975, 'nlc', 'NLC', 'EUROPE', 'http://static.lolesports.com/leagues/1641490922073_nlc_logo.png'); +INSERT INTO public.league VALUES (10, 105266101075764040, 'liga_portuguesa', 'Liga Portuguesa', 'EUROPE', 'http://static.lolesports.com/leagues/1649884876085_LPLOL_2021_ISO_G-c389e9ae85c243e4f76a8028bbd9ca1609c2d12bc47c3709a9250d1b3ca43f58.png'); +INSERT INTO public.league VALUES (11, 105266103462388553, 'lfl', 'La Ligue Française', 'EUROPE', 'http://static.lolesports.com/leagues/LFL_Logo_2020_black1.png'); +INSERT INTO public.league VALUES (12, 105266106309666619, 'hitpoint_masters', 'Hitpoint Masters', 'EUROPE', 'http://static.lolesports.com/leagues/1641465237186_HM_white.png'); +INSERT INTO public.league VALUES (13, 105266108767593290, 'greek_legends', 'Greek Legends League', 'EUROPE', 'http://static.lolesports.com/leagues/GLL_LOGO_WHITE.png'); +INSERT INTO public.league VALUES (14, 105266111679554379, 'esports_balkan_league', 'Esports Balkan League', 'EUROPE', 'http://static.lolesports.com/leagues/1625735031226_ebl_crest-whitePNG.png'); +INSERT INTO public.league VALUES (15, 105549980953490846, 'cblol_academy', 'CBLOL Academy', 'BRAZIL', 'http://static.lolesports.com/leagues/cblol-acad-white.png'); +INSERT INTO public.league VALUES (16, 105709090213554609, 'lco', 'LCO', 'OCEANIA', 'http://static.lolesports.com/leagues/lco-color-white.png'); +INSERT INTO public.league VALUES (17, 106827757669296909, 'ljl_academy', 'LJL Academy', 'JAPAN', 'http://static.lolesports.com/leagues/1630062215891_ljl-al_logo_gradient.png'); +INSERT INTO public.league VALUES (18, 107213827295848783, 'vcs', 'VCS', 'VIETNAM', 'http://static.lolesports.com/leagues/1635953171501_LOL_VCS_Full_White.png'); +INSERT INTO public.league VALUES (19, 107407335299756365, 'elite_series', 'Elite Series', 'EUROPE', 'http://static.lolesports.com/leagues/1641287979138_EliteSeriesMarkWhite.png'); +INSERT INTO public.league VALUES (20, 107581050201097472, 'honor_division', 'Honor Division', 'LATIN AMERICA', 'http://static.lolesports.com/leagues/1641750781829_divhonormxwhite.png'); +INSERT INTO public.league VALUES (21, 107581669166925444, 'elements_league', 'Elements League', 'LATIN AMERICA', 'http://static.lolesports.com/leagues/1642593573670_LOGO_ELEMENTS_White.png'); +INSERT INTO public.league VALUES (22, 107582133359724496, 'volcano_discover_league', 'Volcano League', 'LATIN AMERICA', 'http://static.lolesports.com/leagues/1643106609661_VOLCANO-VERTICAL-ColorLight.png'); +INSERT INTO public.league VALUES (23, 107582580502415838, 'claro_gaming_stars_league', 'Stars League', 'LATIN AMERICA', 'http://static.lolesports.com/leagues/1642595169468_CLARO-GAMING-STARS-LEAGUE-B.png'); +INSERT INTO public.league VALUES (24, 107598636564896416, 'master_flow_league', 'Master Flow League', 'LATIN AMERICA', 'http://static.lolesports.com/leagues/1643794656405_LMF-White.png'); +INSERT INTO public.league VALUES (25, 107598951349015984, 'honor_league', 'Honor League', 'LATIN AMERICA', 'http://static.lolesports.com/leagues/1643036660690_lhe-ColorLight.png'); +INSERT INTO public.league VALUES (26, 107603541524308819, 'movistar_fiber_golden_league', 'Golden League', 'LATIN AMERICA', 'http://static.lolesports.com/leagues/1642445572375_MovistarLeague.png'); +INSERT INTO public.league VALUES (27, 107898214974993351, 'college_championship', 'College Championship', 'NORTH AMERICA', 'http://static.lolesports.com/leagues/1646396098648_CollegeChampionshiplogo.png'); +INSERT INTO public.league VALUES (28, 107921249454961575, 'proving_grounds', 'Proving Grounds', 'NORTH AMERICA', 'http://static.lolesports.com/leagues/1646747578708_download8.png'); +INSERT INTO public.league VALUES (29, 108001239847565215, 'tft_esports', 'TFT Last Chance Qualifier', 'INTERNATIONAL', 'http://static.lolesports.com/leagues/1649439858579_tftesport.png'); +INSERT INTO public.league VALUES (30, 98767975604431411, 'worlds', 'Worlds', 'INTERNATIONAL', 'http://static.lolesports.com/leagues/1592594612171_WorldsDarkBG.png'); +INSERT INTO public.league VALUES (31, 98767991295297326, 'all-star', 'All-Star Event', 'INTERNATIONAL', 'http://static.lolesports.com/leagues/1592594737227_ASEDarkBG.png'); +INSERT INTO public.league VALUES (32, 98767991299243165, 'lcs', 'LCS', 'NORTH AMERICA', 'http://static.lolesports.com/leagues/LCSNew-01-FullonDark.png'); +INSERT INTO public.league VALUES (33, 98767991302996019, 'lec', 'LEC', 'EUROPE', 'http://static.lolesports.com/leagues/1592516184297_LEC-01-FullonDark.png'); +INSERT INTO public.league VALUES (34, 98767991310872058, 'lck', 'LCK', 'KOREA', 'http://static.lolesports.com/leagues/lck-color-on-black.png'); +INSERT INTO public.league VALUES (35, 98767991314006698, 'lpl', 'LPL', 'CHINA', 'http://static.lolesports.com/leagues/1592516115322_LPL-01-FullonDark.png'); +INSERT INTO public.league VALUES (36, 98767991325878492, 'msi', 'MSI', 'INTERNATIONAL', 'http://static.lolesports.com/leagues/1592594634248_MSIDarkBG.png'); +INSERT INTO public.league VALUES (37, 98767991332355509, 'cblol-brazil', 'CBLOL', 'BRAZIL', 'http://static.lolesports.com/leagues/cblol-logo-symbol-offwhite.png'); +INSERT INTO public.league VALUES (38, 98767991335774713, 'lck_challengers_league', 'LCK Challengers', 'KOREA', 'http://static.lolesports.com/leagues/lck-cl-white.png'); +INSERT INTO public.league VALUES (39, 98767991343597634, 'turkiye-sampiyonluk-ligi', 'TCL', 'TURKEY', 'https://lolstatic-a.akamaihd.net/esports-assets/production/league/turkiye-sampiyonluk-ligi-8r9ofb9.png'); +INSERT INTO public.league VALUES (40, 98767991349978712, 'ljl-japan', 'LJL', 'JAPAN', 'http://static.lolesports.com/leagues/1592516354053_LJL-01-FullonDark.png'); +INSERT INTO public.league VALUES (41, 98767991355908944, 'lcl', 'LCL', 'COMMONWEALTH OF INDEPENDENT STATES', 'http://static.lolesports.com/leagues/1593016885758_LCL-01-FullonDark.png'); +INSERT INTO public.league VALUES (42, 99332500638116286, 'lcs-academy', 'LCS Academy', 'NORTH AMERICA', 'http://static.lolesports.com/leagues/lcs-academy-purple.png'); + + +-- Values for player table +INSERT INTO public.player VALUES (1, 98767975906852059, 'Jaehyeok', 'Park', 'Ruler', 'http://static.lolesports.com/players/1642153903692_GEN_Ruler_F.png', 'bottom'); +INSERT INTO public.player VALUES (2, 102186485482484390, 'Hyeonjun', 'Choi', 'Doran', 'http://static.lolesports.com/players/1642153880932_GEN_Doran_F.png', 'top'); +INSERT INTO public.player VALUES (3, 98767975916458257, 'Wangho ', 'Han', 'Peanut', 'http://static.lolesports.com/players/1642153896918_GEN_peanut_A.png', 'jungle'); +INSERT INTO public.player VALUES (4, 99871276342168416, 'Jihun', 'Jung', 'Chovy', 'http://static.lolesports.com/players/1642153873969_GEN_Chovy_F.png', 'mid'); +INSERT INTO public.player VALUES (5, 99871276332909841, 'Siu', 'Son', 'Lehends', 'http://static.lolesports.com/players/1642153887731_GEN_Lehends_F.png', 'support'); +INSERT INTO public.player VALUES (6, 104266797862156067, 'Youngjae', 'Ko', 'YoungJae', 'http://static.lolesports.com/players/1642153913037_GEN_YoungJae_F.png', 'jungle'); +INSERT INTO public.player VALUES (7, 103495716560217968, 'Hyoseong', 'Oh', 'Vsta', 'http://static.lolesports.com/players/1642154102606_HLE_Vsta_F.png', 'support'); +INSERT INTO public.player VALUES (8, 104266795407626462, 'Dongju', 'Lee', 'DuDu', 'http://static.lolesports.com/players/1642154060441_HLE_DuDu_F.png', 'top'); +INSERT INTO public.player VALUES (9, 106267386230851795, 'Junghyeun', 'Kim', 'Willer', 'http://static.lolesports.com/players/1642154110676_HLE_Willer_F.png', 'jungle'); +INSERT INTO public.player VALUES (10, 100725844995692264, 'Janggyeom', 'Kim', 'OnFleek', 'http://static.lolesports.com/players/1642154084709_HLE_Onfleek_F.png', 'jungle'); +INSERT INTO public.player VALUES (11, 105320683858945274, 'Hongjo', 'Kim', 'Karis', 'http://static.lolesports.com/players/1642154066010_HLE_Karis_F.png', 'mid'); +INSERT INTO public.player VALUES (12, 104287359934240404, 'Jaehoon', 'Lee', 'SamD', 'http://static.lolesports.com/players/1642154094651_HLE_SamD_F.png', 'bottom'); +INSERT INTO public.player VALUES (13, 103461966870841210, 'Wyllian', 'Adriano', 'asta', 'http://static.lolesports.com/players/1643226025146_Astacopy.png', 'jungle'); +INSERT INTO public.player VALUES (14, 107559111166843860, 'Felipe', 'Boal', 'Boal', 'http://static.lolesports.com/players/1644095483228_BOALcopiar.png', 'top'); +INSERT INTO public.player VALUES (15, 107559255871511679, 'Giovani', 'Baldan', 'Mito', 'http://static.lolesports.com/players/1643226193262_Mitocopy.png', 'top'); +INSERT INTO public.player VALUES (16, 103478281329357326, 'Arthur', 'Machado', 'Tutsz', 'http://static.lolesports.com/players/1643226293749_Tutszcopy.png', 'mid'); +INSERT INTO public.player VALUES (17, 103743599797538329, 'Luiz Felipe', 'Lobo', 'Flare', 'http://static.lolesports.com/players/1643226082718_Flarecopy.png', 'bottom'); +INSERT INTO public.player VALUES (18, 99566408210057665, 'Natan', 'Braz', 'fNb', 'http://static.lolesports.com/players/1643226467130_Fnbcopiar.png', 'top'); +INSERT INTO public.player VALUES (19, 99566407771166805, 'Filipe', 'Brombilla', 'Ranger', 'http://static.lolesports.com/players/1643226495379_Rangercopiar.png', 'jungle'); +INSERT INTO public.player VALUES (20, 107559327426244686, 'Vinícius', 'Corrêa', 'StineR', 'http://static.lolesports.com/players/1643226666563_Silhueta.png', 'jungle'); +INSERT INTO public.player VALUES (21, 99566407784212776, 'Bruno', 'Farias', 'Envy', 'http://static.lolesports.com/players/1643226430923_Envycopiar.png', 'mid'); +INSERT INTO public.player VALUES (22, 107559338252333149, 'Gabriel', 'Furuuti', 'Fuuu', 'http://static.lolesports.com/players/1643226717192_Silhueta.png', 'mid'); +INSERT INTO public.player VALUES (23, 105397181199735591, 'Lucas', 'Fensterseifer', 'Netuno', 'http://static.lolesports.com/players/1644095521735_Netunocopiar.png', 'bottom'); +INSERT INTO public.player VALUES (24, 98767975947296513, 'Ygor', 'Freitas', 'RedBert', 'http://static.lolesports.com/players/1643226527904_Redbertcopiar.png', 'support'); +INSERT INTO public.player VALUES (25, 100754278890207800, 'Geonyeong', 'Mun', 'Steal', 'http://static.lolesports.com/players/1644905307225_dfm_steal.png', 'jungle'); +INSERT INTO public.player VALUES (26, 99566404536983507, 'Chanju', 'Lee', 'Yaharong', 'http://static.lolesports.com/players/1644905328869_dfm_yaharong.png', 'mid'); +INSERT INTO public.player VALUES (27, 104016425624023728, 'Jiyoong', 'Lee', 'Harp', 'http://static.lolesports.com/players/1644905257358_dfm_harp.png', 'support'); +INSERT INTO public.player VALUES (28, 98767991750309549, 'Danil', 'Reshetnikov', 'Diamondprox', 'http://static.lolesports.com/players/Diamondproxcopy.png', 'jungle'); +INSERT INTO public.player VALUES (29, 105700748891875072, 'Nikita ', 'Gudkov', 'Griffon ', 'http://static.lolesports.com/players/1642071116433_placeholder.png', 'mid'); +INSERT INTO public.player VALUES (30, 105700946934214905, 'YEVHEN', 'ZAVALNYI', 'Mytant', 'http://static.lolesports.com/players/1642071138150_placeholder.png', 'bottom'); +INSERT INTO public.player VALUES (31, 98767991755955790, 'Eduard', 'Abgaryan', 'Edward', 'https://lolstatic-a.akamaihd.net/esports-assets/production/player/gosu-pepper-88anxcql.png', 'support'); +INSERT INTO public.player VALUES (32, 106301600611225723, 'Mark', 'Leksin', 'Dreampull', 'http://static.lolesports.com/players/placeholder.jpg', 'top'); +INSERT INTO public.player VALUES (33, 107721938219680332, 'Azamat', 'Atkanov', 'TESLA', 'http://static.lolesports.com/players/1643706327509_placeholder.png', 'support'); +INSERT INTO public.player VALUES (34, 100725844988653773, 'Su', 'Heo', 'ShowMaker', 'http://static.lolesports.com/players/1642153659258_DK_ShowMaker_F.png', 'mid'); +INSERT INTO public.player VALUES (35, 102483272156027229, 'Daegil', 'Seo', 'deokdam', 'http://static.lolesports.com/players/1642153629340_DK_deokdam_F.png', 'bottom'); +INSERT INTO public.player VALUES (36, 101388913291808185, 'Hyeonggyu', 'Kim', 'Kellin', 'http://static.lolesports.com/players/1642153649009_DK_Kellin_F.png', 'support'); +INSERT INTO public.player VALUES (37, 105705431649727017, 'Taeyoon', 'Noh', 'Burdol', 'http://static.lolesports.com/players/1642153598672_DK_Burdol_F.png', 'top'); +INSERT INTO public.player VALUES (38, 103729432252832975, 'Yongho', 'Yoon', 'Hoya', 'http://static.lolesports.com/players/1642153639500_DK_Hoya_F.png', 'top'); +INSERT INTO public.player VALUES (39, 105320703008048707, 'Dongbum', 'Kim', 'Croco', 'http://static.lolesports.com/players/1642154712531_LSB_Croco_R.png', 'jungle'); +INSERT INTO public.player VALUES (40, 105501829364113001, 'Hobin', 'Jeon', 'Howling', 'http://static.lolesports.com/players/1642154731703_LSB_Howling_F.png', 'top'); +INSERT INTO public.player VALUES (41, 104284310661848687, 'Juhyeon', 'Lee', 'Clozer', 'http://static.lolesports.com/players/1642154706000_LSB_Clozer_R.png', 'mid'); +INSERT INTO public.player VALUES (42, 100725844996918206, 'Jaeyeon', 'Kim', 'Dove', 'http://static.lolesports.com/players/1642154719503_LSB_Dove_R.png', 'top'); +INSERT INTO public.player VALUES (43, 105530583598805234, 'Myeongjun', 'Lee', 'Envyy', 'http://static.lolesports.com/players/1642154726047_LSB_Envyy_F.png', 'bottom'); +INSERT INTO public.player VALUES (44, 105530584812980593, 'Jinhong', 'Kim', 'Kael', 'http://static.lolesports.com/players/1642154745002_LSB_Kael_F.png', 'support'); +INSERT INTO public.player VALUES (45, 105501834624360050, 'Sanghoon', 'Yoon', 'Ice', 'http://static.lolesports.com/players/1642154738262_LSB_Ice_F.png', 'bottom'); +INSERT INTO public.player VALUES (46, 99322214647978964, 'Daniele', 'di Mauro', 'Jiizuke', 'http://static.lolesports.com/players/eg-jiizuke-2021.png', 'mid'); +INSERT INTO public.player VALUES (47, 100787602257283436, 'Minh Loc', 'Pham', 'Zeros', 'https://lolstatic-a.akamaihd.net/esports-assets/production/player/zeros-4keddu17.png', 'top'); +INSERT INTO public.player VALUES (48, 104327502738107767, 'Nicolás', 'Rivero', 'Kiefer', 'http://static.lolesports.com/players/1643047365591_Kiefer-2.png', 'mid'); +INSERT INTO public.player VALUES (49, 102179902322952953, 'Manuel', 'Scala', 'Pancake', 'http://static.lolesports.com/players/1643047550782_Pancake-5.png', 'bottom'); +INSERT INTO public.player VALUES (50, 105516185566739968, 'Cristóbal', 'Arróspide', 'Zothve', 'http://static.lolesports.com/players/1643047287141_Zothve-9.png', 'top'); +INSERT INTO public.player VALUES (51, 99871352196477603, 'Gwanghyeop', 'Kim', 'Hoglet', 'http://static.lolesports.com/players/1643047312405_Hoglet-8.png', 'jungle'); +INSERT INTO public.player VALUES (52, 99871352193690418, 'Changhun', 'Han', 'Luci', 'http://static.lolesports.com/players/1643047438703_Luci-5.png', 'support'); +INSERT INTO public.player VALUES (53, 107635899693202699, 'Thomas', 'Garnsworthy', 'Tronthepom', 'https://static.lolesports.com/players/download.png', 'top'); +INSERT INTO public.player VALUES (54, 107635905118503535, 'James', 'Craig', 'Voice', 'https://static.lolesports.com/players/download.png', 'bottom'); +INSERT INTO public.player VALUES (55, 107635907168238086, 'Rocco', 'Potter', 'rocco521', 'https://static.lolesports.com/players/download.png', 'support'); +INSERT INTO public.player VALUES (56, 107635918452357647, 'Reuben', 'Best', 'Reufury', 'https://static.lolesports.com/players/download.png', 'mid'); +INSERT INTO public.player VALUES (57, 107647480732814180, 'Bryce', 'Zhou', 'Meifan', 'https://static.lolesports.com/players/download.png', 'jungle'); +INSERT INTO public.player VALUES (58, 107657801460158111, 'Benny', 'Nguyen', 'District 1', 'https://static.lolesports.com/players/download.png', 'jungle'); +INSERT INTO public.player VALUES (59, 105709372540742118, 'Blake', 'Schlage', 'Azus', 'http://static.lolesports.com/players/silhouette.png', 'top'); +INSERT INTO public.player VALUES (60, 106350759376304634, 'Shao', 'Zhong', 'Akano', 'https://static.lolesports.com/players/download.png', 'jungle'); +INSERT INTO public.player VALUES (61, 107634941727734818, 'Jeremy', 'Lim', 'foreigner', 'https://static.lolesports.com/players/download.png', 'jungle'); +INSERT INTO public.player VALUES (62, 105709381466108761, 'Reuben', 'Salb', 'Piglet', 'http://static.lolesports.com/players/silhouette.png', 'bottom'); +INSERT INTO public.player VALUES (63, 105747861836427633, 'Yi', 'Chen', 'Thomas Shen', 'https://static.lolesports.com/players/download.png', 'bottom'); +INSERT INTO public.player VALUES (64, 107657786356796634, 'Robert', 'Wells', 'Tyran', 'https://static.lolesports.com/players/download.png', 'top'); +INSERT INTO public.player VALUES (65, 107657790493529410, 'Da Woon', 'Jeung', 'DaJeung', 'https://static.lolesports.com/players/download.png', 'mid'); +INSERT INTO public.player VALUES (66, 107657793079479518, 'Rhett', 'Wiggins', 'Vxpir', 'https://static.lolesports.com/players/download.png', 'support'); +INSERT INTO public.player VALUES (67, 107698225510856278, 'Benson', 'Tsai', 'Entrust', 'https://static.lolesports.com/players/download.png', 'support'); +INSERT INTO public.player VALUES (68, 103525219435043049, 'Lachlan', 'Keene-O''Keefe', 'N0body', 'https://lolstatic-a.akamaihd.net/esports-assets/production/player/n0body-einjqvyk.png', 'top'); +INSERT INTO public.player VALUES (69, 101389749294612370, 'Janik', 'Bartels', 'Jenax', 'http://static.lolesports.com/players/1642003381408_jenax.png', 'top'); +INSERT INTO public.player VALUES (70, 101383793865143549, 'Erik', 'Wessén', 'Treatz', 'http://static.lolesports.com/players/1642003495533_treatz.png', 'support'); +INSERT INTO public.player VALUES (71, 101389737455173027, 'Daniyal ', 'Gamani', 'Sertuss', 'http://static.lolesports.com/players/1642003453914_sertuss.png', 'mid'); +INSERT INTO public.player VALUES (72, 99322214588927915, 'Erberk ', 'Demir', 'Gilius', 'http://static.lolesports.com/players/1642003341615_gilius.png', 'jungle'); +INSERT INTO public.player VALUES (73, 99322214668103078, 'Matti', 'Sormunen', 'WhiteKnight', 'http://static.lolesports.com/players/1642003243059_white-knight.png', 'top'); +INSERT INTO public.player VALUES (74, 100312190807221865, 'Nikolay ', 'Akatov', 'Zanzarah', 'http://static.lolesports.com/players/1642003282324_zanzarah.png', 'jungle'); +INSERT INTO public.player VALUES (75, 99322214243134013, 'Hampus ', 'Abrahamsson', 'promisq', 'http://static.lolesports.com/players/1642003205916_promisq.png', 'support'); +INSERT INTO public.player VALUES (76, 99322214620375780, 'Kasper', 'Kobberup', 'Kobbe', 'http://static.lolesports.com/players/1642003168563_kobbe.png', 'bottom'); +INSERT INTO public.player VALUES (77, 99322214238585389, 'Patrik', 'Jiru', 'Patrik', 'http://static.lolesports.com/players/1642004060212_patrik.png', 'bottom'); +INSERT INTO public.player VALUES (78, 105519722481834694, 'Mark', 'van Woensel', 'Markoon', 'http://static.lolesports.com/players/1642003998089_markoon.png', 'jungle'); +INSERT INTO public.player VALUES (79, 105519724699493915, 'Hendrik', 'Reijenga', 'Advienne', 'http://static.lolesports.com/players/1642003935782_advienne.png', 'support'); +INSERT INTO public.player VALUES (80, 99322214616775017, 'Erlend', 'Holm', 'Nukeduck', 'http://static.lolesports.com/players/1642004031937_nukeduck.png', 'mid'); +INSERT INTO public.player VALUES (81, 101389713973624205, 'Finn', 'WiestÃ¥l', 'Finn', 'http://static.lolesports.com/players/1642003970167_finn.png', 'top'); +INSERT INTO public.player VALUES (82, 99322214629661297, 'Mihael', 'Mehle', 'Mikyx', 'http://static.lolesports.com/players/G2_MIKYX2021_summer.png', 'support'); +INSERT INTO public.player VALUES (83, 100482247959137902, 'Emil', 'Larsson', 'Larssen', 'http://static.lolesports.com/players/1642003206398_larssen.png', 'mid'); +INSERT INTO public.player VALUES (84, 99322214598412197, 'Andrei', 'Pascu', 'Odoamne', 'http://static.lolesports.com/players/1642003264169_odoamne.png', 'top'); +INSERT INTO public.player VALUES (85, 102181528883745160, 'Adrian', 'Trybus', 'Trymbi', 'http://static.lolesports.com/players/1642003301461_trymbi.png', 'support'); +INSERT INTO public.player VALUES (86, 99566406053904433, 'Geun-seong', 'Kim', 'Malrang', 'http://static.lolesports.com/players/1642003233110_malrang.png', 'jungle'); +INSERT INTO public.player VALUES (87, 103536921420956640, 'Markos', 'Stamkopoulos', 'Comp', 'http://static.lolesports.com/players/1642003175488_comp.png', 'bottom'); +INSERT INTO public.player VALUES (88, 101388912808637770, 'Hanxi', 'Xia', 'Chelizi', 'http://static.lolesports.com/players/1593128001829_silhouette.png', 'top'); +INSERT INTO public.player VALUES (89, 105516474039500339, 'Fei-Yang', 'Luo', 'Captain', 'http://static.lolesports.com/players/silhouette.png', 'mid'); +INSERT INTO public.player VALUES (90, 106368709696011395, 'Seung Min', 'Han', 'Patch', 'http://static.lolesports.com/players/silhouette.png', 'support'); +INSERT INTO public.player VALUES (91, 107597376599119596, 'HAOTIAN', 'BI', 'yaoyao', 'http://static.lolesports.com/players/1641805668544_placeholder.png', 'support'); +INSERT INTO public.player VALUES (92, 101388912811586896, 'Zhilin', 'Su', 'Southwind', 'http://static.lolesports.com/players/1593129903866_ig-southwind-web.png', 'support'); +INSERT INTO public.player VALUES (93, 101388912810603854, 'Wang', 'Ding', 'Puff', 'http://static.lolesports.com/players/1593129891452_ig-puff-web.png', 'bottom'); +INSERT INTO public.player VALUES (94, 104287371427354335, 'Zhi-Peng', 'Tian', 'New', 'http://static.lolesports.com/players/1593132511529_rng-new-web.png', 'top'); +INSERT INTO public.player VALUES (95, 107597380474228562, 'WANG', 'XIN', 'frigid', 'http://static.lolesports.com/players/1641805726386_placeholder.png', 'jungle'); +INSERT INTO public.player VALUES (96, 104287365097341858, 'Peng', 'Guo', 'ppgod', 'http://static.lolesports.com/players/1593135580022_v5-ppgod-web.png', 'support'); +INSERT INTO public.player VALUES (97, 103478281359738222, 'Qi-Shen ', 'Ying', 'Photic', 'https://lolstatic-a.akamaihd.net/esports-assets/production/player/photic-k1ttlyxh.png', 'bottom'); +INSERT INTO public.player VALUES (98, 103478281402167891, 'Xiao-Long ', 'Li', 'XLB', 'http://static.lolesports.com/players/1593132528126_rng-xlb-web.png', 'jungle'); +INSERT INTO public.player VALUES (99, 102186438403674539, 'Jaewon', 'Lee', 'Rich', 'http://static.lolesports.com/players/ns-rich.png', 'top'); +INSERT INTO public.player VALUES (100, 99124844346233375, 'Onur', 'Ünalan', 'Zergsting', 'http://static.lolesports.com/players/1633542837856_gs-zergsting-w21.png', 'support'); + + +-- Values for team table +INSERT INTO public.team VALUES (1, 100205573495116443, 'geng', 'Gen.G', 'GEN', 'http://static.lolesports.com/teams/1631819490111_geng-2021-worlds.png', 'http://static.lolesports.com/teams/1592589327624_Gen.GGEN-03-FullonLight.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/geng-bnm75bf5.png', 34); +INSERT INTO public.team VALUES (2, 100205573496804586, 'hanwha-life-esports', 'Hanwha Life Esports', 'HLE', 'http://static.lolesports.com/teams/1631819564399_hle-2021-worlds.png', 'http://static.lolesports.com/teams/hle-2021-color-on-light2.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/hanwha-life-esports-7kh5kjdc.png', 34); +INSERT INTO public.team VALUES (3, 100205576307813373, 'flamengo-esports', 'Flamengo Esports', 'FLA', 'http://static.lolesports.com/teams/1642953977323_Monograma_Branco-Vermelho.png', 'http://static.lolesports.com/teams/1642953977326_Monograma_Branco-Vermelho.png', NULL, 37); +INSERT INTO public.team VALUES (4, 100205576309502431, 'furia', 'FURIA', 'FUR', 'http://static.lolesports.com/teams/FURIA---black.png', 'http://static.lolesports.com/teams/FURIA---black.png', 'http://static.lolesports.com/teams/FuriaUppercutFUR.png', 37); +INSERT INTO public.team VALUES (5, 100285330168091787, 'detonation-focusme', 'DetonatioN FocusMe', 'DFM', 'http://static.lolesports.com/teams/1631820630246_dfm-2021-worlds.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/detonation-focusme-ajvyc8cy.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/detonation-focusme-4pgp383l.png', 40); +INSERT INTO public.team VALUES (6, 100289931264192378, 'team-spirit', 'Team Spirit', 'TSPT', 'http://static.lolesports.com/teams/1643720491696_Whitelogo.png', 'http://static.lolesports.com/teams/1643720491697_Blacklogo.png', NULL, 41); +INSERT INTO public.team VALUES (7, 100725845018863243, 'dwg-kia', 'DWG KIA', 'DK', 'http://static.lolesports.com/teams/1631819456274_dwg-kia-2021-worlds.png', 'http://static.lolesports.com/teams/DK-FullonLight.png', 'http://static.lolesports.com/teams/DamwonGamingDWG.png', 34); +INSERT INTO public.team VALUES (8, 100725845022060229, 'liiv-sandbox', 'Liiv SANDBOX', 'LSB', 'http://static.lolesports.com/teams/liiv-sandbox-new.png', 'http://static.lolesports.com/teams/liiv-sandbox-new.png', NULL, 34); +INSERT INTO public.team VALUES (9, 101157821444002947, 'nexus-blitz-pro-a', 'Nexus Blitz Blue', 'NXB', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/nexus-blitz-pro-a-esrcx58b.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/nexus-blitz-pro-a-3w3j1cwx.png', NULL, 31); +INSERT INTO public.team VALUES (10, 101157821447017610, 'nexus-blitz-pro-b', 'Nexus Blitz Red', 'NXR', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/nexus-blitz-pro-b-j6s80wmi.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/nexus-blitz-pro-b-kjtp467.png', NULL, 31); +INSERT INTO public.team VALUES (11, 101383792559569368, 'all-knights', 'All Knights', 'AK', 'http://static.lolesports.com/teams/AK-Black-BG.png', 'http://static.lolesports.com/teams/AK-White-BG.png', NULL, 3); +INSERT INTO public.team VALUES (12, 101383792887446028, 'mammoth', 'MAMMOTH', 'MEC', 'http://static.lolesports.com/teams/1643079304055_RedMammothIcon.png', 'http://static.lolesports.com/teams/1643079304062_RedMammothIcon.png', NULL, 16); +INSERT INTO public.team VALUES (13, 101383792891050518, 'gravitas', 'Gravitas', 'GRV', 'http://static.lolesports.com/teams/gravitas-logo.png', 'http://static.lolesports.com/teams/gravitas-logo.png', NULL, 16); +INSERT INTO public.team VALUES (14, 101383793567806688, 'sk-gaming', 'SK Gaming', 'SK', 'http://static.lolesports.com/teams/1643979272144_SK_Monochrome.png', 'http://static.lolesports.com/teams/1643979272151_SK_Monochrome.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/sk-gaming-2cd63tzz.png', 33); +INSERT INTO public.team VALUES (15, 101383793569248484, 'astralis', 'Astralis', 'AST', 'http://static.lolesports.com/teams/AST-FullonDark.png', 'http://static.lolesports.com/teams/AST-FullonLight.png', 'http://static.lolesports.com/teams/AstralisAST.png', 33); +INSERT INTO public.team VALUES (16, 101383793572656373, 'excel', 'EXCEL', 'XL', 'http://static.lolesports.com/teams/Excel_FullColor2.png', 'http://static.lolesports.com/teams/Excel_FullColor1.png', 'http://static.lolesports.com/teams/ExcelXL.png', 33); +INSERT INTO public.team VALUES (17, 101383793574360315, 'rogue', 'Rogue', 'RGE', 'http://static.lolesports.com/teams/1631819715136_rge-2021-worlds.png', NULL, 'http://static.lolesports.com/teams/1632941190948_RGE.png', 33); +INSERT INTO public.team VALUES (18, 101388912911039804, 'thunder-talk-gaming', 'Thunder Talk Gaming', 'TT', 'http://static.lolesports.com/teams/TT-FullonDark.png', 'http://static.lolesports.com/teams/TT-FullonLight.png', 'http://static.lolesports.com/teams/TTTT.png', 35); +INSERT INTO public.team VALUES (19, 101388912914513220, 'victory-five', 'Victory Five', 'V5', 'http://static.lolesports.com/teams/1592592149333_VictoryFiveV5-01-FullonDark.png', 'http://static.lolesports.com/teams/1592592149336_VictoryFiveV5-03-FullonLight.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/victory-five-ha9mq1rv.png', 35); +INSERT INTO public.team VALUES (20, 101422616509070746, 'galatasaray-espor', 'Galatasaray Espor', 'GS', 'http://static.lolesports.com/teams/1631820533570_galatasaray-2021-worlds.png', 'http://static.lolesports.com/teams/1631820533572_galatasaray-2021-worlds.png', 'http://static.lolesports.com/teams/1632941006301_GalatasarayGS.png', 39); +INSERT INTO public.team VALUES (21, 101428372598668846, 'burning-core', 'Burning Core', 'BC', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/burning-core-7q0431w1.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/burning-core-8a63k0iu.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/burning-core-fnmfa2td.png', 40); +INSERT INTO public.team VALUES (22, 101428372600307248, 'rascal-jester', 'Rascal Jester', 'RJ', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/rascal-jester-e0g6cud0.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/rascal-jester-g32ay08v.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/rascal-jester-guqjh8kb.png', 40); +INSERT INTO public.team VALUES (23, 101428372602011186, 'v3-esports', 'V3 Esports', 'V3', 'http://static.lolesports.com/teams/v3_500x500.png', 'http://static.lolesports.com/teams/v3_500x500.png', NULL, 40); +INSERT INTO public.team VALUES (24, 101428372603715124, 'crest-gaming-act', 'Crest Gaming Act', 'CGA', 'http://static.lolesports.com/teams/1630058341510_cga_512px.png', 'http://static.lolesports.com/teams/1630058341513_cga_512px.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/crest-gaming-act-7pkgpqa.png', 40); +INSERT INTO public.team VALUES (25, 101428372605353526, 'sengoku-gaming', 'Sengoku Gaming', 'SG', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/sengoku-gaming-ikyxjlfn.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/sengoku-gaming-gnat0l9c.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/sengoku-gaming-3rd8ifie.png', 40); +INSERT INTO public.team VALUES (26, 101428372607057464, 'axiz', 'AXIZ', 'AXZ', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/axiz-frilmkic.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/axiz-fpemv4d2.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/axiz-9hiwgh3l.png', 40); +INSERT INTO public.team VALUES (27, 101428372830010965, 'alpha-esports', 'Alpha Esports', 'ALF', 'http://static.lolesports.com/teams/1592588479686_AlphaEsportsALF-01-FullonDark.png', 'http://static.lolesports.com/teams/1592588479688_AlphaEsportsALF-03-FullonLight.png', NULL, 4); +INSERT INTO public.team VALUES (28, 101978171843206569, 'vega-squadron', 'Vega Squadron', 'VEG', 'http://static.lolesports.com/teams/vega.png', 'http://static.lolesports.com/teams/vega.png', NULL, 41); +INSERT INTO public.team VALUES (29, 102141671181705193, 'michigan-state-university', 'Michigan State University', 'MSU', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/michigan-state-university-au4vndaf.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/michigan-state-university-c5mv9du0.png', NULL, NULL); +INSERT INTO public.team VALUES (30, 102141671182557163, 'university-of-illinois', 'University of Illinois', 'UI', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/university-of-illinois-bwvscsri.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/university-of-illinois-b3jros5r.png', NULL, NULL); +INSERT INTO public.team VALUES (31, 102141671183409133, 'maryville-university', 'Maryville University', 'MU', 'http://static.lolesports.com/teams/1647541915472_200x200_MU_Logo.png', 'http://static.lolesports.com/teams/1647541915475_200x200_MU_Logo.png', NULL, 28); +INSERT INTO public.team VALUES (32, 102141671185047537, 'uci-esports', 'UCI Esports', 'UCI', 'http://static.lolesports.com/teams/1641604280633_UCI.png', 'http://static.lolesports.com/teams/1641548061305_LOLESPORTSICON.png', NULL, NULL); +INSERT INTO public.team VALUES (33, 102141671185899507, 'university-of-western-ontario', 'University of Western Ontario', 'UWO', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/university-of-western-ontario-9q0nn3lw.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/university-of-western-ontario-6csb5dft.png', NULL, NULL); +INSERT INTO public.team VALUES (34, 102141671186685941, 'university-of-waterloo', 'University of Waterloo', 'UW', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/university-of-waterloo-2wuni11l.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/university-of-waterloo-aghmypqf.png', NULL, NULL); +INSERT INTO public.team VALUES (35, 102141671187668983, 'nc-state-university', 'NC State University', 'NCSU', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/nc-state-university-it42b898.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/nc-state-university-6ey19n1w.png', NULL, NULL); +INSERT INTO public.team VALUES (36, 102235771678061291, 'fastpay-wildcats', 'fastPay Wildcats', 'IW', 'http://static.lolesports.com/teams/fastpay-wildcats.png', 'http://static.lolesports.com/teams/fastpay-wildcats.png', NULL, 39); +INSERT INTO public.team VALUES (37, 102747101565183056, 'nongshim-redforce', 'NongShim REDFORCE', 'NS', 'http://static.lolesports.com/teams/NSFullonDark.png', 'http://static.lolesports.com/teams/NSFullonLight.png', 'http://static.lolesports.com/teams/NongshimRedForceNS.png', 34); +INSERT INTO public.team VALUES (38, 102787200120306562, 'mousesports', 'Mousesports', 'MOUZ', 'http://static.lolesports.com/teams/1639486346996_PRM_MOUZ-FullColorDarkBG.png', 'http://static.lolesports.com/teams/1639486346999_PRM_MOUZ-FullColorDarkBG.png', NULL, NULL); +INSERT INTO public.team VALUES (39, 102787200124959636, 'crvena-zvezda-esports', 'Crvena Zvezda Esports', 'CZV', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/crvena-zvezda-esports-ddtlzzhd.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/crvena-zvezda-esports-ddtlzzhd.png', NULL, 1); +INSERT INTO public.team VALUES (40, 102787200126663579, 'giants', 'Giants', 'GIA', 'http://static.lolesports.com/teams/1641412992057_escudowhite.png', 'http://static.lolesports.com/teams/1641412992058_escudo_black.png', NULL, NULL); +INSERT INTO public.team VALUES (41, 102787200129022886, 'esuba', 'eSuba', 'ESB', 'http://static.lolesports.com/teams/1629209489523_esuba_full_pos.png', 'http://static.lolesports.com/teams/1629209489525_esuba_full_pos.png', NULL, NULL); +INSERT INTO public.team VALUES (42, 102787200130988976, 'asus-rog-elite', 'ASUS ROG Elite', 'ASUS', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/asus-rog-elite-iouou6l.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/asus-rog-elite-cz4z103n.png', NULL, NULL); +INSERT INTO public.team VALUES (43, 102787200132955066, 'for-the-win-esports', 'For The Win Esports', 'FTW', 'http://static.lolesports.com/teams/LPLOL_FTW-Logo1.png', 'http://static.lolesports.com/teams/LPLOL_FTW-Logo1.png', NULL, NULL); +INSERT INTO public.team VALUES (44, 102787200134790084, 'hma-fnatic-rising', 'HMA Fnatic Rising', 'FNCR', 'http://static.lolesports.com/teams/NLC_FNCR-logo.png', 'http://static.lolesports.com/teams/NLC_FNCR-logo.png', NULL, NULL); +INSERT INTO public.team VALUES (45, 102787200136756173, 'berlin-international-gaming', 'Berlin International Gaming', 'BIG', 'http://static.lolesports.com/teams/BIG-Logo-2020-White1.png', 'http://static.lolesports.com/teams/BIG-Logo-2020-White1.png', NULL, 7); +INSERT INTO public.team VALUES (46, 102787200138722262, 'devilsone', 'Devils.One', 'DV1', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/devilsone-bfe3xkh.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/devilsone-dmj5ivct.png', NULL, 6); +INSERT INTO public.team VALUES (47, 102787200143309800, 'ensure', 'eNsure', 'EN', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/ensure-5hi6e2cg.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/ensure-fehdkert.png', NULL, 1); +INSERT INTO public.team VALUES (48, 102787200145472495, 'defusekids', 'Defusekids', 'DKI', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/defusekids-finmimok.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/defusekids-wu2z0pj.png', NULL, NULL); +INSERT INTO public.team VALUES (49, 102787200147504121, 'campus-party-sparks', 'Campus Party Sparks', 'SPK', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/campus-party-sparks-5h2d1rjh.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/campus-party-sparks-72ccff49.png', NULL, NULL); +INSERT INTO public.team VALUES (50, 102787200149928963, 'we-love-gaming', 'We Love Gaming', 'WLG', 'http://static.lolesports.com/teams/WLGlogo.png', 'http://static.lolesports.com/teams/WLGlogo.png', NULL, NULL); +INSERT INTO public.team VALUES (51, 102787200151698443, 'vitalitybee', 'Vitality.Bee', 'VITB', 'http://static.lolesports.com/teams/Vitality-logo-color-outline-rgb.png', 'http://static.lolesports.com/teams/Vitality-logo-color-outline-rgb.png', NULL, 1); +INSERT INTO public.team VALUES (52, 102787200153467923, 'bcn-squad', 'BCN Squad', 'BCN', 'http://static.lolesports.com/teams/SL_BCN-Logo_White.png', 'http://static.lolesports.com/teams/SL_BCN-Logo_Dark.png', NULL, NULL); +INSERT INTO public.team VALUES (53, 102787200155434012, 'jdxl', 'JD|XL', 'JDXL', 'http://static.lolesports.com/teams/1641489535868_jdxl.png', NULL, NULL, 9); +INSERT INTO public.team VALUES (54, 102787200157400101, 'falkn', 'FALKN', 'FKN', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/falkn-j72aqsqk.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/falkn-dhvtpixb.png', NULL, 1); +INSERT INTO public.team VALUES (55, 102787200159169580, 'godsent', 'Godsent', 'GOD', 'http://static.lolesports.com/teams/NLC_GOD-light.png', 'http://static.lolesports.com/teams/NLC_GOD-dark.png', NULL, NULL); +INSERT INTO public.team VALUES (56, 102825747701670848, 'azules-esports', 'Azules Esports', 'UCH', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/azules-esports-ak2khbqa.png', NULL, 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/azules-esports-e8yjxxki.png', NULL); +INSERT INTO public.team VALUES (57, 103461966951059521, 'evil-geniuses', 'Evil Geniuses', 'EG', 'http://static.lolesports.com/teams/1592590374862_EvilGeniusesEG-01-FullonDark.png', 'http://static.lolesports.com/teams/1592590374875_EvilGeniusesEG-03-FullonLight.png', 'http://static.lolesports.com/teams/1590003096057_EvilGeniusesEG.png', 32); +INSERT INTO public.team VALUES (58, 103461966965149786, 'mad-lions', 'MAD Lions', 'MAD', 'http://static.lolesports.com/teams/1631819614211_mad-2021-worlds.png', 'http://static.lolesports.com/teams/1592591395341_MadLionsMAD-03-FullonLight.png', 'http://static.lolesports.com/teams/MAD.png', 33); +INSERT INTO public.team VALUES (59, 103461966971048042, 'eg-academy', 'EG Academy', 'EG', 'http://static.lolesports.com/teams/1592590391188_EvilGeniusesEG-01-FullonDark.png', 'http://static.lolesports.com/teams/1592590391200_EvilGeniusesEG-03-FullonLight.png', 'http://static.lolesports.com/teams/1590003135776_EvilGeniusesEG.png', 28); +INSERT INTO public.team VALUES (60, 103461966975897718, 'imt-academy', 'IMT Academy', 'IMT', 'http://static.lolesports.com/teams/imt-new-color.png', 'http://static.lolesports.com/teams/imt-new-color.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/immortals-academy-hmxmnvhe.png', 28); +INSERT INTO public.team VALUES (61, 103461966981927044, 'dig-academy', 'DIG Academy', 'DIG', 'http://static.lolesports.com/teams/DIG-FullonDark.png', 'http://static.lolesports.com/teams/DIG-FullonLight.png', 'http://static.lolesports.com/teams/DignitasDIG.png', 28); +INSERT INTO public.team VALUES (62, 103461966986776720, 'ultra-prime', 'Ultra Prime', 'UP', 'http://static.lolesports.com/teams/ultraprime.png', 'http://static.lolesports.com/teams/ultraprime.png', NULL, 35); +INSERT INTO public.team VALUES (63, 103495716836203404, '5-ronin', '5 Ronin', '5R', 'http://static.lolesports.com/teams/5R_LOGO.png', 'http://static.lolesports.com/teams/5R_LOGO.png', NULL, 39); +INSERT INTO public.team VALUES (100, 104211666442891296, 'ogaming', 'O''Gaming', 'OGA', 'http://static.lolesports.com/teams/1590143833802_Ays7Gjmu_400x400.jpg', NULL, NULL, NULL); +INSERT INTO public.team VALUES (64, 103495716886587312, 'besiktas', 'BeÅŸiktaÅŸ', 'BJK', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/besiktas-e-sports-club-dlw48ntu.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/besiktas-e-sports-club-6ttscu28.png', NULL, 39); +INSERT INTO public.team VALUES (65, 103535282113853330, '5-ronin-akademi', '5 Ronin Akademi', '5R', 'http://static.lolesports.com/teams/5R_LOGO.png', 'http://static.lolesports.com/teams/5R_LOGO.png', NULL, 2); +INSERT INTO public.team VALUES (66, 103535282119620510, 'fukuoka-softbank-hawks-gaming', 'Fukuoka SoftBank HAWKS gaming', 'SHG', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/fukuoka-softbank-hawks-gaming-b99n2uq2.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/fukuoka-softbank-hawks-gaming-4i3ympnq.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/fukuoka-softbank-hawks-gaming-4fl2jmuh.png', 40); +INSERT INTO public.team VALUES (67, 103535282124208038, 'pentanetgg', 'Pentanet.GG', 'PGG', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/pentanetgg-3vnqnv03.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/pentanetgg-3d4g4sbh.png', NULL, 16); +INSERT INTO public.team VALUES (68, 103535282135552642, 'papara-supermassive-blaze-akademi', 'Papara SuperMassive Blaze Akademi', 'SMB', 'http://static.lolesports.com/teams/1628521896643_SMBA_WHITE.png', 'http://static.lolesports.com/teams/1628521896646_SMBA_BLACK.png', NULL, 2); +INSERT INTO public.team VALUES (69, 103535282138043022, 'fenerbahce-espor-akademi', 'Fenerbahçe Espor Akademi', 'FB', 'http://static.lolesports.com/teams/1642680283028_BANPICK_FB.png', 'http://static.lolesports.com/teams/1642680283035_BANPICK_FB.png', NULL, 2); +INSERT INTO public.team VALUES (70, 103535282140533402, 'besiktas-akademi', 'BeÅŸiktaÅŸ Akademi', 'BJK', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/besiktas-akademi-6dlbk21d.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/besiktas-akademi-fobrhai9.png', NULL, 2); +INSERT INTO public.team VALUES (71, 103535282143744679, 'dark-passage-akademi', 'Dark Passage Akademi', 'DP', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/dark-passage-akademi-9ehs6q0l.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/dark-passage-akademi-h4x5hq6.png', NULL, 2); +INSERT INTO public.team VALUES (72, 103535282146169523, 'info-yatrm-aurora-akademi', 'Info Yatırım Aurora Akademi', 'AUR', 'http://static.lolesports.com/teams/1642680351930_BANPICK_AUR.png', 'http://static.lolesports.com/teams/1642680351936_BANPICK_AUR.png', NULL, 2); +INSERT INTO public.team VALUES (73, 103535282148790975, 'galakticos-akademi', 'GALAKTICOS Akademi', 'GAL', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/galakticos-akademi-4x1ww2pc.png', 'https://lolstatic-a.akamaihd.net/esports-assets/production/team/galakticos-akademi-dv3kn0pg.png', NULL, 2); +INSERT INTO public.team VALUES (74, 103535282158162659, 'fastpay-wildcats-akademi', 'fastPay Wildcats Akademi', 'IW', 'http://static.lolesports.com/teams/1582880891336_IW.png', 'http://static.lolesports.com/teams/1582880891351_IW.png', NULL, 2); +INSERT INTO public.team VALUES (75, 103877554248683116, 'schalke-04-evolution', 'Schalke 04 Evolution', 'S04E', 'http://static.lolesports.com/teams/S04_Standard_Logo1.png', 'http://static.lolesports.com/teams/S04_Standard_Logo1.png', NULL, NULL); +INSERT INTO public.team VALUES (76, 103877589042434434, 'gamerlegion', 'GamerLegion', 'GL', 'http://static.lolesports.com/teams/1585046217463_220px-Team_GamerLegionlogo_square.png', NULL, NULL, 1); +INSERT INTO public.team VALUES (77, 103877625775457850, 'movistar-riders', 'Movistar Riders', 'MRS', 'http://static.lolesports.com/teams/1585046777741_220px-Movistar_Riderslogo_square.png', NULL, NULL, NULL); +INSERT INTO public.team VALUES (78, 103877675241047720, 'ldlc-ol', 'LDLC OL', 'LDLC', 'http://static.lolesports.com/teams/LFL-LDLC-logo.png', 'http://static.lolesports.com/teams/LFL-LDLC-logo.png', NULL, 1); +INSERT INTO public.team VALUES (79, 103877737868887783, 'saim-se', 'SAIM SE', 'SSB', 'http://static.lolesports.com/teams/1585048488568_220px-SAIM_SElogo_square.png', 'http://static.lolesports.com/teams/1585048488582_220px-SAIM_SElogo_square.png', NULL, NULL); +INSERT INTO public.team VALUES (80, 103877756742242918, 'racoon', 'Racoon', 'RCN', 'http://static.lolesports.com/teams/1585048776551_220px-Racoon_(Italian_Team)logo_square.png', 'http://static.lolesports.com/teams/1585048776564_220px-Racoon_(Italian_Team)logo_square.png', NULL, NULL); +INSERT INTO public.team VALUES (81, 103877774634323825, 'ydn-gamers', 'YDN Gamers', 'YDN', 'http://static.lolesports.com/teams/1587638409857_LOGO_YDN_-trasp.png', 'http://static.lolesports.com/teams/1587638409876_LOGO_YDN_-trasp.png', NULL, NULL); +INSERT INTO public.team VALUES (82, 103877879209300619, 'vipers-inc', 'Vipers Inc', 'VIP', 'http://static.lolesports.com/teams/1585050644953_220px-Vipers_Inclogo_square.png', 'http://static.lolesports.com/teams/1585050644968_220px-Vipers_Inclogo_square.png', NULL, NULL); +INSERT INTO public.team VALUES (83, 103877891572305836, 'team-singularity', 'Team Singularity', 'SNG', 'http://static.lolesports.com/teams/NLC_SNG-light.png', 'http://static.lolesports.com/teams/NLC_SNG-logo.png', NULL, 9); +INSERT INTO public.team VALUES (84, 103877908090914662, 'kenty', 'Kenty', 'KEN', 'http://static.lolesports.com/teams/1585051086000_220px-Kentylogo_square.png', 'http://static.lolesports.com/teams/1585051086014_220px-Kentylogo_square.png', NULL, NULL); +INSERT INTO public.team VALUES (85, 103877925817094140, 'pigsports', 'PIGSPORTS', 'PIG', 'http://static.lolesports.com/teams/PIGSPORTS_PIG-Logo1.png', 'http://static.lolesports.com/teams/PIGSPORTS_PIG-Logo1.png', NULL, NULL); +INSERT INTO public.team VALUES (86, 103877951616192529, 'cyber-gaming', 'Cyber Gaming', 'CG', 'http://static.lolesports.com/teams/1585051749524_220px-Cyber_Gaminglogo_square.png', 'http://static.lolesports.com/teams/1585051749529_220px-Cyber_Gaminglogo_square.png', NULL, NULL); +INSERT INTO public.team VALUES (87, 103877976717529187, 'intrepid-fox-gaming', 'Intrepid Fox Gaming', 'IF', 'http://static.lolesports.com/teams/1585052132267_220px-Intrepid_Fox_Gaminglogo_square.png', 'http://static.lolesports.com/teams/1585052132281_220px-Intrepid_Fox_Gaminglogo_square.png', NULL, NULL); +INSERT INTO public.team VALUES (88, 103878020539746273, 'egn-esports', 'EGN Esports', 'EGN', 'http://static.lolesports.com/teams/LPLOL_EGN-Logo1.png', 'http://static.lolesports.com/teams/LPLOL_EGN-Logo1.png', NULL, NULL); +INSERT INTO public.team VALUES (89, 103935421249833954, 'mad-lions-madrid', 'MAD Lions Madrid', 'MADM', 'http://static.lolesports.com/teams/SL_MADM-Logo_white.png', 'http://static.lolesports.com/teams/SL_MADM-Logo_dark.png', NULL, 5); +INSERT INTO public.team VALUES (90, 103935446548920777, 'misfits-premier', 'Misfits Premier', 'MSFP', 'http://static.lolesports.com/teams/LFL-MSFP-logo.png', 'http://static.lolesports.com/teams/LFL-MSFP-logo.png', NULL, NULL); +INSERT INTO public.team VALUES (91, 103935468920814040, 'gamersorigin', 'GamersOrigin', 'GO', 'http://static.lolesports.com/teams/1588178480033_logoGO_2020_G_Blanc.png', 'http://static.lolesports.com/teams/1588178480035_logoGO_2020_G_Noir.png', NULL, 11); +INSERT INTO public.team VALUES (92, 103935523328473675, 'k1ck-neosurf', 'K1CK Neosurf', 'K1', 'http://static.lolesports.com/teams/1585930223604_K1ck_Neosurflogo_square.png', NULL, NULL, NULL); +INSERT INTO public.team VALUES (93, 103935530333072898, 'ago-rogue', 'AGO Rogue', 'RGO', 'http://static.lolesports.com/teams/1585930330127_AGO_ROGUElogo_square.png', NULL, NULL, 1); +INSERT INTO public.team VALUES (94, 103935567188806885, 'energypot-wizards', 'Energypot Wizards', 'EWIZ', 'http://static.lolesports.com/teams/1585930892362_Energypot_Wizardslogo_square.png', NULL, NULL, NULL); +INSERT INTO public.team VALUES (95, 103935642731826448, 'sector-one', 'Sector One', 'S1', 'http://static.lolesports.com/teams/1641288621852_1024x1024_sector_one_nameless_white.png', 'http://static.lolesports.com/teams/1641288621854_1024x1024_sector_one_nameless_black.png', NULL, 19); +INSERT INTO public.team VALUES (96, 103963647433204351, 'm19', 'M19', 'M19', 'http://static.lolesports.com/teams/1586359360406_M19logo_square.png', NULL, NULL, NULL); +INSERT INTO public.team VALUES (97, 103963715924353674, 'dragon-army', 'Dragon Army', 'DA', 'http://static.lolesports.com/teams/1586360405423_440px-Dragon_Armylogo_square.png', NULL, NULL, 41); +INSERT INTO public.team VALUES (98, 103963753080578719, 'crowcrowd-moscow', 'CrowCrowd Moscow', 'CC', 'http://static.lolesports.com/teams/Logo_CC.png', NULL, NULL, 41); +INSERT INTO public.team VALUES (99, 104202382255290736, 'rensga', 'RENSGA', 'RNS', 'http://static.lolesports.com/teams/LogoRensgaEsports.png', 'http://static.lolesports.com/teams/LogoRensgaEsports.png', 'http://static.lolesports.com/teams/RensgaRNS.png', 37); + + +-- Values for tournament table +INSERT INTO public.tournament VALUES (1, 107893386210553711, 'european_masters_spring_2022_main_event', '2022-04-13', '2022-05-08', 1); +INSERT INTO public.tournament VALUES (2, 107530554766055254, 'lla_opening_2022', '2022-01-28', '2022-04-17', 3); +INSERT INTO public.tournament VALUES (3, 107693721179065689, 'pcs_2022_spring', '2022-02-11', '2022-04-18', 4); +INSERT INTO public.tournament VALUES (4, 107468241207873310, 'superliga_2022_spring', '2022-01-09', '2022-05-01', 5); +INSERT INTO public.tournament VALUES (5, 107416436272657995, 'ultraliga_2022_spring', '2022-01-01', '2022-05-01', 6); +INSERT INTO public.tournament VALUES (6, 107417741193036913, 'prime_2022_spring', '2022-01-01', '2022-05-01', 7); +INSERT INTO public.tournament VALUES (7, 107457033672415830, 'pg_spring', '2022-01-17', '2022-05-01', 8); +INSERT INTO public.tournament VALUES (8, 107417432877679361, 'nlc_2022_spring', '2022-01-01', '2022-05-15', 9); +INSERT INTO public.tournament VALUES (9, 107468370558963709, 'lfl_2022_spring', '2022-01-09', '2022-05-01', 11); +INSERT INTO public.tournament VALUES (10, 107565607659994755, 'cblol_academy_2022', '2022-01-24', '2022-04-18', 15); +INSERT INTO public.tournament VALUES (11, 107439320897210747, 'lco_spring_2022', '2022-01-23', '2022-04-29', 16); +INSERT INTO public.tournament VALUES (12, 107563481236862420, 'eslol_spring', '2022-01-16', '2022-05-01', 19); +INSERT INTO public.tournament VALUES (13, 107682708465517027, 'discover_volcano_league_opening_2022', '2022-01-25', '2022-04-16', 22); +INSERT INTO public.tournament VALUES (14, 107728324355999617, 'master_flow_league_opening_2022', '2022-01-26', '2022-04-24', 24); +INSERT INTO public.tournament VALUES (15, 107677841285321565, 'honor_league_opening_2022', '2022-01-24', '2022-04-16', 25); +INSERT INTO public.tournament VALUES (16, 107921288851375933, 'proving_grounds_spring_2022', '2022-03-16', '2022-04-16', 28); +INSERT INTO public.tournament VALUES (17, 108097587668586485, 'tft_emea_lcq_2022', '2022-04-16', '2022-04-16', 29); +INSERT INTO public.tournament VALUES (18, 107458367237283414, 'lcs_spring_2022', '2022-02-04', '2022-04-25', 32); +INSERT INTO public.tournament VALUES (19, 107417059262120466, 'lec_2022_spring', '2022-01-01', '2022-05-15', 33); +INSERT INTO public.tournament VALUES (20, 107417779630700437, 'lpl_spring_2022', '2022-01-10', '2022-05-01', 35); +INSERT INTO public.tournament VALUES (21, 107405837336179496, 'cblol_2022_split1', '2022-01-22', '2022-04-23', 37); +INSERT INTO public.tournament VALUES (22, 107417471555810057, 'lcl_spring_2022', '2022-02-11', '2022-04-16', 41); +INSERT INTO public.tournament VALUES (23, 107418086627198298, 'lcs_academy_2022_spring', '2022-01-19', '2022-05-31', 42); diff --git a/src/lib.rs b/src/lib.rs index d89f79b2..307d8b08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,9 @@ pub mod connection { #[cfg(feature = "mssql")] pub use canyon_connection::canyon_database_connector::DatabaseConnection::SqlServer; + + #[cfg(feature = "mysql")] + pub use canyon_connection::canyon_database_connector::DatabaseConnection::MySQL; } /// Crud module serves to reexport the public elements of the `canyon_crud` crate, @@ -53,6 +56,8 @@ pub mod query { /// Reexport the available database clients within Canyon pub mod db_clients { + #[cfg(feature = "mysql")] + pub use canyon_connection::mysql_async; #[cfg(feature = "mssql")] pub use canyon_connection::tiberius; #[cfg(feature = "postgres")] diff --git a/tests/Cargo.toml b/tests/Cargo.toml index da6b0dfc..ef9ee7f0 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -14,3 +14,4 @@ path = "canyon_integration_tests.rs" [features] postgres = ["canyon_sql/postgres"] mssql = ["canyon_sql/mssql"] +mysql = ["canyon_sql/mysql"] \ No newline at end of file diff --git a/tests/canyon.toml b/tests/canyon.toml index 0b0614a4..73c0b023 100644 --- a/tests/canyon.toml +++ b/tests/canyon.toml @@ -22,3 +22,15 @@ sqlserver = { basic = { username = 'sa', password = 'SqlServer-10' } } host = 'localhost' port = 1434 db_name = 'master' + + +[[canyon_sql.datasources]] +name = 'mysql_docker' + +[canyon_sql.datasources.auth] +mysql = { basic = { username = 'root', password = 'root' } } + +[canyon_sql.datasources.properties] +host = 'localhost' +port = 3307 +db_name = 'public' \ No newline at end of file diff --git a/tests/constants.rs b/tests/constants.rs index dd3a268b..26bea3fd 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -4,6 +4,8 @@ pub const PSQL_DS: &str = "postgres_docker"; #[cfg(feature = "mssql")] pub const SQL_SERVER_DS: &str = "sqlserver_docker"; +#[cfg(feature = "mysql")] +pub const MYSQL_DS: &str = "mysql_docker"; #[cfg(all(feature = "postgres", feature = "migrations"))] pub static FETCH_PUBLIC_SCHEMA: &str = diff --git a/tests/crud/delete_operations.rs b/tests/crud/delete_operations.rs index 6420e553..31d1b0ef 100644 --- a/tests/crud/delete_operations.rs +++ b/tests/crud/delete_operations.rs @@ -2,10 +2,13 @@ ///! generates and executes *INSERT* statements use canyon_sql::crud::CrudOperations; +#[cfg(feature = "mysql")] +use crate::constants::MYSQL_DS; #[cfg(feature = "postgres")] use crate::constants::PSQL_DS; #[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; + use crate::tests_models::league::*; /// Deletes a row from the database that is mapped into some instance of a `T` entity. @@ -64,7 +67,7 @@ fn test_crud_delete_method_operation() { /// Same as the delete test, but performing the operations with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_delete_datasource_method_operation() { +fn test_crud_delete_datasource_mssql_method_operation() { // For test the delete, we will insert a new instance of the database, and then, // after inspect it, we will proceed to delete it let mut new_league: League = League { @@ -107,3 +110,50 @@ fn test_crud_delete_datasource_method_operation() { None ); } + +/// Same as the delete test, but performing the operations with the specified datasource +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_delete_datasource_mysql_method_operation() { + // For test the delete, we will insert a new instance of the database, and then, + // after inspect it, we will proceed to delete it + let mut new_league: League = League { + id: Default::default(), + ext_id: 7892635306594_i64, + slug: "some-new-league".to_string(), + name: "Some New League".to_string(), + region: "Bahía de cochinos".to_string(), + image_url: "https://nobodyspectsandimage.io".to_string(), + }; + + // We insert the instance on the database, on the `League` entity + new_league + .insert_datasource(MYSQL_DS) + .await + .expect("Failed insert operation"); + assert_eq!( + new_league.id, + League::find_by_pk_datasource(&new_league.id, MYSQL_DS) + .await + .expect("Request error") + .expect("None value") + .id + ); + + // Now that we have an instance mapped to some entity by a primary key, we can now + // remove that entry from the database with the delete operation + new_league + .delete_datasource(MYSQL_DS) + .await + .expect("Failed to delete the operation"); + + // To check the success, we can query by the primary key value and check if, after unwrap() + // the result of the operation, the find by primary key contains Some(v) or None + // Remember that `find_by_primary_key(&dyn QueryParameter<'a>) -> Result>, Err> + assert_eq!( + League::find_by_pk_datasource(&new_league.id, MYSQL_DS) + .await + .expect("Unwrapping the result, letting the Option"), + None + ); +} diff --git a/tests/crud/foreign_key_operations.rs b/tests/crud/foreign_key_operations.rs index 471dd639..21cae200 100644 --- a/tests/crud/foreign_key_operations.rs +++ b/tests/crud/foreign_key_operations.rs @@ -10,8 +10,11 @@ ///! For more info: TODO -> Link to the docs of the foreign key chapter use canyon_sql::crud::CrudOperations; +#[cfg(feature = "mssql")] +use crate::constants::MYSQL_DS; #[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; + use crate::tests_models::league::*; use crate::tests_models::tournament::*; @@ -42,7 +45,7 @@ fn test_crud_search_by_foreign_key() { /// Same as the search by foreign key, but with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_search_by_foreign_key_datasource() { +fn test_crud_search_by_foreign_key_datasource_mssql() { let some_tournament: Tournament = Tournament::find_by_pk_datasource(&10, SQL_SERVER_DS) .await .expect("Result variant of the query is err") @@ -65,6 +68,32 @@ fn test_crud_search_by_foreign_key_datasource() { } } +/// Same as the search by foreign key, but with the specified datasource +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_search_by_foreign_key_datasource_mysql() { + let some_tournament: Tournament = Tournament::find_by_pk_datasource(&10, MYSQL_DS) + .await + .expect("Result variant of the query is err") + .expect("No result found for the given parameter"); + + // We can get the parent entity for the retrieved child instance + let parent_entity: Option = some_tournament + .search_league_datasource(MYSQL_DS) + .await + .expect("Result variant of the query is err"); + + // These are tests, and we could unwrap the result contained in the option, because + // it always should exist that search for the data inserted when the docker starts. + // But, just for change the style a little bit and offer more options about how to + // handle things done with Canyon + if let Some(league) = parent_entity { + assert_eq!(some_tournament.league, league.id) + } else { + assert_eq!(parent_entity, None) + } +} + /// Given an entity `U` that is know as the "parent" side of the relation with another /// entity `T`, for example, we can ask to the parent for the childrens that belongs /// to `U`. @@ -93,7 +122,7 @@ fn test_crud_search_reverse_side_foreign_key() { /// but with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_search_reverse_side_foreign_key_datasource() { +fn test_crud_search_reverse_side_foreign_key_datasource_mssql() { let some_league: League = League::find_by_pk_datasource(&1, SQL_SERVER_DS) .await .expect("Result variant of the query is err") @@ -110,3 +139,25 @@ fn test_crud_search_reverse_side_foreign_key_datasource() { .iter() .for_each(|t| assert_eq!(t.league, some_league.id)); } + +/// Same as the search by the reverse side of a foreign key relation +/// but with the specified datasource +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_search_reverse_side_foreign_key_datasource_mysql() { + let some_league: League = League::find_by_pk_datasource(&1, MYSQL_DS) + .await + .expect("Result variant of the query is err") + .expect("No result found for the given parameter"); + + // Computes how many tournaments are pointing to the retrieved league + let child_tournaments: Vec = + Tournament::search_league_childrens_datasource(&some_league, MYSQL_DS) + .await + .expect("Result variant of the query is err"); + + assert!(!child_tournaments.is_empty()); + child_tournaments + .iter() + .for_each(|t| assert_eq!(t.league, some_league.id)); +} diff --git a/tests/crud/insert_operations.rs b/tests/crud/insert_operations.rs index d52fa868..898182b6 100644 --- a/tests/crud/insert_operations.rs +++ b/tests/crud/insert_operations.rs @@ -2,8 +2,11 @@ ///! generates and executes *INSERT* statements use canyon_sql::crud::CrudOperations; +#[cfg(feature = "mysql")] +use crate::constants::MYSQL_DS; #[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; + use crate::tests_models::league::*; /// Inserts a new record on the database, given an entity that is @@ -58,7 +61,7 @@ fn test_crud_insert_operation() { /// the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_insert_datasource_operation() { +fn test_crud_insert_datasource_mssql_operation() { let mut new_league: League = League { id: Default::default(), ext_id: 7892635306594_i64, @@ -86,6 +89,38 @@ fn test_crud_insert_datasource_operation() { assert_eq!(new_league.id, inserted_league.id); } +/// Same as the insert operation above, but targeting the database defined in +/// the specified datasource +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_insert_datasource_mysql_operation() { + let mut new_league: League = League { + id: Default::default(), + ext_id: 7892635306594_i64, + slug: "some-new-league".to_string(), + name: "Some New League".to_string(), + region: "Bahía de cochinos".to_string(), + image_url: "https://nobodyspectsandimage.io".to_string(), + }; + + // We insert the instance on the database, on the `League` entity + new_league + .insert_datasource(MYSQL_DS) + .await + .expect("Failed insert datasource operation"); + + // Now, in the `id` field of the instance, we have the autogenerated + // value for the primary key field, which is id. So, we can query the + // database again with the find by primary key operation to check if + // the value was really inserted + let inserted_league = League::find_by_pk_datasource(&new_league.id, MYSQL_DS) + .await + .expect("Failed the query to the database") + .expect("No entity found for the primary key value passed in"); + + assert_eq!(new_league.id, inserted_league.id); +} + /// The multi insert operation is a shorthand for insert multiple instances of *T* /// in the database at once. /// @@ -160,7 +195,7 @@ fn test_crud_multi_insert_operation() { /// Same as the multi insert above, but with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_multi_insert_datasource_operation() { +fn test_crud_multi_insert_datasource_mssql_operation() { let mut new_league_mi: League = League { id: Default::default(), ext_id: 54376478_i64, @@ -218,3 +253,65 @@ fn test_crud_multi_insert_datasource_operation() { assert_eq!(new_league_mi_2.id, inserted_league_2.id); assert_eq!(new_league_mi_3.id, inserted_league_3.id); } + +/// Same as the multi insert above, but with the specified datasource +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_multi_insert_datasource_mysql_operation() { + let mut new_league_mi: League = League { + id: Default::default(), + ext_id: 54376478_i64, + slug: "some-new-random-league".to_string(), + name: "Some New Random League".to_string(), + region: "Unknown".to_string(), + image_url: "https://what-a-league.io".to_string(), + }; + let mut new_league_mi_2: League = League { + id: Default::default(), + ext_id: 3475689769678906_i64, + slug: "new-league-2".to_string(), + name: "New League 2".to_string(), + region: "Really unknown".to_string(), + image_url: "https://what-an-unknown-league.io".to_string(), + }; + let mut new_league_mi_3: League = League { + id: Default::default(), + ext_id: 46756867_i64, + slug: "a-new-multinsert".to_string(), + name: "New League 3".to_string(), + region: "The dark side of the moon".to_string(), + image_url: "https://interplanetary-league.io".to_string(), + }; + + // Insert the instance as database entities + new_league_mi + .insert_datasource(MYSQL_DS) + .await + .expect("Failed insert datasource operation"); + new_league_mi_2 + .insert_datasource(MYSQL_DS) + .await + .expect("Failed insert datasource operation"); + new_league_mi_3 + .insert_datasource(MYSQL_DS) + .await + .expect("Failed insert datasource operation"); + + // Recover the inserted data by primary key + let inserted_league = League::find_by_pk_datasource(&new_league_mi.id, MYSQL_DS) + .await + .expect("[1] - Failed the query to the database") + .expect("[1] - No entity found for the primary key value passed in"); + let inserted_league_2 = League::find_by_pk_datasource(&new_league_mi_2.id, MYSQL_DS) + .await + .expect("[2] - Failed the query to the database") + .expect("[2] - No entity found for the primary key value passed in"); + let inserted_league_3 = League::find_by_pk_datasource(&new_league_mi_3.id, MYSQL_DS) + .await + .expect("[3] - Failed the query to the database") + .expect("[3] - No entity found for the primary key value passed in"); + + assert_eq!(new_league_mi.id, inserted_league.id); + assert_eq!(new_league_mi_2.id, inserted_league_2.id); + assert_eq!(new_league_mi_3.id, inserted_league_3.id); +} diff --git a/tests/crud/querybuilder_operations.rs b/tests/crud/querybuilder_operations.rs index 4bc205f6..f4d03f38 100644 --- a/tests/crud/querybuilder_operations.rs +++ b/tests/crud/querybuilder_operations.rs @@ -1,3 +1,8 @@ +#[cfg(feature = "mysql")] +use crate::constants::MYSQL_DS; +#[cfg(feature = "mssql")] +use crate::constants::SQL_SERVER_DS; + ///! Tests for the QueryBuilder available operations within Canyon. /// ///! QueryBuilder are the way of obtain more flexibility that with @@ -9,10 +14,7 @@ use canyon_sql::{ query::{operators::Comp, operators::Like, ops::QueryBuilder}, }; -#[cfg(feature = "mssql")] -use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; -#[cfg(feature = "mssql")] use crate::tests_models::player::*; use crate::tests_models::tournament::*; @@ -78,7 +80,7 @@ fn test_crud_find_with_querybuilder_and_fulllike() { /// with the parameters that modifies the base SQL to SELECT * FROM #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_with_querybuilder_and_fulllike_datasource() { +fn test_crud_find_with_querybuilder_and_fulllike_datasource_mssql() { // Find all the leagues with "LC" in their name let mut filtered_leagues_result = League::select_query_datasource(SQL_SERVER_DS); filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Full); @@ -89,6 +91,21 @@ fn test_crud_find_with_querybuilder_and_fulllike_datasource() { ) } +/// Builds a new SQL statement for retrieves entities of the `T` type, filtered +/// with the parameters that modifies the base SQL to SELECT * FROM +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_with_querybuilder_and_fulllike_datasource_mysql() { + // Find all the leagues with "LC" in their name + let mut filtered_leagues_result = League::select_query_datasource(MYSQL_DS); + filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Full); + + assert_eq!( + filtered_leagues_result.read_sql(), + "SELECT * FROM league WHERE name LIKE CONCAT('%', CAST($1 AS CHAR) ,'%')" + ) +} + /// Builds a new SQL statement for retrieves entities of the `T` type, filtered /// with the parameters that modifies the base SQL to SELECT * FROM #[cfg(feature = "postgres")] @@ -108,7 +125,7 @@ fn test_crud_find_with_querybuilder_and_leftlike() { /// with the parameters that modifies the base SQL to SELECT * FROM #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_with_querybuilder_and_leftlike_datasource() { +fn test_crud_find_with_querybuilder_and_leftlike_datasource_mssql() { // Find all the leagues whose name ends with "CK" let mut filtered_leagues_result = League::select_query(); filtered_leagues_result.r#where(LeagueFieldValue::name(&"CK"), Like::Left); @@ -119,6 +136,21 @@ fn test_crud_find_with_querybuilder_and_leftlike_datasource() { ) } +/// Builds a new SQL statement for retrieves entities of the `T` type, filtered +/// with the parameters that modifies the base SQL to SELECT * FROM +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_with_querybuilder_and_leftlike_datasource_mysql() { + // Find all the leagues whose name ends with "CK" + let mut filtered_leagues_result = League::select_query_datasource(MYSQL_DS); + filtered_leagues_result.r#where(LeagueFieldValue::name(&"CK"), Like::Left); + + assert_eq!( + filtered_leagues_result.read_sql(), + "SELECT * FROM league WHERE name LIKE CONCAT('%', CAST($1 AS CHAR))" + ) +} + /// Builds a new SQL statement for retrieves entities of the `T` type, filtered /// with the parameters that modifies the base SQL to SELECT * FROM #[cfg(feature = "postgres")] @@ -138,7 +170,7 @@ fn test_crud_find_with_querybuilder_and_rightlike() { /// with the parameters that modifies the base SQL to SELECT * FROM #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_with_querybuilder_and_rightlike_datasource() { +fn test_crud_find_with_querybuilder_and_rightlike_datasource_mssql() { // Find all the leagues whose name starts with "LC" let mut filtered_leagues_result = League::select_query_datasource(SQL_SERVER_DS); filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Right); @@ -148,11 +180,25 @@ fn test_crud_find_with_querybuilder_and_rightlike_datasource() { "SELECT * FROM league WHERE name LIKE CONCAT(CAST($1 AS VARCHAR) ,'%')" ) } +/// Builds a new SQL statement for retrieves entities of the `T` type, filtered +/// with the parameters that modifies the base SQL to SELECT * FROM +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_with_querybuilder_and_rightlike_datasource_mysql() { + // Find all the leagues whose name starts with "LC" + let mut filtered_leagues_result = League::select_query_datasource(MYSQL_DS); + filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Right); + + assert_eq!( + filtered_leagues_result.read_sql(), + "SELECT * FROM league WHERE name LIKE CONCAT(CAST($1 AS CHAR) ,'%')" + ) +} /// Same than the above but with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_with_querybuilder_datasource() { +fn test_crud_find_with_querybuilder_datasource_mssql() { // Find all the players where its ID column value is greater that 50 let filtered_find_players = Player::select_query_datasource(SQL_SERVER_DS) .r#where(PlayerFieldValue::id(&50), Comp::Gt) @@ -162,6 +208,19 @@ fn test_crud_find_with_querybuilder_datasource() { assert!(!filtered_find_players.unwrap().is_empty()); } +/// Same than the above but with the specified datasource +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_with_querybuilder_datasource_mysql() { + // Find all the players where its ID column value is greater that 50 + let filtered_find_players = Player::select_query_datasource(MYSQL_DS) + .r#where(PlayerFieldValue::id(&50), Comp::Gt) + .query() + .await; + + assert!(!filtered_find_players.unwrap().is_empty()); +} + /// Updates the values of the range on entries defined by the constraint parameters /// in the database entity #[cfg(feature = "postgres")] @@ -202,7 +261,7 @@ fn test_crud_update_with_querybuilder() { /// Same as above, but with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_update_with_querybuilder_datasource() { +fn test_crud_update_with_querybuilder_datasource_mssql() { // Find all the leagues with ID less or equals that 7 // and where it's region column value is equals to 'Korea' let mut q = Player::update_query_datasource(SQL_SERVER_DS); @@ -229,6 +288,37 @@ fn test_crud_update_with_querybuilder_datasource() { }); } +/// Same as above, but with the specified datasource +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_update_with_querybuilder_datasource_mysql() { + // Find all the leagues with ID less or equals that 7 + // and where it's region column value is equals to 'Korea' + + let mut q = Player::update_query_datasource(MYSQL_DS); + q.set(&[ + (PlayerField::summoner_name, "Random updated player name"), + (PlayerField::first_name, "I am an updated first name"), + ]) + .r#where(PlayerFieldValue::id(&1), Comp::Gt) + .and(PlayerFieldValue::id(&8), Comp::Lt) + .query() + .await + .expect("Failed to update records with the querybuilder"); + + let found_updated_values = Player::select_query_datasource(MYSQL_DS) + .r#where(PlayerFieldValue::id(&1), Comp::Gt) + .and(PlayerFieldValue::id(&7), Comp::LtEq) + .query() + .await + .expect("Failed to retrieve database League entries with the querybuilder"); + + found_updated_values.iter().for_each(|player| { + assert_eq!(player.summoner_name, "Random updated player name"); + assert_eq!(player.first_name, "I am an updated first name"); + }); +} + /// Deletes entries from the mapped entity `T` that are in the ranges filtered /// with the QueryBuilder /// @@ -251,7 +341,7 @@ fn test_crud_delete_with_querybuilder() { /// Same as the above delete, but with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_delete_with_querybuilder_datasource() { +fn test_crud_delete_with_querybuilder_datasource_mssql() { Player::delete_query_datasource(SQL_SERVER_DS) .r#where(PlayerFieldValue::id(&120), Comp::Gt) .and(PlayerFieldValue::id(&130), Comp::Lt) @@ -267,6 +357,25 @@ fn test_crud_delete_with_querybuilder_datasource() { .is_empty()); } +/// Same as the above delete, but with the specified datasource +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_delete_with_querybuilder_datasource_mysql() { + Player::delete_query_datasource(MYSQL_DS) + .r#where(PlayerFieldValue::id(&120), Comp::Gt) + .and(PlayerFieldValue::id(&130), Comp::Lt) + .query() + .await + .expect("Error connecting with the database when we are going to delete data! :)"); + + assert!(Player::select_query_datasource(MYSQL_DS) + .r#where(PlayerFieldValue::id(&122), Comp::Eq) + .query() + .await + .unwrap() + .is_empty()); +} + /// Tests for the generated SQL query after use the /// WHERE clause #[canyon_sql::macros::canyon_tokio_test] diff --git a/tests/crud/select_operations.rs b/tests/crud/select_operations.rs index 9f9a6f5c..76d263f2 100644 --- a/tests/crud/select_operations.rs +++ b/tests/crud/select_operations.rs @@ -1,5 +1,8 @@ #![allow(clippy::nonminimal_bool)] +#[cfg(feature = "mysql")] +use crate::constants::MYSQL_DS; + #[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; ///! Integration tests for the CRUD operations available in `Canyon` that @@ -42,7 +45,7 @@ fn test_crud_find_all_unchecked() { /// and using the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_all_datasource() { +fn test_crud_find_all_datasource_mssql() { let find_all_result: Result, Box> = League::find_all_datasource(SQL_SERVER_DS).await; // Connection doesn't return an error @@ -50,6 +53,16 @@ fn test_crud_find_all_datasource() { assert!(!find_all_result.unwrap().is_empty()); } +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_all_datasource_mysql() { + let find_all_result: Result, Box> = + League::find_all_datasource(MYSQL_DS).await; + // Connection doesn't return an error + assert!(!find_all_result.is_err()); + assert!(!find_all_result.unwrap().is_empty()); +} + /// Same as the `find_all_datasource()`, but with the unchecked variant and the specified dataosource, /// returning directly `Vec` and not `Result, Err>` #[cfg(feature = "mssql")] @@ -85,11 +98,10 @@ fn test_crud_find_by_pk() { /// Tests the behaviour of a SELECT * FROM {table_name} WHERE = , where the pk is /// defined with the #[primary_key] attribute over some field of the type. /// -/// Uses the *specified datasource* in the second parameter of the function call. -#[cfg(feature = "postgres")] +/// Uses the *specified datasource mssql* in the second parameter of the function call. #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_by_pk_datasource() { +fn test_crud_find_by_pk_datasource_mssql() { let find_by_pk_result: Result, Box> = League::find_by_pk_datasource(&27, SQL_SERVER_DS).await; assert!(find_by_pk_result.as_ref().unwrap().is_some()); @@ -106,6 +118,29 @@ fn test_crud_find_by_pk_datasource() { ); } +/// Tests the behaviour of a SELECT * FROM {table_name} WHERE = , where the pk is +/// defined with the #[primary_key] attribute over some field of the type. +/// +/// Uses the *specified datasource mysql* in the second parameter of the function call. +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_by_pk_datasource_mysql() { + let find_by_pk_result: Result, Box> = + League::find_by_pk_datasource(&27, MYSQL_DS).await; + assert!(find_by_pk_result.as_ref().unwrap().is_some()); + + let some_league = find_by_pk_result.unwrap().unwrap(); + assert_eq!(some_league.id, 27); + assert_eq!(some_league.ext_id, 107898214974993351_i64); + assert_eq!(some_league.slug, "college_championship"); + assert_eq!(some_league.name, "College Championship"); + assert_eq!(some_league.region, "NORTH AMERICA"); + assert_eq!( + some_league.image_url, + "http://static.lolesports.com/leagues/1646396098648_CollegeChampionshiplogo.png" + ); +} + /// Counts how many rows contains an entity on the target database. #[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] @@ -117,10 +152,10 @@ fn test_crud_count_operation() { } /// Counts how many rows contains an entity on the target database using -/// the specified datasource +/// the specified datasource mssql #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_count_datasource_operation() { +fn test_crud_count_datasource_operation_mssql() { assert_eq!( League::find_all_datasource(SQL_SERVER_DS) .await @@ -129,3 +164,14 @@ fn test_crud_count_datasource_operation() { League::count_datasource(SQL_SERVER_DS).await.unwrap() ); } + +/// Counts how many rows contains an entity on the target database using +/// the specified datasource mysql +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_count_datasource_operation_mysql() { + assert_eq!( + League::find_all_datasource(MYSQL_DS).await.unwrap().len() as i64, + League::count_datasource(MYSQL_DS).await.unwrap() + ); +} diff --git a/tests/crud/update_operations.rs b/tests/crud/update_operations.rs index e4085560..18283cb7 100644 --- a/tests/crud/update_operations.rs +++ b/tests/crud/update_operations.rs @@ -1,10 +1,12 @@ +use crate::tests_models::league::*; ///! Integration tests for the CRUD operations available in `Canyon` that ///! generates and executes *UPDATE* statements use canyon_sql::crud::CrudOperations; +#[cfg(feature = "mysql")] +use crate::constants::MYSQL_DS; #[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; -use crate::tests_models::league::*; /// Update operation is a *CRUD* method defined for some entity `T`, that works by appliying /// some change to a Rust's entity instance, and persisting them into the database. @@ -59,7 +61,7 @@ fn test_crud_update_method_operation() { /// Same as the above test, but with the specified datasource. #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_update_datasource_method_operation() { +fn test_crud_update_datasource_mssql_method_operation() { // We first retrieve some entity from the database. Note that we must make // the retrieved instance mutable of clone it to a new mutable resource let mut updt_candidate: League = League::find_by_pk_datasource(&1, SQL_SERVER_DS) @@ -96,3 +98,45 @@ fn test_crud_update_datasource_method_operation() { .await .expect("Failed to restablish the initial value update operation"); } + +/// Same as the above test, but with the specified datasource. +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_update_datasource_mysql_method_operation() { + // We first retrieve some entity from the database. Note that we must make + // the retrieved instance mutable of clone it to a new mutable resource + + let mut updt_candidate: League = League::find_by_pk_datasource(&1, MYSQL_DS) + .await + .expect("[1] - Failed the query to the database") + .expect("[1] - No entity found for the primary key value passed in"); + + // The ext_id field value is extracted from the sql scripts under the + // docker/sql folder. We are retrieving the first entity inserted at the + // wake up time of the database, and now checking some of its properties. + assert_eq!(updt_candidate.ext_id, 100695891328981122_i64); + + // Modify the value, and perform the update + let updt_value: i64 = 59306442534_i64; + updt_candidate.ext_id = updt_value; + updt_candidate + .update_datasource(MYSQL_DS) + .await + .expect("Failed the update operation"); + + // Retrieve it again, and check if the value was really updated + let updt_entity: League = League::find_by_pk_datasource(&1, MYSQL_DS) + .await + .expect("[2] - Failed the query to the database") + .expect("[2] - No entity found for the primary key value passed in"); + + assert_eq!(updt_entity.ext_id, updt_value); + + // We rollback the changes to the initial value to don't broke other tests + // the next time that will run + updt_candidate.ext_id = 100695891328981122_i64; + updt_candidate + .update_datasource(MYSQL_DS) + .await + .expect("Failed to restablish the initial value update operation"); +} From 5cef3a9b4dbc2836f6d6bb54896a2ea648b76693 Mon Sep 17 00:00:00 2001 From: Pylyv <70846394+Pylyv@users.noreply.github.com> Date: Sun, 10 Dec 2023 11:36:23 +0100 Subject: [PATCH 68/82] code: Solved latest raised clippy warnings - this solves #46 (#47) * code: Solved latest raised clippy warnings - this solves #46 * code: Forgot to run the Format command --- canyon_macros/src/utils/helpers.rs | 2 +- canyon_migrations/src/migrations/processor.rs | 7 ++++--- src/lib.rs | 2 +- tests/canyon_integration_tests.rs | 16 ++++++++-------- tests/constants.rs | 2 +- tests/crud/delete_operations.rs | 4 ++-- tests/crud/foreign_key_operations.rs | 18 +++++++++--------- tests/crud/insert_operations.rs | 4 ++-- tests/crud/querybuilder_operations.rs | 8 ++++---- tests/crud/select_operations.rs | 4 ++-- tests/crud/update_operations.rs | 4 ++-- tests/migrations/mod.rs | 2 +- 12 files changed, 37 insertions(+), 36 deletions(-) diff --git a/canyon_macros/src/utils/helpers.rs b/canyon_macros/src/utils/helpers.rs index 81ac7dcd..7022db2b 100644 --- a/canyon_macros/src/utils/helpers.rs +++ b/canyon_macros/src/utils/helpers.rs @@ -164,7 +164,7 @@ pub fn database_table_name_to_struct_ident(name: &str) -> Ident { first_iteration = false; } else { match char { - n if n == '_' => { + '_' => { previous_was_underscore = true; } char if char.is_ascii_lowercase() => { diff --git a/canyon_migrations/src/migrations/processor.rs b/canyon_migrations/src/migrations/processor.rs index aee9a89e..ac183460 100644 --- a/canyon_migrations/src/migrations/processor.rs +++ b/canyon_migrations/src/migrations/processor.rs @@ -1,5 +1,5 @@ -///! File that contains all the datatypes and logic to perform the migrations -///! over a target database +//! File that contains all the datatypes and logic to perform the migrations +//! over a target database use async_trait::async_trait; use canyon_crud::DatabaseType; use regex::Regex; @@ -95,6 +95,7 @@ impl MigrationsProcessor { } // Case when we need to compare the entity with the database contain + #[allow(clippy::unnecessary_unwrap)] if current_table_metadata.is_some() && current_column_metadata.is_some() { self.add_modify_or_remove_constraints( entity_name, @@ -643,7 +644,7 @@ impl MigrationsHelper { #[cfg(feature = "mssql")] fn get_datatype_from_column_metadata(current_column_metadata: &ColumnMetadata) -> String { // TODO Add all SQL Server text datatypes - if vec!["nvarchar", "varchar"] + if ["nvarchar", "varchar"] .contains(¤t_column_metadata.datatype.to_lowercase().as_str()) { let varchar_len = match ¤t_column_metadata.character_maximum_length { diff --git a/src/lib.rs b/src/lib.rs index 307d8b08..c74efbc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -///! The root crate of the `Canyon-SQL` project. +//! The root crate of the `Canyon-SQL` project. /// /// Here it's where all the available functionalities and features /// reaches the top most level, grouping them and making them visible diff --git a/tests/canyon_integration_tests.rs b/tests/canyon_integration_tests.rs index 30687987..799a3747 100644 --- a/tests/canyon_integration_tests.rs +++ b/tests/canyon_integration_tests.rs @@ -1,15 +1,15 @@ +// Integration tests for the heart of a Canyon-SQL application, the CRUD operations. +/// +// This tests will tests mostly the whole source code of Canyon, due to its integration nature +/// +// Guide-style: Almost every operation in Canyon is `Result` wrapped (without the) unckecked +// variants of the `find_all` implementations. We will go to directly `.unwrap()` the results +// because, if there's something wrong in the code reported by the tests, we want to *panic* +// and abort the execution. extern crate canyon_sql; use std::error::Error; -///! Integration tests for the heart of a Canyon-SQL application, the CRUD operations. -/// -///! This tests will tests mostly the whole source code of Canyon, due to its integration nature -/// -/// Guide-style: Almost every operation in Canyon is `Result` wrapped (without the) unckecked -/// variants of the `find_all` implementations. We will go to directly `.unwrap()` the results -/// because, if there's something wrong in the code reported by the tests, we want to *panic* -/// and abort the execution. mod crud; mod migrations; diff --git a/tests/constants.rs b/tests/constants.rs index 26bea3fd..ad4d6ad4 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -1,4 +1,4 @@ -///! Constant values to share across the integration tests +//! Constant values to share across the integration tests #[cfg(feature = "postgres")] pub const PSQL_DS: &str = "postgres_docker"; diff --git a/tests/crud/delete_operations.rs b/tests/crud/delete_operations.rs index 31d1b0ef..5c1f5c1c 100644 --- a/tests/crud/delete_operations.rs +++ b/tests/crud/delete_operations.rs @@ -1,5 +1,5 @@ -///! Integration tests for the CRUD operations available in `Canyon` that -///! generates and executes *INSERT* statements +//! Integration tests for the CRUD operations available in `Canyon` that +//! generates and executes *INSERT* statements use canyon_sql::crud::CrudOperations; #[cfg(feature = "mysql")] diff --git a/tests/crud/foreign_key_operations.rs b/tests/crud/foreign_key_operations.rs index 21cae200..e6281f92 100644 --- a/tests/crud/foreign_key_operations.rs +++ b/tests/crud/foreign_key_operations.rs @@ -1,13 +1,13 @@ -///! Integration tests for the CRUD operations available in `Canyon` that -///! generates and executes *SELECT* statements based on a entity -///! annotated with the `#[foreign_key(... args)]` annotation looking -///! for the related data with some entity `U` that acts as is parent, where `U` -///! impls `ForeignKeyable` (isn't required, but it won't unlock the -///! reverse search features parent -> child, only the child -> parent ones). +// Integration tests for the CRUD operations available in `Canyon` that +// generates and executes *SELECT* statements based on a entity +// annotated with the `#[foreign_key(... args)]` annotation looking +// for the related data with some entity `U` that acts as is parent, where `U` +// impls `ForeignKeyable` (isn't required, but it won't unlock the +// reverse search features parent -> child, only the child -> parent ones). /// -///! Names of the foreign key methods are autogenerated for the direct and -///! reverse side of the implementations. -///! For more info: TODO -> Link to the docs of the foreign key chapter +// Names of the foreign key methods are autogenerated for the direct and +// reverse side of the implementations. +// For more info: TODO -> Link to the docs of the foreign key chapter use canyon_sql::crud::CrudOperations; #[cfg(feature = "mssql")] diff --git a/tests/crud/insert_operations.rs b/tests/crud/insert_operations.rs index 898182b6..13e2747e 100644 --- a/tests/crud/insert_operations.rs +++ b/tests/crud/insert_operations.rs @@ -1,5 +1,5 @@ -///! Integration tests for the CRUD operations available in `Canyon` that -///! generates and executes *INSERT* statements +//! Integration tests for the CRUD operations available in `Canyon` that +//! generates and executes *INSERT* statements use canyon_sql::crud::CrudOperations; #[cfg(feature = "mysql")] diff --git a/tests/crud/querybuilder_operations.rs b/tests/crud/querybuilder_operations.rs index f4d03f38..7fba112e 100644 --- a/tests/crud/querybuilder_operations.rs +++ b/tests/crud/querybuilder_operations.rs @@ -3,11 +3,11 @@ use crate::constants::MYSQL_DS; #[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; -///! Tests for the QueryBuilder available operations within Canyon. +// Tests for the QueryBuilder available operations within Canyon. /// -///! QueryBuilder are the way of obtain more flexibility that with -///! the default generated queries, essentially for build the queries -///! with the SQL filters +// QueryBuilder are the way of obtain more flexibility that with +// the default generated queries, essentially for build the queries +// with the SQL filters /// use canyon_sql::{ crud::CrudOperations, diff --git a/tests/crud/select_operations.rs b/tests/crud/select_operations.rs index 76d263f2..f3342c02 100644 --- a/tests/crud/select_operations.rs +++ b/tests/crud/select_operations.rs @@ -5,8 +5,8 @@ use crate::constants::MYSQL_DS; #[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; -///! Integration tests for the CRUD operations available in `Canyon` that -///! generates and executes *SELECT* statements +// Integration tests for the CRUD operations available in `Canyon` that +/// generates and executes *SELECT* statements use crate::Error; use canyon_sql::crud::CrudOperations; diff --git a/tests/crud/update_operations.rs b/tests/crud/update_operations.rs index 18283cb7..dfc4af15 100644 --- a/tests/crud/update_operations.rs +++ b/tests/crud/update_operations.rs @@ -1,6 +1,6 @@ use crate::tests_models::league::*; -///! Integration tests for the CRUD operations available in `Canyon` that -///! generates and executes *UPDATE* statements +// Integration tests for the CRUD operations available in `Canyon` that +/// generates and executes *UPDATE* statements use canyon_sql::crud::CrudOperations; #[cfg(feature = "mysql")] diff --git a/tests/migrations/mod.rs b/tests/migrations/mod.rs index 01260fb3..9ece4aac 100644 --- a/tests/migrations/mod.rs +++ b/tests/migrations/mod.rs @@ -1,6 +1,6 @@ #![allow(unused_imports)] use crate::constants; -///! Integration tests for the migrations feature of `Canyon-SQL` +/// Integration tests for the migrations feature of `Canyon-SQL` use canyon_sql::crud::Transaction; #[cfg(feature = "migrations")] use canyon_sql::migrations::handler::Migrations; From 048a55681c1cde2af051741c5b687df34fc40994 Mon Sep 17 00:00:00 2001 From: Pylyv <70846394+Pylyv@users.noreply.github.com> Date: Sun, 10 Dec 2023 11:53:08 +0100 Subject: [PATCH 69/82] code: Updated release version 0.5.0 (#51) --- CHANGELOG.md | 6 ++++++ Cargo.toml | 12 ++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bdad1dc..555be516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ Year format is defined as: `YYYY-m-d` ## [Unreleased] +## [0.5.0 - 2023 - 12 - 10] + +### Feature + +- Introduced support to work with MySQL Databases + ## [0.4.2 - 2023 - 05 - 02] ### Bugfix diff --git a/Cargo.toml b/Cargo.toml index c5063ad6..1351412a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,11 +35,11 @@ mysql_common = { workspace = true, optional = true } [workspace.dependencies] -canyon_crud = { version = "0.4.2", path = "canyon_crud" } -canyon_connection = { version = "0.4.2", path = "canyon_connection" } -canyon_entities = { version = "0.4.2", path = "canyon_entities" } -canyon_migrations = { version = "0.4.2", path = "canyon_migrations"} -canyon_macros = { version = "0.4.2", path = "canyon_macros" } +canyon_crud = { version = "0.5.0", path = "canyon_crud" } +canyon_connection = { version = "0.5.0", path = "canyon_connection" } +canyon_entities = { version = "0.5.0", path = "canyon_entities" } +canyon_migrations = { version = "0.5.0", path = "canyon_migrations"} +canyon_macros = { version = "0.5.0", path = "canyon_macros" } tokio = { version = "1.27.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["compat"] } @@ -67,7 +67,7 @@ proc-macro2 = "1.0.27" [workspace.package] -version = "0.4.2" +version = "0.5.0" edition = "2021" authors = ["Alex Vergara, Gonzalo Busto Musi"] documentation = "https://zerodaycode.github.io/canyon-book/" From 8e56ec1f197936034a4d63b14a16b0eed1ff974b Mon Sep 17 00:00:00 2001 From: Alex Vergara <68871459+Pyzyryab@users.noreply.github.com> Date: Sun, 10 Dec 2023 21:39:39 +0100 Subject: [PATCH 70/82] Create greetings.yml --- .github/workflows/greetings.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/greetings.yml diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml new file mode 100644 index 00000000..46774343 --- /dev/null +++ b/.github/workflows/greetings.yml @@ -0,0 +1,16 @@ +name: Greetings + +on: [pull_request_target, issues] + +jobs: + greeting: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: "Message that will be displayed on users' first issue" + pr-message: "Message that will be displayed on users' first pull request" From a822570dfecc13857120d4908af5a217088a7703 Mon Sep 17 00:00:00 2001 From: Alex Vergara <68871459+Pyzyryab@users.noreply.github.com> Date: Sun, 14 Jan 2024 12:40:05 +0100 Subject: [PATCH 71/82] Update greetings.yml --- .github/workflows/greetings.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index 46774343..49a7b0bf 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -12,5 +12,5 @@ jobs: - uses: actions/first-interaction@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: "Message that will be displayed on users' first issue" - pr-message: "Message that will be displayed on users' first pull request" + issue-message: "Thank you for opening your first issue in the Canyon-SQL project!" + pr-message: "Thank you for make your first contribution to the Canyon-SQL project!" From 66e29ea98616c30ad9b9b95b869a09b8f6f70767 Mon Sep 17 00:00:00 2001 From: Pylyv <70846394+Pylyv@users.noreply.github.com> Date: Sun, 25 Feb 2024 00:13:46 +0100 Subject: [PATCH 72/82] feat: Adding new changes to the README file for the issue #52 Projects readme (#53) --- README.md | 67 +++++++++++++++++++++++++++++----------------------- octocat.png | Bin 0 -> 2468 bytes 2 files changed, 37 insertions(+), 30 deletions(-) create mode 100644 octocat.png diff --git a/README.md b/README.md index 9617bf8c..95c93b7e 100755 --- a/README.md +++ b/README.md @@ -1,35 +1,41 @@ -# CANYON-SQL - -**A full written in `Rust` ORM for multiple databases.** - -- ![crates.io](https://img.shields.io/crates/v/canyon_sql.svg) -- [![Continuous Integration](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/continuous-integration.yml) -- [![Code Quality](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-quality.yml/badge.svg)](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-quality.yml) -- [![Code Coverage Measure](https://zerodaycode.github.io/Canyon-SQL/badges/flat.svg)](https://zerodaycode.github.io/Canyon-SQL) -- [![Code Coverage Status](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-coverage.yml/badge.svg)](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-coverage.yml) - -`Canyon-SQL` is a high level abstraction for working with multiple databases concurrently. Is build on top of the `async` language features -to provide a high speed, high performant library to handling data access for consumers. +
+

CANYON-SQL

+

+

A full written in `Rust` ORM for multiple databases

+

`Canyon-SQL` is a high level abstraction for working with multiple databases concurrently. Is build on top of the `async` language features +to provide a high speed, high performant library to handling data access for consumers.

+
+
+
+
+ +![Rust](https://img.shields.io/badge/rust-%23000000.svg?style=for-the-badge&logo=rust&logoColor=white) + +![crates.io](https://img.shields.io/crates/v/canyon_sql?style=for-the-badge) + +[![Continuous Integration](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/continuous-integration.yml) +[![Code Quality](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-quality.yml/badge.svg)](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-quality.yml) +[![Code Coverage Measure](https://zerodaycode.github.io/Canyon-SQL/badges/flat.svg)](https://zerodaycode.github.io/Canyon-SQL) +[![Code Coverage Status](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-coverage.yml/badge.svg)](https://github.com/zerodaycode/Canyon-SQL/actions/workflows/code-coverage.yml) +
## Early stage disclaimer -The library it's still on a `early stage` state. +The library it's still on an `early stage` state. Any contrib via `fork` + `PR` it's really appreciated. Currently we are involved in a really active development on the project. -## Full documentation resources +## :memo: Full documentation resources There is a `work-in-progress` web page, build with `mdBook` containing the official documentation. Here is where you will find all the technical documentation for `Canyon-SQL`. You can read it [by clicking this link](https://zerodaycode.github.io/canyon-book/) -## Most important features +## :pushpin: Most important features - **Async** by default. Almost every functionality provided is ready to be consumed concurrently. -- Use of multiple datasources. You can query multiple databases at the same time, even different ones!. This means that you will be able to query concurrently -a `PostgreSQL` database and an `SqlServer` one in the same project. +- Use of multiple datasources. You can query multiple databases at the same time, even different ones! This means that you will be able to query concurrently a `PostgreSQL` database and a `SqlServer` one in the same project. - Is macro based. With a few annotations and a configuration file, you are ready to write your data access. -- Allows **migrations**. `Canyon-SQL` comes with a *god-mode* that will manage every table on your database for you. You can modify in `Canyon` code your tables internally, altering columns, setting up constraints... -Also, in the future, we have plans to allow you to manipulate the whole server, like creating databases, altering configurations... everything, but in a programmatically approach with `Canyon`! +- Allows **migrations**. `Canyon-SQL` comes with a *god-mode* that will manage every table on your database for you. You can modify in `Canyon` code your tables internally, altering columns, setting up constraints... Also, in the future, we have plans to allow you to manipulate the whole server, like creating databases, altering configurations... everything, but in a programmatically approach with `Canyon`! ## Supported databases @@ -40,7 +46,7 @@ Also, in the future, we have plans to allow you to manipulate the whole server, Every crate listed above is an `async` based crate, in line with the guidelines of the `Canyon-SQL` design. -There are plans for include more databases engines. +There are plans to include more databases engines. ## Better by example @@ -57,7 +63,7 @@ assert!(find_all_result.is_ok()); assert!(!find_all_result.unwrap().is_empty()); ``` -### Performing a search over the primary key column +### :mag_right: Performing a search over the primary key column ```rust let find_by_pk_result: Result, Box> = League::find_by_pk(&1).await; @@ -78,10 +84,10 @@ assert_eq!( Note the leading reference on the `find_by_pk(...)` parameter. This associated function receives an `&dyn QueryParameter<'_>` as argument, not a value. -### Building more complex queries +### :wrench: Building more complex queries -For exemplify the capabilities of `Canyon`, we will use `SelectQueryBuilder`, which implements the `QueryBuilder` trait -for build a more complex where, filteing data and joining tables. +To exemplify the capabilities of `Canyon`, we will use `SelectQueryBuilder`, which implements the `QueryBuilder` trait +to build a more complex where, filtering data and joining tables. ```rust let mut select_with_joins = LeagueTournament::select_query(); @@ -100,15 +106,16 @@ let mut select_with_joins = LeagueTournament::select_query(); ) ``` -> Note: For now, when you use joins, you will need to create a new model with the columns in both tables (in case that you desire the data in such columns), but just follows the habitual process with the CanyonMapper. -It will try to retrieve the data for every field declared. If you don't declare a field that is in the open clause, in this case (*), that field won't be retrieved. No problem. But if you have fields that aren't map -able with some column in the database, the program will panic. +> [!NOTE] +> +> For now, when you use joins, you will need to create a new model with the columns in both tables (in case that you desire the data in such columns), but just follows the usual process with the CanyonMapper. +It will try to retrieve the data for every field declared. If you don't declare a field that is in the open clause, in this case (*), that field won't be retrieved. No problem. But if you have fields that aren't mapable with some column in the database, the program will panic. ## More examples -If you want to see more examples, you can take a look into the `tests` folder, at the root of this repository. Every available database operation is tested there, so you can use it to find the usage of the described operations in the documentation mentioned above +If you want to see more examples, you can take a look into the `tests` folder, at the root of this repository. Every available database operation is tested there, so you can use it to find the usage of the described operations in the documentation mentioned above. -## Contributing to CANYON-SQL +## :octocat: Contributing to CANYON-SQL First of all, thanks for take in consideration help us with the project. You can take a look to our [templated guide]((./CONTRIBUTING.md)). @@ -121,7 +128,7 @@ But, to summarize: - After complete your changes, open a `PR` to the default branch. Fill the template provided in the best way you're able to do it - Wait for the approval. In most of cases, a test over the feature will be required before approve your changes -## What about the tests? +## :question: What about the tests? Typically in `Canyon`, isolated unit tests are written as doc-tests, and the integration ones are under the folder `./tests` diff --git a/octocat.png b/octocat.png new file mode 100644 index 0000000000000000000000000000000000000000..f9050b935792512349e6f1ba0e6e55d99ecbf508 GIT binary patch literal 2468 zcmdT_`8U)H8~>UyLoN;~FS1vbQdF{J8?J4NA)_*3EJd=#Ysq#qBaJ2d9!+*iOqN76 z(^xZO--od$2`LQ5i0|#b_pf+A=RBX~x96PabIucIjWQG9li&jYKmciOZ1Ygs5xr>DERy2_uF=WzDH z?*pQu$A67A2j97g#bUvYVfOAWxI||&X=%zQq$DMKdU`Ze6t`L1At50a78Yt3)UFz6 zLZMLb*EGl)1KE=xIDatIC?g}&*VotE+q>0TGCMo#=H})X6fiL{5$0xh-_CN4Quehz zAM0~(lvF`-vTbQ;;q=!Vo0$#_3`mKIZftB!PftfjM|*pFYiMe8baV_24S_!fIfD)0 zOq;K-?-9NudwYB10Y25?2~4K)K0N77Rnt?f zE1Yh}DvRH`ICrvj1Dmv1-U8)%QtNaV2D$%{knUKIrHY0|4MMNMl3W5NzqV zo86ST7|%~?E1`q<&UOw*uYY(ebJZ~|NOS7@W#sQnpDbYS_9KDp0cDm`BYpX)e{f9Fux9 zIBaHbgExwuw+dM73(IHL3{U;s7JF|gqtiLoF}NcIZQBUEcIl~CQ0y5axVBmuAZ{-A zAgTBPwfENu1Zj~_BHMQ|d!`7x1yBX$!(Zy8KA4gXmMyx3R4y zI5*C@cmz?}SVBEd-r68f(l4QqmpfUJ#~W+S%_&QRiZYcUj&;wwfTN+ZMi=}`nzhbL z7fP1)zQ(zz8R^NPv9KAGk*V;^T^}k#O58HIZxGIJV+N~4HH<*YqGgNuVn|1Fwgtmu zkS3PO%D0l8ROr*A!^7$1vke{((O2)#H*yfuqaM=*@4_hKWZxe73ONNt+%=_>|9|D zwpH@HH9r)ZtCRK#&k2I)Jv?!id=>bQs3+Gu5#X4NBW)2h^7_a;+`?oezT!HhMfcvD z><8mG&fOmsaf0FB-K)ibM?9q#hhM=L6KnPUP>D_{$bf-HSm<9(unyyeb zX#kQ;fX=PIrv7-0Xy=O7BH9+OeG@Vhr+$WUb1T`Zx~Qf{Cgu3%b&AMuNgMbYM2f1+ z!+|WN7BOin8eVH!U_3&#`);i*AZZTGV`OE1$wYX|XMHd#nQV8etCcSJ%Iu~lB zc5Aogv-&v;o;1t0C;N^S8w^NxnO zsS1<;NeAgBTIUI-iuy_43v|f9#yU|y$PCwz8e6-$S=;_a*wvpsh%SHXr?)?(u35XW zt{ECi;2jf*SI)jX85o>dSJe?Roo&v3V25WL z{Bh$!e*(toq=fQw(->1;4oh3IiSuTR(t^@#mKnUd!bkIES!ty zOiRQ~%m+a=f3uVn_ShKd;K@FhP?G@9yCs~obQSr?BL0?ezFa&v{q^5JSA_umycY#oGsD2MCI;jqMAX6hlh@>GIb7qHfpaMoXj zlbzvw+ApCJ{x4S0FtKXLb7>hIodmf&Qty1^yhGGI8>o9ks+Gg(IA#EZEn%LHKQYjK z8-ChZgt9}|(q2RA1_|dR@~A*BQ$EFu6y(pH3)Co3g*}bgmSe+|VvwRsl$lU>q0222 z`4z&d?l%N`sCtcZP7B}o43AcwD;#Ve9c)Fyu2|&%s0g4&e)1F1XV%ak|Nc1u(gbBp IGIEak8# Date: Sun, 25 Feb 2024 00:28:07 +0100 Subject: [PATCH 73/82] Feature/gh 52 projects readme (#54) * feat: Adding new changes to the README file for the issue #52 Projects readme * feat: Adding new changes to the already closed issue #52 Project Readme --------- Co-authored-by: Alex Vergara --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 95c93b7e..edf093a7 100755 --- a/README.md +++ b/README.md @@ -117,24 +117,24 @@ If you want to see more examples, you can take a look into the `tests` folder, a ## :octocat: Contributing to CANYON-SQL -First of all, thanks for take in consideration help us with the project. -You can take a look to our [templated guide]((./CONTRIBUTING.md)). +First of all, thanks for taking in consideration helping us with the project. +You can take a look to our [templated guide](./CONTRIBUTING.md). But, to summarize: -- Take a look at the already opened issues, to see if already exists of it's someone already taking care about solving it. Even tho, you can enter to participate and explain your point of view, or even help to accomplish the task +- Take a look at the already opened issues, to verify if it already exists or if someone is already taking care about solving it. Even though, you can enter to participate and explain your point of view, or even help to accomplish the task. - Make a fork of `Canyon-SQL` -- If you opened an issue, create a branch from the base branch of the repo (that's the default), and point it to your fork -- After complete your changes, open a `PR` to the default branch. Fill the template provided in the best way you're able to do it -- Wait for the approval. In most of cases, a test over the feature will be required before approve your changes +- If you opened an issue, create a branch from the base branch of the repo (that's the default), and point it to your fork. +- After completing your changes, open a `PR` to the default branch. Fill the template provided in the best way possible. +- Wait for the approval. In most of cases, a test over the feature will be required before approving your changes. ## :question: What about the tests? Typically in `Canyon`, isolated unit tests are written as doc-tests, and the integration ones are under the folder `./tests` -If you want to run the tests (because this is the first thing that you want to do after fork the repo), a couple of things have to be considered before. +If you want to run the tests (because this is the first thing that you want to do after fork the repo), before moving forward, there are a couple of things that have to be considered. -- You will need Docker installed in the target machine +- You will need Docker installed in the target machine. - If you have Docker, and `Canyon-SQL` cloned of forked, you can run our docker-compose file `(docker/docker-compose.yml)`, which will initialize a `PostgreSQL` database and will put content on it to make the tests able to work. -- Finally, some tests runs against `MSSQL`. We didn't found a nice way of inserting data directly when the Docker wakes up, but instead, we run a very special test located at `tests/crud/mod.rs`, that is named `initialize_sql_server_docker_instance`. When you run this one, initial data will be inserted into the tables that are created when this test run. -(If you know a better way of doing this, please, open a issue to let us know it, and improve this process!) +- Finally, some tests run against `MSSQL`. We didn't found a nice way of inserting data directly when the Docker wakes up, but instead, we run a very special test located at `tests/crud/mod.rs`, that is named `initialize_sql_server_docker_instance`. When you run this one, initial data will be inserted into the tables that are created when this test run. +(If you know a better way of doing this, please, open an issue to let us know, and improve this process!) From 633ce4ea5220c4d2a3ed70fad5e975a4bea533a2 Mon Sep 17 00:00:00 2001 From: Pylyv <70846394+Pylyv@users.noreply.github.com> Date: Sun, 25 Feb 2024 01:04:41 +0100 Subject: [PATCH 74/82] fix: Fixed Clippy warnings from issue #55 (#56) --- canyon_connection/src/lib.rs | 4 ++-- canyon_migrations/src/migrations/processor.rs | 2 +- tests/crud/querybuilder_operations.rs | 2 +- tests/migrations/mod.rs | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index fd5d009e..bb313941 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -94,7 +94,7 @@ pub fn get_database_connection<'a>( guarded_cache .get_mut( DATASOURCES - .get(0) + .first() .expect("We didn't found any valid datasource configuration. Check your `canyon.toml` file") .name .as_str() @@ -113,7 +113,7 @@ pub fn get_database_config<'a>( ) -> &'a DatasourceConfig { if datasource_name.is_empty() { datasources_config - .get(0) + .first() .unwrap_or_else(|| panic!("Not exist datasource")) } else { datasources_config diff --git a/canyon_migrations/src/migrations/processor.rs b/canyon_migrations/src/migrations/processor.rs index ac183460..9296689f 100644 --- a/canyon_migrations/src/migrations/processor.rs +++ b/canyon_migrations/src/migrations/processor.rs @@ -701,7 +701,7 @@ impl MigrationsHelper { .collect::>(); let table_to_reference = annotation_data - .get(0) + .first() .expect("Error extracting table ref from FK annotation") .to_string(); let column_to_reference = annotation_data diff --git a/tests/crud/querybuilder_operations.rs b/tests/crud/querybuilder_operations.rs index 7fba112e..64ca46dd 100644 --- a/tests/crud/querybuilder_operations.rs +++ b/tests/crud/querybuilder_operations.rs @@ -56,7 +56,7 @@ fn test_crud_find_with_querybuilder() { let filtered_leagues: Vec = filtered_leagues_result.unwrap(); assert!(!filtered_leagues.is_empty()); - let league_idx_0 = filtered_leagues.get(0).unwrap(); + let league_idx_0 = filtered_leagues.first().unwrap(); assert_eq!(league_idx_0.id, 34); assert_eq!(league_idx_0.region, "KOREA"); } diff --git a/tests/migrations/mod.rs b/tests/migrations/mod.rs index 9ece4aac..b0fbed96 100644 --- a/tests/migrations/mod.rs +++ b/tests/migrations/mod.rs @@ -14,16 +14,16 @@ fn test_migrations_postgresql_status_query() { let res = results.unwrap(); let public_schema_info = res.get_postgres_rows(); - let first_result = public_schema_info.get(0).unwrap(); + let first_result = public_schema_info.first().unwrap(); - assert_eq!(first_result.columns().get(0).unwrap().name(), "table_name"); + assert_eq!(first_result.columns().first().unwrap().name(), "table_name"); assert_eq!( - first_result.columns().get(0).unwrap().type_().name(), + first_result.columns().first().unwrap().type_().name(), "name" ); - assert_eq!(first_result.columns().get(0).unwrap().type_().oid(), 19); + assert_eq!(first_result.columns().first().unwrap().type_().oid(), 19); assert_eq!( - first_result.columns().get(0).unwrap().type_().schema(), + first_result.columns().first().unwrap().type_().schema(), "pg_catalog" ); } From 3fbd81445c196a55632fe9fc191b9aa36ea50409 Mon Sep 17 00:00:00 2001 From: OnSystem <48860619+0nSystem@users.noreply.github.com> Date: Wed, 1 May 2024 20:11:02 +0200 Subject: [PATCH 75/82] Feature/gh 58 update documentation of mysql implementation to current status (#59) * changes: readme, add repository canyon book --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index edf093a7..c714c7c2 100755 --- a/README.md +++ b/README.md @@ -29,11 +29,12 @@ Any contrib via `fork` + `PR` it's really appreciated. Currently we are involved There is a `work-in-progress` web page, build with `mdBook` containing the official documentation. Here is where you will find all the technical documentation for `Canyon-SQL`. You can read it [by clicking this link](https://zerodaycode.github.io/canyon-book/) +If you want to contribute in some section of the documentation [canyon-book repository](https://github.com/zerodaycode/canyon-book): ## :pushpin: Most important features - **Async** by default. Almost every functionality provided is ready to be consumed concurrently. -- Use of multiple datasources. You can query multiple databases at the same time, even different ones! This means that you will be able to query concurrently a `PostgreSQL` database and a `SqlServer` one in the same project. +- Use of multiple datasources. You can query multiple databases at the same time, even different ones! This means that you will be able to query concurrently a `PostgreSQL` database and a `SqlServer` or `MySql` one in the same project. - Is macro based. With a few annotations and a configuration file, you are ready to write your data access. - Allows **migrations**. `Canyon-SQL` comes with a *god-mode* that will manage every table on your database for you. You can modify in `Canyon` code your tables internally, altering columns, setting up constraints... Also, in the future, we have plans to allow you to manipulate the whole server, like creating databases, altering configurations... everything, but in a programmatically approach with `Canyon`! @@ -43,6 +44,7 @@ You can read it [by clicking this link](https://zerodaycode.github.io/canyon-boo - PostgreSQL (via `tokio-postgres` crate) - SqlServer (via `tiberius` crate) +- MySql (via `mysql-async` crate) Every crate listed above is an `async` based crate, in line with the guidelines of the `Canyon-SQL` design. @@ -135,6 +137,6 @@ Typically in `Canyon`, isolated unit tests are written as doc-tests, and the int If you want to run the tests (because this is the first thing that you want to do after fork the repo), before moving forward, there are a couple of things that have to be considered. - You will need Docker installed in the target machine. -- If you have Docker, and `Canyon-SQL` cloned of forked, you can run our docker-compose file `(docker/docker-compose.yml)`, which will initialize a `PostgreSQL` database and will put content on it to make the tests able to work. +- If you have Docker, and `Canyon-SQL` cloned of forked, you can run our docker-compose file `(docker/docker-compose.yml)`, which will initialize a `PostgreSQL` and `MySql` database and will put content on it to make the tests able to work. - Finally, some tests run against `MSSQL`. We didn't found a nice way of inserting data directly when the Docker wakes up, but instead, we run a very special test located at `tests/crud/mod.rs`, that is named `initialize_sql_server_docker_instance`. When you run this one, initial data will be inserted into the tables that are created when this test run. (If you know a better way of doing this, please, open an issue to let us know, and improve this process!) From b855518f62afde744528927bcb117b008aa226f8 Mon Sep 17 00:00:00 2001 From: OnSystem <48860619+0nSystem@users.noreply.github.com> Date: Wed, 8 May 2024 21:02:08 +0200 Subject: [PATCH 76/82] Error in MySql implementation detail, related to features configuration, in From<&Auth> for DatabaseType (#61) * changes: impl From<&Auth> for DatabaseType * changes: correction cargo fmt * changes: remove panic From<&Auth> * changes: remove doc empty * changes: cargo fmt --- canyon_connection/src/canyon_database_connector.rs | 3 +++ canyon_connection/src/datasources.rs | 2 +- canyon_connection/src/lib.rs | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs index 438f3548..11530a7d 100644 --- a/canyon_connection/src/canyon_database_connector.rs +++ b/canyon_connection/src/canyon_database_connector.rs @@ -28,8 +28,11 @@ pub enum DatabaseType { impl From<&Auth> for DatabaseType { fn from(value: &Auth) -> Self { match value { + #[cfg(feature = "postgres")] crate::datasources::Auth::Postgres(_) => DatabaseType::PostgreSql, + #[cfg(feature = "mssql")] crate::datasources::Auth::SqlServer(_) => DatabaseType::SqlServer, + #[cfg(feature = "mysql")] crate::datasources::Auth::MySQL(_) => DatabaseType::MySQL, } } diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs index ccfd3694..11edcd31 100644 --- a/canyon_connection/src/datasources.rs +++ b/canyon_connection/src/datasources.rs @@ -92,7 +92,7 @@ fn load_ds_config_from_array() { assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); } } -/// + #[derive(Deserialize, Debug, Clone)] pub struct CanyonSqlConfig { pub canyon_sql: Datasources, diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs index bb313941..5bd7a232 100644 --- a/canyon_connection/src/lib.rs +++ b/canyon_connection/src/lib.rs @@ -85,7 +85,6 @@ pub async fn init_connections_cache() { } } -/// pub fn get_database_connection<'a>( datasource_name: &str, guarded_cache: &'a mut MutexGuard>, From e229264f68f936cb5edd6961dc48d70ba19743c1 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 19 Dec 2024 10:46:33 +0100 Subject: [PATCH 77/82] feat: v0.5.1, solving clippy lintings and adapted the code to Rust v1.82.0 --- Cargo.toml | 30 +++++++-------- README.md | 37 ++++++++++++++++++ .../src/query_elements/query_builder.rs | 38 +++++++++---------- canyon_macros/src/utils/function_parser.rs | 1 + canyon_macros/src/utils/helpers.rs | 3 +- canyon_macros/src/utils/macro_tokens.rs | 3 +- canyon_migrations/src/migrations/memory.rs | 2 - tests/canyon_integration_tests.rs | 12 +++--- tests/crud/foreign_key_operations.rs | 18 ++++----- tests/crud/querybuilder_operations.rs | 8 ++-- 10 files changed, 93 insertions(+), 59 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1351412a..d9bfd724 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,11 +21,11 @@ members = [ [dependencies] # Project crates -canyon_connection = { workspace = true, path = "canyon_connection" } -canyon_crud = { workspace = true, path = "canyon_crud" } -canyon_entities = { workspace = true, path = "canyon_entities" } -canyon_migrations = { workspace = true, path = "canyon_migrations", optional = true } -canyon_macros = { workspace = true, path = "canyon_macros" } +canyon_connection = { workspace = true } +canyon_crud = { workspace = true } +canyon_entities = { workspace = true } +canyon_migrations = { workspace = true, optional = true } +canyon_macros = { workspace = true } # To be marked as opt deps tokio-postgres = { workspace = true, optional = true } @@ -35,11 +35,11 @@ mysql_common = { workspace = true, optional = true } [workspace.dependencies] -canyon_crud = { version = "0.5.0", path = "canyon_crud" } -canyon_connection = { version = "0.5.0", path = "canyon_connection" } -canyon_entities = { version = "0.5.0", path = "canyon_entities" } -canyon_migrations = { version = "0.5.0", path = "canyon_migrations"} -canyon_macros = { version = "0.5.0", path = "canyon_macros" } +canyon_crud = { version = "0.5.1", path = "canyon_crud" } +canyon_connection = { version = "0.5.1", path = "canyon_connection" } +canyon_entities = { version = "0.5.1", path = "canyon_entities" } +canyon_migrations = { version = "0.5.1", path = "canyon_migrations"} +canyon_macros = { version = "0.5.1", path = "canyon_macros" } tokio = { version = "1.27.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["compat"] } @@ -64,10 +64,8 @@ partialdebug = "0.2.0" quote = "1.0.9" proc-macro2 = "1.0.27" - - [workspace.package] -version = "0.5.0" +version = "0.5.1" edition = "2021" authors = ["Alex Vergara, Gonzalo Busto Musi"] documentation = "https://zerodaycode.github.io/canyon-book/" @@ -77,7 +75,7 @@ license = "MIT" description = "A Rust ORM and QueryBuilder" [features] -postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres", "canyon_migrations/postgres", "canyon_macros/postgres"] -mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql", "canyon_migrations/mssql", "canyon_macros/mssql"] -mysql = ["mysql_async", "mysql_common", "canyon_connection/mysql", "canyon_crud/mysql", "canyon_migrations/mysql", "canyon_macros/mysql"] +postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres", "canyon_migrations/postgres", "canyon_macros/postgres"] +mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql", "canyon_migrations/mssql", "canyon_macros/mssql"] +mysql = ["mysql_async", "mysql_common", "canyon_connection/mysql", "canyon_crud/mysql", "canyon_migrations/mysql", "canyon_macros/mysql"] migrations = ["canyon_migrations", "canyon_macros/migrations"] diff --git a/README.md b/README.md index c714c7c2..96e3b90b 100755 --- a/README.md +++ b/README.md @@ -140,3 +140,40 @@ If you want to run the tests (because this is the first thing that you want to d - If you have Docker, and `Canyon-SQL` cloned of forked, you can run our docker-compose file `(docker/docker-compose.yml)`, which will initialize a `PostgreSQL` and `MySql` database and will put content on it to make the tests able to work. - Finally, some tests run against `MSSQL`. We didn't found a nice way of inserting data directly when the Docker wakes up, but instead, we run a very special test located at `tests/crud/mod.rs`, that is named `initialize_sql_server_docker_instance`. When you run this one, initial data will be inserted into the tables that are created when this test run. (If you know a better way of doing this, please, open an issue to let us know, and improve this process!) + +## Known issues + +### Missing dependency: OpenSSL + +There's a certain set of common issues while building `Canyon-SQL` in development or in client code. Those building issues +are related with missing packages or dependencies that `Cargo` doesn't resolves automatically depending on the underlying OS. + +``` +openssl-sys@0.9.104: Could not find directory of OpenSSL installation, and this `-sys` crate cannot proceed without this knowledge. +If OpenSSL is installed and this crate had trouble finding it, you can set the `OPENSSL_DIR` environment variable for the compilation process. +See stderr section below for further information. +``` + +This means that the `OpenSSL` package isn't installed on your system or not in *PATH*. + +In a Debian based system, you can just `sudo apt install libssl-dev`. For others, just use your package manager +to solve it by install it. + +### Missing dependency: pkg-config + +``` +Could not find openssl via pkg-config: + Could not run `PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 pkg-config --libs --cflags openssl` + The pkg-config command could not be found. +``` +`Cargo` may try to discover the `OpenSSL` package via `pkg-config`. If you find this error, you can +`sudo apt install pkg-config` on *apt* based systems. For other systems, you must read your package manager +docs and install it. + +### failed to run custom build command for `libgssapi-sys vX.X.X` + +The problem is missing a *C* header `gssapi.h`. + +- Alpine: `apk --update add krb5-pkinit krb5-dev krb5` +- Ubuntu: `apt-get -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit` + diff --git a/canyon_crud/src/query_elements/query_builder.rs b/canyon_crud/src/query_elements/query_builder.rs index e25ff9fe..4d56401a 100644 --- a/canyon_crud/src/query_elements/query_builder.rs +++ b/canyon_crud/src/query_elements/query_builder.rs @@ -66,9 +66,9 @@ pub mod ops { /// Generates a `WHERE` SQL clause for constraint the query. /// /// * `column` - A [`FieldValueIdentifier`] that will provide the target - /// column name and the value for the filter + /// column name and the value for the filter /// * `op` - Any element that implements [`Operator`] for create the comparison - /// or equality binary operator + /// or equality binary operator fn r#where>( &mut self, column: Z, @@ -80,9 +80,9 @@ pub mod ops { /// Generates an `AND` SQL clause for constraint the query. /// /// * `column` - A [`FieldValueIdentifier`] that will provide the target - /// column name and the value for the filter + /// column name and the value for the filter /// * `op` - Any element that implements [`Operator`] for create the comparison - /// or equality binary operator + /// or equality binary operator fn and>( &mut self, column: Z, @@ -93,10 +93,10 @@ pub mod ops { /// the filter in conjunction with an `IN` operator that will ac /// /// * `column` - A [`FieldIdentifier`] that will provide the target - /// column name for the filter, based on the variant that represents - /// the field name that maps the targeted column name + /// column name for the filter, based on the variant that represents + /// the field name that maps the targeted column name /// * `values` - An array of [`QueryParameter`] with the values to filter - /// inside the `IN` operator + /// inside the `IN` operator fn and_values_in(&mut self, column: Z, values: &'a [Q]) -> &mut Self where Z: FieldIdentifier, @@ -106,10 +106,10 @@ pub mod ops { /// the filter in conjunction with an `IN` operator that will ac /// /// * `column` - A [`FieldIdentifier`] that will provide the target - /// column name for the filter, based on the variant that represents - /// the field name that maps the targeted column name + /// column name for the filter, based on the variant that represents + /// the field name that maps the targeted column name /// * `values` - An array of [`QueryParameter`] with the values to filter - /// inside the `IN` operator + /// inside the `IN` operator fn or_values_in(&mut self, r#or: Z, values: &'a [Q]) -> &mut Self where Z: FieldIdentifier, @@ -118,18 +118,16 @@ pub mod ops { /// Generates an `OR` SQL clause for constraint the query. /// /// * `column` - A [`FieldValueIdentifier`] that will provide the target - /// column name and the value for the filter + /// column name and the value for the filter /// * `op` - Any element that implements [`Operator`] for create the comparison - /// or equality binary operator + /// or equality binary operator fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self; /// Generates a `ORDER BY` SQL clause for constraint the query. /// - /// * `order_by` - A [`FieldIdentifier`] that will provide the target - /// column name - /// * `desc` - a boolean indicating if the generated `ORDER_BY` must be - /// in ascending or descending order + /// * `order_by` - A [`FieldIdentifier`] that will provide the target column name + /// * `desc` - a boolean indicating if the generated `ORDER_BY` must be in ascending or descending order fn order_by>(&mut self, order_by: Z, desc: bool) -> &mut Self; } } @@ -452,9 +450,9 @@ where } /// Contains the specific database operations of the *UPDATE* SQL statements. -/// +/// /// * `set` - To construct a new `SET` clause to determine the columns to -/// update with the provided values +/// update with the provided values #[derive(Debug, Clone)] pub struct UpdateQueryBuilder<'a, T> where @@ -590,9 +588,9 @@ where /// Contains the specific database operations associated with the /// *DELETE* SQL statements. -/// +/// /// * `set` - To construct a new `SET` clause to determine the columns to -/// update with the provided values +/// update with the provided values #[derive(Debug, Clone)] pub struct DeleteQueryBuilder<'a, T> where diff --git a/canyon_macros/src/utils/function_parser.rs b/canyon_macros/src/utils/function_parser.rs index 4ab62025..841e534d 100644 --- a/canyon_macros/src/utils/function_parser.rs +++ b/canyon_macros/src/utils/function_parser.rs @@ -5,6 +5,7 @@ use syn::{ /// Implementation of syn::Parse for the `#[canyon]` proc-macro #[derive(Clone)] +#[allow(dead_code)] pub struct FunctionParser { pub attrs: Vec, pub vis: Visibility, diff --git a/canyon_macros/src/utils/helpers.rs b/canyon_macros/src/utils/helpers.rs index 7022db2b..2db52be5 100644 --- a/canyon_macros/src/utils/helpers.rs +++ b/canyon_macros/src/utils/helpers.rs @@ -127,7 +127,8 @@ fn test_entity_database_name_defaulter() { "MajorLeague".to_owned() ); } -/// + +/// Autogenerates a default table name for an entity given their struct name pub fn default_database_table_name_from_entity_name(ty: &str) -> String { let struct_name: String = ty.to_string(); let mut table_name: String = String::new(); diff --git a/canyon_macros/src/utils/macro_tokens.rs b/canyon_macros/src/utils/macro_tokens.rs index 29de0467..415d9ccc 100644 --- a/canyon_macros/src/utils/macro_tokens.rs +++ b/canyon_macros/src/utils/macro_tokens.rs @@ -6,6 +6,7 @@ use syn::{Attribute, DeriveInput, Fields, Generics, Type, Visibility}; /// Provides a convenient way of store the data for the TokenStream /// received on a macro +#[allow(dead_code)] pub struct MacroTokens<'a> { pub vis: &'a Visibility, pub ty: &'a Ident, @@ -114,7 +115,7 @@ impl<'a> MacroTokens<'a> { column_names_as_chars.as_str().to_owned() } - /// + /// Retrieves the value of the index of an annotated field with #[primary_key] pub fn get_pk_index(&self) -> Option { let mut pk_index = None; for (idx, field) in self.fields.iter().enumerate() { diff --git a/canyon_migrations/src/migrations/memory.rs b/canyon_migrations/src/migrations/memory.rs index 80fbe3ad..1ad6263a 100644 --- a/canyon_migrations/src/migrations/memory.rs +++ b/canyon_migrations/src/migrations/memory.rs @@ -57,7 +57,6 @@ impl Transaction for CanyonMemory {} impl CanyonMemory { /// Queries the database to retrieve internal data about the structures /// tracked by `CanyonSQL` - #[cfg(not(cargo_check))] #[allow(clippy::nonminimal_bool)] pub async fn remember( datasource: &DatasourceConfig, @@ -184,7 +183,6 @@ impl CanyonMemory { /// Parses the Rust source code files to find the one who contains Canyon entities /// ie -> annotated with `#[canyon_entity]` - #[cfg(not(cargo_check))] async fn find_canyon_entity_annotated_structs( &mut self, canyon_entities: &[CanyonRegisterEntity<'_>], diff --git a/tests/canyon_integration_tests.rs b/tests/canyon_integration_tests.rs index 799a3747..6e61b549 100644 --- a/tests/canyon_integration_tests.rs +++ b/tests/canyon_integration_tests.rs @@ -1,11 +1,11 @@ -// Integration tests for the heart of a Canyon-SQL application, the CRUD operations. +/// Integration tests for the heart of a Canyon-SQL application, the CRUD operations. /// -// This tests will tests mostly the whole source code of Canyon, due to its integration nature +/// This tests will tests mostly the whole source code of Canyon, due to its integration nature /// -// Guide-style: Almost every operation in Canyon is `Result` wrapped (without the) unckecked -// variants of the `find_all` implementations. We will go to directly `.unwrap()` the results -// because, if there's something wrong in the code reported by the tests, we want to *panic* -// and abort the execution. +/// Guide-style: Almost every operation in Canyon is `Result` wrapped (without the) unckecked +/// variants of the `find_all` implementations. We will go to directly `.unwrap()` the results +/// because, if there's something wrong in the code reported by the tests, we want to *panic* +/// and abort the execution. extern crate canyon_sql; use std::error::Error; diff --git a/tests/crud/foreign_key_operations.rs b/tests/crud/foreign_key_operations.rs index e6281f92..87630ad1 100644 --- a/tests/crud/foreign_key_operations.rs +++ b/tests/crud/foreign_key_operations.rs @@ -1,13 +1,13 @@ -// Integration tests for the CRUD operations available in `Canyon` that -// generates and executes *SELECT* statements based on a entity -// annotated with the `#[foreign_key(... args)]` annotation looking -// for the related data with some entity `U` that acts as is parent, where `U` -// impls `ForeignKeyable` (isn't required, but it won't unlock the -// reverse search features parent -> child, only the child -> parent ones). +/// Integration tests for the CRUD operations available in `Canyon` that +/// generates and executes *SELECT* statements based on a entity +/// annotated with the `#[foreign_key(... args)]` annotation looking +/// for the related data with some entity `U` that acts as is parent, where `U` +/// impls `ForeignKeyable` (isn't required, but it won't unlock the +/// reverse search features parent -> child, only the child -> parent ones). /// -// Names of the foreign key methods are autogenerated for the direct and -// reverse side of the implementations. -// For more info: TODO -> Link to the docs of the foreign key chapter +/// Names of the foreign key methods are autogenerated for the direct and +/// reverse side of the implementations. +/// For more info: TODO -> Link to the docs of the foreign key chapter use canyon_sql::crud::CrudOperations; #[cfg(feature = "mssql")] diff --git a/tests/crud/querybuilder_operations.rs b/tests/crud/querybuilder_operations.rs index 64ca46dd..f2dc8b57 100644 --- a/tests/crud/querybuilder_operations.rs +++ b/tests/crud/querybuilder_operations.rs @@ -3,11 +3,11 @@ use crate::constants::MYSQL_DS; #[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; -// Tests for the QueryBuilder available operations within Canyon. +/// Tests for the QueryBuilder available operations within Canyon. /// -// QueryBuilder are the way of obtain more flexibility that with -// the default generated queries, essentially for build the queries -// with the SQL filters +/// QueryBuilder are the way of obtain more flexibility that with +/// the default generated queries, essentially for build the queries +/// with the SQL filters /// use canyon_sql::{ crud::CrudOperations, From 0840a5770294417d91a29b9cb9dc808fc05950dd Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 19 Dec 2024 13:41:03 +0100 Subject: [PATCH 78/82] fix: solved a bug in the CanyonMapper macro that was causing issues to the conditionally compiled code --- canyon_macros/src/canyon_macro.rs | 3 +- canyon_macros/src/lib.rs | 60 +++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/canyon_macros/src/canyon_macro.rs b/canyon_macros/src/canyon_macro.rs index 48c89fcc..95379581 100644 --- a/canyon_macros/src/canyon_macro.rs +++ b/canyon_macros/src/canyon_macro.rs @@ -1,4 +1,5 @@ //! Provides helpers to build the `#[canyon_macros::canyon]` procedural like attribute macro +#![cfg(feature = "migrations")] use canyon_connection::CANYON_TOKIO_RUNTIME; use canyon_migrations::migrations::handler::Migrations; @@ -6,7 +7,6 @@ use canyon_migrations::{CM_QUERIES_TO_EXECUTE, QUERIES_TO_EXECUTE}; use proc_macro2::TokenStream; use quote::quote; -#[cfg(feature = "migrations")] pub fn main_with_queries() -> TokenStream { CANYON_TOKIO_RUNTIME.block_on(async { canyon_connection::init_connections_cache().await; @@ -25,7 +25,6 @@ pub fn main_with_queries() -> TokenStream { /// Creates a TokenScream that is used to load the data generated at compile-time /// by the `CanyonManaged` macros again on the queries register -#[cfg(feature = "migrations")] fn wire_queries_to_execute(canyon_manager_tokens: &mut Vec) { let cm_data = CM_QUERIES_TO_EXECUTE.lock().unwrap(); let data = QUERIES_TO_EXECUTE.lock().unwrap(); diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index 6f094fff..d767fd94 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -11,7 +11,7 @@ mod utils; use canyon_entity_macro::parse_canyon_entity_proc_macro_attr; use proc_macro::TokenStream as CompilerTokenStream; use proc_macro2::{Ident, TokenStream}; -use quote::{quote, ToTokens}; +use quote::quote; use syn::{DeriveInput, Fields, Type, Visibility}; use query_operations::{ @@ -442,6 +442,11 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac } }); + + // TODO: refactor the code below after the current bugfixes, to conditinally generate + // the required methods and populate the CanyonMapper trait dependencing on the cfg flags + // enabled with a more elegant solution (a fn for feature, for ex) + #[cfg(feature = "postgres")] // Here it's where the incoming values of the DatabaseResult are wired into a new // instance, mapping the fields of the type against the columns let init_field_values = fields.iter().map(|(_vis, ident, _ty)| { @@ -452,6 +457,8 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac } }); + + #[cfg(feature = "mssql")] let init_field_values_sqlserver = fields.iter().map(|(_vis, ident, ty)| { let ident_name = ident.to_string(); @@ -530,6 +537,7 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac } }); + #[cfg(feature = "mysql")] let init_field_values_mysql = fields.iter().map(|(_vis, ident, _ty)| { let ident_name = ident.to_string(); quote! { @@ -541,27 +549,40 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac // The type of the Struct let ty = ast.ident; - let tokens = quote! { - impl canyon_sql::crud::RowMapper for #ty { - #[cfg(feature="postgres")] - fn deserialize_postgresql(row: &canyon_sql::db_clients::tokio_postgres::Row) -> #ty { - Self { - #(#init_field_values),* - } + let mut impl_methods = quote! {}; // Collect methods conditionally + + #[cfg(feature = "postgres")] + impl_methods.extend(quote! { + fn deserialize_postgresql(row: &canyon_sql::db_clients::tokio_postgres::Row) -> #ty { + Self { + #(#init_field_values),* } - #[cfg(feature="mssql")] - fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { - Self { - #(#init_field_values_sqlserver),* - } + } + }); + + #[cfg(feature = "mssql")] + impl_methods.extend(quote! { + fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { + Self { + #(#init_field_values_sqlserver),* } - #[cfg(feature="mysql")] - fn deserialize_mysql(row: &canyon_sql::db_clients::mysql_async::Row) -> #ty { - Self { - #(#init_field_values_mysql),* - } + } + }); + + #[cfg(feature = "mysql")] + impl_methods.extend(quote! { + fn deserialize_mysql(row: &canyon_sql::db_clients::mysql_async::Row) -> #ty { + Self { + #(#init_field_values_mysql),* } } + }); + + // Wrap everything in the shared `impl` block + let tokens = quote! { + impl canyon_sql::crud::RowMapper for #ty { + #impl_methods + } }; tokens.into() @@ -588,6 +609,9 @@ fn fields_with_types(fields: &Fields) -> Vec<(Visibility, Ident, Type)> { .collect::>() } +#[cfg(feature = "mssql")] +use quote::ToTokens; +#[cfg(feature = "mssql")] fn get_field_type_as_string(typ: &Type) -> String { match typ { Type::Array(type_) => type_.to_token_stream().to_string(), From 6b8681a09aa364e4d4ae3166f3938777befdcc49 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 19 Dec 2024 15:33:52 +0100 Subject: [PATCH 79/82] chore: adding 'docker-compose' on the 'ubuntu-latest' based actions --- .github/workflows/code-quality.yml | 3 ++- .github/workflows/release.yml | 4 +++- canyon_entities/src/register_types.rs | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index b955295c..3d1908fb 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -27,7 +27,8 @@ jobs: - uses: hecrj/setup-rust-action@v1 with: components: clippy - - run: cargo clippy --workspace --all-targets --verbose --all-features + - run: cargo clippy --workspace --all-targets --all-features + rustfmt: name: Verify code formatting runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b2060c2..91520071 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,8 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 + - name: Installing `docker-compose` + run: sudo apt -y install docker-compose - name: Installing `gssapi` headers run: sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit @@ -53,4 +55,4 @@ jobs: with: configuration: "./.github/changelog_configuration.json" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/canyon_entities/src/register_types.rs b/canyon_entities/src/register_types.rs index 45cd1b8d..2702e61f 100644 --- a/canyon_entities/src/register_types.rs +++ b/canyon_entities/src/register_types.rs @@ -1,6 +1,5 @@ /// This file contains `Rust` types that represents an entry on the `CanyonRegister` /// where `Canyon` tracks the user types that has to manage - pub const NUMERIC_PK_DATATYPE: [&str; 6] = ["i16", "u16", "i32", "u32", "i64", "u64"]; /// Gets the necessary identifiers of a CanyonEntity to make it the comparative From 75f7ffa672c35dbe8c76b32272c1614bb18d1823 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Thu, 19 Dec 2024 15:36:48 +0100 Subject: [PATCH 80/82] chore: rustfmt and 'docker-compose' on the CI action --- .github/workflows/continuous-integration.yml | 4 ++-- canyon_macros/src/lib.rs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 3c26ce66..ab59e989 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -23,11 +23,11 @@ jobs: - { rust: stable, os: windows-latest } steps: - - name: Make the USER own the working directory. Installing `gssapi` headers + - name: Make the USER own the working directory. Installing required build dependencies if: ${{ matrix.os == 'ubuntu-latest' }} run: | sudo chown -R $USER:$USER ${{ github.workspace }} - sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit + sudo apt -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit docker-compose - uses: actions/checkout@v3 diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index d767fd94..bd9cff0f 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -442,7 +442,6 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac } }); - // TODO: refactor the code below after the current bugfixes, to conditinally generate // the required methods and populate the CanyonMapper trait dependencing on the cfg flags // enabled with a more elegant solution (a fn for feature, for ex) @@ -457,7 +456,6 @@ pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_mac } }); - #[cfg(feature = "mssql")] let init_field_values_sqlserver = fields.iter().map(|(_vis, ident, ty)| { let ident_name = ident.to_string(); From 71c7951ff16ed7e4b2e95ad68fcaf732c286f2f4 Mon Sep 17 00:00:00 2001 From: Alex Vergara Date: Fri, 7 Aug 2026 13:42:28 +0200 Subject: [PATCH 81/82] General Refactor (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: getting rid of the implementation indirections of Transaction input, avoiding storing intermediate state * feat: The DbConnection trait now is able to tell the client which is the underlying db type for the connection * test: re-enabled the integration tests of the SelectQuerybuilder * feat: re-enabled the insert and delelte operations * test: unit testing the generated tokens for the delete operations macros * test: unit testing the generated tokens for the delete with the querybuilder operations macros * feat: re-enabled the update operations * feat: re-enabled the multi-insert operations * feat: re-enabled the foreign key operations * feat: proc-macro hygiene on the implementation of the crud operations * feat: querybuilder read ops are hidden behind the same facade as the other read ops * chore: refactored the CanyonMapper macro into it's own file * chore: handle_stupid_tiberius_sql_conversions * chore: moving proc macro implementations from the root lib file of canyon_macros to their own separate modules * chore: moving DbConnection to db_connector:: * feat: removed #[async_trait] from all the codebase in favour of the impl Future * fix: update pk operations chore: getting concrete column value from row on CanyonRows * feat: impl of single row result operations * feat: renamed DbConnection::launch(self, ...) to DbConnection::query(self, ...) * feat: query_one to the public interface of Transaction * feat(wip): query_rows impl as the future replacement for the CanyonRows wrapper * feat(wip): reworking the new Transaction::query lifetime and type bounds * feat(wip): only postgres understand relaxing lifetime bounds * feat(wip): re-enabling mysql queries * feat(wip): re-enabling sqlserver on query * feat(wip): re-enabling all the read operations * feat(wip): re-enabling all the insert operations * feat(wip): re-enabling all the fk operations * feat: using the execute new op on the update to return the affected rows after the operation * feat: re-enabled all the crud operations again * fix: mssql parameter binding on query methods * chore: the macro template uses async fn syntax * chore: cleaning clippy warnings * chore: starting to clean the code around canyon connection and its exposed APIs * chore: removing the legacy way of retrieving a database connection * feat: Crud operations and transaction decoupling, reworking the bounds of all the associated types * fix: removing the unnecessary bound of CrudOperations on Foreign Key * feat(WIP!): adjusting the output type parameter on RowMapper across the dependent types * feat(WIP!): preparing the introduction of an EntityMetadata trait to dynamically retrieve the tokens that compose the RowMapper implementor on the queries * feat(wip): TLS issues with Tiberius since Rust 1.86 * feat(wip): proxy info via derive proc matro attr * fix: parsing the inner contents of the new canyon_crud annotation * feat: canyon_crud(maps_to = ) annotation, to determine to which type Canyon Crud will map the results after the queries * clean: removed the x_unchecked operations from the public API * feat(wip): The querybuilder only needs bounds on DbConnection, Transaction got out of the public API * feat(wip): refactored the read ops file structure. Re-enabled the tests without the MacroOpsBuilder * feat(wip): making the querybuilder able to receive str and &str as DS params * feat(wip)!: marcho tomar café, que fai bo día! :) * feat: crud operations are finally implemented (along with the querybuilder ones) according to the new Transaction-DbConnection contract specifications * feat: upgrades to the internal macro implementation of the proc macros of the foreign key crud operations * perf: avoiding unnecessary Vec heap allocations on the pk macros when passing the target input parameter * refactor: upgrades to the `[canyon::main]` macro * perf: reworked almost every bit of the shared global state (or Canyon Context) to work with Arc>. Solved some other perf issues while looking for connections. Made the default user defined connection much faster to access. * refactor: getting rid out of lazy_static! * fix(test): tiberius target types on deserialization process that contains a whitespace due to quote! processing * fix(test): tiberius target types on deserialization process that contains a whitespace due to quote! processing * perf: By redesigning the public API of Canyon, we noticed an increase of a 8x on running the integration tests. This will likely impact the Canyon users with a massive performance boost * chore: legacy dead code cleanup * feat: new macro! to reduce the code required for implement str and &str for DbConnection * docs: canyon_core::connection * fix: doc-comments * fix: doc-comments examples with no_run to ignore * feat: splitting the behaviour of the implementors of QueryBuilder into a small subset of contracts, according each one functionality * feat: new type Query for having a fluent builder process from QueryBuilder -> Query Query is the DbConnection type now * feat: joins on the SelectQuerybuilder types now receive the join fields from the autogenerated enums, so the argument is now a code entity and not an str * feat: new auto-generated enum type that carries the meta-information of a type for giving the user reflection elements over the type ident and the type matching database table name generated by Canyon * fix: cargo fmt * feat: completed all the possible branches for the FromSql and FromSqlOwnedValue depending on what db configuration features exists * refactor: changed some re-exports of the public API that was in the incorrect crate * refactor: the crud_operations attribute in its `maps_to` argument is processed only once per procedural macro creation, and not per method invocation * refactor: little improvements on MacroTokens * fix: hiding non-relevant warnings that are raised when not all cfg features are enabled * feat: The Query type now implement AsRef chore: removed the + Display bound on the generic statement parameters that receives the sql sentences * fix: missing the generics on the generated tokens for CrudOperations * feat(mssql-broken)!: updated the way on how the count crud operation is internally generated (Care:! We broke the Tiberius implementation because the type requeriments of their api for the moment, is pending to be fixed) * fix: missing the generics on the quote interpolation for the CanyonMapper proc macro * fix: correctly applying the generics on the macros for all the implementors of CrudOperations * perf: avoiding vec allocations on the insert query on CrudOperations to pass the query parameters * feat: row_mapper methods now returns the value wrapped on Result, leading to a much better hygiene on the derive proc macro * chore: raised Rust edition to 2024 * fix: removed unneeded Debug + Clone derives in the autogenerated enums of Fields * fix: removed unneeded Debug + Clone derives in the autogenerated enums of Fields * refactor(wip): towards a better default CrudOperations that doesn't use Transaction * refactor(internal): cleaned the insert operation macro tokens generators * feat(wip)!: adding the insert entity variants for insert data given some T with CrudOperations that inserts any other RowMapper type * fix: now, if there's some proc_macro_attribute with the maps_to for CanyonCrud, is taken in consideration for the table_schema_data generation before the default one * feat: entity insertions (default and with) available on CrudOperations * feat: avoiding deadlocks by explicitly return the DbConnection from Canyon in the Arc Mutex, so is the user the one responsible for acquiring the lock over the resource * feat: Query constructor * feat: Row Mapper tokens are now handled by MacroTokens * feat: easier way of handling the fields_values returned by Inspectionable * refactor: macro tokens method operations decoupled to helpers, so it can be used by another trait impls * refactor: reducing the number of collections allocations by returning iterators from th e helpers and allocating only when needed * fix: now the insert entity ops receives the correct values for the queryparameters to be updated * feat: update entity implementations * feat: delete entity implementations * feat(wip)!: setting the value returned of the pk on the insert entity operations, but lifetime problems arises * feat(wip)!: saving intermediate smokes * fix(wip)!: mapper target type will be correctly calculated when the pk must be known at runtime (entity) * feat: entities got the inserted returned value of the pk auto-assigned back * chore: cleanup * chore: simplifying back macro tokens * fix: relaxing lifetime bounds that wasn't correctly specified between in and out params on crud operations * feat(wip)!: provisional blanket implementation for any T that's DbConnection and is wrapped on Arc Mutex combo * feat(wip)!: implementing u32 for QueryParameter types (not for tiberius, as usual, which will panic). We should wrap all the QueryParameter methods output with Result * chore: intermediate code cleaning * fix: improper lifetime bounds on the delete methods of the CrudOperations trait * feat: FieldValues autogenerated enums takes the concrete type of the column, not QueryParameter anymore * chore: simplified the implementation and definition of FieldValueIdentifier * chore: simplified the implementation and definition of QueryParameter, that now does not have a lifetime bound * fix: solved the update entity bug * chore: cognitive refactor of the complexity of the impl of inspectionable * fix: removing undesired lifetime bounds on the insert operations * fix: removing undesired lifetime bounds on the insert operations * refactor: removed find pk by fields for MacroTokens on the public API * refactor: PrimaryKey get direct info about the index position on the fields of the attached struct for the annotation * fix: correcting the update pk index to make the placeholder work in the queryparams * refactor: getting rid of the Arc> pattern when accessing db connections * fix: MSSQL count operation with our 'query_one_for::' * feat: implement connection pooling for performance optimization - Add ConnectionPool with VecDeque-based connection management - Add PooledConnection wrapper with automatic connection return - Add PoolManager global singleton for managing multiple pools - Implement DbConnection trait for PooledConnection - Fix MSSQL COUNT(*) issue by handling i32->i64 conversion - Update macro-generated code to handle database-specific COUNT(*) types - Add pool module to connection module exports - All 53 tests passing with significant performance improvements This implementation provides: - Automatic connection reuse and lifecycle management - Thread-safe connection pooling with Arc> - Non-blocking async connection return via tokio::spawn - Database type awareness for proper type handling - Seamless integration with existing Canyon-SQL APIs * fix: revert async Canyon API and remove pool initialization from macro - Revert Canyon::get_connection and get_default_connection back to synchronous - Remove pool import from Canyon since it's not being used yet - Fix compilation errors by reverting async changes in macros and migrations - The pool implementation exists but is not integrated into the main flow yet The current issue is that the macro is trying to initialize database connections during compilation, which is causing connection errors. The pool should only be used at runtime, not during macro expansion. * feat: add optimized connection creation for better performance - Add new_optimized() method to DatabaseConnection for performance-critical operations - Implement optimized PostgreSQL connection with better timeout and keepalive settings - Add optimized MSSQL connection with TCP optimizations - Add optimized MySQL connection with pool constraints - Keep existing API unchanged for backward compatibility - Provide foundation for connection pooling integration This addresses the performance issues by: - Reducing connection establishment overhead - Adding connection keepalive settings - Optimizing TCP settings for better throughput - Maintaining backward compatibility with existing code The optimized connections can be used for performance-critical operations while keeping the existing API unchanged. * Revert "fix: MSSQL count operation with our 'query_one_for::'" This reverts commit 863e679fff3351cc4e6e1d0feda055eb388379f8. * Reapply "fix: MSSQL count operation with our 'query_one_for::'" This reverts commit b503bc72ea0f2eabe097403c98f62bb01a03c720. * reapply: the fix on the mssql code generation * fix: compiler lints about unnecessary parenthesis * fix: new clippy lints on v.1.90 onwards for the migrations module * fix: new clippy lints on v.1.90 onwards for the core module * fix: new clippy lints on v.1.90 onwards for the macros module * fix: eliminating the warning about the mssql cfg feature on client code * fix: different approach for the cfg_if * feat(wip)!: Initial implementation of a real connection pooling * feat(wip)!: Unifying the pool within the database connection type * feat: connection pooling for all the supported databases * chore: renamed DatabaseConnection to DatabaseConnector * refactor: clients got again their own db caller code * refactor: custom macro for reduce the code of the implementation of DbConnection for DatabaseConnector, owned, ref and mut ref types * refactor: connections and pool creation moved to their wrapper client types * refactor: default ports for each kind of supported database * fix: missing cfg features * refactor(WIP): Querybuilder public interface * refactor(WIP-2)!: Querybuilder public interface * refactor(WIP-3)!: Querybuilder public interface * refactor(WIP-4)!: Querybuilder public interface * refactor(WIP-5)!: Querybuilder public interface * fix: removed async modifier on DatabaseType default_type * refactor: more querybuilder public interface * refactor(WIP)!: querybuilder now is made of AST types for query generation * refactor(WIP)!: querybuilder now is made of AST types for query generation * refactor(WIP)!: querybuilder now is made of AST types for query generation * refactor(WIP)!: querybuilder now is made of AST types for query generation * refactor(WIP)!: querybuilder now is made of AST types for query generation 2 * refactor(WIP)!: rollback the introduction of the querybuilder on the crud operations macros * refactor(WIP)!: convertions from types that makes sense on the join clause params * chore: cargo fmt * refactor(WIP)!: towards a better token emission phase * refactor(WIP)!: given the emitters real entity like behaviour * refactor(WIP)!: making the AST downcasteables to resolve the type at runtime * refactor(WIP)!: emit_columns shared logic. New helpers mod for the shared emission impl details * refactor(WIP)!: 1 * refactor(WIP)!: ToSqlTokens now returns a lazy generator, instead of taking the out collection by mut reference * refactor(WIP)!: Select tokens has now a complete lifecycle on the querybuilder * refactor(WIP)!: Insert tokens * refactor(WIP)!: Insert impl * refactor(WIP)!: querybuilder whitespaces(); * refactor(WIP)!: operator from Comp to Operator type * refactor(querybuilder helpers test)!: redone * refactor: fixed the LBracket for mssql query generator * refactor(wip)!: refactoring unit tests! * refactor(wip)!: consuming the sqltokens at render time instead of taking them by mut ref * refactor(wip)!: consolidating the querybuilder tests * fix: empty canyon_entity proc macro was requiring strange empty brackets * feat: custom types for Ranged elements * chore: writer.rs doesn't care anymore about creating tokens * chore: removed SqlToken whitespace from the codebase, since modeling " " at code level was being more disturbing that beneficious * chore: opts to the writer new logic * chore: re-enabling querybuilder like concat tests * fix: update set-values method * feat: autogenerated enum field values are now returning ColumnRef in the tuple with the value instead of str * fix(wip): borrowing issues on with_columns for the select_querybuilder * feat: avoiding using unneeded Result when constructing any kind of querybuilder * fix: mysql params was being used incorrectly * fix(perf): more mysql client opts * chore: compose updates * chore: minors * chore: cleaning unused crate deps * chore: creating some new const associated functions to construct the inner querybuilder elements at compile time * refactor(syn)!: field_annotation migrated and refactored syn version raised to major (2) * refactor(syn)!: helpers.rs * refactor(syn)!: macro_tokens.rs * refactor(syn)!: syn v2 migration * refactor(syn)!: minors * fix: wrong value-index params counter for the values-in methods * feat(WIP)!: now the build method for the concrete instances of the querybuilder is mandatory due to it's related type bound: QueryBuilderOps * feat(WIP)!: guárdame-esta * feat: finally really introducing the QueryBuilder to the macro queries * feat: new const new fn for the qerybuilder * feat: InsertQuerybuilder * feat: replacing the find_all methods to use the QueryBuilder * feat(WIP!): fk operations are being adecuated to use querybuilder * feat(WIP!): fk child operations are being adequate to use querybuilder * faet: fk operations refactor * feat: using the querybuilder on the foreign key * fix: missing dbtype on fk querybuilder * feat(wip): refactor of the insert macro to user querybuilder * feat(wip): refactor of the insert macro to user querybuilder * feat(wip)!: towards static dispatch on the backend emitters * feat(wip)!: cleaned unused placeholder query params functions * feat(wip)!: insert with the new static dispatch model on the querybuilder * chore(wip): partial cleanup of the AstProcessor dependent code for the emitters * feat(wip)!: finishing the reformulation of the querybuilder internal dispatch method * chore: cleanup * feat(wip)!: qualification of the emitted columns * feat(wip)!: qualification of the emitted columns * feat(wip)!: redo of the update macro to use the querybuilder * fix(wip)!: avoiding to update the fk * fix(wip)!: avoiding to update the fk * feat: refactor of the update method * feat: refactor of the delete method * feat: refactor of the find by pk methods * feat: update entity with the QueryBuilder * fix: COUNT(*) previous whitespace on MySQL * fix: find_by_primary_key entity operations was retrieving wrong table name * fix: find_by_primary_key entity operations was retrieving wrong table name * chore: cleaning Inspectionable (changed to EntityRuntimeInfo) * chore: cleaning legacy placeholders generators code * chore: DRY for the default db conn init * feat: Delete entity operations are now constructed with the QueryBuilder * chore: docs for the Querybuilder types * chore: more regular code cleanup * fix: obsolete querybuilder tests * chore: obsolete code cleanup * chore: more code cleanup * fix: MySql DB client implementation * chore: disabled and removed the Integrated auth of MsSql due to the annoying coupling with the gssapi headers * chore: removed the gssapi installation on upstream * fix: corrected intra-links docs * feat(wip)!: splitting CRUD into more granular macros * feat(wip)!: canyon macros getting even more granular * feat: Canyon big refactor finally completed * fix: broken intra-doc link * feat: redone the FromSql and FromSqlOwned trait bounds * chore(WIP)!: redone the querybuilder constructors to make them compile time const * chore: refactoring query querybuilder inits * chore: removed cfg-if * chore: fine-tuning the migrations feature for more granular control * fix: querybuilder operations was missing the correct db type for some operations * fix: correcting the dependency graph for canyon migrations - migrations feature * chore: cleanup --- .DS_Store | Bin 0 -> 8196 bytes .github/workflows/code-coverage.yml | 5 - .github/workflows/code-quality.yml | 21 +- .github/workflows/continuous-integration.yml | 8 +- .github/workflows/release.yml | 7 +- .gitignore | 1 - .vscode/settings.json | 11 + Cargo.toml | 58 +- README.md | 12 +- bash_aliases.sh | 7 +- .../src/canyon_database_connector.rs | 322 ------- canyon_connection/src/datasources.rs | 177 ---- canyon_connection/src/lib.rs | 123 --- {canyon_connection => canyon_core}/Cargo.toml | 21 +- canyon_core/src/canyon.rs | 240 +++++ canyon_core/src/column.rs | 55 ++ canyon_core/src/connection/clients/mod.rs | 6 + canyon_core/src/connection/clients/mssql.rs | 282 ++++++ canyon_core/src/connection/clients/mysql.rs | 221 +++++ .../src/connection/clients/postgresql.rs | 263 ++++++ canyon_core/src/connection/conn_errors.rs | 26 + canyon_core/src/connection/contracts/mod.rs | 118 +++ canyon_core/src/connection/database_type.rs | 60 ++ canyon_core/src/connection/datasources.rs | 218 +++++ canyon_core/src/connection/db_connector.rs | 64 ++ .../connection/impl_db_connection_macro.rs | 183 ++++ canyon_core/src/connection/mod.rs | 122 +++ canyon_core/src/lib.rs | 28 + canyon_core/src/mapper.rs | 45 + canyon_core/src/query/bounds.rs | 73 ++ canyon_core/src/query/mod.rs | 10 + canyon_core/src/query/operators.rs | 279 ++++++ canyon_core/src/query/parameters.rs | 630 +++++++++++++ canyon_core/src/query/query.rs | 83 ++ .../src/query/querybuilder/contracts/mod.rs | 221 +++++ canyon_core/src/query/querybuilder/mod.rs | 5 + .../query/querybuilder/syntax/ast/delete.rs | 17 + .../query/querybuilder/syntax/ast/insert.rs | 25 + .../src/query/querybuilder/syntax/ast/mod.rs | 52 ++ .../query/querybuilder/syntax/ast/select.rs | 53 ++ .../query/querybuilder/syntax/ast/update.rs | 28 + .../src/query/querybuilder/syntax/clause.rs | 91 ++ .../src/query/querybuilder/syntax/column.rs | 415 +++++++++ .../src/query/querybuilder/syntax/dialect.rs | 159 ++++ .../syntax/emitter/backends/mod.rs | 12 + .../syntax/emitter/backends/mssql.rs | 167 ++++ .../syntax/emitter/backends/mysql.rs | 38 + .../syntax/emitter/backends/pg.rs | 38 + .../query/querybuilder/syntax/emitter/mod.rs | 180 ++++ .../syntax/emitter/types/delete.rs | 162 ++++ .../syntax/emitter/types/helpers.rs | 351 +++++++ .../syntax/emitter/types/insert.rs | 204 ++++ .../querybuilder/syntax/emitter/types/mod.rs | 5 + .../syntax/emitter/types/select.rs | 305 ++++++ .../syntax/emitter/types/update.rs | 216 +++++ .../src/query/querybuilder/syntax/having.rs | 36 + .../src/query/querybuilder/syntax/join.rs | 123 +++ .../src/query/querybuilder/syntax/keyword.rs | 89 ++ .../src/query/querybuilder/syntax/mod.rs | 14 + .../src/query/querybuilder/syntax/order.rs | 35 + .../query/querybuilder/syntax/query_kind.rs | 19 + .../src/query/querybuilder/syntax/symbol.rs | 36 + .../querybuilder/syntax/table_metadata.rs | 129 +++ .../src/query/querybuilder/syntax/tokens.rs | 201 ++++ .../src/query/querybuilder/syntax/writer.rs | 371 ++++++++ .../src/query/querybuilder/types/delete.rs | 113 +++ .../src/query/querybuilder/types/insert.rs | 170 ++++ .../src/query/querybuilder/types/mod.rs | 387 ++++++++ .../src/query/querybuilder/types/select.rs | 259 ++++++ .../src/query/querybuilder/types/update.rs | 168 ++++ canyon_core/src/row.rs | 185 ++++ canyon_core/src/rows.rs | 215 +++++ canyon_core/src/transaction.rs | 108 +++ canyon_crud/Cargo.toml | 13 +- canyon_crud/src/bounds.rs | 875 ------------------ canyon_crud/src/crud.rs | 384 ++------ canyon_crud/src/entity.rs | 52 ++ canyon_crud/src/lib.rs | 12 +- canyon_crud/src/mapper.rs | 20 - canyon_crud/src/query_elements/mod.rs | 3 - canyon_crud/src/query_elements/operators.rs | 69 -- canyon_crud/src/query_elements/query.rs | 28 - .../src/query_elements/query_builder.rs | 687 -------------- canyon_crud/src/rows.rs | 88 -- canyon_entities/Cargo.toml | 3 +- canyon_entities/src/entity.rs | 52 +- canyon_entities/src/field_annotation.rs | 463 ++++++--- canyon_entities/src/helpers.rs | 88 ++ canyon_entities/src/lib.rs | 1 + canyon_entities/src/manager_builder.rs | 158 +++- canyon_macros/Cargo.toml | 32 +- canyon_macros/src/canyon_entity_macro.rs | 217 +++-- canyon_macros/src/canyon_macro.rs | 41 +- canyon_macros/src/canyon_mapper_macro.rs | 406 ++++++++ canyon_macros/src/canyon_tokio_test.rs | 36 + canyon_macros/src/foreignkeyable_macro.rs | 55 ++ canyon_macros/src/lib.rs | 714 +++----------- canyon_macros/src/query_operations/consts.rs | 77 ++ canyon_macros/src/query_operations/delete.rs | 117 --- .../src/query_operations/delete/entity.rs | 133 +++ .../src/query_operations/delete/method.rs | 139 +++ .../src/query_operations/delete/mod.rs | 52 ++ .../query_operations/delete/querybuilder.rs | 38 + .../src/query_operations/doc_comments.rs | 36 + canyon_macros/src/query_operations/insert.rs | 519 ----------- .../src/query_operations/insert/entity.rs | 147 +++ .../src/query_operations/insert/method.rs | 149 +++ .../src/query_operations/insert/mod.rs | 39 + canyon_macros/src/query_operations/mod.rs | 156 +++- .../src/query_operations/read/count.rs | 120 +++ .../src/query_operations/read/find_all.rs | 67 ++ .../read/find_by_primary_key.rs | 204 ++++ .../src/query_operations/read/foreign_key.rs | 481 ++++++++++ .../src/query_operations/read/mod.rs | 39 + .../read/select_querybuilder.rs | 16 + canyon_macros/src/query_operations/select.rs | 488 ---------- canyon_macros/src/query_operations/update.rs | 142 --- .../src/query_operations/update/entity.rs | 144 +++ .../src/query_operations/update/method.rs | 149 +++ .../src/query_operations/update/mod.rs | 48 + .../query_operations/update/querybuilder.rs | 37 + .../src/utils/canyon_crud_attribute.rs | 33 + canyon_macros/src/utils/function_parser.rs | 21 +- canyon_macros/src/utils/helpers.rs | 434 ++++++--- canyon_macros/src/utils/macro_tokens.rs | 260 +++--- canyon_macros/src/utils/mod.rs | 2 + .../src/utils/primary_key_attribute.rs | 34 + canyon_migrations/Cargo.toml | 15 +- canyon_migrations/src/constants.rs | 95 -- canyon_migrations/src/lib.rs | 29 +- canyon_migrations/src/migrations/handler.rs | 103 ++- .../src/migrations/information_schema.rs | 13 +- canyon_migrations/src/migrations/memory.rs | 91 +- canyon_migrations/src/migrations/processor.rs | 602 +++++++----- docker/docker-compose.yml | 9 +- octocat.png | Bin 2468 -> 0 bytes src/lib.rs | 48 +- tests/Cargo.toml | 3 +- tests/canyon_integration_tests.rs | 1 + tests/crud/delete_operations.rs | 33 +- tests/crud/foreign_key_operations.rs | 35 +- tests/crud/hex_arch_example.rs | 236 +++++ tests/crud/init_mssql.rs | 46 +- tests/crud/insert_operations.rs | 404 ++++---- tests/crud/mod.rs | 5 +- tests/crud/querybuilder_operations.rs | 597 ++++++++---- ...elect_operations.rs => read_operations.rs} | 58 +- tests/crud/update_operations.rs | 38 +- tests/migrations/mod.rs | 25 +- tests/simple_canyon.toml | 12 + tests/tests_models/league.rs | 3 +- tests/tests_models/player.rs | 6 +- 152 files changed, 14350 insertions(+), 6156 deletions(-) create mode 100644 .DS_Store create mode 100644 .vscode/settings.json delete mode 100644 canyon_connection/src/canyon_database_connector.rs delete mode 100644 canyon_connection/src/datasources.rs delete mode 100644 canyon_connection/src/lib.rs rename {canyon_connection => canyon_core}/Cargo.toml (78%) create mode 100644 canyon_core/src/canyon.rs create mode 100644 canyon_core/src/column.rs create mode 100644 canyon_core/src/connection/clients/mod.rs create mode 100644 canyon_core/src/connection/clients/mssql.rs create mode 100644 canyon_core/src/connection/clients/mysql.rs create mode 100644 canyon_core/src/connection/clients/postgresql.rs create mode 100644 canyon_core/src/connection/conn_errors.rs create mode 100644 canyon_core/src/connection/contracts/mod.rs create mode 100644 canyon_core/src/connection/database_type.rs create mode 100644 canyon_core/src/connection/datasources.rs create mode 100644 canyon_core/src/connection/db_connector.rs create mode 100644 canyon_core/src/connection/impl_db_connection_macro.rs create mode 100644 canyon_core/src/connection/mod.rs create mode 100644 canyon_core/src/lib.rs create mode 100644 canyon_core/src/mapper.rs create mode 100644 canyon_core/src/query/bounds.rs create mode 100644 canyon_core/src/query/mod.rs create mode 100644 canyon_core/src/query/operators.rs create mode 100644 canyon_core/src/query/parameters.rs create mode 100644 canyon_core/src/query/query.rs create mode 100644 canyon_core/src/query/querybuilder/contracts/mod.rs create mode 100644 canyon_core/src/query/querybuilder/mod.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/ast/delete.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/ast/insert.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/ast/mod.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/ast/select.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/ast/update.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/clause.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/column.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/dialect.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/emitter/backends/mod.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/emitter/backends/mssql.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/emitter/backends/mysql.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/emitter/backends/pg.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/emitter/mod.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/emitter/types/delete.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/emitter/types/helpers.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/emitter/types/insert.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/emitter/types/mod.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/emitter/types/select.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/emitter/types/update.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/having.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/join.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/keyword.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/mod.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/order.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/query_kind.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/symbol.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/table_metadata.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/tokens.rs create mode 100644 canyon_core/src/query/querybuilder/syntax/writer.rs create mode 100644 canyon_core/src/query/querybuilder/types/delete.rs create mode 100644 canyon_core/src/query/querybuilder/types/insert.rs create mode 100644 canyon_core/src/query/querybuilder/types/mod.rs create mode 100644 canyon_core/src/query/querybuilder/types/select.rs create mode 100644 canyon_core/src/query/querybuilder/types/update.rs create mode 100644 canyon_core/src/row.rs create mode 100644 canyon_core/src/rows.rs create mode 100644 canyon_core/src/transaction.rs delete mode 100644 canyon_crud/src/bounds.rs create mode 100644 canyon_crud/src/entity.rs delete mode 100644 canyon_crud/src/mapper.rs delete mode 100644 canyon_crud/src/query_elements/mod.rs delete mode 100644 canyon_crud/src/query_elements/operators.rs delete mode 100644 canyon_crud/src/query_elements/query.rs delete mode 100644 canyon_crud/src/query_elements/query_builder.rs delete mode 100644 canyon_crud/src/rows.rs create mode 100644 canyon_entities/src/helpers.rs create mode 100644 canyon_macros/src/canyon_mapper_macro.rs create mode 100644 canyon_macros/src/canyon_tokio_test.rs create mode 100644 canyon_macros/src/foreignkeyable_macro.rs create mode 100644 canyon_macros/src/query_operations/consts.rs delete mode 100644 canyon_macros/src/query_operations/delete.rs create mode 100644 canyon_macros/src/query_operations/delete/entity.rs create mode 100644 canyon_macros/src/query_operations/delete/method.rs create mode 100644 canyon_macros/src/query_operations/delete/mod.rs create mode 100644 canyon_macros/src/query_operations/delete/querybuilder.rs create mode 100644 canyon_macros/src/query_operations/doc_comments.rs delete mode 100644 canyon_macros/src/query_operations/insert.rs create mode 100644 canyon_macros/src/query_operations/insert/entity.rs create mode 100644 canyon_macros/src/query_operations/insert/method.rs create mode 100644 canyon_macros/src/query_operations/insert/mod.rs create mode 100644 canyon_macros/src/query_operations/read/count.rs create mode 100644 canyon_macros/src/query_operations/read/find_all.rs create mode 100644 canyon_macros/src/query_operations/read/find_by_primary_key.rs create mode 100644 canyon_macros/src/query_operations/read/foreign_key.rs create mode 100644 canyon_macros/src/query_operations/read/mod.rs create mode 100644 canyon_macros/src/query_operations/read/select_querybuilder.rs delete mode 100644 canyon_macros/src/query_operations/select.rs delete mode 100644 canyon_macros/src/query_operations/update.rs create mode 100644 canyon_macros/src/query_operations/update/entity.rs create mode 100644 canyon_macros/src/query_operations/update/method.rs create mode 100644 canyon_macros/src/query_operations/update/mod.rs create mode 100644 canyon_macros/src/query_operations/update/querybuilder.rs create mode 100644 canyon_macros/src/utils/canyon_crud_attribute.rs create mode 100644 canyon_macros/src/utils/primary_key_attribute.rs delete mode 100644 octocat.png create mode 100644 tests/crud/hex_arch_example.rs rename tests/crud/{select_operations.rs => read_operations.rs} (76%) create mode 100644 tests/simple_canyon.toml diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..303e71a49951a0f7f04457f60495b57cac8cfbf2 GIT binary patch literal 8196 zcmeHMzi-qq6#je^dZIc}wn{K$WkKr18mdDSi3N2B?nhKAdd;cV8(1KJ02>o49atD( zVTFnA44u0$F@X>>-?I&8CrvM15Ebu9_DkaT#ozm!i(?-Evo;;B0xJO8EaI1!SRG^1 zE~R2Ee?`ECcwmAYyW5+q8-u(Otrt`QRX`O`1yli5;9pRHbGBH!X5M$LTB`!8z<;TL zydM%4@u~NP^Y+!j$}R!u7kJqkkGT%8n8f?k`@(q(SdKJ~tE z-oarU9LD!-{0ha`-ib?WI85ri)~bLiP*p(I?u)pN0jAgu*YBfo5L}8F<=t+7JMS~1 zebBl4a(MONX7xU!eApmr{77~SqTZIsriUFo;cmhX9`KC23dr2Pf{$!X-?7J<$dKQw z&z%ojl{9zS1CuDtnb^El!D;WN_=5%F(*6#kbh{I{@4 z{yjS)@=V2JV#Z&Zp~8IjuAP6s@A7HTUzpeFkWqfd=63-@j>Q0vk^SCnbH9q^d(WRT zlc+u9e2B$w&kpSvlwxL2PF+mM, Gonzalo Busto Musi"] +edition = "2024" +authors = ["Alex Vergara, Gonzalo Busto Musi"] documentation = "https://zerodaycode.github.io/canyon-book/" homepage = "https://github.com/zerodaycode/Canyon-SQL" readme = "README.md" @@ -75,7 +72,32 @@ license = "MIT" description = "A Rust ORM and QueryBuilder" [features] -postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres", "canyon_migrations/postgres", "canyon_macros/postgres"] -mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql", "canyon_migrations/mssql", "canyon_macros/mssql"] -mysql = ["mysql_async", "mysql_common", "canyon_connection/mysql", "canyon_crud/mysql", "canyon_migrations/mysql", "canyon_macros/mysql"] -migrations = ["canyon_migrations", "canyon_macros/migrations"] +postgres = [ + "dep:tokio-postgres", + "canyon_core/postgres", + "canyon_crud/postgres", + "canyon_migrations?/postgres", + "canyon_macros/postgres", +] + +mssql = [ + "dep:tiberius", + "canyon_core/mssql", + "canyon_crud/mssql", + "canyon_migrations?/mssql", + "canyon_macros/mssql", +] + +mysql = [ + "dep:mysql_async", + "dep:mysql_common", + "canyon_core/mysql", + "canyon_crud/mysql", + "canyon_migrations?/mysql", + "canyon_macros/mysql", +] + +migrations = [ + "dep:canyon_migrations", + "canyon_macros/migrations", +] diff --git a/README.md b/README.md index 96e3b90b..4eded051 100755 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ assert_eq!( ); ``` -Note the leading reference on the `find_by_pk(...)` parameter. This associated function receives an `&dyn QueryParameter<'_>` as argument, not a value. +Note the leading reference on the `find_by_pk(...)` parameter. This associated function receives an `&dyn QueryParameter` as argument, not a value. ### :wrench: Building more complex queries @@ -96,8 +96,8 @@ let mut select_with_joins = LeagueTournament::select_query(); select_with_joins .inner_join("tournament", "league.id", "tournament.league_id") .left_join("team", "tournament.id", "player.tournament_id") - .r#where(LeagueFieldValue::id(&7), Comp::Gt) - .and(LeagueFieldValue::name(&"KOREA"), Comp::Eq) + .r#where(LeagueFieldValue::id(&7), Operator::Gt) + .and(LeagueFieldValue::name(&"KOREA"), Operator::Eq) .and_values_in(LeagueField::name, &["LCK", "STRANGER THINGS"]); // NOTE: We don't have in the docker the generated relationships // with the joins, so for now, we are just going to check that the @@ -170,10 +170,4 @@ Could not find openssl via pkg-config: `sudo apt install pkg-config` on *apt* based systems. For other systems, you must read your package manager docs and install it. -### failed to run custom build command for `libgssapi-sys vX.X.X` - -The problem is missing a *C* header `gssapi.h`. - -- Alpine: `apk --update add krb5-pkinit krb5-dev krb5` -- Ubuntu: `apt-get -y install gcc libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit` diff --git a/bash_aliases.sh b/bash_aliases.sh index 64b40415..3c3aeed9 100755 --- a/bash_aliases.sh +++ b/bash_aliases.sh @@ -14,6 +14,10 @@ alias DockerDown='docker-compose -f ./docker/docker-compose.yml down' # Cleans the generated cache folder for the postgres in the docker alias CleanPostgres='rm -rf ./docker/postgres-data' +# Code Quality +alias Clippy='cargo clippy --all-targets --all-features --workspace -- -D warnings' +alias Fmt='cargo fmt --all -- --check' + # Build the project for Windows targets alias BuildCanyonWin='cargo build --all-features --target=x86_64-pc-windows-msvc' alias BuildCanyonWinFull='cargo clean && cargo build --all-features --target=x86_64-pc-windows-msvc' @@ -37,10 +41,11 @@ alias IntegrationTestsLinux='cargo test --all-features --no-fail-fast -p tests - alias ITIncludeIgnoredLinux='cargo test --all-features --no-fail-fast -p tests --target=x86_64-unknown-linux-gnu -- --show-output --test-threads=1 --nocapture --test-threads=1 --include-ignored' alias SqlServerInitializationLinux='cargo test initialize_sql_server_docker_instance -p tests --all-features --no-fail-fast --target=x86_64-unknown-linux-gnu -- --show-output --test-threads=1 --nocapture --include-ignored' - +# ----- # Publish Canyon-SQL to the registry with its dependencies alias PublishCanyon='cargo publish -p canyon_connection && cargo publish -p canyon_crud && cargo publish -p canyon_migrations && cargo publish -p canyon_macros && cargo publish -p canyon_sql_root' +# ----- # Collects the code coverage for the project (tests must run before this) alias CcEnvVars='export CARGO_INCREMENTAL=0 export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs deleted file mode 100644 index 11530a7d..00000000 --- a/canyon_connection/src/canyon_database_connector.rs +++ /dev/null @@ -1,322 +0,0 @@ -use serde::Deserialize; - -#[cfg(feature = "mssql")] -use async_std::net::TcpStream; -#[cfg(feature = "mysql")] -use mysql_async::Pool; -#[cfg(feature = "mssql")] -use tiberius::{AuthMethod, Config}; -#[cfg(feature = "postgres")] -use tokio_postgres::{Client, NoTls}; - -use crate::datasources::{Auth, DatasourceConfig}; - -/// Represents the current supported databases by Canyon -#[derive(Deserialize, Debug, Eq, PartialEq, Clone, Copy)] -pub enum DatabaseType { - #[serde(alias = "postgres", alias = "postgresql")] - #[cfg(feature = "postgres")] - PostgreSql, - #[serde(alias = "sqlserver", alias = "mssql")] - #[cfg(feature = "mssql")] - SqlServer, - #[serde(alias = "mysql")] - #[cfg(feature = "mysql")] - MySQL, -} - -impl From<&Auth> for DatabaseType { - fn from(value: &Auth) -> Self { - match value { - #[cfg(feature = "postgres")] - crate::datasources::Auth::Postgres(_) => DatabaseType::PostgreSql, - #[cfg(feature = "mssql")] - crate::datasources::Auth::SqlServer(_) => DatabaseType::SqlServer, - #[cfg(feature = "mysql")] - crate::datasources::Auth::MySQL(_) => DatabaseType::MySQL, - } - } -} - -/// A connection with a `PostgreSQL` database -#[cfg(feature = "postgres")] -pub struct PostgreSqlConnection { - pub client: Client, - // pub connection: Connection, // TODO Hold it, or not to hold it... that's the question! -} - -/// A connection with a `SqlServer` database -#[cfg(feature = "mssql")] -pub struct SqlServerConnection { - pub client: &'static mut tiberius::Client, -} - -/// A connection with a `Mysql` database -#[cfg(feature = "mysql")] -pub struct MysqlConnection { - pub client: Pool, -} - -/// The Canyon database connection handler. When the client's program -/// starts, Canyon gets the information about the desired datasources, -/// process them and generates a pool of 1 to 1 database connection for -/// every datasource defined. -pub enum DatabaseConnection { - #[cfg(feature = "postgres")] - Postgres(PostgreSqlConnection), - #[cfg(feature = "mssql")] - SqlServer(SqlServerConnection), - #[cfg(feature = "mysql")] - MySQL(MysqlConnection), -} - -unsafe impl Send for DatabaseConnection {} -unsafe impl Sync for DatabaseConnection {} - -impl DatabaseConnection { - pub async fn new( - datasource: &DatasourceConfig, - ) -> Result> { - match datasource.get_db_type() { - #[cfg(feature = "postgres")] - DatabaseType::PostgreSql => { - let (username, password) = match &datasource.auth { - crate::datasources::Auth::Postgres(postgres_auth) => match postgres_auth { - crate::datasources::PostgresAuth::Basic { username, password } => { - (username.as_str(), password.as_str()) - } - }, - #[cfg(feature = "mssql")] - crate::datasources::Auth::SqlServer(_) => { - panic!("Found SqlServer auth configuration for a PostgreSQL datasource") - } - #[cfg(feature = "mysql")] - crate::datasources::Auth::MySQL(_) => { - panic!("Found MySql auth configuration for a PostgreSQL datasource") - } - }; - let (new_client, new_connection) = tokio_postgres::connect( - &format!( - "postgres://{user}:{pswd}@{host}:{port}/{db}", - user = username, - pswd = password, - host = datasource.properties.host, - port = datasource.properties.port.unwrap_or_default(), - db = datasource.properties.db_name - )[..], - NoTls, - ) - .await?; - - tokio::spawn(async move { - if let Err(e) = new_connection.await { - eprintln!("An error occurred while trying to connect to the PostgreSQL database: {e}"); - } - }); - - Ok(DatabaseConnection::Postgres(PostgreSqlConnection { - client: new_client, - // connection: new_connection, - })) - } - #[cfg(feature = "mssql")] - DatabaseType::SqlServer => { - let mut config = Config::new(); - - config.host(&datasource.properties.host); - config.port(datasource.properties.port.unwrap_or_default()); - config.database(&datasource.properties.db_name); - - // Using SQL Server authentication. - config.authentication(match &datasource.auth { - #[cfg(feature = "postgres")] - crate::datasources::Auth::Postgres(_) => { - panic!("Found PostgreSQL auth configuration for a SqlServer database") - } - crate::datasources::Auth::SqlServer(sql_server_auth) => match sql_server_auth { - crate::datasources::SqlServerAuth::Basic { username, password } => { - AuthMethod::sql_server(username, password) - } - crate::datasources::SqlServerAuth::Integrated => AuthMethod::Integrated, - }, - #[cfg(feature = "mysql")] - crate::datasources::Auth::MySQL(_) => { - panic!("Found PostgreSQL auth configuration for a SqlServer database") - } - }); - - // on production, it is not a good idea to do this. We should upgrade - // Canyon in future versions to allow the user take care about this - // configuration - config.trust_cert(); - - // Taking the address from the configuration, using async-std's - // TcpStream to connect to the server. - let tcp = TcpStream::connect(config.get_addr()) - .await - .expect("Error instantiating the SqlServer TCP Stream"); - - // We'll disable the Nagle algorithm. Buffering is handled - // internally with a `Sink`. - tcp.set_nodelay(true) - .expect("Error in the SqlServer `nodelay` config"); - - // Handling TLS, login and other details related to the SQL Server. - let client = tiberius::Client::connect(config, tcp).await; - - Ok(DatabaseConnection::SqlServer(SqlServerConnection { - client: Box::leak(Box::new( - client.expect("A failure happened connecting to the database"), - )), - })) - } - #[cfg(feature = "mysql")] - DatabaseType::MySQL => { - let (user, password) = match &datasource.auth { - #[cfg(feature = "mssql")] - crate::datasources::Auth::SqlServer(_) => { - panic!("Found SqlServer auth configuration for a PostgreSQL datasource") - } - #[cfg(feature = "postgres")] - crate::datasources::Auth::Postgres(_) => { - panic!("Found MySql auth configuration for a PostgreSQL datasource") - } - #[cfg(feature = "mysql")] - crate::datasources::Auth::MySQL(mysql_auth) => match mysql_auth { - crate::datasources::MySQLAuth::Basic { username, password } => { - (username, password) - } - }, - }; - - //TODO add options to optionals params in url - - let url = format!( - "mysql://{}:{}@{}:{}/{}", - user, - password, - datasource.properties.host, - datasource.properties.port.unwrap_or_default(), - datasource.properties.db_name - ); - let mysql_connection = Pool::from_url(url)?; - - Ok(DatabaseConnection::MySQL(MysqlConnection { - client: { mysql_connection }, - })) - } - } - } - - #[cfg(feature = "postgres")] - pub fn postgres_connection(&self) -> &PostgreSqlConnection { - match self { - DatabaseConnection::Postgres(conn) => conn, - #[cfg(all(feature = "postgres", feature = "mssql", feature = "mysql"))] - _ => panic!(), - } - } - - #[cfg(feature = "mssql")] - pub fn sqlserver_connection(&mut self) -> &mut SqlServerConnection { - match self { - DatabaseConnection::SqlServer(conn) => conn, - #[cfg(all(feature = "postgres", feature = "mssql", feature = "mysql"))] - _ => panic!(), - } - } - - #[cfg(feature = "mysql")] - pub fn mysql_connection(&self) -> &MysqlConnection { - match self { - DatabaseConnection::MySQL(conn) => conn, - #[cfg(all(feature = "postgres", feature = "mssql", feature = "mysql"))] - _ => panic!(), - } - } -} - -#[cfg(test)] -mod database_connection_handler { - use super::*; - use crate::CanyonSqlConfig; - - /// Tests the behaviour of the `DatabaseType::from_datasource(...)` - #[test] - fn check_from_datasource() { - #[cfg(all(feature = "postgres", feature = "mssql", feature = "mysql"))] - { - const CONFIG_FILE_MOCK_ALT_ALL: &str = r#" - [canyon_sql] - datasources = [ - {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, - {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' }, - {name = 'MysqlDS', auth = { mysql = { basic = { username = "root", password = "root" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } - ] - "#; - let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_ALL) - .expect("A failure happened retrieving the [canyon_sql] section"); - assert_eq!( - config.canyon_sql.datasources[0].get_db_type(), - DatabaseType::PostgreSql - ); - assert_eq!( - config.canyon_sql.datasources[1].get_db_type(), - DatabaseType::SqlServer - ); - assert_eq!( - config.canyon_sql.datasources[2].get_db_type(), - DatabaseType::MySQL - ); - } - - #[cfg(feature = "postgres")] - { - const CONFIG_FILE_MOCK_ALT_PG: &str = r#" - [canyon_sql] - datasources = [ - {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, - ] - "#; - let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_PG) - .expect("A failure happened retrieving the [canyon_sql] section"); - assert_eq!( - config.canyon_sql.datasources[0].get_db_type(), - DatabaseType::PostgreSql - ); - } - - #[cfg(feature = "mssql")] - { - const CONFIG_FILE_MOCK_ALT_MSSQL: &str = r#" - [canyon_sql] - datasources = [ - {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } - ] - "#; - let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_MSSQL) - .expect("A failure happened retrieving the [canyon_sql] section"); - assert_eq!( - config.canyon_sql.datasources[0].get_db_type(), - DatabaseType::SqlServer - ); - } - - #[cfg(feature = "mysql")] - { - const CONFIG_FILE_MOCK_ALT_MYSQL: &str = r#" - [canyon_sql] - datasources = [ - {name = 'MysqlDS', auth = { mysql = { basic = { username = "root", password = "root" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } - ] - "#; - - let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_MYSQL) - .expect("A failure happened retrieving the [canyon_sql] section"); - assert_eq!( - config.canyon_sql.datasources[0].get_db_type(), - DatabaseType::MySQL - ); - } - } -} diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs deleted file mode 100644 index 11edcd31..00000000 --- a/canyon_connection/src/datasources.rs +++ /dev/null @@ -1,177 +0,0 @@ -use serde::Deserialize; - -use crate::canyon_database_connector::DatabaseType; - -/// ``` -#[test] -fn load_ds_config_from_array() { - #[cfg(feature = "postgres")] - { - const CONFIG_FILE_MOCK_ALT_PG: &str = r#" - [canyon_sql] - datasources = [ - {name = 'PostgresDS', auth = { postgresql = { basic = { username = "postgres", password = "postgres" } } }, properties.host = 'localhost', properties.db_name = 'triforce', properties.migrations='enabled' }, - ] - "#; - let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_PG) - .expect("A failure happened retrieving the [canyon_sql] section"); - - let ds_0 = &config.canyon_sql.datasources[0]; - - assert_eq!(ds_0.name, "PostgresDS"); - assert_eq!(ds_0.get_db_type(), DatabaseType::PostgreSql); - assert_eq!( - ds_0.auth, - Auth::Postgres(PostgresAuth::Basic { - username: "postgres".to_string(), - password: "postgres".to_string() - }) - ); - assert_eq!(ds_0.properties.host, "localhost"); - assert_eq!(ds_0.properties.port, None); - assert_eq!(ds_0.properties.db_name, "triforce"); - assert_eq!(ds_0.properties.migrations, Some(Migrations::Enabled)); - } - - #[cfg(feature = "mssql")] - { - const CONFIG_FILE_MOCK_ALT_MSSQL: &str = r#" - [canyon_sql] - datasources = [ - {name = 'SqlServerDS', auth = { sqlserver = { basic = { username = "sa", password = "SqlServer-10" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' }, - {name = 'SqlServerDS', auth = { sqlserver = { integrated = {} } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } - ] - "#; - let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_MSSQL) - .expect("A failure happened retrieving the [canyon_sql] section"); - - let ds_1 = &config.canyon_sql.datasources[0]; - let ds_2 = &config.canyon_sql.datasources[1]; - - assert_eq!(ds_1.name, "SqlServerDS"); - assert_eq!(ds_1.get_db_type(), DatabaseType::SqlServer); - assert_eq!( - ds_1.auth, - Auth::SqlServer(SqlServerAuth::Basic { - username: "sa".to_string(), - password: "SqlServer-10".to_string() - }) - ); - assert_eq!(ds_1.properties.host, "192.168.0.250.1"); - assert_eq!(ds_1.properties.port, Some(3340)); - assert_eq!(ds_1.properties.db_name, "triforce2"); - assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); - - assert_eq!(ds_2.auth, Auth::SqlServer(SqlServerAuth::Integrated)); - } - #[cfg(feature = "mysql")] - { - const CONFIG_FILE_MOCK_ALT_MYSQL: &str = r#" - [canyon_sql] - datasources = [ - {name = 'MysqlDS', auth = { mysql = { basic = { username = "root", password = "root" } } }, properties.host = '192.168.0.250.1', properties.port = 3340, properties.db_name = 'triforce2', properties.migrations='disabled' } - ] - "#; - let config: CanyonSqlConfig = toml::from_str(CONFIG_FILE_MOCK_ALT_MYSQL) - .expect("A failure happened retrieving the [canyon_sql] section"); - - let ds_1 = &config.canyon_sql.datasources[0]; - - assert_eq!(ds_1.name, "MysqlDS"); - assert_eq!(ds_1.get_db_type(), DatabaseType::MySQL); - assert_eq!( - ds_1.auth, - Auth::MySQL(MySQLAuth::Basic { - username: "root".to_string(), - password: "root".to_string() - }) - ); - assert_eq!(ds_1.properties.host, "192.168.0.250.1"); - assert_eq!(ds_1.properties.port, Some(3340)); - assert_eq!(ds_1.properties.db_name, "triforce2"); - assert_eq!(ds_1.properties.migrations, Some(Migrations::Disabled)); - } -} - -#[derive(Deserialize, Debug, Clone)] -pub struct CanyonSqlConfig { - pub canyon_sql: Datasources, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct Datasources { - pub datasources: Vec, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct DatasourceConfig { - pub name: String, - pub auth: Auth, - pub properties: DatasourceProperties, -} - -impl DatasourceConfig { - pub fn get_db_type(&self) -> DatabaseType { - match self.auth { - #[cfg(feature = "postgres")] - Auth::Postgres(_) => DatabaseType::PostgreSql, - #[cfg(feature = "mssql")] - Auth::SqlServer(_) => DatabaseType::SqlServer, - #[cfg(feature = "mysql")] - Auth::MySQL(_) => DatabaseType::MySQL, - } - } -} - -#[derive(Deserialize, Debug, Clone, PartialEq)] -pub enum Auth { - #[serde(alias = "PostgresSQL", alias = "postgresql", alias = "postgres")] - #[cfg(feature = "postgres")] - Postgres(PostgresAuth), - #[serde(alias = "SqlServer", alias = "sqlserver", alias = "mssql")] - #[cfg(feature = "mssql")] - SqlServer(SqlServerAuth), - #[serde(alias = "MYSQL", alias = "mysql", alias = "MySQL")] - #[cfg(feature = "mysql")] - MySQL(MySQLAuth), -} - -#[derive(Deserialize, Debug, Clone, PartialEq)] -#[cfg(feature = "postgres")] -pub enum PostgresAuth { - #[serde(alias = "Basic", alias = "basic")] - Basic { username: String, password: String }, -} - -#[derive(Deserialize, Debug, Clone, PartialEq)] -#[cfg(feature = "mssql")] -pub enum SqlServerAuth { - #[serde(alias = "Basic", alias = "basic")] - Basic { username: String, password: String }, - #[serde(alias = "Integrated", alias = "integrated")] - Integrated, -} - -#[derive(Deserialize, Debug, Clone, PartialEq)] -#[cfg(feature = "mysql")] -pub enum MySQLAuth { - #[serde(alias = "Basic", alias = "basic")] - Basic { username: String, password: String }, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct DatasourceProperties { - pub host: String, - pub port: Option, - pub db_name: String, - pub migrations: Option, -} - -/// Represents the enabled or disabled migrations for a whole datasource -#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] -pub enum Migrations { - #[serde(alias = "Enabled", alias = "enabled")] - Enabled, - #[serde(alias = "Disabled", alias = "disabled")] - Disabled, -} diff --git a/canyon_connection/src/lib.rs b/canyon_connection/src/lib.rs deleted file mode 100644 index 5bd7a232..00000000 --- a/canyon_connection/src/lib.rs +++ /dev/null @@ -1,123 +0,0 @@ -#[cfg(feature = "mssql")] -pub extern crate async_std; -pub extern crate futures; -pub extern crate lazy_static; -#[cfg(feature = "mysql")] -pub extern crate mysql_async; -#[cfg(feature = "mssql")] -pub extern crate tiberius; -pub extern crate tokio; -#[cfg(feature = "postgres")] -pub extern crate tokio_postgres; -pub extern crate tokio_util; - -pub mod canyon_database_connector; -pub mod datasources; - -use std::fs; -use std::path::PathBuf; - -use crate::datasources::{CanyonSqlConfig, DatasourceConfig}; -use canyon_database_connector::DatabaseConnection; -use indexmap::IndexMap; -use lazy_static::lazy_static; -use tokio::sync::{Mutex, MutexGuard}; -use walkdir::WalkDir; - -lazy_static! { - pub static ref CANYON_TOKIO_RUNTIME: tokio::runtime::Runtime = - tokio::runtime::Runtime::new() // TODO Make the config with the builder - .expect("Failed initializing the Canyon-SQL Tokio Runtime"); - - static ref RAW_CONFIG_FILE: String = fs::read_to_string(find_canyon_config_file()) - .expect("Error opening or reading the Canyon configuration file"); - static ref CONFIG_FILE: CanyonSqlConfig = toml::from_str(RAW_CONFIG_FILE.as_str()) - .expect("Error generating the configuration for Canyon-SQL"); - - pub static ref DATASOURCES: Vec = - CONFIG_FILE.canyon_sql.datasources.clone(); - - pub static ref CACHED_DATABASE_CONN: Mutex> = - Mutex::new(IndexMap::new()); -} - -fn find_canyon_config_file() -> PathBuf { - for e in WalkDir::new(".") - .max_depth(2) - .into_iter() - .filter_map(|e| e.ok()) - { - let filename = e.file_name().to_str().unwrap(); - if e.metadata().unwrap().is_file() - && filename.starts_with("canyon") - && filename.ends_with(".toml") - { - return e.path().to_path_buf(); - } - } - - panic!() -} - -/// Convenient free function to initialize a kind of connection pool based on the datasources present defined -/// in the configuration file. -/// -/// This avoids Canyon to create a new connection to the database on every query, potentially avoiding bottlenecks -/// coming from the instantiation of that new conn every time. -/// -/// Note: We noticed with the integration tests that the [`tokio_postgres`] crate (PostgreSQL) is able to work in an async environment -/// with a new connection per query without no problem, but the [`tiberius`] crate (MSSQL) suffers a lot when it has continuous -/// statements with multiple queries, like and insert followed by a find by id to check if the insert query has done its -/// job done. -pub async fn init_connections_cache() { - for datasource in DATASOURCES.iter() { - CACHED_DATABASE_CONN.lock().await.insert( - &datasource.name, - DatabaseConnection::new(datasource) - .await - .unwrap_or_else(|_| { - panic!( - "Error pooling a new connection for the datasource: {:?}", - datasource.name - ) - }), - ); - } -} - -pub fn get_database_connection<'a>( - datasource_name: &str, - guarded_cache: &'a mut MutexGuard>, -) -> &'a mut DatabaseConnection { - if datasource_name.is_empty() { - guarded_cache - .get_mut( - DATASOURCES - .first() - .expect("We didn't found any valid datasource configuration. Check your `canyon.toml` file") - .name - .as_str() - ).unwrap_or_else(|| panic!("No default datasource found. Check your `canyon.toml` file")) - } else { - guarded_cache.get_mut(datasource_name) - .unwrap_or_else(|| - panic!("Canyon couldn't find a datasource in the pool with the argument provided: {datasource_name}") - ) - } -} - -pub fn get_database_config<'a>( - datasource_name: &str, - datasources_config: &'a [DatasourceConfig], -) -> &'a DatasourceConfig { - if datasource_name.is_empty() { - datasources_config - .first() - .unwrap_or_else(|| panic!("Not exist datasource")) - } else { - datasources_config - .iter() - .find(|dc| dc.name == datasource_name) - .unwrap_or_else(|| panic!("Not found datasource expected {datasource_name}")) - } -} diff --git a/canyon_connection/Cargo.toml b/canyon_core/Cargo.toml similarity index 78% rename from canyon_connection/Cargo.toml rename to canyon_core/Cargo.toml index fac88ef5..2385c703 100644 --- a/canyon_connection/Cargo.toml +++ b/canyon_core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "canyon_connection" +name = "canyon_core" version.workspace = true edition.workspace = true authors.workspace = true @@ -10,27 +10,26 @@ license.workspace = true description.workspace = true [dependencies] -tokio = { workspace = true } -tokio-util = { workspace = true } - tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } mysql_async = { workspace = true, optional = true } mysql_common = { workspace = true, optional = true } +chrono = { workspace = true } +async-std = { workspace = true, optional = true } + +tokio = { workspace = true, features = ["sync"] } +tokio-util = { workspace = true } futures = { workspace = true } -indexmap = { workspace = true } -lazy_static = { workspace = true } toml = { workspace = true } serde = { workspace = true } -async-std = { workspace = true, optional = true } walkdir = { workspace = true } - +bb8-postgres = "0.9.0" +bb8-tiberius = "0.16.0" +bb8 = "0.9.1" [features] postgres = ["tokio-postgres"] mssql = ["tiberius", "async-std"] -mysql = ["mysql_async","mysql_common"] - - +mysql = ["mysql_async", "mysql_common"] diff --git a/canyon_core/src/canyon.rs b/canyon_core/src/canyon.rs new file mode 100644 index 00000000..54b9b5b0 --- /dev/null +++ b/canyon_core/src/canyon.rs @@ -0,0 +1,240 @@ +use crate::connection::conn_errors::DatasourceNotFound; +use crate::connection::database_type::DatabaseType; +use crate::connection::datasources::{CanyonSqlConfig, DatasourceConfig, Datasources}; +use crate::connection::{CANYON_INSTANCE, db_connector, get_canyon_tokio_runtime}; +use db_connector::DatabaseConnector; +use std::collections::HashMap; +use std::{error::Error, fs}; + +/// The `Canyon` struct provides the main entry point for interacting with the Canyon-SQL context. +/// +/// This struct is responsible for managing database connections, configuration, and datasources. +/// It acts as a singleton, ensuring that only one instance of the Canyon context exists throughout +/// the application lifecycle. The `Canyon` struct provides methods for initializing the context, +/// accessing datasources, and retrieving database connections. +/// +/// # Features +/// - Singleton access to the Canyon context. +/// - Automatic discovery and loading of configuration files. +/// - Management of multiple database connections. +/// - Support for retrieving connections by name or default. +/// +/// # Examples +/// ```ignore +/// #[tokio::main] +/// async fn main() -> Result<(), Box> { +/// // Initialize the Canyon context +/// let canyon = Canyon::init().await?; +/// +/// // Access datasources +/// let datasources = canyon.datasources(); +/// for ds in datasources { +/// println!("Datasource: {}", ds.name); +/// } +/// +/// // Retrieve a connection by name +/// let connection = canyon.get_connection("MyDatasource").await?; +/// // Use the connection... +/// +/// Ok(()) +/// } +/// ``` +/// +/// # Methods +/// - `init`: Initializes the Canyon context by loading configuration and setting up connections. +/// - `instance`: Provides singleton access to the Canyon context. +/// - `datasources`: Returns a list of configured datasources. +/// - `find_datasource_by_name_or_default`: Finds a datasource by name or returns the default. +/// - `get_connection`: Retrieves a read-only connection from the cache. +/// - `get_mut_connection`: Retrieves a mutable connection from the cache. +pub struct Canyon { + config: Datasources, + connections: HashMap<&'static str, DatabaseConnector>, + default_connection: Option, + default_db_type: Option, +} + +impl Canyon { + /// Returns the global singleton instance of `Canyon`. + /// + /// This function allows access to the singleton instance of the Canyon engine + /// after it has been initialized through [`Canyon::init`]. It returns a shared, + /// read-only reference to the internal `Canyon` state. + /// + /// # Errors + /// + /// Returns an error if the `Canyon` instance has not yet been initialized. + /// In that case, the user must call [`Canyon::init`] before accessing the singleton. + pub fn instance() -> Result<&'static Self, Box> { + Ok(CANYON_INSTANCE.get().ok_or_else(|| { + // TODO: just call Canyon::init()? Why should we raise this error? + // I guess that there's no point in making it fail for the user to manually start Canyon when we can handle everything + // internally + Box::new(std::io::Error::other( + "Canyon not initialized. Call `Canyon::init()` first.", + )) + })?) + } + + /// Initializes the global `Canyon` instance from a configuration file. + /// + /// Loads the `Datasources` configuration from the expected `canyon.toml` file (or another + /// discoverable location), establishes one or more database connections, and sets up the default + /// connection and database type. + /// + /// This function is idempotent: calling it multiple times will reuse the already-initialized instance. + /// + /// # Errors + /// + /// - If the configuration file is missing or malformed. + /// - If deserialization into `CanyonSqlConfig` fails. + /// - If any configured datasource fails to initialize. + /// + /// # Example + /// + /// ```ignore + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let canyon = Canyon::init().await?; + /// Ok(()) + /// } + /// ``` + pub async fn init() -> Result<&'static Self, Box> { + if CANYON_INSTANCE.get().is_some() { + return Canyon::instance(); // Already initialized, no need to do it again + } + + let path = __impl::find_config_path()?; + let config_content = fs::read_to_string(&path)?; + let config: Datasources = toml::from_str::(&config_content)?.canyon_sql; + + let mut connections: HashMap<&str, DatabaseConnector> = HashMap::new(); + let mut default_connection: Option = None; + let mut default_db_type: Option = None; + + for ds in config.datasources.iter() { + __impl::process_new_conn_by_datasource( + ds, + &mut connections, + &mut default_connection, + &mut default_db_type, + ) + .await?; + } + + let canyon = Canyon { + config, + connections, + default_connection, + default_db_type, + }; + + get_canyon_tokio_runtime(); // Just ensuring that is initialized in manual-mode + Ok(CANYON_INSTANCE.get_or_init(|| canyon)) + } + + #[inline(always)] + pub fn datasources(&self) -> &[DatasourceConfig] { + &self.config.datasources + } + + // Retrieve a datasource by name or returns the first one declared in the configuration file + // or added by the user via the builder interface as the default one (if exists at least one) + pub fn find_datasource_by_name_or_default( + &self, + name: &str, + ) -> Result<&DatasourceConfig, DatasourceNotFound> { + if name.is_empty() { + self.datasources() + .first() + .ok_or_else(|| DatasourceNotFound::from(None)) + } else { + self.datasources() + .iter() + .find(|ds| ds.name == name) + .ok_or_else(|| DatasourceNotFound::from(Some(name))) + } + } + + pub fn get_default_db_type(&self) -> Result { + self.default_db_type + .ok_or_else(|| DatasourceNotFound::from(None)) + } + + // Retrieves a connector to the configured connection as the default connection by the user + // (the first defined in the configuration file) + pub fn get_default_connection(&self) -> Result<&DatabaseConnector, DatasourceNotFound> { + self.default_connection + .as_ref() + .ok_or_else(|| DatasourceNotFound::from(None)) + } + + // Retrieve a read-only connection from the cache + pub fn get_connection(&self, name: &str) -> Result<&DatabaseConnector, DatasourceNotFound> { + if name.is_empty() { + return self.get_default_connection(); + } + + let conn = self + .connections + .get(name) + .ok_or_else(|| DatasourceNotFound::from(Some(name)))?; + + Ok(conn) + } +} + +mod __impl { + use crate::connection::database_type::DatabaseType; + use crate::connection::datasources::DatasourceConfig; + use crate::connection::db_connector::DatabaseConnector; + use std::collections::HashMap; + use std::error::Error; + use std::path::PathBuf; + use walkdir::WalkDir; + + // Internal helper to locate the config file + pub(crate) fn find_config_path() -> Result { + WalkDir::new(".") + .max_depth(2) + .into_iter() + .filter_map(Result::ok) + .find_map(|e| { + let filename = e.file_name().to_string_lossy().to_lowercase(); + if e.metadata().ok()?.is_file() + && filename.starts_with("canyon") + && filename.ends_with(".toml") + { + Some(e.path().to_path_buf()) + } else { + None + } + }) + .ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotFound, "No Canyon config found") + }) + } + + pub(crate) async fn process_new_conn_by_datasource( + ds: &DatasourceConfig, + connections: &mut HashMap<&str, DatabaseConnector>, + default: &mut Option, + default_db_type: &mut Option, + ) -> Result<(), Box> { + if default.is_none() { + let cloned_ds_for_default = ds.clone(); + *default = Some(DatabaseConnector::new(&cloned_ds_for_default).await?); // Only cloning the smart pointer + } + let conn = DatabaseConnector::new(ds).await?; + let name: &'static str = Box::leak(ds.name.clone().into_boxed_str()); + + if default_db_type.is_none() { + *default_db_type = Some(conn.get_db_type()); + } + + let connection_sp = conn; + connections.insert(name, connection_sp); + + Ok(()) + } +} diff --git a/canyon_core/src/column.rs b/canyon_core/src/column.rs new file mode 100644 index 00000000..2d5cd6d6 --- /dev/null +++ b/canyon_core/src/column.rs @@ -0,0 +1,55 @@ +use std::{any::Any, borrow::Cow}; + +#[cfg(feature = "mysql")] +use mysql_async::{self}; +#[cfg(feature = "mssql")] +use tiberius::{self}; +#[cfg(feature = "postgres")] +use tokio_postgres::{self}; + +/// Generic abstraction for hold a Column type that will be one of the Column +/// types present in the dependent crates +pub struct Column<'a> { + pub(crate) name: Cow<'a, str>, + pub(crate) type_: ColumnType, +} +impl Column<'_> { + pub fn name(&self) -> &str { + &self.name + } + pub fn column_type(&self) -> &ColumnType { + &self.type_ + } +} + +pub trait ColType { + fn as_any(&self) -> &dyn Any; +} +#[cfg(feature = "postgres")] +impl ColType for tokio_postgres::types::Type { + fn as_any(&self) -> &dyn Any { + self + } +} +#[cfg(feature = "mssql")] +impl ColType for tiberius::ColumnType { + fn as_any(&self) -> &dyn Any { + self + } +} +#[cfg(feature = "mysql")] +impl ColType for mysql_async::consts::ColumnType { + fn as_any(&self) -> &dyn Any { + self + } +} + +/// Wrapper over the dependencies Column's types +pub enum ColumnType { + #[cfg(feature = "postgres")] + Postgres(tokio_postgres::types::Type), + #[cfg(feature = "mssql")] + SqlServer(tiberius::ColumnType), + #[cfg(feature = "mysql")] + MySQL(mysql_async::consts::ColumnType), +} diff --git a/canyon_core/src/connection/clients/mod.rs b/canyon_core/src/connection/clients/mod.rs new file mode 100644 index 00000000..366249d5 --- /dev/null +++ b/canyon_core/src/connection/clients/mod.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "mssql")] +pub mod mssql; +#[cfg(feature = "mysql")] +pub mod mysql; +#[cfg(feature = "postgres")] +pub mod postgresql; diff --git a/canyon_core/src/connection/clients/mssql.rs b/canyon_core/src/connection/clients/mssql.rs new file mode 100644 index 00000000..184ca43b --- /dev/null +++ b/canyon_core/src/connection/clients/mssql.rs @@ -0,0 +1,282 @@ +use crate::connection::clients::mssql::sqlserver_query_launcher::execute_query; +use crate::connection::contracts::DbConnection; +use crate::connection::database_type::DatabaseType; +use crate::connection::datasources::DatasourceConfig; +use crate::mapper::RowMapper; +use crate::query::parameters::QueryParameter; +use crate::rows::{CanyonRows, FromSqlOwnedValue}; +use bb8::PooledConnection; +use bb8_tiberius::ConnectionManager as TiberiusConnectionManager; +use std::error::Error; +use std::sync::Arc; +use tiberius::Query; + +type SqlServerConnectionPool = Arc>; + +/// A connector for a `SqlServer` database +pub struct SqlServerConnector(SqlServerConnectionPool); + +impl SqlServerConnector { + pub async fn new(config: &DatasourceConfig) -> Result> { + Ok(Self(__impl::create_sqlserver_connector(config).await?)) + } + pub async fn get_pooled( + &self, + ) -> Result, Box> { + Ok(self.0.get().await?) + } +} + +impl DbConnection for SqlServerConnector { + async fn query_rows( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + let mut conn = self.get_pooled().await?; + let result = execute_query(stmt, params, &mut conn) + .await? + .into_results() + .await? + .into_iter() + .flatten() + .collect(); + + Ok(CanyonRows::Tiberius(result)) + } + + async fn query( + &self, + stmt: S, + params: &[&'_ dyn QueryParameter], + ) -> Result, Box> + where + S: AsRef + Send, + R: RowMapper, + Vec: FromIterator<::Output>, + { + let mut conn = self.get_pooled().await?; + Ok(execute_query(stmt.as_ref(), params, &mut conn) + .await? + .into_results() + .await? + .into_iter() + .flatten() + .flat_map(|row| R::deserialize_sqlserver(&row)) + .collect::>()) + } + + async fn query_one( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result, Box> + where + R: RowMapper, + { + let mut conn = self.get_pooled().await?; + + let result = execute_query(stmt, params, &mut conn) + .await? + .into_row() + .await?; + + match result { + Some(r) => Ok(Some(R::deserialize_sqlserver(&r)?)), + None => Ok(None), + } + } + + async fn query_one_for( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + let mut conn = self.get_pooled().await?; + let row = crate::connection::clients::mssql::sqlserver_query_launcher::execute_query( + stmt, params, &mut conn, + ) + .await? + .into_row() + .await? + .ok_or_else(|| { + format!( + "Failure executing 'query_one_for' while retrieving the first row with stmt: {:?}", + stmt + ) + })?; + + Ok(row + .into_iter() + .map(T::from_sql_owned) + .collect::>() + .remove(0)? + .ok_or_else(|| format!("Failure executing 'query_one_for' while retrieving the first column value on the first row with stmt: {:?}", stmt))? + ) + } + + async fn execute( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + let mssql_query = crate::connection::clients::mssql::sqlserver_query_launcher::generate_mssql_query_client(stmt, params).await; + let mut conn = self.get_pooled().await?; + + mssql_query + .execute(&mut conn) + .await + .map(|r| r.total()) + .map_err(From::from) + } + + fn get_database_type(&self) -> Result> { + Ok(DatabaseType::SqlServer) + } +} + +pub(crate) mod sqlserver_query_launcher { + use super::*; + use tiberius::QueryStream; + + pub(crate) async fn execute_query<'a>( + stmt: &str, + params: &[&dyn QueryParameter], + conn: &'a mut bb8::PooledConnection<'_, bb8_tiberius::ConnectionManager>, + ) -> Result, Box> { + let mssql_query = generate_mssql_query_client(stmt, params).await; + mssql_query.query(conn).await.map_err(From::from) + } + + pub(crate) async fn generate_mssql_query_client<'a>( + stmt: &str, + params: &[&'a dyn QueryParameter], + ) -> Query<'a> { + let mut stmt = String::from(stmt); + + if stmt.contains("RETURNING") { + // TODO: when the InsertQuerybuilder with a api on the builder for the returning clause + let c = stmt.clone(); + let temp = c.split_once("RETURNING").unwrap(); + let temp2 = temp.0.split_once("VALUES").unwrap(); + + stmt = format!( + "{} OUTPUT inserted.{} VALUES {}", + temp2.0.trim(), + temp.1.trim(), + temp2.1.trim() + ); + } + + let stmt = stmt.replace('$', "@P"); // TODO: this should be solved by the querybuilder + generate_query_and_bind_params(stmt, params) + } + + // Query and parameters are generated in this procedure together to avoid lifetime errors + fn generate_query_and_bind_params<'a>( + stmt: String, + params: &[&'a (dyn QueryParameter + 'a)], + ) -> Query<'a> { + let mut mssql_query = Query::new(stmt); + params.iter().for_each(|param| { + mssql_query.bind(*param); + }); + mssql_query + } +} + +pub(crate) mod __impl { + use super::*; + use crate::connection::datasources::{Auth, SqlServerAuth}; + use bb8::Pool; + use std::sync::Arc; + use tiberius::Config; + + pub(crate) async fn create_sqlserver_connector( + datasource: &DatasourceConfig, + ) -> Result>, Box> { + let sqlserver_config = sqlserver_config_from_datasource(datasource)?; + + let manager = TiberiusConnectionManager::new(sqlserver_config); + let pool = bb8::Pool::builder().max_size(10u32).build(manager).await?; + + Ok(SqlServerConnectionPool::from(pool)) + } + + pub(crate) fn sqlserver_config_from_datasource( + datasource: &DatasourceConfig, + ) -> Result> { + let mut tiberius_config = tiberius::Config::new(); + + tiberius_config.host(&datasource.properties.host); + tiberius_config.port(datasource.get_port_or_default_by_db()); + tiberius_config.database(&datasource.properties.db_name); + + let auth_config = extract_mssql_auth(&datasource.auth)?; + tiberius_config.authentication(auth_config); + tiberius_config.trust_cert(); // TODO: this should be specifically set via user input + tiberius_config.encryption(tiberius::EncryptionLevel::NotSupported); // TODO: user input + // TODO: in MacOS 15, this is the actual workaround. We need to investigate further + // https://github.com/prisma/tiberius/issues/364 + + Ok(tiberius_config) + } + + pub(crate) fn extract_mssql_auth( + auth: &Auth, + ) -> Result> { + match auth { + Auth::SqlServer(sql_server_auth) => match sql_server_auth { + SqlServerAuth::Basic { username, password } => { + Ok(tiberius::AuthMethod::sql_server(username, password)) + } + }, + #[cfg(any(feature = "postgres", feature = "mysql"))] + _ => Err("Invalid auth configuration for a SqlServer datasource.".into()), + } + } +} +#[cfg(test)] +mod tests { + use super::__impl; + use crate::connection::datasources::{ + Auth, DatasourceConfig, DatasourceProperties, SqlServerAuth, + }; + use tiberius::AuthMethod; + + #[test] + fn test_extract_mssql_auth_basic() { + let auth = Auth::SqlServer(SqlServerAuth::Basic { + username: "sa".to_string(), + password: "password123".to_string(), + }); + + let result = __impl::extract_mssql_auth(&auth).unwrap(); + + match result { + // We can only check the variant, not its internals (private fields) + AuthMethod::SqlServer(_) => {} // success + _ => panic!("Expected AuthMethod::SqlServer variant"), + } + } + + #[test] + fn test_sqlserver_config_from_datasource_basic() { + let datasource = DatasourceConfig { + name: "test_source".into(), + properties: DatasourceProperties { + host: "localhost".into(), + db_name: "test_db".into(), + port: None, // default + migrations: None, + }, + auth: Auth::SqlServer(SqlServerAuth::Basic { + username: "sa".into(), + password: "pass123".into(), + }), + }; + + let config = __impl::sqlserver_config_from_datasource(&datasource).unwrap(); + assert_eq!(config.get_addr(), "localhost:1433"); + } +} diff --git a/canyon_core/src/connection/clients/mysql.rs b/canyon_core/src/connection/clients/mysql.rs new file mode 100644 index 00000000..e3cbb995 --- /dev/null +++ b/canyon_core/src/connection/clients/mysql.rs @@ -0,0 +1,221 @@ +use crate::connection::clients::mysql::mysql_query_launcher::{execute_query, generate_mysql_stmt}; +use crate::connection::contracts::DbConnection; +use crate::connection::database_type::DatabaseType; +use crate::connection::datasources::DatasourceConfig; +use crate::mapper::RowMapper; +use crate::rows::FromSqlOwnedValue; +use crate::{query::parameters::QueryParameter, rows::CanyonRows}; +use mysql_async::Row; +use mysql_async::prelude::Query; +use mysql_common::constants::ColumnType; +use mysql_common::row; +use std::error::Error; + +/// A connection with a MySQL database. +pub struct MySQLConnector(mysql_async::Pool); + +impl MySQLConnector { + pub async fn new(config: &DatasourceConfig) -> Result> { + Ok(Self(__impl::load_mysql_config(config).await?)) + } +} + +impl DbConnection for MySQLConnector { + async fn query_rows( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + Ok(CanyonRows::MySQL(execute_query(stmt, params, self).await?)) + } + + async fn query( + &self, + stmt: S, + params: &[&'_ dyn QueryParameter], + ) -> Result, Box> + where + S: AsRef + Send, + R: RowMapper, + Vec: FromIterator<::Output>, + { + Ok(execute_query(stmt, params, self) + .await? + .iter() + .flat_map(R::deserialize_mysql) + .collect()) + } + + async fn query_one( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result, Box> + where + R: RowMapper, + { + let result = execute_query(stmt, params, self).await?; + + match result.first() { + Some(row) => Ok(Some(R::deserialize_mysql(row)?)), + None => Ok(None), + } + } + + async fn query_one_for( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + Ok(execute_query(stmt, params, self) + .await? + .first() + .ok_or_else(|| format!("Failure executing 'query_one_for' while retrieving the first row with stmt: {:?}", stmt))? + .get::(0) + .ok_or_else(|| format!("Failure executing 'query_one_for' while retrieving the first column value on the first row with stmt: {:?}", stmt))? + ) + } + + async fn execute( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + let mysql_connection = self.0.get_conn().await?; + let mysql_stmt = generate_mysql_stmt(stmt, params)?; + + Ok(mysql_stmt.stmt.run(mysql_connection).await?.affected_rows()) + } + + fn get_database_type(&self) -> Result> { + Ok(DatabaseType::MySQL) + } +} + +pub(crate) mod mysql_query_launcher { + use super::*; + + use mysql_async::{QueryWithParams, Value}; + use std::sync::Arc; + + pub(crate) struct MySqlGeneratedStmt { + pub(crate) stmt: QueryWithParams>, + returns_last_insert_id: bool, + } + + pub(crate) async fn execute_query( + stmt: S, + params: &[&'_ dyn QueryParameter], + connector: &MySQLConnector, + ) -> Result, Box> + where + S: AsRef + Send, + { + let mysql_connection = connector.0.get_conn().await?; + let mysql_stmt = generate_mysql_stmt(stmt.as_ref(), params)?; + + let returns_last_insert_id = mysql_stmt.returns_last_insert_id; + let mut query_result = mysql_stmt.stmt.run(mysql_connection).await?; + + if returns_last_insert_id { + let last_insert_id = query_result + .last_insert_id() + .ok_or("MySQL did not return an identifier for the inserted row")?; + + return Ok(vec![row::new_row( + vec![Value::UInt(last_insert_id)], + Arc::new([mysql_async::Column::new(ColumnType::MYSQL_TYPE_LONGLONG)]), + )]); + } + + Ok(query_result.collect::().await?) + } + + pub(crate) fn generate_mysql_stmt( + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + let params = params + .iter() + .map(|param| param.as_mysql_param().to_value()) + .collect(); + + Ok(MySqlGeneratedStmt { + stmt: QueryWithParams { + query: stmt.to_owned(), + params, + }, + returns_last_insert_id: is_insert_statement(stmt), + }) + } + + fn is_insert_statement(stmt: &str) -> bool { + stmt.trim_start() + .split_once(char::is_whitespace) + .map_or(stmt.trim_start(), |(keyword, _)| keyword) + .eq_ignore_ascii_case("INSERT") + } + + #[cfg(test)] + mod tests { + use super::is_insert_statement; + + #[test] + fn detects_insert_statements() { + assert!(is_insert_statement( + "INSERT INTO `users` (`name`) VALUES (?)" + )); + assert!(is_insert_statement( + " \n INSERT INTO `users` (`name`) VALUES (?)" + )); + assert!(is_insert_statement( + "insert into `users` (`name`) values (?)" + )); + } + + #[test] + fn does_not_treat_other_statements_as_inserts() { + assert!(!is_insert_statement("SELECT * FROM `users`")); + assert!(!is_insert_statement( + "UPDATE `users` SET `name` = ? WHERE `id` = ?" + )); + assert!(!is_insert_statement("DELETE FROM `users` WHERE `id` = ?")); + } + } +} + +pub(crate) mod __impl { + use crate::connection::datasources::{Auth, DatasourceConfig, MySQLAuth}; + use mysql_async::Pool; + use std::error::Error; + + pub(crate) async fn load_mysql_config( + datasource: &DatasourceConfig, + ) -> Result> { + let (user, password) = extract_mysql_auth(&datasource.auth)?; + + // TODO: pool constraints must be obtained from the datasource configuration. + let pool_constraints = + mysql_async::PoolConstraints::new(2, 10).ok_or("Failure launching the MySQL pool")?; + + let mysql_opts_builder = mysql_async::OptsBuilder::default() + .pool_opts(mysql_async::PoolOpts::default().with_constraints(pool_constraints)) + .user(Some(user)) + .pass(Some(password)) + .db_name(Some(&datasource.properties.db_name)) + .ip_or_hostname(&datasource.properties.host) + .tcp_port(datasource.get_port_or_default_by_db()); + + Ok(mysql_async::Pool::new(mysql_opts_builder)) + } + + pub(crate) fn extract_mysql_auth( + auth: &Auth, + ) -> Result<(&str, &str), Box> { + match auth { + Auth::MySQL(MySQLAuth::Basic { username, password }) => Ok((username, password)), + #[cfg(any(feature = "postgres", feature = "mssql"))] + _ => Err("Invalid auth configuration for a MySQL datasource.".into()), + } + } +} diff --git a/canyon_core/src/connection/clients/postgresql.rs b/canyon_core/src/connection/clients/postgresql.rs new file mode 100644 index 00000000..20d33d34 --- /dev/null +++ b/canyon_core/src/connection/clients/postgresql.rs @@ -0,0 +1,263 @@ +use crate::connection::contracts::DbConnection; +use crate::connection::database_type::DatabaseType; +use crate::connection::datasources::{Auth, DatasourceConfig, PostgresAuth}; +use crate::mapper::RowMapper; +use crate::rows::FromSqlOwnedValue; +use crate::{query::parameters::QueryParameter, rows::CanyonRows}; +use bb8::{Pool, PooledConnection}; +use bb8_postgres::PostgresConnectionManager; +use std::error::Error; +use std::sync::Arc; +use tokio_postgres::types::ToSql; +use tokio_postgres::{Config, NoTls}; + +type PgManager = PostgresConnectionManager; +type PostgresConnectionPool = Arc>; + +/// A connector with a `PostgreSQL` database +pub struct PostgresConnector(PostgresConnectionPool); +impl PostgresConnector { + pub async fn new(datasource: &DatasourceConfig) -> Result> { + Ok(Self(create_postgres_connector(datasource).await?)) + } + + pub async fn get_pooled( + &self, + ) -> Result, Box> { + Ok(self.0.get().await?) + } +} + +impl DbConnection for PostgresConnector { + async fn query_rows( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + let r = self + .get_pooled() + .await? + .query(stmt, &get_psql_params(params)) + .await?; + Ok(CanyonRows::Postgres(r)) + } + + async fn query( + &self, + stmt: S, + params: &[&dyn QueryParameter], + ) -> Result, Box> + where + S: AsRef + Send, + R: RowMapper, + Vec: FromIterator<::Output>, + { + Ok(self + .get_pooled() + .await? + .query(stmt.as_ref(), &get_psql_params(params)) + .await? + .iter() + .flat_map(|row| R::deserialize_postgresql(row)) + .collect()) + } + + async fn query_one( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result, Box> + where + R: RowMapper, + { + let result = self + .get_pooled() + .await? + .query_one(stmt, &get_psql_params(params)) + .await; + + match result { + Ok(row) => Ok(Some(R::deserialize_postgresql(&row)?)), + Err(e) => match e.to_string().contains("unexpected number of rows") { + true => Ok(None), + _ => Err(e)?, + }, + } + } + + async fn query_one_for( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + let r = self + .get_pooled() + .await? + .query_one(stmt, &get_psql_params(params)) + .await?; + r.try_get::(0).map_err(From::from) + } + + async fn execute( + &self, + stmt: &str, + params: &[&dyn QueryParameter], + ) -> Result> { + self.get_pooled() + .await? + .execute(stmt, &get_psql_params(params)) + .await + .map_err(From::from) + } + + fn get_database_type(&self) -> Result> { + Ok(DatabaseType::PostgreSql) + } +} + +fn get_psql_params<'a>(params: &'a [&'a dyn QueryParameter]) -> Vec<&'a (dyn ToSql + Sync)> { + params + .iter() + .map(|param| param.as_postgres_param()) + .collect::>() +} + +// Façade helper to create a new postgres connector +async fn create_postgres_connector( + datasource: &DatasourceConfig, +) -> Result>, Box> { + let (user, password) = __impl::extract_postgres_auth(&datasource.auth)?; + let config = __impl::set_tokio_postgres_configs(datasource, user, password); + let conn_pool = __impl::create_postgres_connection_pool(config).await?; + + Ok(PostgresConnectionPool::from(conn_pool)) +} + +mod __impl { + use super::*; + + pub(crate) fn set_tokio_postgres_configs( + datasource_config: &DatasourceConfig, + user: &str, + password: &str, + ) -> Config { + let mut config = tokio_postgres::Config::new(); + config.host(&datasource_config.properties.host); + config.port(datasource_config.get_port_or_default_by_db()); + config.dbname(&datasource_config.properties.db_name); + config.user(user); + config.password(password); + + // Optimize connection settings for better performance + config.connect_timeout(std::time::Duration::from_secs(5)); + config.keepalives_idle(std::time::Duration::from_secs(30)); + config.keepalives_interval(std::time::Duration::from_secs(10)); + config.keepalives_retries(3); + + config + } + + pub(crate) fn extract_postgres_auth( + auth: &Auth, + ) -> Result<(&str, &str), Box> { + match auth { + Auth::Postgres(pg_auth) => match pg_auth { + PostgresAuth::Basic { username, password } => Ok((username, password)), + }, + #[cfg(any(feature = "mssql", feature = "mysql"))] + _ => Err("Invalid auth configuration for a Postgres datasource.".into()), + } + } + + pub(crate) async fn create_postgres_connection_pool( + config: Config, + ) -> Result, Box> { + let manager = PgManager::new(config, NoTls); + let pool = bb8::Pool::builder().max_size(10u32).build(manager).await?; + Ok(pool) + } +} + +#[cfg(test)] +mod tests { + use super::__impl; + use crate::connection::datasources::{ + Auth, DatasourceConfig, DatasourceProperties, PostgresAuth, + }; + + #[test] + fn test_extract_postgres_auth_basic() { + let auth = Auth::Postgres(PostgresAuth::Basic { + username: "pguser".into(), + password: "pgpass".into(), + }); + + let (user, pass) = __impl::extract_postgres_auth(&auth).unwrap(); + assert_eq!(user, "pguser"); + assert_eq!(pass, "pgpass"); + } + + #[test] + fn test_set_tokio_postgres_configs_basic() { + let datasource = DatasourceConfig { + name: "pg_test".into(), + properties: DatasourceProperties { + host: "localhost".into(), + db_name: "pg_db".into(), + port: Some(5433), + migrations: None, + }, + auth: Auth::Postgres(PostgresAuth::Basic { + username: "pguser".into(), + password: "pgpass".into(), + }), + }; + + let config = __impl::set_tokio_postgres_configs(&datasource, "pguser", "pgpass"); + + assert_eq!( + config.get_hosts(), + vec![tokio_postgres::config::Host::Tcp("localhost".into())] + ); + assert_eq!(config.get_dbname(), Some("pg_db")); + assert_eq!(config.get_user(), Some("pguser")); + assert_eq!(*config.get_ports().first().unwrap(), 5433); + + // sanity check for configured timeouts and keepalives + assert_eq!( + config.get_connect_timeout(), + Some(std::time::Duration::from_secs(5)).as_ref() + ); + assert_eq!( + config.get_keepalives_idle(), + std::time::Duration::from_secs(30) + ); + assert_eq!( + config.get_keepalives_interval(), + Some(std::time::Duration::from_secs(10)) + ); + assert_eq!(config.get_keepalives_retries(), Some(3)); + } + + #[test] + fn test_set_tokio_postgres_configs_default_port() { + let datasource = DatasourceConfig { + name: "pg_test_default".into(), + properties: DatasourceProperties { + host: "127.0.0.1".into(), + db_name: "default_db".into(), + port: None, + migrations: None, + }, + auth: Auth::Postgres(PostgresAuth::Basic { + username: "user".into(), + password: "pass".into(), + }), + }; + + let config = __impl::set_tokio_postgres_configs(&datasource, "user", "pass"); + assert_eq!(*config.get_ports().first().unwrap(), 5432); // default Postgres port + assert_eq!(config.get_dbname(), Some("default_db")); + assert_eq!(config.get_user(), Some("user")); + } +} diff --git a/canyon_core/src/connection/conn_errors.rs b/canyon_core/src/connection/conn_errors.rs new file mode 100644 index 00000000..3b2ed455 --- /dev/null +++ b/canyon_core/src/connection/conn_errors.rs @@ -0,0 +1,26 @@ +//! Defines the Canyon-SQL custom connection error types + +/// Raised when a [`crate::connection::datasources::DatasourceConfig`] isn't found given a user input +#[derive(Debug, Clone)] +pub struct DatasourceNotFound { + pub datasource_name: String, +} +impl From> for DatasourceNotFound { + fn from(value: Option<&str>) -> Self { + DatasourceNotFound { + datasource_name: value + .map(String::from) + .unwrap_or_else(|| String::from("No datasource name was provided")), + } + } +} +impl std::fmt::Display for DatasourceNotFound { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "Unable to found a datasource that matches: {:?}", + self.datasource_name + ) + } +} +impl std::error::Error for DatasourceNotFound {} diff --git a/canyon_core/src/connection/contracts/mod.rs b/canyon_core/src/connection/contracts/mod.rs new file mode 100644 index 00000000..edef7cb2 --- /dev/null +++ b/canyon_core/src/connection/contracts/mod.rs @@ -0,0 +1,118 @@ +use crate::connection::database_type::DatabaseType; +use crate::mapper::RowMapper; +use crate::query::parameters::QueryParameter; +use crate::rows::{CanyonRows, FromSqlOwnedValue}; +use std::error::Error; +use std::future::Future; + +/// The `DbConnection` trait defines the core functionality required for interacting with a database connection. +/// It provides methods for executing queries, retrieving rows, and obtaining metadata about the database type. +/// +/// This trait is designed to be implemented by various database connection types, enabling a unified interface +/// for database operations. Each method is asynchronous and returns a `Future` to support non-blocking operations. +/// +/// # Examples +/// +/// ```ignore +/// use crate::connection::DbConnection; +/// +/// async fn execute_query(conn: &C) { +/// let result = conn.execute("INSERT INTO users (name) VALUES ($1)", &[&"John"]).await; +/// match result { +/// Ok(rows_affected) => println!("Rows affected: {}", rows_affected), +/// Err(e) => eprintln!("Error executing query: {}", e), +/// } +/// } +/// ``` +/// +/// # Required Methods +/// Each method in this trait must be implemented by the implementor. +pub trait DbConnection { + /// Executes a query and retrieves multiple rows from the database. + /// + /// # Arguments + /// * `stmt` - A SQL statement to execute. + /// * `params` - A slice of query parameters to bind to the statement. + /// + /// # Returns + /// A [Future] that resolves to a [Result] containing [`CanyonRows`] on success or an error on failure. + fn query_rows( + &self, + stmt: &str, + params: &[&dyn QueryParameter], + ) -> impl Future>> + Send; + + /// Executes a query and maps the result to a collection of rows of type `R`. + /// + /// # Arguments + /// * `stmt` - A SQL statement to execute. + /// * `params` - A slice of query parameters to bind to the statement. + /// + /// # Returns + /// A [Future] that resolves to a [Result] containing a `Vec` on success or an error on failure. + /// + /// The `R` type must implement the [`RowMapper`] trait. + fn query( + &self, + stmt: S, + params: &[&dyn QueryParameter], + ) -> impl Future, Box>> + Send + where + S: AsRef + Send, + R: RowMapper, + Vec: FromIterator<::Output>; + + /// Executes a query and retrieves a single row mapped to type `R`. + /// + /// # Arguments + /// * `stmt` - A SQL statement to execute. + /// * `params` - A slice of query parameters to bind to the statement. + /// + /// # Returns + /// A [Future] that resolves to a [Result] containing an `Option` on success or an error on failure. + /// + /// The `R` type must implement the [`RowMapper`] trait. + fn query_one( + &self, + stmt: &str, + params: &[&dyn QueryParameter], + ) -> impl Future, Box>> + Send + where + R: RowMapper; + + /// Executes a query and retrieves a single value of type `T`. + /// + /// # Arguments + /// * `stmt` - A SQL statement to execute. + /// * `params` - A slice of query parameters to bind to the statement. + /// + /// # Returns + /// A [Future] that resolves to a [Result] containing the value of type `T` on success or an error on failure. + /// + /// The `T` type must implement the [`FromSqlOwnedValue`] trait. + fn query_one_for( + &self, + stmt: &str, + params: &[&dyn QueryParameter], + ) -> impl Future>> + Send; + + /// Executes a SQL statement and returns the number of affected rows. + /// + /// # Arguments + /// * `stmt` - A SQL statement to execute. + /// * `params` - A slice of query parameters to bind to the statement. + /// + /// # Returns + /// A [Future] that resolves to a [Result] containing the number of affected rows on success or an error on failure. + fn execute( + &self, + stmt: &str, + params: &[&dyn QueryParameter], + ) -> impl Future>> + Send; + + /// Retrieves the type of the database associated with the connection. + /// + /// # Returns + /// A `Result` containing the [`DatabaseType`] on success or an error on failure. + fn get_database_type(&self) -> Result>; +} diff --git a/canyon_core/src/connection/database_type.rs b/canyon_core/src/connection/database_type.rs new file mode 100644 index 00000000..8f5af3b9 --- /dev/null +++ b/canyon_core/src/connection/database_type.rs @@ -0,0 +1,60 @@ +use super::datasources::Auth; +use crate::canyon::Canyon; +use serde::Deserialize; +use std::{error::Error, fmt::Display}; + +/// Represents the supported database backends in **Canyon-SQL**. +/// +/// This enum abstracts over the specific database dialects supported by Canyon, +/// allowing queries and builders to adapt automatically to the correct SQL syntax +/// and placeholder conventions (`$1`, `?`, `@P1`, etc.) according to the active +/// [`DatabaseType`]. +/// +/// The variant used at runtime is determined either: +/// - Explicitly, when passed to a [`crate::query::querybuilder::QueryBuilder`] constructor, or +/// - Implicitly, from the first configured data source via +/// [`Canyon::get_default_db_type()`]. +/// +/// # Example +/// ```rust,ignore +/// use canyon_core::connection::database_type::DatabaseType; +/// ``` +#[derive(Deserialize, Debug, Eq, PartialEq, Clone, Copy)] +pub enum DatabaseType { + /// The Postgres database backend. + #[cfg(feature = "postgres")] + #[serde(alias = "postgres", alias = "postgresql")] + PostgreSql, + + /// The Microsoft SQL Server backend. + #[cfg(feature = "mssql")] + #[serde(alias = "sqlserver", alias = "mssql")] + SqlServer, + + /// The MySQL or MariaDB backend. + #[cfg(feature = "mysql")] + #[serde(alias = "mysql")] + MySQL, +} + +impl Display for DatabaseType { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(fmt, "{:?}", self) + } +} + +impl From<&Auth> for DatabaseType { + fn from(value: &Auth) -> Self { + value.get_db_type() + } +} + +/// The default implementation for [`DatabaseType`] returns the database type for the first +/// datasource configured +impl DatabaseType { + pub fn default_type() -> Result> { + Canyon::instance()? + .get_default_db_type() + .map_err(|err| Box::new(err) as Box) + } +} diff --git a/canyon_core/src/connection/datasources.rs b/canyon_core/src/connection/datasources.rs new file mode 100644 index 00000000..bbaa3abb --- /dev/null +++ b/canyon_core/src/connection/datasources.rs @@ -0,0 +1,218 @@ +//! The datasources module of Canyon-SQL. +//! +//! This module defines the configuration and authentication mechanisms for database datasources. +//! It includes support for multiple database backends and provides utilities for managing +//! datasource properties. + +use serde::{Deserialize, Deserializer}; + +use super::database_type::DatabaseType; + +#[derive(Deserialize, Debug, Clone)] +pub struct CanyonSqlConfig { + pub canyon_sql: Datasources, +} + +#[derive(Debug, Clone)] +pub struct Datasources { + pub datasources: Vec, +} + +impl<'de> Deserialize<'de> for Datasources { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let raw = RawDatasources::deserialize(deserializer)?; + + let datasources = raw + .datasources + .into_iter() + .filter_map(DatasourceConfig::from_raw) + .collect(); + + Ok(Self { datasources }) + } +} + +#[derive(Deserialize)] +struct RawDatasources { + datasources: Vec, +} + +#[derive(Deserialize)] +struct RawDatasourceConfig { + name: String, + auth: RawAuth, + properties: DatasourceProperties, +} + +#[derive(Deserialize)] +enum RawAuth { + #[serde(alias = "PostgresSQL", alias = "postgresql", alias = "postgres")] + Postgres(RawPostgresAuth), + + #[serde(alias = "SqlServer", alias = "sqlserver", alias = "mssql")] + SqlServer(RawSqlServerAuth), + + #[serde(alias = "MYSQL", alias = "mysql", alias = "MySQL")] + MySQL(RawMySQLAuth), +} + +#[cfg(feature = "postgres")] +type RawPostgresAuth = PostgresAuth; + +#[cfg(not(feature = "postgres"))] +type RawPostgresAuth = serde::de::IgnoredAny; + +#[cfg(feature = "mssql")] +type RawSqlServerAuth = SqlServerAuth; + +#[cfg(not(feature = "mssql"))] +type RawSqlServerAuth = serde::de::IgnoredAny; + +#[cfg(feature = "mysql")] +type RawMySQLAuth = MySQLAuth; + +#[cfg(not(feature = "mysql"))] +type RawMySQLAuth = serde::de::IgnoredAny; + +#[derive(Debug, Clone)] +pub struct DatasourceConfig { + pub name: String, + pub auth: Auth, + pub properties: DatasourceProperties, +} + +impl DatasourceConfig { + fn from_raw(raw: RawDatasourceConfig) -> Option { + let RawDatasourceConfig { + name, + auth, + properties, + } = raw; + + let auth = match auth { + #[cfg(feature = "postgres")] + RawAuth::Postgres(auth) => Auth::Postgres(auth), + + #[cfg(not(feature = "postgres"))] + RawAuth::Postgres(_) => return None, + + #[cfg(feature = "mssql")] + RawAuth::SqlServer(auth) => Auth::SqlServer(auth), + + #[cfg(not(feature = "mssql"))] + RawAuth::SqlServer(_) => return None, + + #[cfg(feature = "mysql")] + RawAuth::MySQL(auth) => Auth::MySQL(auth), + + #[cfg(not(feature = "mysql"))] + RawAuth::MySQL(_) => return None, + }; + + Some(Self { + name, + auth, + properties, + }) + } + + pub fn get_db_type(&self) -> DatabaseType { + self.auth.get_db_type() + } + + pub fn has_migrations_enabled(&self) -> bool { + self.properties + .migrations + .is_some_and(|migrations| migrations.has_migrations_enabled()) + } + + pub fn get_port_or_default_by_db(&self) -> u16 { + self.properties + .port + .unwrap_or_else(|| match self.get_db_type() { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => 5432, + + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => 1433, + + #[cfg(feature = "mysql")] + DatabaseType::MySQL => 3306, + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Auth { + #[cfg(feature = "postgres")] + Postgres(PostgresAuth), + + #[cfg(feature = "mssql")] + SqlServer(SqlServerAuth), + + #[cfg(feature = "mysql")] + MySQL(MySQLAuth), +} + +impl Auth { + pub fn get_db_type(&self) -> DatabaseType { + match self { + #[cfg(feature = "postgres")] + Self::Postgres(_) => DatabaseType::PostgreSql, + + #[cfg(feature = "mssql")] + Self::SqlServer(_) => DatabaseType::SqlServer, + + #[cfg(feature = "mysql")] + Self::MySQL(_) => DatabaseType::MySQL, + } + } +} + +#[cfg(feature = "postgres")] +#[derive(Deserialize, Debug, Clone, PartialEq)] +pub enum PostgresAuth { + #[serde(alias = "Basic", alias = "basic")] + Basic { username: String, password: String }, +} + +#[cfg(feature = "mssql")] +#[derive(Deserialize, Debug, Clone, PartialEq)] +pub enum SqlServerAuth { + #[serde(alias = "Basic", alias = "basic")] + Basic { username: String, password: String }, +} + +#[cfg(feature = "mysql")] +#[derive(Deserialize, Debug, Clone, PartialEq)] +pub enum MySQLAuth { + #[serde(alias = "Basic", alias = "basic")] + Basic { username: String, password: String }, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct DatasourceProperties { + pub host: String, + pub port: Option, + pub db_name: String, + pub migrations: Option, +} + +/// Represents the enabled or disabled migrations for a whole datasource. +#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] +pub enum Migrations { + #[serde(alias = "Enabled", alias = "enabled")] + Enabled, + + #[serde(alias = "Disabled", alias = "disabled")] + Disabled, +} + +impl Migrations { + pub fn has_migrations_enabled(&self) -> bool { + matches!(self, Self::Enabled) + } +} diff --git a/canyon_core/src/connection/db_connector.rs b/canyon_core/src/connection/db_connector.rs new file mode 100644 index 00000000..e41126fb --- /dev/null +++ b/canyon_core/src/connection/db_connector.rs @@ -0,0 +1,64 @@ +#[cfg(feature = "mssql")] +use crate::connection::clients::mssql::SqlServerConnector; +#[cfg(feature = "mysql")] +use crate::connection::clients::mysql::MySQLConnector; +#[cfg(feature = "postgres")] +use crate::connection::clients::postgresql::PostgresConnector; + +use crate::connection::database_type::DatabaseType; +use crate::connection::datasources::DatasourceConfig; +use crate::mapper::RowMapper; +use crate::query::parameters::QueryParameter; +use crate::rows::{CanyonRows, FromSqlOwnedValue}; +use std::error::Error; + +/// The Canyon database connection handler. When the client's program +/// starts, Canyon gets the information about the desired datasources, +/// process them and generates a pool of connections for +/// every datasource defined. +pub enum DatabaseConnector { + #[cfg(feature = "postgres")] + Postgres(PostgresConnector), + #[cfg(feature = "mssql")] + SqlServer(SqlServerConnector), + #[cfg(feature = "mysql")] + MySQL(MySQLConnector), +} + +unsafe impl Send for DatabaseConnector {} +unsafe impl Sync for DatabaseConnector {} + +crate::impl_db_connection_for_db_connector!(DatabaseConnector); +crate::impl_db_connection_for_db_connector!(&DatabaseConnector); +crate::impl_db_connection_for_db_connector!(&mut DatabaseConnector); + +impl DatabaseConnector { + pub async fn new(datasource: &DatasourceConfig) -> Result> { + // Add connection pooling at the client level for better performance + match datasource.get_db_type() { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => { + Ok(Self::Postgres(PostgresConnector::new(datasource).await?)) + } + + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => { + Ok(Self::SqlServer(SqlServerConnector::new(datasource).await?)) + } + + #[cfg(feature = "mysql")] + DatabaseType::MySQL => Ok(Self::MySQL(MySQLConnector::new(datasource).await?)), + } + } + + pub fn get_db_type(&self) -> DatabaseType { + match self { + #[cfg(feature = "postgres")] + DatabaseConnector::Postgres(_) => DatabaseType::PostgreSql, + #[cfg(feature = "mssql")] + DatabaseConnector::SqlServer(_) => DatabaseType::SqlServer, + #[cfg(feature = "mysql")] + DatabaseConnector::MySQL(_) => DatabaseType::MySQL, + } + } +} diff --git a/canyon_core/src/connection/impl_db_connection_macro.rs b/canyon_core/src/connection/impl_db_connection_macro.rs new file mode 100644 index 00000000..206201eb --- /dev/null +++ b/canyon_core/src/connection/impl_db_connection_macro.rs @@ -0,0 +1,183 @@ +//! This module contains macros for helping us to reduce boilerplate implementation code of the +//! [`crate::connection::DbConnection`] + +#[macro_export] +macro_rules! impl_db_connection_for_db_connector { + ($type:ty) => { + impl $crate::connection::contracts::DbConnection for $type { + async fn query_rows( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + match self { + #[cfg(feature = "postgres")] + DatabaseConnector::Postgres(client) => client.query_rows(stmt, params).await, + + #[cfg(feature = "mssql")] + DatabaseConnector::SqlServer(client) => client.query_rows(stmt, params).await, + + #[cfg(feature = "mysql")] + DatabaseConnector::MySQL(client) => client.query_rows(stmt, params).await, + } + } + + async fn query( + &self, + stmt: S, + params: &[&'_ dyn QueryParameter], + ) -> Result, Box> + where + S: AsRef + Send, + R: RowMapper, + Vec: FromIterator<::Output>, + { + match self { + #[cfg(feature = "postgres")] + DatabaseConnector::Postgres(client) => client.query(stmt, params).await, + + #[cfg(feature = "mssql")] + DatabaseConnector::SqlServer(client) => client.query(stmt, params).await, + + #[cfg(feature = "mysql")] + DatabaseConnector::MySQL(client) => client.query(stmt, params).await, + } + } + + async fn query_one( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result, Box> + where + R: RowMapper, + { + match self { + #[cfg(feature = "postgres")] + DatabaseConnector::Postgres(client) => { + client.query_one::(stmt, params).await + } + + #[cfg(feature = "mssql")] + DatabaseConnector::SqlServer(client) => { + client.query_one::(stmt, params).await + } + + #[cfg(feature = "mysql")] + DatabaseConnector::MySQL(client) => client.query_one::(stmt, params).await, + } + } + + async fn query_one_for( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + match self { + #[cfg(feature = "postgres")] + DatabaseConnector::Postgres(client) => client.query_one_for(stmt, params).await, + + #[cfg(feature = "mssql")] + DatabaseConnector::SqlServer(client) => { + client.query_one_for(stmt, params).await + } + + #[cfg(feature = "mysql")] + DatabaseConnector::MySQL(client) => client.query_one_for(stmt, params).await, + } + } + + async fn execute( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + match self { + #[cfg(feature = "postgres")] + DatabaseConnector::Postgres(client) => client.execute(stmt, params).await, + + #[cfg(feature = "mssql")] + DatabaseConnector::SqlServer(client) => client.execute(stmt, params).await, + + #[cfg(feature = "mysql")] + DatabaseConnector::MySQL(client) => client.execute(stmt, params).await, + } + } + + fn get_database_type(&self) -> Result> { + Ok(self.get_db_type()) + } + } + }; +} + +#[macro_export] +macro_rules! impl_db_connection_for_str { + ($type:ty) => { + impl $crate::connection::contracts::DbConnection for $type { + async fn query_rows( + &self, + stmt: &str, + params: &[&'_ dyn $crate::query::parameters::QueryParameter], + ) -> Result<$crate::rows::CanyonRows, Box> { + let conn = $crate::connection::Canyon::instance()?.get_connection(self)?; + conn.query_rows(stmt, params).await + } + + async fn query( + &self, + stmt: S, + params: &[&'_ dyn $crate::query::parameters::QueryParameter], + ) -> Result, Box> + where + S: AsRef + Send, + R: $crate::mapper::RowMapper, + Vec: std::iter::FromIterator<::Output>, + { + let conn = $crate::connection::Canyon::instance()?.get_connection(self)?; + conn.query(stmt, params).await + } + + async fn query_one( + &self, + stmt: &str, + params: &[&'_ dyn $crate::query::parameters::QueryParameter], + ) -> Result, Box> + where + R: $crate::mapper::RowMapper, + { + let conn = $crate::connection::Canyon::instance()?.get_connection(self)?; + conn.query_one::(stmt, params).await + } + + async fn query_one_for( + &self, + stmt: &str, + params: &[&'_ dyn $crate::query::parameters::QueryParameter], + ) -> Result> { + let conn = $crate::connection::Canyon::instance()?.get_connection(self)?; + conn.query_one_for(stmt, params).await + } + + async fn execute( + &self, + stmt: &str, + params: &[&'_ dyn $crate::query::parameters::QueryParameter], + ) -> Result> { + let conn = $crate::connection::Canyon::instance()?.get_connection(self)?; + conn.execute(stmt, params).await + } + + fn get_database_type( + &self, + ) -> Result< + $crate::connection::database_type::DatabaseType, + Box, + > { + Ok($crate::connection::Canyon::instance()? + .find_datasource_by_name_or_default(self)? + .get_db_type()) + } + } + }; +} diff --git a/canyon_core/src/connection/mod.rs b/canyon_core/src/connection/mod.rs new file mode 100644 index 00000000..eab2a5f4 --- /dev/null +++ b/canyon_core/src/connection/mod.rs @@ -0,0 +1,122 @@ +//! The connection module of Canyon-SQL. +//! +//! This module handles database connections, including connection pooling and configuration. +//! It provides abstractions for managing multiple datasources and supports asynchronous operations. + +#[cfg(feature = "postgres")] +pub extern crate tokio_postgres; + +#[cfg(feature = "mssql")] +pub extern crate async_std; +#[cfg(feature = "mssql")] +pub extern crate tiberius; + +#[cfg(feature = "mysql")] +pub extern crate mysql_async; + +pub extern crate futures; +pub extern crate tokio; +pub extern crate tokio_util; + +#[macro_use] +pub mod impl_db_connection_macro; + +pub mod clients; +pub mod conn_errors; +pub mod contracts; +pub mod database_type; +pub mod datasources; +pub mod db_connector; + +use crate::canyon::Canyon; +use crate::connection::contracts::DbConnection; +use crate::connection::database_type::DatabaseType; + +use std::error::Error; +use std::sync::{Arc, OnceLock}; + +use tokio::runtime::Runtime; +use tokio::sync::Mutex; + +// // TODO's: DatabaseConnector and DataSource can implement default, so there's no need to use str and &str +// // as defaults anymore, since the can load as the default the first one defined in the config file, or have more +// // complex workflows that are deferred to initialization time +// +// // TODO: Crud Operations should be split into two different derives, splitting the automagic from the _with ones + +pub(crate) static CANYON_INSTANCE: OnceLock = OnceLock::new(); + +// Use OnceLock for the Tokio runtime +static CANYON_TOKIO_RUNTIME: OnceLock = OnceLock::new(); + +// Function to get the runtime (lazy initialization) +pub fn get_canyon_tokio_runtime() -> &'static Runtime { + CANYON_TOKIO_RUNTIME + .get_or_init(|| Runtime::new().expect("Failed initializing the Canyon-SQL Tokio Runtime")) +} + +use crate::mapper::RowMapper; +use crate::query::parameters::QueryParameter; +use crate::rows::{CanyonRows, FromSqlOwnedValue}; + +// Apply the macro to implement DbConnection for &str and str +impl_db_connection_for_str!(str); +impl_db_connection_for_str!(&str); + +impl DbConnection for Arc> +where + T: DbConnection + Send, + Self: Clone, +{ + async fn query_rows( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + self.lock().await.query_rows(stmt, params).await + } + + async fn query( + &self, + stmt: S, + params: &[&'_ dyn QueryParameter], + ) -> Result, Box> + where + S: AsRef + Send, + R: RowMapper, + Vec: FromIterator, + { + self.lock().await.query(stmt, params).await + } + + async fn query_one( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result, Box> + where + R: RowMapper, + { + self.lock().await.query_one::(stmt, params).await + } + + async fn query_one_for( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + self.lock().await.query_one_for::(stmt, params).await + } + + async fn execute( + &self, + stmt: &str, + params: &[&'_ dyn QueryParameter], + ) -> Result> { + self.lock().await.execute(stmt, params).await + } + + fn get_database_type(&self) -> Result> { + todo!() + } +} diff --git a/canyon_core/src/lib.rs b/canyon_core/src/lib.rs new file mode 100644 index 00000000..01a90728 --- /dev/null +++ b/canyon_core/src/lib.rs @@ -0,0 +1,28 @@ +//! The core module of Canyon-SQL. +//! +//! This module provides the foundational components for database connections, query execution, +//! and data mapping. It includes support for multiple database backends such as PostgreSQL, +//! MySQL, and SQL Server, and defines traits and utilities for interacting with these databases. + +#[cfg(feature = "postgres")] +pub extern crate tokio_postgres; + +#[cfg(feature = "mssql")] +pub extern crate async_std; +#[cfg(feature = "mssql")] +pub extern crate tiberius; + +#[cfg(feature = "mysql")] +pub extern crate mysql_async; + +extern crate core; + +pub mod canyon; + +pub mod column; +pub mod connection; +pub mod mapper; +pub mod query; +pub mod row; +pub mod rows; +pub mod transaction; diff --git a/canyon_core/src/mapper.rs b/canyon_core/src/mapper.rs new file mode 100644 index 00000000..ba0af768 --- /dev/null +++ b/canyon_core/src/mapper.rs @@ -0,0 +1,45 @@ +//! The mapper module of Canyon-SQL. +//! +//! This module defines traits and utilities for mapping database query results to user-defined +//! types. It includes the `RowMapper` trait and related functionality for deserialization. + +/// Declares functions that takes care to deserialize data incoming +/// from some supported database in Canyon-SQL into a user's defined +/// type `T` +pub trait RowMapper: Sized { + type Output; + + #[cfg(feature = "postgres")] + fn deserialize_postgresql( + row: &tokio_postgres::Row, + ) -> Result<::Output, CanyonError>; + #[cfg(feature = "mssql")] + fn deserialize_sqlserver( + row: &tiberius::Row, + ) -> Result<::Output, CanyonError>; + #[cfg(feature = "mysql")] + fn deserialize_mysql( + row: &mysql_async::Row, + ) -> Result<::Output, CanyonError>; +} + +pub trait DefaultRowMapper { + type Mapper: RowMapper; +} + +// Blanket impl to make `Mapper = Self` for any `T: RowMapper` +impl DefaultRowMapper for T +where + T: RowMapper, +{ + type Mapper = T; +} + +pub type CanyonError = Box; // TODO: convert this into a +// real error +pub trait IntoResults { + fn into_results(self) -> Result, CanyonError> + where + R: RowMapper, + Vec: FromIterator<::Output>; +} diff --git a/canyon_core/src/query/bounds.rs b/canyon_core/src/query/bounds.rs new file mode 100644 index 00000000..f6d1f63d --- /dev/null +++ b/canyon_core/src/query/bounds.rs @@ -0,0 +1,73 @@ +use std::error::Error; +use std::fmt::Display; + +use crate::query::parameters::QueryParameter; +use crate::query::querybuilder::syntax::column::ColumnRef; +use crate::rows::FromSqlOwnedValue; + +/// Runtime metadata and field access generated for an entity. +/// +/// This contract is primarily consumed by Canyon's generated CRUD operations. +/// Field collections exclude the primary key because they currently represent +/// the values and columns used by entity insertion. +pub trait EntityRuntimeInfo { + type PrimaryKey: FromSqlOwnedValue; + + /// Returns the insertable field values in declaration order. + /// + /// The primary-key field is excluded. + fn field_values(&self) -> Vec<&dyn QueryParameter>; + + /// Returns the insertable columns in the same order as [`Self::field_values`]. + /// + /// The primary-key column is excluded. + fn field_columns() -> Vec>; + + fn primary_key_name() -> Option<&'static str>; + + fn primary_key_value(&self) -> Option<&dyn QueryParameter>; + + fn set_primary_key( + &mut self, + value: Self::PrimaryKey, + ) -> Result<(), Box>; + + fn primary_key_column() -> Option>; +} + +/// Provides the table name associated with an entity. +/// +/// Consider renaming this trait if it coexists with the concrete +/// `TableMetadata` syntax type. +pub trait EntityTable: Display { + fn table_name<'a>(&self) -> &'a str; +} + +/// Identifies an entity field and its mapped database column. +/// +/// Implementations are normally generated as an enum with one variant per +/// mapped field. +pub trait FieldIdentifier: Display { + fn as_str(&self) -> &'static str; + + fn as_column_ref(&self) -> ColumnRef<'static> { + ColumnRef::from(self.as_str()) + } +} + +/// Provides a mapped column together with the parameter value used by a query +/// condition. +pub trait FieldValueIdentifier { + fn column(&self) -> ColumnRef<'_>; + + fn value(&self) -> &dyn QueryParameter; +} + +/// Provides access to the local field participating in a foreign-key relation. +/// +/// `Related` identifies the entity on the referenced side of the relation, +/// allowing generated code to select the correct implementation when several +/// relationships exist. +pub trait ForeignKeyable { + fn foreign_key_value(&self, column: &str) -> Option<&dyn QueryParameter>; +} diff --git a/canyon_core/src/query/mod.rs b/canyon_core/src/query/mod.rs new file mode 100644 index 00000000..a3b79b53 --- /dev/null +++ b/canyon_core/src/query/mod.rs @@ -0,0 +1,10 @@ +#![allow(clippy::module_inception)] +pub mod query; + +pub mod bounds; +pub mod operators; +pub mod parameters; +pub mod querybuilder; + +// Re-exports +pub use crate::query::querybuilder::syntax::column::ColumnRef; diff --git a/canyon_core/src/query/operators.rs b/canyon_core/src/query/operators.rs new file mode 100644 index 00000000..1213decd --- /dev/null +++ b/canyon_core/src/query/operators.rs @@ -0,0 +1,279 @@ +use crate::query::querybuilder::syntax::dialect::PlaceholderDatatype; +use crate::query::querybuilder::syntax::{ + dialect::SqlDialect, + keyword::Keyword, + tokens::{SqlToken, SqlTokens, Symbol, ToSqlTokens}, +}; +use std::borrow::Cow; +use std::fmt::Display; + +/// Enumerated type for represent the available operators +/// in SQL sentences +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum Operator { + /// Operator "=" equals + Eq, + /// Operator "!=" not equals + Neq, + /// Operator ">" greater than value + Gt, + /// Operator ">=" greater or equals than value + GtEq, + /// Operator "<" less than value + Lt, + /// Operator "=<" less or equals than value + LtEq, + /// A "LIKE" comp operator + Like(LikeKind), + /// A "NOT LIKE" comp operator + NotLike(LikeKind), + /// Operator "IN" for value in (value1, value2, ...) + In, +} + +impl Display for Operator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let op = match *self { + Self::Eq => "=", + Self::Neq => "<>", + Self::Gt => ">", + Self::GtEq => ">=", + Self::Lt => "<", + Self::LtEq => "<=", + Self::Like(ref __kind) => "LIKE", + Self::NotLike(ref __kind) => "NOT LIKE", + Self::In => "IN", + }; + write!(f, "{}", op) + } +} + +impl<'a, D: SqlDialect> ToSqlTokens<'a, D> for Operator { + fn to_tokens(&self) -> impl IntoIterator> + 'a { + let mut out = SqlTokens::default(); + + match *self { + Self::Eq => out.symbol(Symbol::Equals), + Self::Neq => { + out.symbol(Symbol::Not); + out.symbol(Symbol::Equals); + } + Self::Gt => out.symbol(Symbol::RAngle), + Self::GtEq => { + out.symbol(Symbol::RAngle); + out.symbol(Symbol::Equals); + } + Self::Lt => out.symbol(Symbol::LAngle), + Self::LtEq => { + out.symbol(Symbol::LAngle); + out.symbol(Symbol::Equals); + } + Self::Like(kind) => out.extend(>::to_tokens(&kind)), + Self::NotLike(kind) => { + out.keyword(Keyword::Not); + + out.extend(>::to_tokens(&kind)); + } + Self::In => out.keyword(Keyword::In), + } + + out + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum LikeKind { + /// Operator `LIKE` as `%pattern%`. + Full, + /// Operator `LIKE` as `%pattern`. + Left, + /// Operator `LIKE` as `pattern%`. + Right, +} + +impl LikeKind { + #[inline] + fn push_casted_placeholder(out: &mut SqlTokens) { + out.keyword(Keyword::Cast); + out.symbol(Symbol::LParen); + out.placeholder(); + + out.keyword(Keyword::As); + out.ident(Cow::from(>::into( + D::PLACEHOLDER_DATA_TYPE, + ))); + out.symbol(Symbol::RParen); + } + + #[inline] + fn push_percent_literal(out: &mut SqlTokens) { + out.symbol(Symbol::Quote); + out.symbol(Symbol::PercentSign); + out.symbol(Symbol::Quote); + } + + #[inline] + fn push_comma_sep(out: &mut SqlTokens) { + out.symbol(Symbol::Comma); + } +} + +impl<'a, D: SqlDialect> ToSqlTokens<'a, D> for LikeKind { + fn to_tokens(&self) -> impl IntoIterator> + 'a { + let mut out = SqlTokens::with_capacity(19); + + out.keyword(Keyword::Concat); + out.symbol(Symbol::LParen); + + match *self { + Self::Full => { + Self::push_percent_literal(&mut out); + Self::push_comma_sep(&mut out); + Self::push_casted_placeholder::(&mut out); + Self::push_comma_sep(&mut out); + Self::push_percent_literal(&mut out); + } + Self::Left => { + Self::push_percent_literal(&mut out); + Self::push_comma_sep(&mut out); + Self::push_casted_placeholder::(&mut out); + } + Self::Right => { + Self::push_casted_placeholder::(&mut out); + Self::push_comma_sep(&mut out); + Self::push_percent_literal(&mut out); + } + } + + out.symbol(Symbol::RParen); + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "postgres")] + use crate::query::querybuilder::syntax::dialect::PgDialect; + use crate::query::querybuilder::syntax::dialect::PlaceholderDatatype; + + fn tokens(value: T) -> Vec> + where + D: SqlDialect, + T: ToSqlTokens<'static, D>, + { + value.to_tokens().into_iter().collect() + } + + fn full_like_tokens() -> Vec> { + vec![ + SqlToken::Keyword(Keyword::Concat), + SqlToken::Symbol(Symbol::LParen), + SqlToken::Symbol(Symbol::Quote), + SqlToken::Symbol(Symbol::PercentSign), + SqlToken::Symbol(Symbol::Quote), + SqlToken::Symbol(Symbol::Comma), + SqlToken::Keyword(Keyword::Cast), + SqlToken::Symbol(Symbol::LParen), + SqlToken::Placeholder, + SqlToken::Keyword(Keyword::As), + SqlToken::Ident(Cow::from(>::into( + D::PLACEHOLDER_DATA_TYPE, + ))), + SqlToken::Symbol(Symbol::RParen), + SqlToken::Symbol(Symbol::Comma), + SqlToken::Symbol(Symbol::Quote), + SqlToken::Symbol(Symbol::PercentSign), + SqlToken::Symbol(Symbol::Quote), + SqlToken::Symbol(Symbol::RParen), + ] + } + + fn left_like_tokens() -> Vec> { + vec![ + SqlToken::Keyword(Keyword::Concat), + SqlToken::Symbol(Symbol::LParen), + SqlToken::Symbol(Symbol::Quote), + SqlToken::Symbol(Symbol::PercentSign), + SqlToken::Symbol(Symbol::Quote), + SqlToken::Symbol(Symbol::Comma), + SqlToken::Keyword(Keyword::Cast), + SqlToken::Symbol(Symbol::LParen), + SqlToken::Placeholder, + SqlToken::Keyword(Keyword::As), + SqlToken::Ident(Cow::from(>::into( + D::PLACEHOLDER_DATA_TYPE, + ))), + SqlToken::Symbol(Symbol::RParen), + SqlToken::Symbol(Symbol::RParen), + ] + } + + fn right_like_tokens() -> Vec> { + vec![ + SqlToken::Keyword(Keyword::Concat), + SqlToken::Symbol(Symbol::LParen), + SqlToken::Keyword(Keyword::Cast), + SqlToken::Symbol(Symbol::LParen), + SqlToken::Placeholder, + SqlToken::Keyword(Keyword::As), + SqlToken::Ident(Cow::from(>::into( + D::PLACEHOLDER_DATA_TYPE, + ))), + SqlToken::Symbol(Symbol::RParen), + SqlToken::Symbol(Symbol::Comma), + SqlToken::Symbol(Symbol::Quote), + SqlToken::Symbol(Symbol::PercentSign), + SqlToken::Symbol(Symbol::Quote), + SqlToken::Symbol(Symbol::RParen), + ] + } + + #[cfg(feature = "postgres")] + #[test] + fn full_like_kind_emits_like_concat_wrapping_placeholder_on_both_sides() { + assert_eq!( + tokens::(LikeKind::Full), + full_like_tokens::(), + ); + } + + #[cfg(feature = "postgres")] + #[test] + fn left_like_kind_emits_like_concat_with_leading_percent() { + assert_eq!( + tokens::(LikeKind::Left), + left_like_tokens::(), + ); + } + + #[cfg(feature = "postgres")] + #[test] + fn right_like_kind_emits_like_concat_with_trailing_percent() { + assert_eq!( + tokens::(LikeKind::Right), + right_like_tokens::(), + ); + } + + #[cfg(feature = "postgres")] + #[test] + fn like_operator_delegates_to_like_kind() { + assert_eq!( + tokens::(Operator::Like(LikeKind::Full)), + full_like_tokens::(), + ); + } + + #[cfg(feature = "postgres")] + #[test] + fn not_like_operator_emits_not_like_instead_of_not_equals() { + let mut expected = vec![SqlToken::Keyword(Keyword::Not)]; + expected.extend(full_like_tokens::()); + assert_eq!( + tokens::(Operator::NotLike(LikeKind::Full)), + expected, + ); + } +} diff --git a/canyon_core/src/query/parameters.rs b/canyon_core/src/query/parameters.rs new file mode 100644 index 00000000..7094da3c --- /dev/null +++ b/canyon_core/src/query/parameters.rs @@ -0,0 +1,630 @@ +#[cfg(feature = "mysql")] +use mysql_async::{self, prelude::ToValue}; +use std::any::Any; +use std::fmt::Debug; +#[cfg(feature = "mssql")] +use tiberius::{self, ColumnData, IntoSql}; +#[cfg(feature = "postgres")] +use tokio_postgres::{self, types::ToSql}; + +// TODO: cfg feature for this re-exports, as date-time or something +use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc}; + +pub trait QueryParameterValue<'a> { + fn downcast_ref(&'a self) -> Option<&'a T>; + fn to_owned_any(&'a self) -> Box; +} +impl<'a> QueryParameterValue<'a> for dyn QueryParameter { + fn downcast_ref(&'a self) -> Option<&'a T> { + self.as_any().downcast_ref() + } + + fn to_owned_any(&'a self) -> Box { + Box::new(self.downcast_ref::().cloned().unwrap()) + } +} +impl<'a> QueryParameterValue<'a> for &'a dyn QueryParameter { + fn downcast_ref(&'a self) -> Option<&'a T> { + self.as_any().downcast_ref() + } + + fn to_owned_any(&self) -> Box { + todo!() + } +} + +// Define a zero-sized type to represent the absence of a primary key +// #[derive(Debug, Clone, Copy)] +// pub struct NoPrimaryKey; +// +// // Implement the QueryParameter trait for the zero-sized type +// impl QueryParameter for NoPrimaryKey { +// fn as_any(&'a self) -> &'a dyn Any { +// todo!() +// } +// +// fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { +// todo!() +// } +// +// fn as_sqlserver_param(&self) -> ColumnData<'_> { +// todo!() +// } +// +// fn as_mysql_param(&self) -> &dyn ToValue { +// todo!() +// } +// } +// + +/// Defines a trait for represent type bounds against the allowed +/// data types supported by Canyon to be used as query parameters. +pub trait QueryParameter: Debug + Send + Sync { + fn as_any(&self) -> &dyn Any; + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync); + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_>; + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue; +} + +/// The implementation of the [`crate::connection::tiberius`] [`IntoSql`] for the +/// query parameters. +/// +/// This implementation is necessary because of the generic amplitude +/// of the arguments of the [`crate::transaction::Transaction::query`], that should work with +/// a collection of [`QueryParameter`], in order to allow a workflow +/// that is not dependent of the specific type of the argument that holds +/// the query parameters of the database connectors +#[cfg(feature = "mssql")] +impl<'b> IntoSql<'b> for &'b dyn QueryParameter { + fn into_sql(self) -> ColumnData<'b> { + self.as_sqlserver_param() + } +} + +//TODO Pending to review and see if it is necessary to apply something similar to the previous implementation. + +impl QueryParameter for bool { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::Bit(Some(*self)) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for i16 { + fn as_any(&'_ self) -> &'_ dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::I16(Option::from(*self)) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for Option<&'static i16> { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::I16(Some(*self.unwrap())) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for i32 { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::I32(Some(*self)) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for Option { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::I32(*self) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for u32 { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + panic!("Unsupported sqlserver parameter type "); + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for Option { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + panic!("Unsupported sqlserver parameter type "); + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for f32 { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::F32(Some(*self)) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for Option { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::F32(*self) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for f64 { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::F64(Some(*self)) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for Option { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::F64(*self) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for i64 { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::I64(Some(*self)) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for Option { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::I64(*self) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for String { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::String(Some(std::borrow::Cow::Owned(self.to_owned()))) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for Option { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + match self { + Some(string) => ColumnData::String(Some(std::borrow::Cow::Owned(string.to_owned()))), + None => ColumnData::String(None), + } + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for Option<&'static String> { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + match self { + Some(string) => ColumnData::String(Some(std::borrow::Cow::Borrowed(string))), + None => ColumnData::String(None), + } + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for &'static str { + fn as_any(&self) -> &dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + ColumnData::String(Some(std::borrow::Cow::Borrowed(self))) + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for Option<&'static str> { + fn as_any(&'_ self) -> &'_ dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + match *self { + Some(str) => ColumnData::String(Some(std::borrow::Cow::Borrowed(str))), + None => ColumnData::String(None), + } + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } +} + +impl QueryParameter for NaiveDate { + fn as_any(&'_ self) -> &'_ dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + self.into_sql() + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for Option { + fn as_any(&'_ self) -> &'_ dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + self.into_sql() + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for NaiveTime { + fn as_any(&'_ self) -> &'_ dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + self.into_sql() + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for Option { + fn as_any(&'_ self) -> &'_ dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + self.into_sql() + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +impl QueryParameter for NaiveDateTime { + fn as_any(&'_ self) -> &'_ dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + self.into_sql() + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { + self + } +} + +impl QueryParameter for Option { + fn as_any(&'_ self) -> &'_ dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + self.into_sql() + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + self + } +} + +//TODO pending +impl QueryParameter for DateTime { + fn as_any(&'_ self) -> &'_ dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + self.into_sql() + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + todo!() + } +} + +impl QueryParameter for Option> { + fn as_any(&'_ self) -> &'_ dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + self.into_sql() + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + todo!() + } +} + +impl QueryParameter for DateTime { + fn as_any(&'_ self) -> &'_ dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + self.into_sql() + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + todo!() + } +} + +impl QueryParameter for Option> { + fn as_any(&'_ self) -> &'_ dyn Any { + self + } + + #[cfg(feature = "postgres")] + fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { + self + } + #[cfg(feature = "mssql")] + fn as_sqlserver_param(&self) -> ColumnData<'_> { + self.into_sql() + } + #[cfg(feature = "mysql")] + fn as_mysql_param(&self) -> &dyn ToValue { + todo!() + } +} diff --git a/canyon_core/src/query/query.rs b/canyon_core/src/query/query.rs new file mode 100644 index 00000000..a533c68d --- /dev/null +++ b/canyon_core/src/query/query.rs @@ -0,0 +1,83 @@ +use crate::canyon::Canyon; +use crate::connection::contracts::DbConnection; +use crate::mapper::RowMapper; +use crate::query::parameters::QueryParameter; +use crate::rows::FromSqlOwnedValue; +use crate::transaction::Transaction; +use std::error::Error; +use std::fmt::Debug; + +// TODO: query should implement ToStatement (as the drivers underneath Canyon) or similar +// to be usable directly in the input of Transaction and DbConnenction +/// Holds a sql sentence details +#[derive(Debug)] +pub struct Query<'a> { + sql: String, + params: Vec<&'a dyn QueryParameter>, +} + +impl AsRef for Query<'_> { + fn as_ref(&self) -> &str { + self.sql.as_str() + } +} + +unsafe impl Send for Query<'_> {} +unsafe impl Sync for Query<'_> {} + +impl<'a> Query<'a> { + /// Constructs a new [`Self`] but receiving the number of expected query parameters, allowing + /// to pre-allocate the underlying linear collection that holds the arguments to the exact capacity, + /// potentially saving re-allocations when the query is created + pub fn new(sql: String, params: Vec<&'a dyn QueryParameter>) -> Query<'a> { + Self { sql, params } + } + + /// Returns the SQL sentence of the query + pub const fn sql(&self) -> &str { + self.sql.as_str() + } + + pub const fn params(&self) -> &[&'a dyn QueryParameter] { + self.params.as_slice() + } + + /// Launches the generated query against the database assuming the default + /// [`DbConnection`] + pub async fn launch_default( + self, + ) -> Result, Box> + where + Vec: FromIterator<::Output>, + { + let default_conn = Canyon::instance()?.get_default_connection()?; + ::query(&self.sql, &self.params, default_conn).await + } + + pub async fn launch_one_for_default( + self, + ) -> Result> { + let default_conn = Canyon::instance()?.get_default_connection()?; + ::query_one_for(&self.sql, &self.params, default_conn).await + } + + pub async fn launch_one_for_with( + self, + input: I, + ) -> Result> { + input.query_one_for(&self.sql, &self.params).await + } + + /// Launches the generated query against the database with the selected [`DbConnection`] + pub async fn launch_with( + self, + input: I, + ) -> Result, Box> + where + Vec: FromIterator<::Output>, + { + input.query(&self.sql, &self.params).await + } +} + +impl<'a> Transaction for Query<'a> {} diff --git a/canyon_core/src/query/querybuilder/contracts/mod.rs b/canyon_core/src/query/querybuilder/contracts/mod.rs new file mode 100644 index 00000000..0296eecc --- /dev/null +++ b/canyon_core/src/query/querybuilder/contracts/mod.rs @@ -0,0 +1,221 @@ +//! Defines the operation traits exposed by Canyon-SQL query builders. +//! +//! Each trait groups the operations available for a specific SQL statement, +//! while [`QueryBuilderOps`] contains the behaviour shared by all builders. + +use crate::query::bounds::{FieldIdentifier, FieldValueIdentifier}; +use crate::query::operators::Operator; +use crate::query::parameters::QueryParameter; +use crate::query::query::Query; +use crate::query::querybuilder::syntax::column::ColumnRef; +use crate::query::querybuilder::syntax::table_metadata::TableMetadata; +use std::error::Error; + +/// Operations supported by a delete query builder. +/// +/// Delete queries currently require no statement-specific operations beyond +/// those provided by [`QueryBuilderOps`]. +pub trait DeleteQueryBuilderOps<'a>: QueryBuilderOps<'a> {} + +/// Operations supported by an update query builder. +pub trait UpdateQueryBuilderOps<'a>: QueryBuilderOps<'a> { + /// Defines the columns assigned by the generated `SET` clause. + /// + /// This method only registers column references. It does not collect the + /// values corresponding to the generated placeholders. + /// + /// The caller is therefore responsible for supplying matching parameters + /// when the query is executed. + fn set>>( + self, + columns: Vec, + ) -> Result> + where + Self: Sized; + + /// Defines the `SET` clause and collects one update value for each column. + /// + /// Each tuple contains the target column identifier and the parameter value + /// assigned to it. + fn set_values( + self, + columns: &'a [(Z, Q)], + ) -> Result> + where + Z: FieldIdentifier + Into> + Clone, + Q: QueryParameter, + Self: Sized; +} + +/// Operations supported by an insert query builder. +pub trait InsertQueryBuilderOps<'a>: QueryBuilderOps<'a> { + /// Defines the columns targeted by the insert statement. + /// + /// When omitted, the generated statement does not include an explicit + /// column list. + fn with_columns>>(self, columns: Vec) -> Self; + + /// Collects the values inserted by the statement. + /// + /// The generated placeholder count must match the number of configured + /// insert columns when an explicit column list is present. + fn with_values(self, values: &'a [Q]) -> Result> + where + Q: QueryParameter, + Self: Sized; + + /// Defines the columns returned after a successful insert. + /// + /// The resulting SQL is emitted according to the target database dialect, + /// such as `RETURNING` or `OUTPUT INSERTED`. + fn returning(self, columns: Vec>>) -> Self; +} + +/// Operations supported by a select query builder. +pub trait SelectQueryBuilderOps<'a>: QueryBuilderOps<'a> { + /// Defines the columns projected by the select statement. + /// + /// When omitted, the query projects all columns using `SELECT *`. + fn with_columns>>(self, columns: Vec) -> Self; + + /// Marks the select statement as `DISTINCT`. + fn with_distinct(self) -> Self; + + /// Changes the select projection to a row count. + fn count(self) -> Self; + + /// Adds a `LEFT JOIN` to the select statement. + /// + /// `join_table` identifies the joined table, while `col1` and `col2` + /// define the two column references used by the join condition. + /// + /// The order of the column references does not affect the generated + /// equality condition. + fn left_join( + self, + join_table: impl Into>, + col1: impl Into>, + col2: impl Into>, + ) -> Self; + + /// Adds an `INNER JOIN` to the select statement. + /// + /// `join_table` identifies the joined table, while `col1` and `col2` + /// define the two column references used by the join condition. + /// + /// The order of the column references does not affect the generated + /// equality condition. + fn inner_join( + self, + join_table: impl Into>, + col1: impl Into>, + col2: impl Into>, + ) -> Self; + + /// Adds a `RIGHT JOIN` to the select statement. + /// + /// `join_table` identifies the joined table, while `col1` and `col2` + /// define the two column references used by the join condition. + /// + /// The order of the column references does not affect the generated + /// equality condition. + fn right_join( + self, + join_table: impl Into>, + col1: impl Into>, + col2: impl Into>, + ) -> Self; + + /// Adds a `FULL JOIN` to the select statement. + /// + /// `join_table` identifies the joined table, while `col1` and `col2` + /// define the two column references used by the join condition. + /// + /// The order of the column references does not affect the generated + /// equality condition. + fn full_join( + self, + join_table: impl Into>, + col1: impl Into>, + col2: impl Into>, + ) -> Self; + + /// Adds an `ORDER BY` clause for the specified column. + /// + /// When `desc` is `true`, descending order is used. Otherwise, the + /// generated ordering is ascending. + fn order_by>>(self, order_by: Z, desc: bool) -> Self; +} + +/// Common operations supported by every query builder. +/// +/// Statement-specific builders expose this shared filtering and build API, +/// while traits such as [`SelectQueryBuilderOps`], [`InsertQueryBuilderOps`], +/// and [`UpdateQueryBuilderOps`] add operations that only apply to their +/// corresponding SQL statement. +/// +/// Implementations collect structured query data and parameters. SQL generation +/// is deferred until [`Self::build`] consumes the builder and emits a [`Query`] +/// for the configured database dialect. +pub trait QueryBuilderOps<'a> { + /// Consumes the builder and generates the final query. + /// + /// The returned [`Query`] contains both the emitted SQL statement and the + /// parameters collected while constructing it. + fn build(self) -> Result, Box>; + + /// Adds a `WHERE` condition without collecting a parameter value. + /// + /// `column` identifies the left-hand side of the condition and `op` + /// defines the comparison operator. + /// + /// The condition emits a placeholder whose corresponding value must be + /// supplied separately. + fn r#where>>(self, column: I, op: Operator) -> Self; + + /// Adds a `WHERE` condition and collects its parameter value. + /// + /// The [`FieldValueIdentifier`] provides both the target column and the + /// value bound to the generated placeholder. + fn where_value(self, column: &'a Z, op: Operator) -> Self; + + /// Adds an `AND` condition and collects its parameter value. + /// + /// The [`FieldValueIdentifier`] provides both the target column and the + /// value bound to the generated placeholder. + fn and(self, column: &'a Z, op: Operator) -> Self; + + /// Adds an `AND IN (...)` condition. + /// + /// One placeholder and one collected query parameter are generated for + /// every element in `values`. + fn and_values_in<'b, Z, Q>( + self, + column: Z, + values: &'a [Q], + ) -> Result> + where + Z: FieldIdentifier, + Q: QueryParameter, + Self: Sized; + + /// Adds an `OR IN (...)` condition. + /// + /// One placeholder and one collected query parameter are generated for + /// every element in `values`. + fn or_values_in<'b, Z, Q>( + self, + r#or: Z, + values: &'a [Q], + ) -> Result> + where + Z: FieldIdentifier, + Q: QueryParameter, + Self: Sized; + + /// Adds an `OR` condition and collects its parameter value. + /// + /// The [`FieldValueIdentifier`] provides both the target column and the + /// value bound to the generated placeholder. + fn or(self, column: &'a Z, op: Operator) -> Self; +} diff --git a/canyon_core/src/query/querybuilder/mod.rs b/canyon_core/src/query/querybuilder/mod.rs new file mode 100644 index 00000000..a2e872da --- /dev/null +++ b/canyon_core/src/query/querybuilder/mod.rs @@ -0,0 +1,5 @@ +pub mod contracts; +pub mod syntax; +pub mod types; + +pub use self::{contracts::*, types::*}; diff --git a/canyon_core/src/query/querybuilder/syntax/ast/delete.rs b/canyon_core/src/query/querybuilder/syntax/ast/delete.rs new file mode 100644 index 00000000..159d2288 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/ast/delete.rs @@ -0,0 +1,17 @@ +use crate::query::querybuilder::syntax::{emitter::AstProcessor, query_kind::QueryKind}; + +/// Structured representation of a `DELETE` statement. +#[derive(Default)] +pub struct DeleteAst {} + +impl<'a> AstProcessor<'a> for DeleteAst { + fn query_kind(&self) -> QueryKind { + QueryKind::Delete + } +} + +impl DeleteAst { + pub const fn new() -> Self { + Self {} + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/ast/insert.rs b/canyon_core/src/query/querybuilder/syntax/ast/insert.rs new file mode 100644 index 00000000..65138c2a --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/ast/insert.rs @@ -0,0 +1,25 @@ +pub(crate) use crate::query::querybuilder::syntax::{ + column::ColumnRef, emitter::AstProcessor, query_kind::QueryKind, +}; + +/// Structured representation of a `INSERT` statement. +#[derive(Default)] +pub struct InsertAst<'a> { + pub columns: Vec>, + pub returning_columns: Vec>, +} + +impl<'a> AstProcessor<'a> for InsertAst<'a> { + fn query_kind(&self) -> QueryKind { + QueryKind::Insert + } +} + +impl<'a> InsertAst<'a> { + pub const fn new() -> Self { + Self { + columns: Vec::new(), + returning_columns: Vec::new(), + } + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/ast/mod.rs b/canyon_core/src/query/querybuilder/syntax/ast/mod.rs new file mode 100644 index 00000000..0d9b92eb --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/ast/mod.rs @@ -0,0 +1,52 @@ +pub(crate) mod delete; +pub(crate) mod insert; +pub(crate) mod select; +pub(crate) mod update; + +use crate::query::querybuilder::syntax::clause::ConditionClause; +use crate::query::querybuilder::syntax::table_metadata::TableMetadata; + +/// Query data shared by every statement-specific AST. +/// +/// `BaseAst` stores the target table and the ordered collection of conditions +/// used by `SELECT`, `INSERT`, `UPDATE`, and `DELETE` statements. +#[derive(Default)] +pub struct BaseAst<'a> { + table: TableMetadata<'a>, + conditions: Vec>, +} + +impl<'a> BaseAst<'a> { + /// Creates a base AST from an already constructed [`TableMetadata`]. + pub const fn new_ast(table: TableMetadata<'a>) -> Self { + Self { + table, + conditions: Vec::new(), + } + } + + /// Creates a base AST for the provided table. + pub fn new(table: impl Into>) -> Self { + Self { + table: table.into(), + conditions: Vec::new(), + } + } + + /// Returns the table targeted by the query. + #[inline(always)] + pub const fn table(&self) -> &TableMetadata<'a> { + &self.table + } + + /// Returns the conditions registered on the query, in insertion order. + #[inline(always)] + pub const fn conditions(&self) -> &[ConditionClause<'a>] { + self.conditions.as_slice() + } + + /// Appends a condition to the query. + pub fn add_condition(&mut self, condition: ConditionClause<'a>) { + self.conditions.push(condition); + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/ast/select.rs b/canyon_core/src/query/querybuilder/syntax/ast/select.rs new file mode 100644 index 00000000..96b83ada --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/ast/select.rs @@ -0,0 +1,53 @@ +use crate::query::querybuilder::syntax::{ + column::ColumnRef, emitter::AstProcessor, having::HavingClause, join::JoinClause, + order::OrderByClause, query_kind::QueryKind, +}; + +/// Structured representation of a `SELECT` statement. +/// +/// `SelectAst` stores the clauses and modifiers that are specific to selection +/// queries. +/// +/// NOTE: The target table and filtering conditions are held separately by +/// the shared base AST. +#[derive(Default)] +pub struct SelectAst<'a> { + pub columns: Vec>, + /// Indicates whether the projection must emit a row count. + pub is_count_query: bool, + /// Indicates whether the query must emit `SELECT DISTINCT`. + pub with_distinct: bool, + /// Join clauses, preserved in insertion order. + pub joins: Vec>, + pub order_by: Option>, + pub having: Option>, + pub group_by: Option>>, + // TODO: replace the primitive value with a dedicated domain type. + pub limit: Option, + // TODO: replace the primitive value with a dedicated domain type. + pub offset: Option, +} + +impl<'a> SelectAst<'a> { + /// Creates an empty `SELECT` AST with no optional clauses or modifiers. + pub const fn new() -> Self { + Self { + columns: Vec::new(), + is_count_query: false, + with_distinct: false, + joins: Vec::new(), + order_by: None, + group_by: None, + having: None, + limit: None, + offset: None, + } + } +} + +impl<'a> AstProcessor<'a> for SelectAst<'a> { + /// Identifies this AST as a `SELECT` query. + fn query_kind(&self) -> QueryKind { + QueryKind::Select + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/ast/update.rs b/canyon_core/src/query/querybuilder/syntax/ast/update.rs new file mode 100644 index 00000000..986cb910 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/ast/update.rs @@ -0,0 +1,28 @@ +use crate::query::querybuilder::syntax::{ + column::ColumnRef, emitter::AstProcessor, query_kind::QueryKind, +}; + +/// Structured representation of a `UPDATE` statement. +pub struct UpdateAst<'a> { + pub columns: Vec>, +} + +impl<'a> AstProcessor<'a> for UpdateAst<'a> { + fn query_kind(&self) -> QueryKind { + QueryKind::Update + } +} + +impl<'a> Default for UpdateAst<'a> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> UpdateAst<'a> { + pub fn new() -> Self { + Self { + columns: Vec::new(), + } + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/clause.rs b/canyon_core/src/query/querybuilder/syntax/clause.rs new file mode 100644 index 00000000..ebf61a9c --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/clause.rs @@ -0,0 +1,91 @@ +use crate::query::operators::{LikeKind, Operator}; +use crate::query::querybuilder::syntax::column::ColumnRef; +use crate::query::querybuilder::syntax::dialect::SqlDialect; +use crate::query::querybuilder::syntax::emitter::types::helpers::Range; +use crate::query::querybuilder::syntax::keyword::Keyword; +use crate::query::querybuilder::syntax::tokens::{SqlToken, SqlTokens, ToSqlTokens}; + +pub struct ConditionClause<'a> { + pub(crate) kind: ConditionClauseKind, + pub(crate) column_name: ColumnRef<'a>, + pub(crate) operator: Operator, + pub(crate) value_indexes: Option, +} + +#[derive(Eq, PartialEq, Copy, Clone, Debug)] +pub enum ConditionClauseKind { + Where, + And, + In, + Or, + AndValuesIn, + OrValuesIn, +} + +impl From for Keyword { + fn from(keyword: ConditionClauseKind) -> Self { + match keyword { + ConditionClauseKind::Where => Keyword::Where, + ConditionClauseKind::And | ConditionClauseKind::AndValuesIn => Keyword::And, + ConditionClauseKind::Or | ConditionClauseKind::OrValuesIn => Keyword::Or, + ConditionClauseKind::In => Keyword::In, + } + } +} + +impl<'a, D: SqlDialect> ToSqlTokens<'a, D> for ConditionClause<'a> { + fn to_tokens(&self) -> impl IntoIterator> + 'a { + let mut out = SqlTokens::with_capacity(4); + + // Clause keyword + out.keyword(self.kind.into()); + + // Column + out.extend( as ToSqlTokens<'_, D>>::to_tokens( + &self.column_name, + )); + + // Operator + out.operator(self.operator); + + match self.operator { + Operator::Like(kind) | Operator::NotLike(kind) => { + let like_tokens = >::to_tokens(&kind); + out.extend(like_tokens); + } + _ => { + if let Some(ref range) = self.value_indexes + && range.is_range() + { + __impl::output_range_of_placeholders::(range, &mut out); + } else { + out.placeholder(); + } + } + } + + out + } +} + +mod __impl { + use crate::query::querybuilder::syntax::dialect::SqlDialect; + use crate::query::querybuilder::syntax::emitter::types::helpers::Range; + use crate::query::querybuilder::syntax::symbol::Symbol; + use crate::query::querybuilder::syntax::tokens::SqlTokens; + + pub(crate) fn output_range_of_placeholders( + range: &Range, + out: &mut SqlTokens<'_>, + ) { + out.symbol(Symbol::LParen); + let mut indexes = range.into_iter().peekable(); + while indexes.next().is_some() { + out.placeholder(); + if indexes.peek().is_some() { + out.symbol(Symbol::Comma); + } + } + out.symbol(Symbol::RParen); + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/column.rs b/canyon_core/src/query/querybuilder/syntax/column.rs new file mode 100644 index 00000000..929240b0 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/column.rs @@ -0,0 +1,415 @@ +use crate::query::bounds::FieldIdentifier; +use crate::query::querybuilder::syntax::dialect::SqlDialect; +use crate::query::querybuilder::syntax::tokens::{SqlToken, SqlTokens, ToSqlTokens}; +use std::borrow::Cow; + +/// Whether a column reference is qualified with a table name or not, meaning that will be emitted as `table.column` or just `column`. +#[derive(Copy, Clone)] +pub(crate) enum Qualification { + Qualified, + Unqualified, +} + +#[derive(Default, Clone)] +pub struct ColumnRef<'a> { + pub table: Option>, + pub column: Cow<'a, str>, + pub alias: Option>, +} + +impl<'a, T> From for ColumnRef<'a> +where + T: FieldIdentifier + 'a, +{ + fn from(value: T) -> Self { + value.as_column_ref() + } +} + +impl<'a> From<&'a str> for ColumnRef<'a> { + fn from(value: &'a str) -> Self { + __impl::column_ref_from_str_ref(value) + } +} + +impl<'a> From<&'a &'a str> for ColumnRef<'a> { + // This impl is provided to avoid to impl quote::ToTokens to some artificial types that maps values at compile time from this + fn from(value: &'a &'a str) -> Self { + Self::from(*value) + } +} + +impl<'a> From<&'a String> for ColumnRef<'a> { + fn from(value: &'a String) -> Self { + __impl::column_ref_from_str_ref(value.as_str()) + } +} + +impl<'a> From for ColumnRef<'a> { + fn from(value: String) -> Self { + __impl::column_ref_from_string(value) + } +} + +impl<'a> From> for ColumnRef<'a> { + fn from(value: Cow<'a, str>) -> Self { + match value { + Cow::Borrowed(value) => __impl::column_ref_from_str_ref(value), + Cow::Owned(value) => __impl::column_ref_from_string(value), + } + } +} + +impl<'a, D: SqlDialect> ToSqlTokens<'a, D> for ColumnRef<'a> { + fn to_tokens(&self) -> impl IntoIterator> + 'a { + let mut out = SqlTokens::with_capacity(__detail::calculate_column_ref_capacity(self)); + __impl::generate_column_ref_tokens::(self, &mut out); + out + } +} + +impl<'a> ColumnRef<'a> { + pub fn new(table_name: &'a str, column_name: &'a str) -> Self { + Self { + column: Cow::Borrowed(column_name), + table: Some(Cow::Borrowed(table_name)), + alias: None, + } + } + + pub(crate) fn emit( + &self, + qualification: Qualification, + tokens: &mut SqlTokens<'a>, + ) { + match qualification { + Qualification::Qualified => { + tokens.extend( as ToSqlTokens<'_, D>>::to_tokens(self)); + } + Qualification::Unqualified => { + tokens.extend( as ToSqlTokens<'_, D>>::to_tokens( + &self.column, + )); + } + } + } + + /// Returns the column name + #[inline(always)] + pub fn name(&self) -> Cow<'a, str> { + self.column.clone() + } +} + +mod __impl { + use crate::query::querybuilder::syntax::column::{__detail, ColumnRef}; + use crate::query::querybuilder::syntax::dialect::SqlDialect; + use crate::query::querybuilder::syntax::emitter::types::helpers; + use crate::query::querybuilder::syntax::keyword::Keyword; + use crate::query::querybuilder::syntax::symbol::Symbol::Dot; + use crate::query::querybuilder::syntax::tokens::SqlTokens; + use std::borrow::Cow; + + pub(crate) fn column_ref_from_str_ref(value: &str) -> ColumnRef<'_> { + let trimmed = value.trim(); + + let (before_alias, alias) = match __detail::find_case_insensitive_as(trimmed) { + Some(idx) => { + let (left, right) = trimmed.split_at(idx); + let right = right[2..].trim_start(); + (left.trim(), Some(Cow::Borrowed(right.trim()))) + } + None => (trimmed, None), + }; + + let (table, column) = match before_alias.split_once('.') { + Some((tbl, col)) => (Some(Cow::Borrowed(tbl.trim())), Cow::Borrowed(col.trim())), + None => (None, Cow::Borrowed(before_alias.trim())), + }; + + ColumnRef { + table, + column, + alias, + } + } + + pub(crate) fn column_ref_from_string(value: String) -> ColumnRef<'static> { + let trimmed = value.trim(); + + let (before_alias, alias) = match __detail::find_case_insensitive_as(trimmed) { + Some(idx) => { + let (left, right) = trimmed.split_at(idx); + let right = right[2..].trim_start(); + (left.trim(), Some(right.trim().to_owned())) + } + None => (trimmed, None), + }; + + let (table, column) = match before_alias.split_once('.') { + Some((tbl, col)) => (Some(tbl.trim().to_owned()), col.trim().to_owned()), + None => (None, before_alias.trim().to_owned()), + }; + + ColumnRef { + table: table.map(Cow::Owned), + column: Cow::Owned(column), + alias: alias.map(Cow::Owned), + } + } + + pub(crate) fn generate_column_ref_tokens<'a, D: SqlDialect>( + __self: &ColumnRef<'a>, + out: &mut SqlTokens<'a>, + ) { + if let Some(table_ref) = &__self.table { + helpers::push_quoted_ident::(table_ref.clone(), out); + out.symbol(Dot) + } + + helpers::push_quoted_ident::(__self.column.clone(), out); + + if let Some(alias) = &__self.alias { + out.keyword(Keyword::As); + helpers::push_quoted_ident::(alias.clone(), out); + } + } +} + +mod __detail { + use crate::query::querybuilder::syntax::column::ColumnRef; + + pub(crate) fn find_case_insensitive_as(s: &str) -> Option { + let bytes = s.as_bytes(); + for i in 0..bytes.len().saturating_sub(2) { + let a = bytes[i]; + let b = bytes[i + 1]; + + // Match case-insensitive ASCII + let is_a = a == b'a' || a == b'A'; + let is_s = b == b's' || b == b'S'; + + if is_a && is_s { + let before_ok = i > 0 && bytes[i - 1].is_ascii_whitespace(); + let after_ok = i + 2 < bytes.len() && bytes[i + 2].is_ascii_whitespace(); + + if before_ok && after_ok { + return Some(i); + } + } + } + None + } + + pub(crate) fn calculate_column_ref_capacity(__self: &ColumnRef) -> usize { + let mut counter = 1; // at least the column name + if __self.table.is_some() { + counter += 2; // table name + dot + } + if __self.alias.is_some() { + counter += 2; // AS + alias name + } + counter + } +} + +#[cfg(test)] +mod column_ref_from_str_tests { + use super::ColumnRef; + use std::borrow::Cow; + + #[test] + fn test_column_ref_simple_column() { + let c = ColumnRef::from("name"); + assert_eq!(c.table.as_deref(), None); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), None); + } + + #[test] + fn test_column_ref_table_column() { + let c = ColumnRef::from("users.name"); + assert_eq!(c.table.as_deref(), Some("users")); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), None); + } + + #[test] + fn test_column_ref_with_alias_uppercase_as() { + let c = ColumnRef::from("users.name AS n"); + assert_eq!(c.table.as_deref(), Some("users")); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), Some("n")); + } + + #[test] + fn test_column_ref_with_alias_lowercase_as() { + let c = ColumnRef::from("users.name as n"); + assert_eq!(c.table.as_deref(), Some("users")); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), Some("n")); + } + + #[test] + fn test_column_ref_with_alias_mixed_case_as() { + let c = ColumnRef::from("users.name As n"); + assert_eq!(c.table.as_deref(), Some("users")); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), Some("n")); + } + + #[test] + fn test_column_ref_multiple_spaces_around_as() { + let c = ColumnRef::from("users.name AS n"); + assert_eq!(c.table.as_deref(), Some("users")); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), Some("n")); + } + + #[test] + fn test_column_ref_alias_without_table() { + let c = ColumnRef::from("name AS n"); + assert_eq!(c.table.as_deref(), None); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), Some("n")); + } + + #[test] + fn test_column_ref_no_alias_when_as_not_valid() { + let c = ColumnRef::from("nameASn"); + assert_eq!(c.table.as_deref(), None); + assert_eq!(c.column.as_ref(), "nameASn"); + assert_eq!(c.alias.as_deref(), None); + } + + #[test] + fn test_column_ref_trim_whitespace() { + let c = ColumnRef::from(" users.name AS n "); + assert_eq!(c.table.as_deref(), Some("users")); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), Some("n")); + } + + #[test] + fn test_column_ref_alias_complex() { + let c = ColumnRef::from("users.full_name AS fullNameAlias"); + assert_eq!(c.table.as_deref(), Some("users")); + assert_eq!(c.column.as_ref(), "full_name"); + assert_eq!(c.alias.as_deref(), Some("fullNameAlias")); + } + + #[test] + fn test_column_ref_no_table_but_alias() { + let c = ColumnRef::from("email AS e"); + assert_eq!(c.table.as_deref(), None); + assert_eq!(c.column.as_ref(), "email"); + assert_eq!(c.alias.as_deref(), Some("e")); + } + + #[test] + fn test_column_ref_only_column_and_spaces() { + let c = ColumnRef::from(" column_name "); + assert_eq!(c.table.as_deref(), None); + assert_eq!(c.column.as_ref(), "column_name"); + assert_eq!(c.alias.as_deref(), None); + } + + #[test] + fn test_column_ref_only_table_column_with_spaces() { + let c = ColumnRef::from(" users . name "); + assert_eq!(c.table.as_deref(), Some("users")); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), None); + } + + #[test] + fn test_column_ref_from_owned_string() { + let c = ColumnRef::from(String::from("users.name AS n")); + assert_eq!(c.table.as_deref(), Some("users")); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), Some("n")); + } + + #[test] + fn test_column_ref_from_string_ref() { + let value = String::from("users.name AS n"); + let c = ColumnRef::from(&value); + assert_eq!(c.table.as_deref(), Some("users")); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), Some("n")); + } + + #[test] + fn test_column_ref_from_owned_cow() { + let c = ColumnRef::from(Cow::Owned(String::from("users.name AS n"))); + assert_eq!(c.table.as_deref(), Some("users")); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), Some("n")); + } + + #[test] + fn test_column_ref_from_borrowed_cow() { + let c = ColumnRef::from(Cow::Borrowed("users.name AS n")); + assert_eq!(c.table.as_deref(), Some("users")); + assert_eq!(c.column.as_ref(), "name"); + assert_eq!(c.alias.as_deref(), Some("n")); + } +} + +#[cfg(test)] +mod column_ref_alias_as_detection_tests { + use crate::query::querybuilder::syntax::column::__detail::find_case_insensitive_as; + + #[test] + fn test_find_as_basic_uppercase() { + let idx = find_case_insensitive_as("col AS x").unwrap(); + assert_eq!(&"col AS x"[idx..idx + 2], "AS"); + } + + #[test] + fn test_find_as_lowercase() { + let idx = find_case_insensitive_as("col as x").unwrap(); + assert_eq!(&"col as x"[idx..idx + 2], "as"); + } + + #[test] + fn test_find_as_mixed_case() { + let idx = find_case_insensitive_as("col As x").unwrap(); + assert_eq!(&"col As x"[idx..idx + 2], "As"); + } + + #[test] + fn test_find_as_with_multiple_spaces() { + let idx = find_case_insensitive_as("col AS x").unwrap(); + assert_eq!(&"col AS x"[idx..idx + 2], "AS"); + } + + #[test] + fn test_find_as_requires_space_before_and_after() { + assert!(find_case_insensitive_as("colASx").is_none()); + assert!(find_case_insensitive_as("col ASx").is_none()); + assert!(find_case_insensitive_as("colAS x").is_none()); + assert!(find_case_insensitive_as("ASx").is_none()); + assert!(find_case_insensitive_as("xAS").is_none()); + } + + #[test] + fn test_find_as_at_start_or_end() { + assert!(find_case_insensitive_as(" AS x").is_some()); + assert!(find_case_insensitive_as("x AS ").is_some()); + } + + #[test] + fn test_find_as_no_match() { + assert!(find_case_insensitive_as("column something").is_none()); + assert!(find_case_insensitive_as("").is_none()); + assert!(find_case_insensitive_as("a s").is_none()); + assert!(find_case_insensitive_as("col AX x").is_none()); + } + + #[test] + fn test_find_as_with_table_column() { + let idx = find_case_insensitive_as("table.col as alias").unwrap(); + assert_eq!(&"table.col as alias"[idx..idx + 2], "as"); + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/dialect.rs b/canyon_core/src/query/querybuilder/syntax/dialect.rs new file mode 100644 index 00000000..a71461fc --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/dialect.rs @@ -0,0 +1,159 @@ +use crate::connection::database_type::DatabaseType; +use std::fmt::Display; + +/// Governs syntax rules such as placeholder format, +/// quoting style, and supported clauses. +/// For example, PostgreSQL may allow `RETURNING`, while MySQL does not. +/// +/// Default values are set to the most common and widely supported syntax, which is +/// the ANSI SQL standard. Specific dialects can override these defaults as needed. +pub trait SqlDialect { + const DB: DatabaseType; + const SUPPORTS_RETURNING: bool = true; + const _SUPPORTS_LIMIT_OFFSET: bool = true; // TODO: pending to implement + const IDENT_QUOTING: IdentQuotingStyle = IdentQuotingStyle::DoubleQuote; + const PLACEHOLDER_SYMBOL: PlaceholderSymbol = PlaceholderSymbol::DollarNumbered; + const PLACEHOLDER_DATA_TYPE: PlaceholderDatatype = PlaceholderDatatype::Varchar; +} + +#[cfg(feature = "postgres")] +pub struct PgDialect; +#[cfg(feature = "postgres")] +impl SqlDialect for PgDialect { + const DB: DatabaseType = DatabaseType::PostgreSql; + const IDENT_QUOTING: IdentQuotingStyle = IdentQuotingStyle::DoubleQuote; + + const PLACEHOLDER_SYMBOL: PlaceholderSymbol = PlaceholderSymbol::DollarNumbered; +} + +#[cfg(feature = "mssql")] +pub struct MsSql; +#[cfg(feature = "mssql")] +impl SqlDialect for MsSql { + const DB: DatabaseType = DatabaseType::SqlServer; + const IDENT_QUOTING: IdentQuotingStyle = IdentQuotingStyle::Bracket; + const PLACEHOLDER_SYMBOL: PlaceholderSymbol = PlaceholderSymbol::AtPNumbered; +} + +#[cfg(feature = "mysql")] +pub struct MySql; +#[cfg(feature = "mysql")] +impl SqlDialect for MySql { + const DB: DatabaseType = DatabaseType::MySQL; + const SUPPORTS_RETURNING: bool = false; + const IDENT_QUOTING: IdentQuotingStyle = IdentQuotingStyle::Backtick; + const PLACEHOLDER_SYMBOL: PlaceholderSymbol = PlaceholderSymbol::QuestionMark; + const PLACEHOLDER_DATA_TYPE: PlaceholderDatatype = PlaceholderDatatype::Char; +} + +/// Identifier quoting strategy for a SQL dialect. +/// +/// This is intentionally small and purely syntactic: it only describes how +/// table names, column names, schema names, aliases, etc. must be delimited +/// when quoting is required. +/// +/// Backend mapping: +/// - ANSI / generic SQL: `"ident"` +/// - PostgreSQL: `"ident"` +/// - MySQL: `` `ident` `` +/// - SQL Server: `[ident]` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IdentQuotingStyle { + /// ANSI SQL style, used by PostgreSQL and as the generic default. + DoubleQuote, + /// MySQL style. + Backtick, + /// SQL Server style. + Bracket, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IdentQuoting { + DoubleQuote, + Backtick, + OpeningBracket, + ClosingBracket, +} + +impl IdentQuotingStyle { + #[inline] + pub const fn opening(self) -> IdentQuoting { + match self { + Self::DoubleQuote => IdentQuoting::DoubleQuote, + Self::Backtick => IdentQuoting::Backtick, + Self::Bracket => IdentQuoting::OpeningBracket, + } + } + + #[inline] + pub const fn closing(self) -> IdentQuoting { + match self { + Self::DoubleQuote => IdentQuoting::DoubleQuote, + Self::Backtick => IdentQuoting::Backtick, + Self::Bracket => IdentQuoting::ClosingBracket, + } + } +} + +impl Display for IdentQuoting { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let t = match self { + Self::DoubleQuote => "\"", + Self::Backtick => "`", + Self::OpeningBracket => "[", + Self::ClosingBracket => "]", + }; + write!(f, "{}", t) + } +} + +/// Represents the syntax style for parameter placeholders in prepared statements. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PlaceholderSymbol { + /// ?, ?, ? + QuestionMark, + /// $1, $2, $3 + DollarNumbered, + /// @p1, @p2, @p3 + AtPNumbered, + /// :1, :2, :3 + _ColonNumbered, +} + +impl Display for PlaceholderSymbol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let symbol = match self { + Self::QuestionMark => "?", + Self::DollarNumbered => "$", + Self::AtPNumbered => "@P", + Self::_ColonNumbered => ":", + }; + write!(f, "{}", symbol) + } +} + +/// Represents the syntax style for parameter placeholders in prepared statements. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PlaceholderDatatype { + Varchar, + Char, +} + +impl From for &'static str { + fn from(datatype: PlaceholderDatatype) -> Self { + match datatype { + PlaceholderDatatype::Varchar => "VARCHAR", + PlaceholderDatatype::Char => "CHAR", + } + } +} + +impl Display for PlaceholderDatatype { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let datatype = match self { + Self::Varchar => "VARCHAR", + Self::Char => "CHAR", + }; + write!(f, "{}", datatype) + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/emitter/backends/mod.rs b/canyon_core/src/query/querybuilder/syntax/emitter/backends/mod.rs new file mode 100644 index 00000000..6b74cb73 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/emitter/backends/mod.rs @@ -0,0 +1,12 @@ +#[cfg(feature = "postgres")] +mod pg; +#[cfg(feature = "postgres")] +pub use pg::PgEmitter; +#[cfg(feature = "mssql")] +mod mssql; +#[cfg(feature = "mssql")] +pub use mssql::SqlServerEmitter; +#[cfg(feature = "mysql")] +mod mysql; +#[cfg(feature = "mysql")] +pub use mysql::MySqlEmitter; diff --git a/canyon_core/src/query/querybuilder/syntax/emitter/backends/mssql.rs b/canyon_core/src/query/querybuilder/syntax/emitter/backends/mssql.rs new file mode 100644 index 00000000..e3685423 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/emitter/backends/mssql.rs @@ -0,0 +1,167 @@ +use crate::query::querybuilder::syntax::{ + ast::{delete::DeleteAst, insert::InsertAst, select::SelectAst, update::UpdateAst}, + dialect::MsSql, + emitter::{ + EmitStep, SqlEmitter, types::delete::delete_default_plan, types::helpers, types::insert, + types::select::select_default_plan, types::update::update_default_plan, + }, +}; + +#[derive(Default)] +pub struct SqlServerEmitter {} + +impl<'a> SqlEmitter<'a, SelectAst<'a>> for SqlServerEmitter { + type Dialect = MsSql; + + const PLAN: &'a [EmitStep<'a, SelectAst<'a>>] = select_default_plan!(Self::Dialect); +} + +impl<'a> SqlEmitter<'a, InsertAst<'a>> for SqlServerEmitter { + type Dialect = MsSql; + + const PLAN: &'a [EmitStep<'a, InsertAst<'a>>] = &[ + insert::__impl::emit_insert_into_keywords, + |_ast, base_ast, tokens| helpers::emit_table::(base_ast.table(), tokens), + |ast, _base_ast, tokens| { + __impl::emit_unqualified_columns::(&ast.columns, tokens) + }, + |ast, base_ast, tokens| __impl::emit_output::(ast, base_ast, tokens), + |ast, base_ast, tokens| insert::__impl::emit_values(ast, base_ast, tokens), + ]; +} + +impl<'a> SqlEmitter<'a, UpdateAst<'a>> for SqlServerEmitter { + type Dialect = MsSql; + + const PLAN: &'a [EmitStep<'a, UpdateAst<'a>>] = update_default_plan!(Self::Dialect); +} + +impl<'a> SqlEmitter<'a, DeleteAst> for SqlServerEmitter { + type Dialect = MsSql; + + const PLAN: &'a [EmitStep<'a, DeleteAst>] = delete_default_plan!(Self::Dialect); +} + +mod __impl { + use crate::query::ColumnRef; + use crate::query::querybuilder::syntax::column::Qualification; + use crate::query::querybuilder::syntax::emitter::types::helpers; + use crate::query::querybuilder::syntax::symbol::Symbol; + use crate::query::querybuilder::syntax::symbol::Symbol::LParen; + use crate::query::querybuilder::syntax::{ + ast::{BaseAst, insert::InsertAst}, + dialect::SqlDialect, + keyword::Keyword, + tokens::SqlTokens, + }; + + pub(crate) fn emit_unqualified_columns<'a, D: SqlDialect>( + columns: &[ColumnRef<'a>], + tokens: &mut SqlTokens<'a>, + ) { + tokens.symbol(LParen); + helpers::emit_columns::(columns, Qualification::Unqualified, tokens); + tokens.symbol(Symbol::RParen); + } + + pub(super) fn emit_output<'a, D>( + ast: &InsertAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) where + D: SqlDialect, + { + if ast.returning_columns.is_empty() { + return; + } + + tokens.keyword(Keyword::Output); + + for (index, column) in ast.returning_columns.iter().enumerate() { + if index != 0 { + tokens.comma(); + } + + tokens.keyword(Keyword::Inserted); + tokens.dot(); + + helpers::push_quoted_ident::(column.name(), tokens); + } + } +} + +#[cfg(test)] +mod tests { + use super::__impl::emit_output; + use crate::query::querybuilder::syntax::{ + ast::{BaseAst, insert::InsertAst}, + column::ColumnRef, + dialect::MsSql, + tokens::SqlTokens, + writer::TokenWriter, + }; + + fn render_output<'a>(ast: &'a InsertAst<'a>) -> String { + let mut base_ast = BaseAst::default(); + let mut tokens = SqlTokens::default(); + + emit_output::(ast, &mut base_ast, &mut tokens); + + TokenWriter::new() + .render::(tokens) + .expect("OUTPUT tokens should render successfully") + } + + #[test] + fn does_not_emit_output_when_returning_columns_are_empty() { + let ast = InsertAst { + returning_columns: vec![], + ..Default::default() + }; + + assert_eq!(render_output(&ast), ";"); + } + + #[test] + fn emits_output_for_one_returning_column() { + let ast = InsertAst { + returning_columns: vec![ColumnRef::from("id")], + ..Default::default() + }; + + assert_eq!(render_output(&ast), "OUTPUT INSERTED.[id];"); + } + + #[test] + fn emits_output_for_multiple_returning_columns() { + let ast = InsertAst { + returning_columns: vec![ + ColumnRef::from("id"), + ColumnRef::from("created_at"), + ColumnRef::from("updated_at"), + ], + ..Default::default() + }; + + assert_eq!( + render_output(&ast), + "OUTPUT INSERTED.[id], INSERTED.[created_at], INSERTED.[updated_at];" + ); + } + + #[test] + fn ignores_the_source_table_qualifier_in_returning_columns() { + let ast = InsertAst { + returning_columns: vec![ + ColumnRef::from("league.id"), + ColumnRef::from("league.created_at"), + ], + ..Default::default() + }; + + assert_eq!( + render_output(&ast), + "OUTPUT INSERTED.[id], INSERTED.[created_at];" + ); + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/emitter/backends/mysql.rs b/canyon_core/src/query/querybuilder/syntax/emitter/backends/mysql.rs new file mode 100644 index 00000000..15d5717f --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/emitter/backends/mysql.rs @@ -0,0 +1,38 @@ +use crate::query::querybuilder::syntax::{ + ast::{delete::DeleteAst, insert::InsertAst, select::SelectAst, update::UpdateAst}, + dialect::MySql, + emitter::{ + EmitStep, SqlEmitter, + types::{ + delete::delete_default_plan, insert::insert_default_plan, select::select_default_plan, + update::update_default_plan, + }, + }, +}; + +#[derive(Default)] +pub struct MySqlEmitter {} + +impl<'a> SqlEmitter<'a, SelectAst<'a>> for MySqlEmitter { + type Dialect = MySql; + + const PLAN: &'a [EmitStep<'a, SelectAst<'a>>] = select_default_plan!(Self::Dialect); +} + +impl<'a> SqlEmitter<'a, InsertAst<'a>> for MySqlEmitter { + type Dialect = MySql; + + const PLAN: &'a [EmitStep<'a, InsertAst<'a>>] = insert_default_plan!(Self::Dialect); +} + +impl<'a> SqlEmitter<'a, UpdateAst<'a>> for MySqlEmitter { + type Dialect = MySql; + + const PLAN: &'a [EmitStep<'a, UpdateAst<'a>>] = update_default_plan!(Self::Dialect); +} + +impl<'a> SqlEmitter<'a, DeleteAst> for MySqlEmitter { + type Dialect = MySql; + + const PLAN: &'a [EmitStep<'a, DeleteAst>] = delete_default_plan!(Self::Dialect); +} diff --git a/canyon_core/src/query/querybuilder/syntax/emitter/backends/pg.rs b/canyon_core/src/query/querybuilder/syntax/emitter/backends/pg.rs new file mode 100644 index 00000000..b5e769b9 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/emitter/backends/pg.rs @@ -0,0 +1,38 @@ +use crate::query::querybuilder::syntax::{ + ast::{delete::DeleteAst, insert::InsertAst, select::SelectAst, update::UpdateAst}, + dialect::PgDialect, + emitter::{ + EmitStep, SqlEmitter, + types::{ + delete::delete_default_plan, insert::insert_default_plan, select::select_default_plan, + update::update_default_plan, + }, + }, +}; + +#[derive(Default)] +pub struct PgEmitter {} + +impl<'a> SqlEmitter<'a, SelectAst<'a>> for PgEmitter { + type Dialect = PgDialect; + + const PLAN: &'a [EmitStep<'a, SelectAst<'a>>] = select_default_plan!(Self::Dialect); +} + +impl<'a> SqlEmitter<'a, InsertAst<'a>> for PgEmitter { + type Dialect = PgDialect; + + const PLAN: &'a [EmitStep<'a, InsertAst<'a>>] = insert_default_plan!(Self::Dialect); +} + +impl<'a> SqlEmitter<'a, UpdateAst<'a>> for PgEmitter { + type Dialect = PgDialect; + + const PLAN: &'a [EmitStep<'a, UpdateAst<'a>>] = update_default_plan!(Self::Dialect); +} + +impl<'a> SqlEmitter<'a, DeleteAst> for PgEmitter { + type Dialect = PgDialect; + + const PLAN: &'a [EmitStep<'a, DeleteAst>] = delete_default_plan!(Self::Dialect); +} diff --git a/canyon_core/src/query/querybuilder/syntax/emitter/mod.rs b/canyon_core/src/query/querybuilder/syntax/emitter/mod.rs new file mode 100644 index 00000000..2c4196e6 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/emitter/mod.rs @@ -0,0 +1,180 @@ +pub(crate) mod backends; +pub(crate) mod types; + +use crate::connection::database_type::DatabaseType; +use crate::query::querybuilder::syntax::{ + ast::BaseAst, dialect::SqlDialect, query_kind::QueryKind, tokens::SqlTokens, +}; + +#[cfg(feature = "postgres")] +use crate::query::querybuilder::syntax::emitter::backends::PgEmitter; + +#[cfg(feature = "mssql")] +use crate::query::querybuilder::syntax::emitter::backends::SqlServerEmitter; + +#[cfg(feature = "mysql")] +use crate::query::querybuilder::syntax::emitter::backends::MySqlEmitter; + +// ---------- AST Processor marker trait ---------- + +pub trait AstProcessor<'a>: Default { + fn query_kind(&self) -> QueryKind; +} + +pub type EmitStep<'a, P> = fn(&P, &mut BaseAst<'a>, &mut SqlTokens<'a>); + +// ---------- Backend-specific conditional bounds ---------- + +mod backend_bounds { + use super::{AstProcessor, BaseAst, SqlEmitter, SqlTokens}; + + // ------------------------------------------------------------------------- + // PostgreSQL + // ------------------------------------------------------------------------- + + #[cfg(feature = "postgres")] + use super::PgEmitter; + + #[cfg(feature = "postgres")] + pub trait PostgresBackendEmittable<'a>: AstProcessor<'a> { + fn emit_postgres(&self, base_ast: &mut BaseAst<'a>) -> SqlTokens<'a>; + } + + #[cfg(feature = "postgres")] + impl<'a, P> PostgresBackendEmittable<'a> for P + where + P: AstProcessor<'a> + 'a, + PgEmitter: SqlEmitter<'a, P>, + { + #[inline] + fn emit_postgres(&self, base_ast: &mut BaseAst<'a>) -> SqlTokens<'a> { + PgEmitter::default().emit(self, base_ast) + } + } + + #[cfg(not(feature = "postgres"))] + pub trait PostgresBackendEmittable<'a>: AstProcessor<'a> {} + + #[cfg(not(feature = "postgres"))] + impl<'a, P> PostgresBackendEmittable<'a> for P where P: AstProcessor<'a> + 'a {} + + // ------------------------------------------------------------------------- + // MySQL + // ------------------------------------------------------------------------- + + #[cfg(feature = "mysql")] + use super::MySqlEmitter; + + #[cfg(feature = "mysql")] + pub trait MySqlBackendEmittable<'a>: AstProcessor<'a> { + fn emit_mysql(&self, base_ast: &mut BaseAst<'a>) -> SqlTokens<'a>; + } + + #[cfg(feature = "mysql")] + impl<'a, P> MySqlBackendEmittable<'a> for P + where + P: AstProcessor<'a> + 'a, + MySqlEmitter: SqlEmitter<'a, P>, + { + #[inline] + fn emit_mysql(&self, base_ast: &mut BaseAst<'a>) -> SqlTokens<'a> { + MySqlEmitter::default().emit(self, base_ast) + } + } + + #[cfg(not(feature = "mysql"))] + pub trait MySqlBackendEmittable<'a>: AstProcessor<'a> {} + + #[cfg(not(feature = "mysql"))] + impl<'a, P> MySqlBackendEmittable<'a> for P where P: AstProcessor<'a> + 'a {} + + // ------------------------------------------------------------------------- + // SQL Server + // ------------------------------------------------------------------------- + + #[cfg(feature = "mssql")] + use super::SqlServerEmitter; + + #[cfg(feature = "mssql")] + pub trait SqlServerBackendEmittable<'a>: AstProcessor<'a> { + fn emit_sql_server(&self, base_ast: &mut BaseAst<'a>) -> SqlTokens<'a>; + } + + #[cfg(feature = "mssql")] + impl<'a, P> SqlServerBackendEmittable<'a> for P + where + P: AstProcessor<'a> + 'a, + SqlServerEmitter: SqlEmitter<'a, P>, + { + #[inline] + fn emit_sql_server(&self, base_ast: &mut BaseAst<'a>) -> SqlTokens<'a> { + SqlServerEmitter::default().emit(self, base_ast) + } + } + + #[cfg(not(feature = "mssql"))] + pub trait SqlServerBackendEmittable<'a>: AstProcessor<'a> {} + + #[cfg(not(feature = "mssql"))] + impl<'a, P> SqlServerBackendEmittable<'a> for P where P: AstProcessor<'a> + 'a {} +} + +use backend_bounds::{MySqlBackendEmittable, PostgresBackendEmittable, SqlServerBackendEmittable}; + +// ---------- Runtime backend dispatch ---------- + +pub trait BackendEmittable<'a>: AstProcessor<'a> { + fn emit_for( + database_type: DatabaseType, + ast: &Self, + base_ast: &mut BaseAst<'a>, + ) -> SqlTokens<'a>; +} + +impl<'a, P> BackendEmittable<'a> for P +where + P: AstProcessor<'a> + + PostgresBackendEmittable<'a> + + MySqlBackendEmittable<'a> + + SqlServerBackendEmittable<'a> + + 'a, +{ + fn emit_for( + database_type: DatabaseType, + ast: &Self, + base_ast: &mut BaseAst<'a>, + ) -> SqlTokens<'a> { + match database_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => ast.emit_postgres(base_ast), + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => ast.emit_sql_server(base_ast), + #[cfg(feature = "mysql")] + DatabaseType::MySQL => ast.emit_mysql(base_ast), + } + } +} + +// ---------- SQL emitter ---------- + +pub trait SqlEmitter<'a, P> +where + Self: Sized, + P: AstProcessor<'a> + 'a, +{ + type Dialect: SqlDialect; + + /// Ordered emission plan for this AST and backend combination. + const PLAN: &'a [EmitStep<'a, P>]; + + #[inline] + fn emit(&mut self, ast: &P, base_ast: &mut BaseAst<'a>) -> SqlTokens<'a> { + let mut tokens = SqlTokens::default(); + + for step in Self::PLAN { + step(ast, base_ast, &mut tokens); + } + + tokens + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/emitter/types/delete.rs b/canyon_core/src/query/querybuilder/syntax/emitter/types/delete.rs new file mode 100644 index 00000000..cfacc6a7 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/emitter/types/delete.rs @@ -0,0 +1,162 @@ +macro_rules! delete_default_plan { + ($dialect:ty) => { + &[ + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::delete::__impl::emit_delete_keyword( + ast, + base_ast, + tokens, + ) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::delete::__impl::emit_from_keyword( + ast, + base_ast, + tokens, + ) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::delete::__impl::emit_table::<$dialect>( + ast, + base_ast, + tokens, + ) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::delete::__impl::emit_conditions::<$dialect>( + ast, + base_ast, + tokens, + ) + }, + ] + }; +} + +pub(crate) use delete_default_plan; + +pub(crate) mod __impl { + use crate::query::querybuilder::syntax::{ + ast::{BaseAst, delete::DeleteAst}, + dialect::SqlDialect, + emitter::types::helpers, + keyword::Keyword, + tokens::SqlTokens, + }; + + pub(crate) fn emit_delete_keyword<'a>( + _ast: &DeleteAst, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + tokens.keyword(Keyword::Delete); + } + + pub(crate) fn emit_from_keyword<'a>( + _ast: &DeleteAst, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + tokens.keyword(Keyword::From); + } + + pub(crate) fn emit_table<'a, D>( + _ast: &DeleteAst, + base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) where + D: SqlDialect, + { + helpers::emit_table::(base_ast.table(), tokens); + } + + pub(crate) fn emit_conditions<'a, D>( + _ast: &DeleteAst, + base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) where + D: SqlDialect, + { + helpers::emit_query_conditions::(base_ast.conditions(), tokens); + } +} + +#[cfg(test)] +mod tests { + use crate::query::querybuilder::syntax::emitter::EmitStep; + use crate::query::querybuilder::syntax::{ + ast::{BaseAst, delete::DeleteAst}, + dialect::{MsSql, PgDialect}, + emitter::{SqlEmitter, types::helpers::Range}, + writer::TokenWriter, + }; + + #[derive(Default)] + struct TestDeleteEmitter; + + impl<'a> SqlEmitter<'a, DeleteAst> for TestDeleteEmitter { + type Dialect = PgDialect; + + const PLAN: &'a [EmitStep<'a, DeleteAst>] = delete_default_plan!(Self::Dialect); + } + + #[derive(Default)] + struct TestDeleteEmitterMsSql; + impl<'a> SqlEmitter<'a, DeleteAst> for TestDeleteEmitterMsSql { + type Dialect = MsSql; + + const PLAN: &'a [EmitStep<'a, DeleteAst>] = delete_default_plan!(Self::Dialect); + } + + fn render_standard<'a>(ast: &DeleteAst, base_ast: &mut BaseAst<'a>) -> String { + let mut emitter = TestDeleteEmitter; + let tokens = emitter.emit(ast, base_ast); + TokenWriter::new().render::(tokens).unwrap() + } + + fn render_mssql<'a>(ast: &DeleteAst, base_ast: &mut BaseAst<'a>) -> String { + let mut emitter = TestDeleteEmitterMsSql; + let tokens = emitter.emit(ast, base_ast); + TokenWriter::new().render::(tokens).unwrap() + } + + #[test] + fn emits_delete_from_table_without_conditions() { + let ast = DeleteAst::default(); + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render_standard(&ast, &mut base_ast); + assert_eq!(sql.trim(), "DELETE FROM \"users\";"); + } + + #[test] + fn emits_delete_from_table_without_conditions_in_mssql() { + let ast = DeleteAst::default(); + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render_mssql(&ast, &mut base_ast); + assert_eq!(sql.trim(), "DELETE FROM [users];"); + } + + #[test] + fn emits_delete_with_where_condition() { + use crate::query::operators::Operator; + use crate::query::querybuilder::syntax::clause::{ConditionClause, ConditionClauseKind}; + + let ast = DeleteAst::default(); + + let mut base_ast = BaseAst::new_ast("users".into()); + + base_ast.add_condition(ConditionClause { + kind: ConditionClauseKind::Where, + column_name: "id".into(), + operator: Operator::Eq, + value_indexes: Some(Range::new_unbounded(3)), + }); + + let sql = render_standard(&ast, &mut base_ast); + assert_eq!(sql.trim(), "DELETE FROM \"users\" WHERE \"id\" = $1;"); + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/emitter/types/helpers.rs b/canyon_core/src/query/querybuilder/syntax/emitter/types/helpers.rs new file mode 100644 index 00000000..e69bc6b9 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/emitter/types/helpers.rs @@ -0,0 +1,351 @@ +//! Standalone functions that shares the same behaviour for different AST kinds + +use crate::query::querybuilder::syntax::clause::ConditionClause; +use crate::query::querybuilder::syntax::column::{ColumnRef, Qualification}; +use crate::query::querybuilder::syntax::dialect::SqlDialect; +use crate::query::querybuilder::syntax::symbol::Symbol; +use crate::query::querybuilder::syntax::symbol::Symbol::Comma; +use crate::query::querybuilder::syntax::table_metadata::TableMetadata; +use crate::query::querybuilder::syntax::tokens::{SqlTokens, ToSqlTokens}; +use std::borrow::Cow; + +pub(crate) struct Range(usize, Option); +impl Range { + pub(crate) const fn new(start: usize, end: usize) -> Self { + Self(start, Some(end)) + } + + /// Creates a new unbounded range starting from the given index. + /// + /// Here `None` does not mean infinity. It represents a single-value range: + /// `[start, start]`. + pub(crate) const fn new_unbounded(start: usize) -> Self { + Self(start, None) + } + + pub(crate) const fn is_range(&self) -> bool { + self.1.is_some() + } + + pub(crate) const fn start(&self) -> usize { + self.0 + } + + pub(crate) const fn end(&self) -> usize { + match self.1 { + Some(end) => end, + None => self.0, + } + } +} + +impl IntoIterator for &Range { + type Item = usize; + type IntoIter = std::ops::Range; + + fn into_iter(self) -> Self::IntoIter { + self.start()..self.end() + } +} + +/// Helper function to push a quoted identifier (like table or column names) into the token stream +pub fn push_quoted_ident<'a, D, S>(element: S, tokens: &mut SqlTokens<'a>) +where + D: SqlDialect, + S: Into>, +{ + let q = D::IDENT_QUOTING; + tokens.symbol(q.opening().into()); + tokens.ident(element); + tokens.symbol(q.closing().into()); +} + +/// Helper function to emit a list of columns, separated by commas +pub(crate) fn emit_columns<'a, D: SqlDialect>( + columns: &[ColumnRef<'a>], + qualification: Qualification, + tokens: &mut SqlTokens<'a>, +) { + if columns.is_empty() { + tokens.symbol(Symbol::Asterisk); + return; + } + + for (i, column) in columns.iter().enumerate() { + if i > 0 { + tokens.symbol(Comma); + } + column.emit::(qualification, tokens); + } +} + +pub(crate) fn emit_qualified_columns<'a, D: SqlDialect>( + columns: &[ColumnRef<'a>], + tokens: &mut SqlTokens<'a>, +) { + emit_columns::(columns, Qualification::Qualified, tokens); +} + +#[cfg(any(feature = "postgres", feature = "mysql"))] +pub(crate) fn emit_unqualified_columns<'a, D: SqlDialect>( + columns: &[ColumnRef<'a>], + tokens: &mut SqlTokens<'a>, +) { + emit_columns::(columns, Qualification::Unqualified, tokens); +} + +pub(crate) fn emit_placeholders<'a>(columns: &Vec>, tokens: &mut SqlTokens<'a>) { + for (i, _) in columns.iter().enumerate() { + if i > 0 { + tokens.symbol(Comma); + } + tokens.placeholder(); + } +} + +pub(crate) fn emit_query_conditions<'a, D: SqlDialect>( + query_conditions: &[ConditionClause<'a>], + tokens: &mut SqlTokens<'a>, +) { + if query_conditions.is_empty() { + return; + } + + for cond in query_conditions { + tokens.extend( as ToSqlTokens<'_, D>>::to_tokens(cond)); + } +} + +pub(crate) fn emit_table<'a, D: SqlDialect>(table: &TableMetadata<'a>, tokens: &mut SqlTokens<'a>) { + tokens.extend( as ToSqlTokens<'_, D>>::to_tokens(table)); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::querybuilder::syntax::{ + dialect::{IdentQuotingStyle, MsSql, MySql, PgDialect, PlaceholderSymbol}, + tokens::SqlToken, + }; + + fn make_column(column: &'_ str) -> ColumnRef<'_> { + ColumnRef::from(column) + } + + fn make_qualified_column<'a>( + table: &'a str, + column: &'a str, + alias: Option<&'a str>, + ) -> ColumnRef<'a> { + ColumnRef { + table: (!table.is_empty()).then_some(Cow::Borrowed(table)), + column: Cow::Borrowed(column), + alias: alias.map(Cow::Borrowed), + } + } + + fn assert_ident_quoting_contract( + expected_opening: &str, + expected_closing: &str, + ) { + assert_eq!(D::IDENT_QUOTING.opening().to_string(), expected_opening); + assert_eq!(D::IDENT_QUOTING.closing().to_string(), expected_closing); + } + + #[test] + fn standard_dialect_uses_double_quotes_for_identifiers() { + assert_eq!(PgDialect::IDENT_QUOTING, IdentQuotingStyle::DoubleQuote); + assert_ident_quoting_contract::("\"", "\""); + } + + #[cfg(feature = "postgres")] + #[test] + fn postgres_uses_double_quotes_for_identifiers() { + assert_eq!(PgDialect::IDENT_QUOTING, IdentQuotingStyle::DoubleQuote); + assert_ident_quoting_contract::("\"", "\""); + } + + #[cfg(feature = "mysql")] + #[test] + fn mysql_uses_backticks_for_identifiers() { + assert_eq!(MySql::IDENT_QUOTING, IdentQuotingStyle::Backtick); + assert_ident_quoting_contract::("`", "`"); + } + + #[cfg(feature = "mssql")] + #[test] + fn mssql_uses_brackets_for_identifiers() { + assert_eq!(MsSql::IDENT_QUOTING, IdentQuotingStyle::Bracket); + assert_ident_quoting_contract::("[", "]"); + } + + #[test] + fn push_quoted_ident_with_standard_dialect() { + let mut tokens = SqlTokens::default(); + // TODO: this isn't taking in consideration the scape quotes, care + push_quoted_ident::("users", &mut tokens); + assert_eq!( + tokens.inner(), + get_columns_test_expr_values::(&["users"]) + ); + } + + #[cfg(feature = "postgres")] + #[test] + fn push_quoted_ident_with_postgres() { + let mut tokens = SqlTokens::default(); + push_quoted_ident::("users", &mut tokens); + assert_eq!( + tokens.inner(), + get_columns_test_expr_values::(&["users"]) + ); + } + + fn get_columns_test_expr_values( + literals: &[&'static str], + ) -> Vec> { + let mut tokens = SqlTokens::default(); + + for (idx, lit) in literals.iter().enumerate() { + if idx > 0 { + tokens.symbol(Comma); + } + tokens.extend( as ToSqlTokens<'_, D>>::to_tokens( + &make_column(lit), + )); + } + + tokens.inner() + } + + #[cfg(feature = "mysql")] + #[test] + fn push_quoted_ident_with_mysql() { + let mut tokens = SqlTokens::default(); + push_quoted_ident::("users", &mut tokens); + assert_eq!( + tokens.inner(), + get_columns_test_expr_values::(&["users"]) + ); + } + + #[cfg(feature = "mssql")] + #[test] + fn push_quoted_ident_with_mssql() { + let mut tokens = SqlTokens::default(); + push_quoted_ident::("users", &mut tokens); + assert_eq!( + tokens.inner(), + get_columns_test_expr_values::(&["users"]) + ); + } + + #[test] + fn emit_qualified_columns_with_empty_vec_emits_asterisk() { + let columns = vec![]; + let mut tokens = SqlTokens::default(); + emit_qualified_columns::(&columns, &mut tokens); + assert_eq!(tokens.inner(), vec![SqlToken::Symbol(Symbol::Asterisk)]); + } + + #[test] + fn emit_qualified_columns_with_one_column_quotes_only_column_name_and_emit_column_alias() { + let columns = vec![make_qualified_column("user", "name", Some("username"))]; + let mut tokens = SqlTokens::default(); + emit_qualified_columns::(&columns, &mut tokens); + assert_eq!( + tokens.inner(), + get_columns_test_expr_values::(&["user.name as username"]) + ); + } + + #[test] + fn emit_qualified_columns_with_many_columns_separates_with_comma_and_space() { + let columns = vec![ + make_qualified_column("users", "id", None), + make_qualified_column("users", "name", None), + make_qualified_column("users", "email", None), + ]; + let mut tokens = SqlTokens::default(); + emit_qualified_columns::(&columns, &mut tokens); + + assert_eq!( + tokens.inner(), + get_columns_test_expr_values::(&["users.id", "users.name", "users.email"]) + ); + } + + #[cfg(feature = "mysql")] + #[test] + fn emit_qualified_columns_with_mysql_uses_backticks() { + let columns = vec![ + make_qualified_column("users", "id", None), + make_qualified_column("users", "name", None), + ]; + let mut tokens = SqlTokens::default(); + + emit_qualified_columns::(&columns, &mut tokens); + + assert_eq!( + tokens.inner(), + get_columns_test_expr_values::(&["users.id", "users.name"]) + ); + } + + #[cfg(feature = "mssql")] + #[test] + fn emit_qualified_columns_with_mssql_uses_brackets() { + let columns = vec![make_column("id"), make_column("name")]; + + let mut tokens = SqlTokens::default(); + emit_qualified_columns::(&columns, &mut tokens); + + assert_eq!( + tokens.inner(), + get_columns_test_expr_values::(&["id", "name"]) + ); + } + + #[test] + fn emit_qualified_columns_ignores_table_and_alias_and_only_emits_column_names() { + let columns = vec![ + make_qualified_column("user", "id", None), + make_qualified_column("account", "name", None), + ]; + let mut tokens = SqlTokens::default(); + emit_qualified_columns::(&columns, &mut tokens); + + let expected = get_columns_test_expr_values::(&["user.id", "account.name"]); + + assert_eq!(tokens.inner(), expected); + } + + #[cfg(feature = "mssql")] + #[test] + fn emit_placeholders_with_mssql_uses_at_p_numbering() { + let columns = vec![make_column("id"), make_column("name"), make_column("email")]; + + let mut tokens = SqlTokens::default(); + emit_placeholders(&columns, &mut tokens); + let tokens_vec = tokens.inner(); + + assert_eq!( + &tokens_vec, + &vec![ + SqlToken::Placeholder, + SqlToken::Symbol(Comma), + SqlToken::Placeholder, + SqlToken::Symbol(Comma), + SqlToken::Placeholder, + ] + ); + assert_eq!( + tokens_vec + .iter() + .filter(|t| (*t).eq(&SqlToken::Placeholder)) + .count(), + 3 + ); + assert_eq!(MsSql::PLACEHOLDER_SYMBOL, PlaceholderSymbol::AtPNumbered); + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/emitter/types/insert.rs b/canyon_core/src/query/querybuilder/syntax/emitter/types/insert.rs new file mode 100644 index 00000000..125c76df --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/emitter/types/insert.rs @@ -0,0 +1,204 @@ +#[cfg(any(feature = "postgres", feature = "mysql"))] +macro_rules! insert_default_plan { + ($dialect:ty) => { + &[ + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::insert::__impl::emit_insert_into_keywords( + ast, + base_ast, + tokens, + ) + }, + |_ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::helpers::emit_table::<$dialect>( + base_ast.table(), + tokens, + ) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::insert::__impl::emit_columns::<$dialect>( + ast, + base_ast, + tokens, + ) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::insert::__impl::emit_values( + ast, + base_ast, + tokens, + ) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::insert::__impl::emit_returning::<$dialect>( + ast, + base_ast, + tokens, + ) + }, + ] + } +} + +pub(crate) mod __impl { + use crate::query::querybuilder::syntax::ast::BaseAst; + use crate::query::querybuilder::syntax::symbol::Symbol; + use crate::query::querybuilder::syntax::{ + ast::insert::InsertAst, emitter::types::helpers, keyword::Keyword, tokens::SqlTokens, + }; + + #[cfg(any(feature = "postgres", feature = "mysql"))] + use crate::query::querybuilder::syntax::dialect::SqlDialect; + + pub(crate) fn emit_insert_into_keywords<'a>( + _ast: &InsertAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + tokens.keyword(Keyword::Insert); + tokens.keyword(Keyword::Into); + } + + #[cfg(any(feature = "postgres", feature = "mysql"))] + pub(crate) fn emit_columns<'a, D: SqlDialect>( + ast: &InsertAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + tokens.symbol(Symbol::LParen); + helpers::emit_unqualified_columns::(&ast.columns, tokens); + tokens.symbol(Symbol::RParen); + } + + pub(crate) fn emit_values<'a>( + ast: &InsertAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + tokens.keyword(Keyword::Values); + tokens.symbol(Symbol::LParen); + helpers::emit_placeholders(&ast.columns, tokens); + tokens.symbol(Symbol::RParen); + } + + #[cfg(any(feature = "postgres", feature = "mysql"))] + pub(crate) fn emit_returning<'a, D: SqlDialect>( + ast: &InsertAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + if !D::SUPPORTS_RETURNING || ast.returning_columns.is_empty() { + return; + } + tokens.keyword(Keyword::Returning); + helpers::emit_unqualified_columns::(&ast.returning_columns, tokens) + } +} + +#[cfg(any(feature = "postgres", feature = "mysql"))] +pub(crate) use insert_default_plan; + +#[cfg(test)] +mod tests { + use crate::query::querybuilder::syntax::{ + ast::BaseAst, + ast::insert::InsertAst, + column::ColumnRef, + dialect::{MySql, PgDialect}, + emitter::EmitStep, + emitter::SqlEmitter, + writer::TokenWriter, + }; + + #[derive(Default)] + struct TestInsertEmitter; + impl<'a> SqlEmitter<'a, InsertAst<'a>> for TestInsertEmitter { + type Dialect = PgDialect; + const PLAN: &'a [EmitStep<'a, InsertAst<'a>>] = insert_default_plan!(Self::Dialect); + } + + #[derive(Default)] + struct TestInsertEmitterNoReturning; + impl<'a> SqlEmitter<'a, InsertAst<'a>> for TestInsertEmitterNoReturning { + type Dialect = MySql; + const PLAN: &'a [EmitStep<'a, InsertAst<'a>>] = insert_default_plan!(Self::Dialect); + } + + fn col(name: &'_ str) -> ColumnRef<'_> { + ColumnRef::from(name) + } + + fn render_with_returning<'a>(ast: &InsertAst<'a>, base_ast: &mut BaseAst<'a>) -> String { + let mut emitter = TestInsertEmitter; + let tokens = emitter.emit(ast, base_ast); + TokenWriter::new().render::(tokens).unwrap() + } + + fn render_without_returning<'a>(ast: &InsertAst<'a>, base_ast: &mut BaseAst<'a>) -> String { + let mut emitter = TestInsertEmitterNoReturning; + let tokens = emitter.emit(ast, base_ast); + TokenWriter::new().render::(tokens).unwrap() + } + + #[test] + fn emits_insert_columns_values_and_returning_when_supported() { + let ast = InsertAst { + columns: vec![col("id"), col("name")], + returning_columns: vec![col("id")], + }; + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render_with_returning(&ast, &mut base_ast); + assert_eq!( + sql, + "INSERT INTO \"users\" (\"id\", \"name\") VALUES ($1, $2) RETURNING \"id\";" + ); + } + + #[test] + fn omits_returning_when_dialect_does_not_support_it() { + let ast = InsertAst { + columns: vec![col("id"), col("name")], + returning_columns: vec![col("id")], + }; + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render_without_returning(&ast, &mut base_ast); + assert_eq!( + sql.trim(), + "INSERT INTO `users` (`id`, `name`) VALUES (?, ?);" + ); + } + + #[test] + fn emits_multiple_returning_columns_when_supported() { + let ast = InsertAst { + columns: vec![col("name"), col("email")], + returning_columns: vec![col("id"), col("created_at")], + }; + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render_with_returning(&ast, &mut base_ast); + assert_eq!( + sql.trim(), + "INSERT INTO \"users\" (\"name\", \"email\") VALUES ($1, $2) RETURNING \"id\", \"created_at\";" + ); + } + + #[test] + fn does_not_emit_returning_keyword_when_returning_columns_are_empty_and_dialect_supports_returning() + { + let ast = InsertAst { + columns: vec![col("name")], + returning_columns: vec![], + }; + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render_with_returning(&ast, &mut base_ast); + assert_eq!(sql.trim(), "INSERT INTO \"users\" (\"name\") VALUES ($1);"); + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/emitter/types/mod.rs b/canyon_core/src/query/querybuilder/syntax/emitter/types/mod.rs new file mode 100644 index 00000000..6d88d89b --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/emitter/types/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod delete; +pub(crate) mod helpers; +pub(crate) mod insert; +pub(crate) mod select; +pub(crate) mod update; diff --git a/canyon_core/src/query/querybuilder/syntax/emitter/types/select.rs b/canyon_core/src/query/querybuilder/syntax/emitter/types/select.rs new file mode 100644 index 00000000..985be9ab --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/emitter/types/select.rs @@ -0,0 +1,305 @@ +macro_rules! select_default_plan { + ($dialect:ty) => { + &[ + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::select::__impl::emit_select_keyword(ast, base_ast, tokens) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::select::__impl::emit_distinct(ast, base_ast, tokens) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::select::__impl::emit_columns::<$dialect>(ast, base_ast, tokens) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::select::__impl::emit_from::<$dialect>(ast, base_ast, tokens) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::select::__impl::emit_joins::<$dialect>(ast, base_ast, tokens) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::select::__impl::emit_conditions::<$dialect>(ast, base_ast, tokens) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::select::__impl::emit_group_by::<$dialect>(ast, base_ast, tokens) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::select::__impl::emit_having::<$dialect>(ast, base_ast, tokens) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::select::__impl::emit_order_by::<$dialect>(ast, base_ast, tokens) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::select::__impl::emit_limit(ast, base_ast, tokens) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::select::__impl::emit_offset(ast, base_ast, tokens) + }, + ] + }; +} + +pub(crate) use select_default_plan; + +pub(crate) mod __impl { + use crate::query::querybuilder::syntax::ast::BaseAst; + use crate::query::querybuilder::syntax::ast::select::SelectAst; + use crate::query::querybuilder::syntax::dialect::SqlDialect; + use crate::query::querybuilder::syntax::emitter::types::helpers; + use crate::query::querybuilder::syntax::having::HavingClause; + use crate::query::querybuilder::syntax::join::JoinClause; + use crate::query::querybuilder::syntax::keyword::Keyword; + use crate::query::querybuilder::syntax::order::OrderByClause; + use crate::query::querybuilder::syntax::symbol::Symbol; + use crate::query::querybuilder::syntax::table_metadata::TableMetadata; + use crate::query::querybuilder::syntax::tokens::{SqlTokens, ToSqlTokens}; + + pub(crate) fn emit_select_keyword<'a>( + _ast: &SelectAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + tokens.keyword(Keyword::Select); + } + + pub(crate) fn emit_columns<'a, D: SqlDialect>( + ast: &SelectAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + let is_count_query = ast.is_count_query; + if is_count_query { + tokens.keyword(Keyword::Count); + tokens.symbol(Symbol::LParen); + } + helpers::emit_qualified_columns::(&ast.columns, tokens); + if is_count_query { + tokens.symbol(Symbol::RParen); + } + } + + pub(crate) fn emit_from<'a, D: SqlDialect>( + _ast: &SelectAst<'a>, + base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + tokens.keyword(Keyword::From); + tokens.extend( as ToSqlTokens<'a, D>>::to_tokens( + base_ast.table(), + )); + } + + pub(crate) fn emit_joins<'a, D: SqlDialect>( + ast: &SelectAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + for join in &ast.joins { + tokens.extend( as ToSqlTokens<'a, D>>::to_tokens(join)); + } + } + + pub(crate) fn emit_conditions<'a, D>( + _ast: &SelectAst<'a>, + base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) where + D: SqlDialect, + { + helpers::emit_query_conditions::(base_ast.conditions(), tokens); + } + + pub(crate) fn emit_group_by<'a, D: SqlDialect>( + ast: &SelectAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + if let Some(group_by) = &ast.group_by { + tokens.keyword(Keyword::GroupBy); + helpers::emit_qualified_columns::(group_by, tokens); + } + } + + pub(crate) fn emit_having<'a, D: SqlDialect>( + ast: &SelectAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + if let Some(having) = &ast.having { + tokens.keyword(Keyword::Having); + tokens.extend( as ToSqlTokens<'_, D>>::to_tokens(having)); + } + } + + pub(crate) fn emit_order_by<'a, D: SqlDialect>( + ast: &SelectAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + if let Some(order_by) = &ast.order_by { + tokens.extend( as ToSqlTokens<'_, D>>::to_tokens( + order_by, + )); + } + } + + pub(crate) fn emit_limit<'a>( + ast: &SelectAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + if let Some(limit) = ast.limit { + tokens.keyword(Keyword::Limit); + tokens.numeric(limit); + } + } + + pub(crate) fn emit_offset<'a>( + ast: &SelectAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + if let Some(offset) = ast.offset { + tokens.keyword(Keyword::Offset); + tokens.numeric(offset); + } + } + + pub(crate) fn emit_distinct<'a>( + ast: &SelectAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + if ast.with_distinct { + tokens.keyword(Keyword::Distinct); + } + } +} + +#[cfg(test)] +mod tests { + use crate::query::{ + operators::Operator, + querybuilder::syntax::{ + ast::BaseAst, ast::select::SelectAst, column::ColumnRef, dialect::PgDialect, + emitter::EmitStep, emitter::SqlEmitter, order::OrderByClause, writer::TokenWriter, + }, + }; + + struct TestEmitter; + impl<'a> SqlEmitter<'a, SelectAst<'a>> for TestEmitter { + type Dialect = PgDialect; + + const PLAN: &'a [EmitStep<'a, SelectAst<'a>>] = select_default_plan!(Self::Dialect); + } + + fn col(name: &'_ str) -> ColumnRef<'_> { + ColumnRef::from(name) + } + + fn render<'a>(ast: &SelectAst<'a>, base_ast: &mut BaseAst<'a>) -> String { + let mut emitter = TestEmitter; + let tokens = emitter.emit(ast, base_ast); + TokenWriter::new().render::(tokens).unwrap() + } + + #[test] + fn emits_select_with_columns_and_from() { + let mut ast = SelectAst::new(); + ast.columns = vec![col("id"), col("name")]; + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render(&ast, &mut base_ast); + assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"users\";"); + } + + #[test] + fn emits_select_with_order_by_limit_and_offset() { + let mut ast = SelectAst::new(); + ast.columns = vec![col("id")]; + ast.order_by = Some(OrderByClause::new("id", true)); + ast.limit = Some(10); + ast.offset = Some(20); + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render(&ast, &mut base_ast); + assert_eq!( + sql, + "SELECT \"id\" FROM \"users\" ORDER BY \"id\" DESC LIMIT 10 OFFSET 20;" + ); + } + + #[test] + fn emits_select_without_optional_clauses() { + let mut ast = SelectAst::new(); + ast.columns = vec![]; + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render(&ast, &mut base_ast); + assert_eq!(sql, "SELECT * FROM \"users\";"); + } + + #[test] + fn emits_group_by_when_present() { + let mut ast = SelectAst::new(); + ast.columns = vec![col("users.country")]; + ast.group_by = Some(vec![col("users.country")]); + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render(&ast, &mut base_ast); + assert_eq!( + sql, + "SELECT \"users\".\"country\" FROM \"users\" GROUP BY \"users\".\"country\";" + ); + } + + #[test] + fn emits_select_with_all_join_kinds() { + use crate::query::querybuilder::syntax::join::{JoinClause, JoinKind}; + + let mut ast = SelectAst::new(); + ast.columns = vec![col("users.id"), col("profiles.bio"), col("roles.name")]; + + ast.joins = vec![ + JoinClause::new( + JoinKind::Inner, + "profiles".into(), + col("users.id"), + Operator::Eq, + col("profiles.user_id"), + ), + JoinClause::new( + JoinKind::Left, + "roles".into(), + col("users.role_id"), + Operator::Eq, + col("roles.id"), + ), + JoinClause::new( + JoinKind::Right, + "teams".into(), + col("users.team_id"), + Operator::Eq, + col("teams.id"), + ), + JoinClause::new( + JoinKind::FullOuter, + "permissions".into(), + col("users.id"), + Operator::Eq, + col("permissions.user_id"), + ), + ]; + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render(&ast, &mut base_ast); + assert_eq!( + sql, + "SELECT \"users\".\"id\", \"profiles\".\"bio\", \"roles\".\"name\" FROM \"users\" INNER JOIN \"profiles\" ON \"users\".\"id\" = \"profiles\".\"user_id\" LEFT JOIN \"roles\" ON \"users\".\"role_id\" = \"roles\".\"id\" RIGHT JOIN \"teams\" ON \"users\".\"team_id\" = \"teams\".\"id\" FULL OUTER JOIN \"permissions\" ON \"users\".\"id\" = \"permissions\".\"user_id\";" + ); + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/emitter/types/update.rs b/canyon_core/src/query/querybuilder/syntax/emitter/types/update.rs new file mode 100644 index 00000000..68ef9c82 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/emitter/types/update.rs @@ -0,0 +1,216 @@ +macro_rules! update_default_plan { + ($dialect:ty) => { + &[ + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::update::__impl::emit_update_keyword( + ast, + base_ast, + tokens, + ) + }, + |_ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::helpers::emit_table::<$dialect>( + base_ast.table(), + tokens, + ) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::update::__impl::emit_set_keyword( + ast, + base_ast, + tokens, + ) + }, + |ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::update::__impl::emit_set_clause::<$dialect>( + ast, + base_ast, + tokens, + ) + }, + |_ast, base_ast, tokens| { + $crate::query::querybuilder::syntax::emitter::types::helpers::emit_query_conditions::<$dialect>( + base_ast.conditions(), + tokens, + ) + }, + ] + }; +} + +pub(crate) use update_default_plan; + +pub(crate) mod __impl { + + use crate::query::querybuilder::syntax::column::Qualification; + + use crate::query::querybuilder::syntax::symbol::Symbol; + use crate::query::querybuilder::syntax::{ + ast::{BaseAst, update::UpdateAst}, + dialect::SqlDialect, + keyword::Keyword, + tokens::SqlTokens, + }; + + pub(crate) fn emit_update_keyword<'a>( + _ast: &UpdateAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + tokens.keyword(Keyword::Update); + } + + pub(crate) fn emit_set_keyword<'a>( + _ast: &UpdateAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + tokens.keyword(Keyword::Set); + } + + pub(crate) fn emit_set_clause<'a, D: SqlDialect>( + ast: &UpdateAst<'a>, + _base_ast: &mut BaseAst<'a>, + tokens: &mut SqlTokens<'a>, + ) { + for (i, col) in ast.columns.iter().enumerate() { + if i > 0 { + tokens.symbol(Symbol::Comma); + } + col.emit::(Qualification::Unqualified, tokens); + tokens.symbol(Symbol::Equals); + tokens.placeholder(); + } + } +} + +#[cfg(test)] +mod tests { + use crate::query::operators::Operator; + use crate::query::querybuilder::syntax::clause::{ConditionClause, ConditionClauseKind}; + use crate::query::querybuilder::syntax::dialect::MsSql; + use crate::query::querybuilder::syntax::emitter::EmitStep; + use crate::query::querybuilder::syntax::emitter::types::helpers::Range; + use crate::query::querybuilder::syntax::writer::TokenWriter; + use crate::query::querybuilder::syntax::{ + ast::BaseAst, ast::update::UpdateAst, column::ColumnRef, dialect::PgDialect, + emitter::SqlEmitter, + }; + + #[derive(Default)] + struct TestUpdateEmitter; + impl<'a> SqlEmitter<'a, UpdateAst<'a>> for TestUpdateEmitter { + type Dialect = PgDialect; + const PLAN: &'a [EmitStep<'a, UpdateAst<'a>>] = update_default_plan!(Self::Dialect); + } + + #[derive(Default)] + struct TestUpdateEmitterMsSql; + impl<'a> SqlEmitter<'a, UpdateAst<'a>> for TestUpdateEmitterMsSql { + type Dialect = MsSql; + const PLAN: &'a [EmitStep<'a, UpdateAst<'a>>] = update_default_plan!(Self::Dialect); + } + + fn col(name: &'_ str) -> ColumnRef<'_> { + ColumnRef::from(name) + } + + fn render_standard<'a>(ast: &UpdateAst<'a>, base_ast: &mut BaseAst<'a>) -> String { + let mut emitter = TestUpdateEmitter; + let tokens = emitter.emit(ast, base_ast); + TokenWriter::new().render::(tokens).unwrap() + } + + fn render_mssql<'a>(ast: &UpdateAst<'a>, base_ast: &mut BaseAst<'a>) -> String { + let mut emitter = TestUpdateEmitterMsSql; + let tokens = emitter.emit(ast, base_ast); + TokenWriter::new().render::(tokens).unwrap() + } + + #[test] + fn emits_update_with_single_set_column() { + let ast = UpdateAst { + columns: vec![col("name")], + }; + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render_standard(&ast, &mut base_ast); + assert_eq!(sql, "UPDATE \"users\" SET \"name\" = $1;"); + } + + #[test] + fn emits_update_with_multiple_set_columns() { + let ast = UpdateAst { + columns: vec![col("name"), col("email"), col("updated_at")], + }; + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render_standard(&ast, &mut base_ast); + + assert_eq!( + sql, + "UPDATE \"users\" SET \"name\" = $1, \"email\" = $2, \"updated_at\" = $3;" + ); + } + + #[test] + fn emits_update_with_where_conditions() { + let ast = UpdateAst { + columns: vec![col("name"), col("email")], + }; + + let mut base_ast = BaseAst::new_ast("users".into()); + + base_ast.add_condition(ConditionClause { + kind: ConditionClauseKind::Where, + column_name: "id".into(), + operator: Operator::Eq, + value_indexes: Some(Range::new_unbounded(3)), + }); + + let sql = render_standard(&ast, &mut base_ast); + + assert_eq!( + sql, + "UPDATE \"users\" SET \"name\" = $1, \"email\" = $2 WHERE \"id\" = $3;" + ); + } + + #[test] + fn emits_update_in_mssql_with_dialect_specific_identifiers_and_placeholders() { + let ast = UpdateAst { + columns: vec![col("name"), col("email")], + }; + + let mut base_ast = BaseAst::new_ast("users".into()); + + let sql = render_mssql(&ast, &mut base_ast); + + assert_eq!(sql, "UPDATE [users] SET [name] = @P1, [email] = @P2;"); + } + + #[test] + fn preserves_placeholder_sequence_between_set_and_where() { + let ast = UpdateAst { + columns: vec![col("name"), col("email")], + }; + + let mut base_ast = BaseAst::new_ast("users".into()); + + base_ast.add_condition(ConditionClause { + kind: ConditionClauseKind::Where, + column_name: "id".into(), + operator: Operator::Eq, + value_indexes: Some(Range::new_unbounded(3)), + }); + + let sql = render_standard(&ast, &mut base_ast); + + assert_eq!( + sql, + "UPDATE \"users\" SET \"name\" = $1, \"email\" = $2 WHERE \"id\" = $3;" + ); + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/having.rs b/canyon_core/src/query/querybuilder/syntax/having.rs new file mode 100644 index 00000000..c20c43f3 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/having.rs @@ -0,0 +1,36 @@ +use crate::query::operators::Operator; +use crate::query::querybuilder::syntax::column::ColumnRef; +use crate::query::querybuilder::syntax::dialect::SqlDialect; +use crate::query::querybuilder::syntax::keyword::Keyword; +use crate::query::querybuilder::syntax::tokens::{SqlToken, SqlTokens, ToSqlTokens}; + +pub struct HavingClause<'a> { + pub column: ColumnRef<'a>, + pub operator: Operator, +} + +impl<'a> HavingClause<'a> { + pub fn new>>(column: I, operator: Operator) -> Self { + Self { + column: column.into(), + operator, + } + } + + pub const fn new_const(column: ColumnRef<'a>, operator: Operator) -> Self { + Self { column, operator } + } +} + +impl<'a, D: SqlDialect> ToSqlTokens<'a, D> for HavingClause<'a> { + fn to_tokens(&self) -> impl IntoIterator> + 'a { + let mut out = SqlTokens::with_capacity(4); + + out.keyword(Keyword::Having); + as ToSqlTokens<'_, D>>::to_tokens(&self.column); + out.operator(self.operator); + out.placeholder(); + + out + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/join.rs b/canyon_core/src/query/querybuilder/syntax/join.rs new file mode 100644 index 00000000..9bcb35cd --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/join.rs @@ -0,0 +1,123 @@ +use crate::query::operators::Operator; +use crate::query::querybuilder::syntax::column::ColumnRef; +use crate::query::querybuilder::syntax::dialect::SqlDialect; +use crate::query::querybuilder::syntax::keyword::Keyword; +use crate::query::querybuilder::syntax::table_metadata::TableMetadata; +use crate::query::querybuilder::syntax::tokens::{SqlToken, SqlTokens, ToSqlTokens}; + +#[derive(Debug, Clone, Copy)] +pub enum JoinKind { + Inner, + Left, + Right, + Full, + FullOuter, +} + +impl From for Keyword { + fn from(join_kind: JoinKind) -> Self { + match join_kind { + JoinKind::Inner => Keyword::Inner, + JoinKind::Left => Keyword::Left, + JoinKind::Right => Keyword::Right, + JoinKind::Full => Keyword::Full, + JoinKind::FullOuter => Keyword::FullOuter, + } + } +} + +pub struct JoinClause<'a> { + pub kind: JoinKind, + pub target_table: TableMetadata<'a>, + pub left: ColumnRef<'a>, + pub operator: Operator, + pub right: ColumnRef<'a>, // e.g. "t2.t1_id" // TODO: this is always the base or the previous (at least, in one of the sides) + // so we could look in the vector for the previous clause and auto-add the join +} + +impl<'a> JoinClause<'a> { + pub const fn new( + kind: JoinKind, + target_table: TableMetadata<'a>, + left: ColumnRef<'a>, + operator: Operator, + right: ColumnRef<'a>, + ) -> Self { + Self { + kind, + target_table, + left, + operator, + right, + } + } +} + +impl<'a, D: SqlDialect> ToSqlTokens<'a, D> for JoinClause<'a> { + fn to_tokens(&self) -> impl IntoIterator> + 'a { + let mut out = SqlTokens::with_capacity(6); + + out.keyword(self.kind.into()); + out.keyword(Keyword::Join); + out.extend( as ToSqlTokens<'a, D>>::to_tokens( + &self.target_table, + )); + + out.keyword(Keyword::On); + out.extend( as ToSqlTokens<'a, D>>::to_tokens(&self.left)); + + out.operator(self.operator); + + out.extend( as ToSqlTokens<'a, D>>::to_tokens( + &self.right, + )); + + out + } +} +#[test] +fn test_join_clause_basic() { + use crate::query::operators::Operator; + use crate::query::querybuilder::syntax::dialect::PgDialect; + use crate::query::querybuilder::syntax::tokens::{SqlToken, Symbol}; + use std::borrow::Cow; + + let join = JoinClause::new( + JoinKind::Inner, + TableMetadata::new_table(None, Cow::from("users")), + ColumnRef::from("t.id"), + Operator::Eq, + "users.team_id".into(), + ); + + let mut tokens = SqlTokens::default(); + tokens.extend( as ToSqlTokens<'_, PgDialect>>::to_tokens( + &join, + )); + + let expected = vec![ + SqlToken::Keyword(Keyword::Inner), + SqlToken::Keyword(Keyword::Join), + SqlToken::Symbol(Symbol::DoubleQuote), + SqlToken::Ident("users".into()), + SqlToken::Symbol(Symbol::DoubleQuote), + SqlToken::Keyword(Keyword::On), + SqlToken::Symbol(Symbol::DoubleQuote), + SqlToken::Ident("t".into()), + SqlToken::Symbol(Symbol::DoubleQuote), + SqlToken::Symbol(Symbol::Dot), + SqlToken::Symbol(Symbol::DoubleQuote), + SqlToken::Ident("id".into()), + SqlToken::Symbol(Symbol::DoubleQuote), + SqlToken::Operator(Operator::Eq), + SqlToken::Symbol(Symbol::DoubleQuote), + SqlToken::Ident("users".into()), + SqlToken::Symbol(Symbol::DoubleQuote), + SqlToken::Symbol(Symbol::Dot), + SqlToken::Symbol(Symbol::DoubleQuote), + SqlToken::Ident("team_id".into()), + SqlToken::Symbol(Symbol::DoubleQuote), + ]; + + assert_eq!(tokens.inner(), expected); +} diff --git a/canyon_core/src/query/querybuilder/syntax/keyword.rs b/canyon_core/src/query/querybuilder/syntax/keyword.rs new file mode 100644 index 00000000..08b02fba --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/keyword.rs @@ -0,0 +1,89 @@ +use std::fmt::{Display, Formatter}; + +#[derive(Debug, PartialEq, Eq)] +pub enum Keyword { + Select, + Insert, + Update, + Delete, + + From, + Into, + + Join, + Left, + Right, + Inner, + Outer, + Full, + FullOuter, + + On, + As, + Like, + Returning, + + Where, + And, + Or, + In, + GroupBy, + Having, + Limit, + OrderBy, + Desc, + Offset, + Values, + Set, + Not, + Cast, + Concat, + Distinct, + Count, + Output, + Inserted, +} + +impl Display for Keyword { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let i = match self { + Keyword::Select => "SELECT", + Keyword::Insert => "INSERT", + Keyword::Update => "UPDATE", + Keyword::Delete => "DELETE", + Keyword::From => "FROM", + Keyword::Into => "INTO", + Keyword::On => "ON", + Keyword::As => "AS", + Keyword::Like => "LIKE", + Keyword::Returning => "RETURNING", + Keyword::Join => "JOIN", + Keyword::Left => "LEFT", + Keyword::Right => "RIGHT", + Keyword::Inner => "INNER", + Keyword::Outer => "OUTER", + Keyword::Full => "FULL", + Keyword::FullOuter => "FULL OUTER", + Keyword::Where => "WHERE", + Keyword::And => "AND", + Keyword::Or => "OR", + Keyword::In => "IN", + Keyword::Desc => "DESC", + Keyword::GroupBy => "GROUP BY", + Keyword::Having => "HAVING", + Keyword::Limit => "LIMIT", + Keyword::OrderBy => "ORDER BY", + Keyword::Offset => "OFFSET", + Keyword::Values => "VALUES", + Keyword::Set => "SET", + Keyword::Not => "NOT", + Keyword::Cast => "CAST", + Keyword::Concat => "CONCAT", + Keyword::Distinct => "DISTINCT", + Keyword::Count => "COUNT", + Keyword::Output => "OUTPUT", + Keyword::Inserted => "INSERTED", + }; + write!(f, "{}", i) + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/mod.rs b/canyon_core/src/query/querybuilder/syntax/mod.rs new file mode 100644 index 00000000..3c2312fa --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/mod.rs @@ -0,0 +1,14 @@ +pub(crate) mod ast; +pub(crate) mod clause; +pub(crate) mod column; +pub(crate) mod dialect; +pub(crate) mod emitter; +pub(crate) mod having; +pub(crate) mod join; +pub(crate) mod keyword; +pub(crate) mod order; +pub(crate) mod query_kind; +mod symbol; +pub mod table_metadata; +pub(crate) mod tokens; +pub(crate) mod writer; diff --git a/canyon_core/src/query/querybuilder/syntax/order.rs b/canyon_core/src/query/querybuilder/syntax/order.rs new file mode 100644 index 00000000..1f41138e --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/order.rs @@ -0,0 +1,35 @@ +use crate::query::querybuilder::syntax::column::ColumnRef; +use crate::query::querybuilder::syntax::dialect::SqlDialect; +use crate::query::querybuilder::syntax::keyword::Keyword; +use crate::query::querybuilder::syntax::tokens::{SqlToken, SqlTokens, ToSqlTokens}; + +#[derive(Default)] +pub struct OrderByClause<'a> { + pub column: ColumnRef<'a>, + pub descending: bool, +} + +impl<'a> OrderByClause<'a> { + pub fn new>>(column: I, descending: bool) -> Self { + Self { + column: column.into(), + descending, + } + } +} + +impl<'a, D: SqlDialect> ToSqlTokens<'a, D> for OrderByClause<'a> { + fn to_tokens(&self) -> impl IntoIterator> + 'a { + let mut out = SqlTokens::with_capacity(3); + + out.keyword(Keyword::OrderBy); + out.extend( as ToSqlTokens<'_, D>>::to_tokens( + &self.column, + )); + if self.descending { + out.keyword(Keyword::Desc); + } + + out + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/query_kind.rs b/canyon_core/src/query/querybuilder/syntax/query_kind.rs new file mode 100644 index 00000000..59327f78 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/query_kind.rs @@ -0,0 +1,19 @@ +#[derive(Default, Debug)] +pub enum QueryKind { + #[default] + Select, + Insert, + Update, + Delete, +} + +impl AsRef for QueryKind { + fn as_ref(&self) -> &str { + match self { + QueryKind::Select => "SELECT", + QueryKind::Insert => "INSERT", + QueryKind::Update => "UPDATE ", + QueryKind::Delete => "DELETE ", + } + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/symbol.rs b/canyon_core/src/query/querybuilder/syntax/symbol.rs new file mode 100644 index 00000000..433a6d5c --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/symbol.rs @@ -0,0 +1,36 @@ +use crate::query::querybuilder::syntax::dialect::IdentQuoting; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Symbol { + Not, + LParen, + RParen, + Apostrophe, + Comma, + Dot, + Equals, + Semicolon, + Asterisk, + LAngle, + RAngle, + PercentSign, + Quote, + DoubleQuote, + Backtick, + LBracket, + RBracket, + Backslash, + + Empty, //<-- Special symbol to represent an empty symbol, used for cases where we want to represent the absence of a symbol without using Option +} + +impl From for Symbol { + fn from(quoting: IdentQuoting) -> Self { + match quoting { + IdentQuoting::DoubleQuote => Symbol::DoubleQuote, + IdentQuoting::Backtick => Symbol::Backtick, + IdentQuoting::OpeningBracket => Symbol::LBracket, + IdentQuoting::ClosingBracket => Symbol::RBracket, + } + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/table_metadata.rs b/canyon_core/src/query/querybuilder/syntax/table_metadata.rs new file mode 100644 index 00000000..b05e0854 --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/table_metadata.rs @@ -0,0 +1,129 @@ +use crate::query::bounds; +use crate::query::querybuilder::syntax::{ + dialect::SqlDialect, + emitter::types::helpers::push_quoted_ident, + symbol::Symbol, + tokens::{SqlToken, SqlTokens, ToSqlTokens}, +}; +use std::borrow::Cow; +use std::fmt::{Display, Formatter}; + +#[derive(Clone, Default, Debug)] +pub struct TableMetadata<'a> { + pub schema: Option>, + pub name: Cow<'a, str>, +} + +impl<'a, T> From for TableMetadata<'a> +where + T: bounds::EntityTable + 'a, +{ + fn from(value: T) -> Self { + Self::from(value.table_name()) // this covers the need of producing . + } +} + +impl<'a, D: SqlDialect> ToSqlTokens<'a, D> for TableMetadata<'a> { + fn to_tokens(&self) -> impl IntoIterator> + 'a { + let mut out = SqlTokens::with_capacity(3); + + if let Some(schema) = &self.schema { + push_quoted_ident::(schema.clone(), &mut out); + out.symbol(Symbol::Dot); + }; + + push_quoted_ident::(self.name.clone(), &mut out); + out + } +} + +impl<'a> From<&'a str> for TableMetadata<'a> { + /// Creates a new [`TableMetadata<'a>`] from a string slice. + /// + /// If the slice contains a dot, we assume that is a schema.table_name format, otherwise, + /// we assume that the client is just creating a [`Self`] from the passed in string + fn from(value: &'a str) -> Self { + if let Some((schema, table)) = value.split_once('.') { + Self { + schema: Some(Cow::Borrowed(schema)), + name: Cow::Borrowed(table), + } + } else { + Self { + schema: None, + name: Cow::Borrowed(value), + } + } + } +} + +impl From for TableMetadata<'static> { + /// Creates a new [`TableMetadata`] from an owned string. + /// + /// If the string contains a dot, we split it into owned schema and table name components. + fn from(value: String) -> Self { + if let Some((schema, table)) = value.split_once('.') { + Self { + schema: Some(Cow::Owned(schema.to_owned())), + name: Cow::Owned(table.to_owned()), + } + } else { + Self { + schema: None, + name: Cow::Owned(value), + } + } + } +} + +impl<'a> TableMetadata<'a> { + pub fn new(table_name: &'a str) -> Self { + Self::from(table_name) + } + + pub const fn new_table(schema: Option>, name: Cow<'a, str>) -> Self { + Self { schema, name } + } + + pub fn schema(&mut self, schema: S) + where + S: Into>, + { + self.schema = Some(schema.into()); + } + + pub fn table_name(&mut self, table_name: S) + where + S: Into>, + { + self.name = table_name.into(); + } + + /// Returns an already formatted version of the schema and table of a target database table + /// ready to be used in a SQL statement. + /// + /// This method allocates a new string, so it returns an owned one to the callee. + /// Just take it in consideration if someday someone uses it outside the macro generation + /// and there's some heavy callee procedure + pub fn sql(&self) -> String { + match &self.schema { + Some(schema_name) => { + format!("{}.{}", schema_name, self.name) + } + None => self.name.to_string(), + } + } +} + +impl<'a> Display for TableMetadata<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.schema { + Some(schema_name) => { + write!(f, "{}.{}", schema_name, self.name) + } + None => { + write!(f, "{}", self.name) + } + } + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/tokens.rs b/canyon_core/src/query/querybuilder/syntax/tokens.rs new file mode 100644 index 00000000..9cedd8aa --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/tokens.rs @@ -0,0 +1,201 @@ +use crate::query::querybuilder::syntax::dialect::SqlDialect; +use crate::query::querybuilder::syntax::emitter::types::helpers; +pub(crate) use crate::query::{ + operators::Operator, + querybuilder::syntax::{keyword::Keyword, symbol::Symbol, tokens::SqlToken::Number}, +}; +use std::borrow::Cow; + +pub trait ToSqlTokens<'a, D: SqlDialect> { + fn to_tokens(&self) -> impl IntoIterator> + 'a; +} + +impl<'a, D: SqlDialect> ToSqlTokens<'a, D> for Cow<'a, str> { + fn to_tokens(&self) -> impl IntoIterator> + 'a { + let mut tokens = SqlTokens::with_capacity(1); + helpers::push_quoted_ident::>(self.clone(), &mut tokens); + tokens + } +} + +/// 'newtype' (strong type) for the SqlToken container +#[derive(Debug, Default)] +pub struct SqlTokens<'a>(Vec>); +impl<'a> SqlTokens<'a> { + // our custom internal APIs over the underlying wrapped collection + pub fn ident(&mut self, ident: S) -> &mut Self + where + S: Into>, + { + self.0.push(SqlToken::Ident(ident.into())); + self + } + + pub fn numeric>(&mut self, num: N) { + self.0.push(Number(num.into())) + } + + pub fn keyword(&mut self, kw: Keyword) { + self.0.push(SqlToken::Keyword(kw)) + } + + pub fn operator(&mut self, op: Operator) { + self.0.push(SqlToken::Operator(op)) + } + + pub fn symbol(&mut self, sym: Symbol) { + self.0.push(SqlToken::Symbol(sym)) + } + + pub fn placeholder(&mut self) { + self.0.push(SqlToken::Placeholder) + } + + pub fn inner(self) -> Vec> { + self.0 + } + + pub fn with_capacity(capacity: usize) -> Self { + Self(Vec::with_capacity(capacity)) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn first(&self) -> Option<&SqlToken<'a>> { + self.0.first() + } + + pub fn last(&self) -> Option<&SqlToken<'a>> { + self.0.last() + } + + pub fn remove_last_if(&mut self, predicate: F) -> Option> + where + F: FnOnce(&SqlToken<'a>) -> bool, + { + if let Some(last) = self.0.last() + && predicate(last) + { + return self.0.pop(); + } + None + } + + #[inline] + pub fn iter(&self) -> std::slice::Iter<'_, SqlToken<'a>> { + self.0.iter() + } + + pub fn comma(&mut self) -> &mut Self { + self.0.push(SqlToken::Symbol(Symbol::Comma)); + self + } + + pub fn dot(&mut self) -> &mut Self { + self.0.push(SqlToken::Symbol(Symbol::Dot)); + self + } +} + +impl<'a> IntoIterator for SqlTokens<'a> { + type Item = SqlToken<'a>; + type IntoIter = std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a SqlTokens<'a> { + type Item = &'a SqlToken<'a>; + type IntoIter = std::slice::Iter<'a, SqlToken<'a>>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl<'a> IntoIterator for &'a mut SqlTokens<'a> { + type Item = &'a mut SqlToken<'a>; + type IntoIter = std::slice::IterMut<'a, SqlToken<'a>>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter_mut() + } +} + +impl<'a> Extend> for SqlTokens<'a> { + fn extend>>(&mut self, iter: T) { + self.0.extend(iter); + } +} + +impl<'a> Extend> for &'a mut SqlTokens<'a> { + fn extend>>(&mut self, iter: T) { + self.0.extend(iter); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum SqlToken<'a> { + Keyword(Keyword), // SELECT, WHERE, AND, OR, FROM, UPDATE, DELETE // TODO: model them as ctc + Ident(Cow<'a, str>), // a raw literal value + Number(NumberKind), // a raw literal numeric value + Symbol(Symbol), // =, ( ) , . + Operator(Operator), // Operator::Eq, Operator::GtEq... + Placeholder, // $1, ? , @P1 +} + +#[derive(Debug, PartialEq, Eq)] +pub enum NumberKind { + Integer(usize), +} + +mod __impl_sql_token { + use super::*; + use crate::query::querybuilder::syntax::dialect::IdentQuoting; + + impl<'a> From for SqlToken<'a> { + fn from(quoting: IdentQuoting) -> Self { + match quoting { + IdentQuoting::Backtick => SqlToken::Symbol(Symbol::Backtick), + IdentQuoting::DoubleQuote => SqlToken::Symbol(Symbol::DoubleQuote), + IdentQuoting::OpeningBracket => SqlToken::Symbol(Symbol::LBracket), // Note: we use LBracket for both [ and ] since they are used in pairs + IdentQuoting::ClosingBracket => SqlToken::Symbol(Symbol::RBracket), // Note: we use LBracket for both [ and ] since they are used in pairs + } + } + } +} + +mod __impl { + use super::*; + use std::fmt::Display; + + impl From for NumberKind { + fn from(value: usize) -> Self { + NumberKind::Integer(value) + } + } + + impl From for NumberKind { + fn from(value: u32) -> Self { + NumberKind::Integer(value as usize) + } + } + + impl From for NumberKind { + fn from(value: u64) -> Self { + NumberKind::Integer(value as usize) + } + } + + impl Display for NumberKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NumberKind::Integer(i) => write!(f, "{}", i), + } + } + } +} diff --git a/canyon_core/src/query/querybuilder/syntax/writer.rs b/canyon_core/src/query/querybuilder/syntax/writer.rs new file mode 100644 index 00000000..6e56fa8f --- /dev/null +++ b/canyon_core/src/query/querybuilder/syntax/writer.rs @@ -0,0 +1,371 @@ +use crate::query::querybuilder::syntax::{ + dialect::SqlDialect, symbol::Symbol, tokens::SqlToken, tokens::SqlTokens, +}; + +pub struct TokenWriter {} + +impl TokenWriter { + pub fn new() -> Self { + Self {} + } + + pub fn render<'a, D: SqlDialect>( + self, + mut tokens: SqlTokens<'a>, + ) -> Result { + let mut out = String::new(); + tokens.symbol(Symbol::Semicolon); + + let mut placeholder_counter = 1usize; + let mut previous: Option<&SqlToken<'a>> = None; + + for token in tokens.iter() { + if __impl::requires_space_between(previous, token) { + out.push(' '); + } + + __impl::output_token_to_string_buffer::(token, &mut out, &mut placeholder_counter)?; + + previous = Some(token); + } + + Ok(out) + } +} + +mod __impl { + use crate::query::querybuilder::syntax::keyword::Keyword; + use crate::query::querybuilder::syntax::{ + dialect::SqlDialect, symbol::Symbol, tokens::SqlToken, writer::__detail, + }; + use std::fmt::Write; + + pub(crate) fn output_token_to_string_buffer( + token: &SqlToken<'_>, + f: &mut String, + placeholder_counter: &mut usize, + ) -> Result<(), std::fmt::Error> { + let _: () = match token { + SqlToken::Keyword(s) => write!(f, "{}", s)?, + SqlToken::Ident(s) => write!(f, "{}", s)?, + SqlToken::Symbol(sym) => __detail::render_symbol(*sym, f)?, + SqlToken::Operator(op) => write!(f, "{}", op)?, + SqlToken::Placeholder => { + __detail::write_value_placeholder::(placeholder_counter, f)? + } + SqlToken::Number(num) => write!(f, "{}", num)?, + }; + Ok(()) + } + + pub(crate) fn requires_space_between( + previous: Option<&SqlToken<'_>>, + current: &SqlToken<'_>, + ) -> bool { + let Some(previous) = previous else { + return false; + }; + + if is_quoted_ident_boundary(previous, current) + || suppresses_trailing_space(previous) + || is_function_call_boundary(previous, current) + { + return false; + } + + wants_leading_space_after(previous, current) + } + + fn is_function_call_boundary(previous: &SqlToken<'_>, current: &SqlToken<'_>) -> bool { + matches!( + (previous, current), + ( + SqlToken::Keyword(Keyword::Count), + SqlToken::Symbol(Symbol::LParen), + ) + ) + } + + fn wants_leading_space_after(_previous: &SqlToken<'_>, current: &SqlToken<'_>) -> bool { + matches!( + current, + SqlToken::Keyword(_) + | SqlToken::Ident(_) + | SqlToken::Number(_) + | SqlToken::Placeholder + | SqlToken::Operator(_) + | SqlToken::Symbol( + Symbol::Asterisk + | Symbol::LParen + | Symbol::Quote + | Symbol::DoubleQuote + | Symbol::Backtick + | Symbol::LBracket + | Symbol::Equals + ) + ) + } + + fn suppresses_trailing_space(token: &SqlToken<'_>) -> bool { + matches!( + token, + SqlToken::Symbol( + Symbol::Dot + | Symbol::LParen + | Symbol::LBracket + | Symbol::PercentSign + | Symbol::Backslash + ) + ) + } + + fn is_quoted_ident_boundary(previous: &SqlToken<'_>, current: &SqlToken<'_>) -> bool { + matches!( + (previous, current), + ( + SqlToken::Symbol(Symbol::Quote | Symbol::DoubleQuote | Symbol::Backtick), + SqlToken::Ident(_), + ) | ( + SqlToken::Ident(_), + SqlToken::Symbol(Symbol::Quote | Symbol::DoubleQuote | Symbol::Backtick), + ) + ) + } +} + +mod __detail { + use crate::query::querybuilder::syntax::dialect::PlaceholderSymbol; + use crate::query::querybuilder::syntax::{dialect::SqlDialect, symbol::Symbol}; + use std::fmt::Write; + + pub(crate) fn render_symbol(sym: Symbol, f: &mut String) -> Result<(), std::fmt::Error> { + let _: () = match sym { + Symbol::Not => write!(f, "!")?, + Symbol::Comma => write!(f, ",")?, + Symbol::LParen => write!(f, "(")?, + Symbol::RParen => write!(f, ")")?, + Symbol::Dot => write!(f, ".")?, + Symbol::Semicolon => write!(f, ";")?, + Symbol::Equals => write!(f, "=")?, + Symbol::Asterisk => write!(f, "*")?, + Symbol::Apostrophe => write!(f, "'")?, + Symbol::LAngle => write!(f, "<")?, + Symbol::RAngle => write!(f, ">")?, + Symbol::PercentSign => write!(f, "%")?, + Symbol::Quote => write!(f, "'")?, + Symbol::DoubleQuote => write!(f, "\"")?, + Symbol::Backtick => write!(f, "`")?, + Symbol::LBracket => write!(f, "[")?, + Symbol::RBracket => write!(f, "]")?, + Symbol::Backslash => write!(f, "\\")?, + Symbol::Empty => write!(f, "")?, + }; + Ok(()) + } + + pub(crate) fn write_value_placeholder( + placeholder_counter: &mut usize, + f: &mut String, + ) -> Result<(), std::fmt::Error> { + if D::PLACEHOLDER_SYMBOL.eq(&PlaceholderSymbol::QuestionMark) { + write!(f, "{}", D::PLACEHOLDER_SYMBOL)?; + } else { + write!(f, "{}{}", D::PLACEHOLDER_SYMBOL, placeholder_counter)?; + *placeholder_counter += 1; + } + + Ok(()) + } +} + +#[cfg(test)] +#[cfg(feature = "mssql")] +mod mssql_tests { + use crate::query::ColumnRef; + use crate::query::querybuilder::syntax::dialect::{MsSql, SqlDialect}; + use crate::query::querybuilder::syntax::emitter::types::helpers::{ + emit_qualified_columns, push_quoted_ident, + }; + use crate::query::querybuilder::syntax::symbol::Symbol; + use crate::query::querybuilder::syntax::tokens::{SqlToken, SqlTokens}; + use std::borrow::Cow; + + #[cfg(feature = "mssql")] + #[test] + fn mssql_ident_quoting_opening_and_closing_convert_to_expected_symbols() { + use crate::query::querybuilder::syntax::symbol::Symbol; + + let opening: Symbol = MsSql::IDENT_QUOTING.opening().into(); + let closing: Symbol = MsSql::IDENT_QUOTING.closing().into(); + + assert_eq!(opening, Symbol::LBracket); + assert_eq!(closing, Symbol::RBracket); + assert_ne!(opening, closing); + } + + #[cfg(feature = "mssql")] + #[test] + fn push_quoted_ident_with_mssql_emits_left_ident_right_bracket_sequence() { + let mut tokens = SqlTokens::default(); + push_quoted_ident::("users", &mut tokens); + + assert_eq!( + tokens.inner(), + vec![ + SqlToken::Symbol(Symbol::LBracket), + SqlToken::Ident(Cow::Borrowed("users")), + SqlToken::Symbol(Symbol::RBracket), + ] + ); + } + + #[cfg(feature = "mssql")] + #[test] + fn emit_columns_with_mssql_emits_balanced_brackets_for_every_identifier() { + let columns = get_columns_mock(); + let mut tokens = SqlTokens::default(); + emit_qualified_columns::(&columns, &mut tokens); + + assert_eq!( + tokens.inner(), + get_columns_assert_values( + MsSql::IDENT_QUOTING.opening().into(), + MsSql::IDENT_QUOTING.closing().into() + ) + ); + } + + fn get_columns_mock() -> Vec> { + vec![ + ColumnRef::from("id"), + ColumnRef::from("name"), + ColumnRef::from("email"), + ] + } + + fn get_columns_assert_values(opening: Symbol, closing: Symbol) -> Vec> { + vec![ + SqlToken::Symbol(opening), + SqlToken::Ident(Cow::Borrowed("id")), + SqlToken::Symbol(closing), + SqlToken::Symbol(Symbol::Comma), + SqlToken::Symbol(opening), + SqlToken::Ident(Cow::Borrowed("name")), + SqlToken::Symbol(closing), + SqlToken::Symbol(Symbol::Comma), + SqlToken::Symbol(opening), + SqlToken::Ident(Cow::Borrowed("email")), + SqlToken::Symbol(closing), + ] + } +} + +#[cfg(test)] +mod spacing_tests { + use super::*; + use crate::query::{ + operators::Operator, + querybuilder::syntax::{dialect::PgDialect, keyword::Keyword, tokens::SqlTokens}, + }; + + #[test] + fn render_spaces_select_from_where_and_operators_without_whitespace_tokens() { + let mut tokens = SqlTokens::default(); + tokens.keyword(Keyword::Select); + tokens.symbol(Symbol::Asterisk); + tokens.keyword(Keyword::From); + tokens.symbol(Symbol::DoubleQuote); + tokens.ident("league"); + tokens.symbol(Symbol::DoubleQuote); + tokens.keyword(Keyword::Where); + tokens.symbol(Symbol::DoubleQuote); + tokens.ident("id"); + tokens.symbol(Symbol::DoubleQuote); + tokens.operator(Operator::Gt); + tokens.placeholder(); + + let sql = TokenWriter::new() + .render::(tokens) + .expect("failed to render SQL"); + + assert_eq!(sql, "SELECT * FROM \"league\" WHERE \"id\" > $1;"); + } + + #[test] + fn render_does_not_insert_spaces_inside_quoted_identifiers() { + let mut tokens = SqlTokens::default(); + tokens.keyword(Keyword::Select); + tokens.symbol(Symbol::DoubleQuote); + tokens.ident("league"); + tokens.symbol(Symbol::DoubleQuote); + tokens.symbol(Symbol::Dot); + tokens.symbol(Symbol::DoubleQuote); + tokens.ident("id"); + tokens.symbol(Symbol::DoubleQuote); + tokens.keyword(Keyword::From); + tokens.symbol(Symbol::DoubleQuote); + tokens.ident("league"); + tokens.symbol(Symbol::DoubleQuote); + + let sql = TokenWriter::new() + .render::(tokens) + .expect("failed to render SQL"); + + assert_eq!(sql, "SELECT \"league\".\"id\" FROM \"league\";"); + } + + #[test] + fn render_spaces_commas_function_calls_and_parentheses_without_trailing_comma_space() { + let mut tokens = SqlTokens::default(); + tokens.keyword(Keyword::In); + tokens.symbol(Symbol::LParen); + tokens.placeholder(); + tokens.symbol(Symbol::Comma); + tokens.placeholder(); + tokens.symbol(Symbol::RParen); + + let sql = TokenWriter::new() + .render::(tokens) + .expect("failed to render SQL"); + + assert_eq!(sql, "IN ($1, $2);"); + } + + #[test] + fn render_spaces_function_call_parentheses_and_in_parentheses() { + let mut tokens = SqlTokens::default(); + tokens.keyword(Keyword::Like); + tokens.keyword(Keyword::Concat); + tokens.symbol(Symbol::LParen); + tokens.symbol(Symbol::Quote); + tokens.symbol(Symbol::PercentSign); + tokens.symbol(Symbol::Quote); + tokens.symbol(Symbol::Comma); + tokens.keyword(Keyword::Cast); + tokens.symbol(Symbol::LParen); + tokens.placeholder(); + tokens.keyword(Keyword::As); + tokens.ident("VARCHAR"); + tokens.symbol(Symbol::RParen); + tokens.symbol(Symbol::Comma); + tokens.symbol(Symbol::Quote); + tokens.symbol(Symbol::PercentSign); + tokens.symbol(Symbol::Quote); + tokens.symbol(Symbol::RParen); + tokens.keyword(Keyword::In); + tokens.symbol(Symbol::LParen); + tokens.placeholder(); + tokens.symbol(Symbol::Comma); + tokens.placeholder(); + tokens.symbol(Symbol::RParen); + + let sql = TokenWriter::new() + .render::(tokens) + .expect("failed to render SQL"); + + assert_eq!( + sql, + "LIKE CONCAT ('%', CAST ($1 AS VARCHAR), '%') IN ($2, $3);" + ); + } +} diff --git a/canyon_core/src/query/querybuilder/types/delete.rs b/canyon_core/src/query/querybuilder/types/delete.rs new file mode 100644 index 00000000..1235508c --- /dev/null +++ b/canyon_core/src/query/querybuilder/types/delete.rs @@ -0,0 +1,113 @@ +use std::error::Error; + +use crate::{ + connection::database_type::DatabaseType, + query::{ + ColumnRef, + bounds::{FieldIdentifier, FieldValueIdentifier}, + operators::Operator, + parameters::QueryParameter, + query::Query, + querybuilder::{ + DeleteQueryBuilderOps, QueryBuilder, QueryBuilderOps, syntax::ast::delete::DeleteAst, + types::TableMetadata, + }, + }, +}; + +/// Fluent builder for `DELETE` statements +pub struct DeleteQueryBuilder<'a> { + pub(crate) _inner: QueryBuilder<'a, DeleteAst>, +} + +impl<'a> DeleteQueryBuilder<'a> { + /// Creates a delete builder whose database dialect will be resolved later. + pub fn new( + table_schema_data: impl Into>, + database_type: DatabaseType, + ) -> Self { + Self { + _inner: QueryBuilder::new(table_schema_data, DeleteAst::new(), database_type), + } + } + + /// Creates a delete builder for a specific database dialect. + pub const fn new_querybuilder( + table_schema_data: TableMetadata<'a>, + database_type: DatabaseType, + ) -> Self { + Self { + _inner: QueryBuilder::new_querybuilder( + table_schema_data, + DeleteAst::new(), + database_type, + ), + } + } + + #[inline(always)] + pub fn build(self) -> Result, Box> { + self._inner.build() + } +} + +impl<'a> DeleteQueryBuilderOps<'a> for DeleteQueryBuilder<'a> {} + +impl<'a> QueryBuilderOps<'a> for DeleteQueryBuilder<'a> { + #[inline(always)] + fn build(self) -> Result, Box> { + self._inner.build() + } + + #[inline] + fn r#where>>(mut self, column: I, op: Operator) -> Self { + self._inner.r#where(column, op); + self + } + + #[inline] + fn where_value(mut self, column: &'a Z, op: Operator) -> Self { + self._inner.where_value(column, op); + self + } + + #[inline] + fn and(mut self, column: &'a Z, op: Operator) -> Self { + self._inner.and(column, op); + self + } + + #[inline] + fn and_values_in<'b, Z, Q>( + mut self, + column: Z, + values: &'a [Q], + ) -> Result> + where + Z: FieldIdentifier, + Q: QueryParameter, + { + self._inner.and_values_in(column, values)?; + Ok(self) + } + + #[inline] + fn or_values_in<'b, Z, Q>( + mut self, + column: Z, + values: &'a [Q], + ) -> Result> + where + Z: FieldIdentifier, + Q: QueryParameter, + { + self._inner.or_values_in(column, values)?; + Ok(self) + } + + #[inline] + fn or(mut self, column: &'a Z, op: Operator) -> Self { + self._inner.or(column, op); + self + } +} diff --git a/canyon_core/src/query/querybuilder/types/insert.rs b/canyon_core/src/query/querybuilder/types/insert.rs new file mode 100644 index 00000000..431fc58b --- /dev/null +++ b/canyon_core/src/query/querybuilder/types/insert.rs @@ -0,0 +1,170 @@ +use std::borrow::Cow; +use std::error::Error; + +use crate::{ + connection::database_type::DatabaseType, + query::{ + bounds::{FieldIdentifier, FieldValueIdentifier}, + operators::Operator, + parameters::QueryParameter, + query::Query, + querybuilder::{ + InsertQueryBuilderOps, QueryBuilder, QueryBuilderOps, + syntax::{ast::insert::InsertAst, column::ColumnRef, table_metadata::TableMetadata}, + }, + }, +}; + +/// Fluent builder for `INSERT` statements +pub struct InsertQueryBuilder<'a> { + pub(crate) _inner: QueryBuilder<'a, InsertAst<'a>>, +} + +impl<'a> InsertQueryBuilder<'a> { + /// Creates an insert builder for a specific database dialect. + pub fn new( + table_schema_data: impl Into>, + database_type: DatabaseType, + ) -> Self { + Self { + _inner: QueryBuilder::new(table_schema_data, InsertAst::new(), database_type), + } + } + + /// Creates a const-compatible builder from normalized table metadata. + pub const fn new_querybuilder( + table_schema_data: TableMetadata<'a>, + database_type: DatabaseType, + ) -> Self { + Self { + _inner: QueryBuilder::new_querybuilder( + table_schema_data, + InsertAst::new(), + database_type, + ), + } + } + + /// Creates a const-compatible builder directly from schema and table parts. + pub const fn new_from_parts( + schema: Option>, + table_name: Cow<'a, str>, + database_type: DatabaseType, + ) -> Self { + let table_schema_data = TableMetadata { + schema, + name: table_name, + }; + + Self::new_querybuilder(table_schema_data, database_type) + } + + /// Appends columns that are already represented by the query syntax model. + /// + /// This is primarily useful for generated code and internal APIs that do + /// not require identifier normalization. + pub fn with_known_columns(mut self, columns: I) -> Self + where + I: IntoIterator>, + { + self._inner.ast.columns.extend(columns); + self + } + + /// Appends an already normalized returning projection. + pub fn returning_columns(mut self, columns: I) -> Self + where + I: IntoIterator>, + { + self._inner.ast.returning_columns.extend(columns); + self + } + + #[inline(always)] + pub fn build(self) -> Result, Box> { + self._inner.build() + } +} + +impl<'a> QueryBuilderOps<'a> for InsertQueryBuilder<'a> { + #[inline(always)] + fn build(self) -> Result, Box> { + self._inner.build() + } + + #[inline] + fn r#where>>(mut self, column: I, op: Operator) -> Self { + self._inner.r#where(column, op); + self + } + + #[inline] + fn where_value(mut self, column: &'a Z, op: Operator) -> Self { + self._inner.where_value(column, op); + self + } + + #[inline] + fn and(mut self, column: &'a Z, op: Operator) -> Self { + self._inner.and(column, op); + self + } + + #[inline] + fn and_values_in<'b, Z, Q>( + mut self, + column: Z, + values: &'a [Q], + ) -> Result> + where + Z: FieldIdentifier, + Q: QueryParameter, + Self: Sized, + { + self._inner.and_values_in(column, values)?; + Ok(self) + } + + #[inline] + fn or_values_in<'b, Z, Q>( + mut self, + column: Z, + values: &'a [Q], + ) -> Result> + where + Z: FieldIdentifier, + Q: QueryParameter, + Self: Sized, + { + self._inner.or_values_in(column, values)?; + Ok(self) + } + + #[inline] + fn or(mut self, column: &'a Z, op: Operator) -> Self { + self._inner.or(column, op); + self + } +} + +impl<'a> InsertQueryBuilderOps<'a> for InsertQueryBuilder<'a> { + fn with_columns>>(mut self, columns: Vec) -> Self { + self._inner.ast.columns = columns.into_iter().map(Into::into).collect(); + self + } + + fn with_values(mut self, values: &'a [Q]) -> Result> + where + Q: QueryParameter, + { + for value in values { + self._inner.params.push(value); + } + Ok(self) + } + + fn returning(mut self, columns: Vec>>) -> Self { + self._inner.ast.returning_columns = columns.into_iter().map(Into::into).collect(); + self + } +} diff --git a/canyon_core/src/query/querybuilder/types/mod.rs b/canyon_core/src/query/querybuilder/types/mod.rs new file mode 100644 index 00000000..28dd1022 --- /dev/null +++ b/canyon_core/src/query/querybuilder/types/mod.rs @@ -0,0 +1,387 @@ +pub mod delete; +pub mod insert; +pub mod select; +pub mod update; + +pub use self::{delete::*, insert::*, select::*, update::*}; +use crate::query::querybuilder::syntax::emitter::BackendEmittable; +use crate::{ + connection::database_type::DatabaseType, + query::ColumnRef, + query::querybuilder::syntax::emitter::types::helpers::Range, + query::{ + bounds::{FieldIdentifier, FieldValueIdentifier}, + operators::Operator, + parameters::QueryParameter, + query::Query, + querybuilder::syntax::{ + ast::BaseAst, clause::ConditionClauseKind, table_metadata::TableMetadata, + }, + }, +}; +use std::error::Error; + +/// Type for construct more complex queries than the classical CRUD ones. +pub struct QueryBuilder<'a, P: BackendEmittable<'a> + 'a> { + pub(crate) base_ast: BaseAst<'a>, + pub(crate) ast: P, + pub(crate) database_type: DatabaseType, + pub(crate) params: Vec<&'a dyn QueryParameter>, +} + +unsafe impl<'a, P: BackendEmittable<'a>> Send for QueryBuilder<'a, P> {} +unsafe impl<'a, P: BackendEmittable<'a>> Sync for QueryBuilder<'a, P> {} + +impl<'a, P: BackendEmittable<'a> + 'a> QueryBuilder<'a, P> { + pub fn new( + table_metadata: impl Into>, + ast: P, + database_type: DatabaseType, + ) -> Self { + Self { + base_ast: BaseAst::new(table_metadata), + ast, + database_type, + params: Vec::new(), + } + } + + pub const fn new_querybuilder( + table_metadata: TableMetadata<'a>, + ast: P, + database_type: DatabaseType, + ) -> Self { + Self { + base_ast: BaseAst::new_ast(table_metadata), + ast, + database_type, + params: Vec::new(), + } + } + + pub fn build(self) -> Result, Box> { + __impl::check_invariants_over_condition_clauses(&self)?; + + let Self { + mut base_ast, + ast, + database_type, + params, + } = self; + + let sql = __detail::sql(database_type, &ast, &mut base_ast)?; + // __dbg::log_sql(&sql, database_type, ast.query_kind(), ¶ms); + Ok(Query::new(sql, params)) + } + + fn r#where>>(&mut self, column_name: I, operator: Operator) { + __impl::create_condition_clause(self, ConditionClauseKind::Where, column_name, operator); + } + + pub fn where_value(&mut self, r#where: &'a Z, operator: Operator) { + self.params.push(r#where.value()); + __impl::create_condition_clause( + self, + ConditionClauseKind::Where, + r#where.column(), + operator, + ); + } + + pub fn and(&mut self, r#and: &'a Z, operator: Operator) { + self.params.push(and.value()); + __impl::create_condition_clause(self, ConditionClauseKind::And, and.column(), operator); + } + + pub fn and_values_in<'b, Z, Q>( + &mut self, + field: Z, + values: &'a [Q], + ) -> Result<(), Box> + where + Z: FieldIdentifier, + Q: QueryParameter, + { + let actual_params_len = self.params.len(); + __impl::create_ranged_condition_clause( + self, + ConditionClauseKind::And, + field.as_str(), + Operator::In, + Range::new(actual_params_len, actual_params_len + values.len()), + ); + __impl::add_values_in_for_and_or_or_clause(self, ConditionClauseKind::And, field, values) + } + + pub fn or_values_in<'b, Z, Q>( + &mut self, + r#or: Z, + values: &'a [Q], + ) -> Result<(), Box> + where + Z: FieldIdentifier, + Q: QueryParameter, + { + let actual_params_len = self.params.len(); + __impl::create_ranged_condition_clause( + self, + ConditionClauseKind::Or, + r#or.as_str(), + Operator::In, + Range::new(actual_params_len, actual_params_len + values.len()), + ); + __impl::add_values_in_for_and_or_or_clause(self, ConditionClauseKind::Or, r#or, values) + } + + pub fn or(&mut self, r#or: &'a Z, operator: Operator) { + self.params.push(or.value()); + __impl::create_condition_clause(self, ConditionClauseKind::Or, or.column(), operator); + } +} + +mod __impl { + use crate::query::bounds::FieldIdentifier; + use crate::query::operators::Operator; + use crate::query::parameters::QueryParameter; + use crate::query::querybuilder::QueryBuilder; + use crate::query::querybuilder::syntax::clause::{ConditionClause, ConditionClauseKind}; + use crate::query::querybuilder::syntax::column::ColumnRef; + use crate::query::querybuilder::syntax::emitter::BackendEmittable; + use crate::query::querybuilder::syntax::emitter::types::helpers::Range; + use crate::query::querybuilder::types::__validators; + use std::error::Error; + + pub(crate) fn add_values_in_for_and_or_or_clause<'a, 'b, P, Z, Q>( + _self: &mut QueryBuilder<'a, P>, + _conjunction_clause_kind: ConditionClauseKind, + field: Z, + values: &'a [Q], + ) -> Result<(), Box> + where + Q: QueryParameter, + Z: FieldIdentifier, + P: BackendEmittable<'a>, + { + let target_column = field.as_str(); + __validators::check_not_empty_in_clause_values( + _self.base_ast.table(), + target_column, + values, + )?; + + for value in values { + _self.params.push(value); + } + + Ok(()) + } + + /// Quick standalone that acts as a façade for an orchestrator that just organizes a procedural way of testing + /// that the constructed underlying query is syntactically correct + pub(crate) fn check_invariants_over_condition_clauses<'a, 'b, P: BackendEmittable<'a>>( + _self: &QueryBuilder<'a, P>, + ) -> Result<(), Box> { + __validators::check_where_clause_position(_self) + } + + pub(crate) fn create_condition_clause<'a, P: BackendEmittable<'a>>( + _self: &mut QueryBuilder<'a, P>, + kind: ConditionClauseKind, + column_name: impl Into>, + operator: Operator, + ) { + _self.base_ast.add_condition(ConditionClause { + kind, + column_name: column_name.into(), + operator, + value_indexes: Some(Range::new_unbounded(_self.params.len())), + }); + } + + pub(crate) fn create_ranged_condition_clause<'a, P: BackendEmittable<'a>>( + _self: &mut QueryBuilder<'a, P>, + kind: ConditionClauseKind, + column_name: impl Into>, + operator: Operator, + value_indexes_range: Range, + ) { + _self.base_ast.add_condition(ConditionClause { + kind, + column_name: column_name.into(), + operator, + value_indexes: Some(value_indexes_range), + }); + } +} + +mod __detail { + use crate::connection::database_type::DatabaseType; + use crate::query::querybuilder::syntax::ast::BaseAst; + + #[cfg(feature = "postgres")] + use crate::query::querybuilder::syntax::dialect::PgDialect; + + #[cfg(feature = "mssql")] + use crate::query::querybuilder::syntax::dialect::MsSql; + + #[cfg(feature = "mysql")] + use crate::query::querybuilder::syntax::dialect::MySql; + + use crate::query::querybuilder::syntax::emitter::BackendEmittable; + use crate::query::querybuilder::syntax::tokens::SqlTokens; + use crate::query::querybuilder::syntax::writer::TokenWriter; + use std::error::Error; + + pub(super) fn sql<'a, P>( + database_type: DatabaseType, + ast: &P, + base_ast: &mut BaseAst<'a>, + ) -> Result> + where + P: BackendEmittable<'a> + 'a, + { + let tokens = run_emission_phase(database_type, ast, base_ast); + run_render_phase(tokens, database_type) + } + + /// Executes the SQL emission phase for the given AST and database backend. + /// + /// This function selects the appropriate backend-specific emitter + /// based on `database_type` and delegates SQL generation to it. + /// + /// It acts as the orchestration boundary between: + /// - The backend-agnostic query representation (`ast`, `base_ast`) + /// - The backend-specific SQL emission strategy (`PgEmitter`, `MySqlEmitter`, etc.) + /// + /// # Parameters + /// + /// - `database_type`: Target database backend used to determine + /// which SQL dialect implementation will be executed. + /// - `ast`: The query AST that describes the high-level structure + /// of the query. + /// - `base_ast`: Shared base metadata required for emission, + /// such as table information and condition clauses. + /// + /// # Behavior + /// + /// The function: + /// 1. Extracts the query kind from the AST. + /// 2. Instantiates the corresponding backend emitter. + /// 3. Executes the emission phase for that backend. + /// + /// # Panics + /// + /// Panics if the provided database backend is not supported. + pub(super) fn run_emission_phase<'a, P>( + database_type: DatabaseType, + ast: &P, + base_ast: &mut BaseAst<'a>, + ) -> SqlTokens<'a> + where + P: BackendEmittable<'a> + 'a, + { + P::emit_for(database_type, ast, base_ast) + } + + pub(crate) fn run_render_phase<'a>( + tokens: SqlTokens<'a>, + db: DatabaseType, + ) -> Result> { + let writer = TokenWriter::new(); + match db { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => writer.render::(tokens), + #[cfg(feature = "mysql")] + DatabaseType::MySQL => writer.render::(tokens), + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => writer.render::(tokens), + } + .map_err(|e| e.into()) + } +} + +mod __validators { + use crate::query::parameters::QueryParameter; + use crate::query::querybuilder::QueryBuilder; + use crate::query::querybuilder::syntax::clause::ConditionClauseKind; + use crate::query::querybuilder::syntax::emitter::BackendEmittable; + use crate::query::querybuilder::types::__errors; + use std::error::Error; + use std::fmt::Display; + + /// For now, it's mandatory because we need to ensure what's the placeholder index which is the element + /// that should swap with the where clause if isn't put in an incorrect order, no implementation ready + pub(crate) fn check_where_clause_position<'a, 'b, P: BackendEmittable<'a>>( + _self: &QueryBuilder<'a, P>, + ) -> Result<(), Box> { + if let Some(condition_clause) = &_self.base_ast.conditions().first() + && condition_clause.kind.ne(&ConditionClauseKind::Where) + { + __errors::where_clause_position() + } else { + Ok(()) + } + } + + pub(crate) fn check_not_empty_in_clause_values<'a, 'b, Q>( + table_metadata: impl Display, + column: &'a str, + values: &'a [Q], + ) -> Result<(), Box> + where + Q: QueryParameter, + { + if values.is_empty() { + return __errors::empty_in_clause(table_metadata, column); + } + Ok(()) + } +} + +mod __errors { + use std::error::Error; + use std::fmt::Display; + use std::io::ErrorKind; + + pub(crate) fn where_clause_position<'a>() -> Result<(), Box> { + Err(std::io::Error::new( + // TODO: CanyonError + ErrorKind::Unsupported, + "Where clauses should be the first condition clause on a SQL sentence", + ) + .into()) + } + + pub(crate) fn empty_in_clause<'a, 'b>( + table_metadata: impl Display, + column: &'a str, + ) -> Result<(), Box> { + Err(std::io::Error::new( // TODO: CanyonError + ErrorKind::Unsupported, + format!("An IN clause has been added with empty values for {table_metadata} on the column: {column}", )).into()) + } +} + +#[allow(unused)] +mod __dbg { + use crate::connection::database_type::DatabaseType; + use crate::query::parameters::QueryParameter; + use crate::query::querybuilder::syntax::query_kind::QueryKind; + + pub(crate) fn log_sql( + sql: &str, + database_type: DatabaseType, + query_kind: QueryKind, + args: &[&dyn QueryParameter], + ) { + eprintln!( + "\ + \n + ========================================================== + \ + [Canyon-SQL] [{database_type:?}] [{query_kind:?}]\n\t{sql}\ + Args: [{args:#?}] + " + ); + } +} diff --git a/canyon_core/src/query/querybuilder/types/select.rs b/canyon_core/src/query/querybuilder/types/select.rs new file mode 100644 index 00000000..cad3c07f --- /dev/null +++ b/canyon_core/src/query/querybuilder/types/select.rs @@ -0,0 +1,259 @@ +use crate::{ + connection::database_type::DatabaseType, + query::{ + bounds::{FieldIdentifier, FieldValueIdentifier}, + operators::Operator, + parameters::QueryParameter, + query::Query, + querybuilder::{ + QueryBuilder, QueryBuilderOps, SelectQueryBuilderOps, + syntax::{ + ast::select::SelectAst, column::ColumnRef, join::JoinKind, order::OrderByClause, + table_metadata::TableMetadata, + }, + }, + }, +}; +use std::borrow::Cow; +use std::error::Error; + +/// Fluent builder for `SELECT` queries +pub struct SelectQueryBuilder<'a> { + pub(crate) _inner: QueryBuilder<'a, SelectAst<'a>>, +} + +impl<'a> SelectQueryBuilder<'a> { + /// Creates a builder for the given table and target database. + pub fn new( + table_schema_data: impl Into>, + database_type: DatabaseType, + ) -> Self { + Self { + _inner: QueryBuilder::new(table_schema_data, SelectAst::new(), database_type), + } + } + + /// Creates a builder from already normalized table metadata. + /// + /// This constructor is const-compatible and avoids the conversion performed + /// by [`Self::new`]. + pub const fn new_querybuilder( + table_schema_data: TableMetadata<'a>, + database_type: DatabaseType, + ) -> Self { + Self { + _inner: QueryBuilder::new_querybuilder( + table_schema_data, + SelectAst::new(), + database_type, + ), + } + } + + /// Creates a builder directly from schema and table components. + pub const fn new_from_parts( + schema: Option>, + table_name: Cow<'a, str>, + database_type: DatabaseType, + ) -> Self { + let table_schema_data = TableMetadata { + schema, + name: table_name, + }; + + Self::new_querybuilder(table_schema_data, database_type) + } + + /// Appends columns that have already been converted into [`ColumnRef`] values. + /// + /// This avoids repeating identifier conversion in internal or generated code + /// that already works with the query syntax types. + pub fn with_known_columns(mut self, columns: I) -> Self + where + I: IntoIterator>, + { + self._inner.ast.columns.extend(columns); + self + } + + /// Appends borrowed column names from the representation produced by the + /// entity metadata APIs. + pub fn with_known_column_names(mut self, columns: I) -> Self + where + I: IntoIterator, + { + self._inner + .ast + .columns + .extend(columns.into_iter().map(Into::into)); + + self + } + + #[inline(always)] + pub fn build(self) -> Result, Box> { + self._inner.build() + } +} + +impl<'a> SelectQueryBuilderOps<'a> for SelectQueryBuilder<'a> { + fn with_columns>>(mut self, columns: Vec) -> Self { + self._inner + .ast + .columns + .extend(columns.into_iter().map(Into::into)); + + self + } + + fn with_distinct(mut self) -> Self { + self._inner.ast.with_distinct = true; + self + } + + fn count(mut self) -> Self { + self._inner.ast.is_count_query = true; + self + } + + fn left_join( + self, + join_table: impl Into>, + left: impl Into>, + right: impl Into>, + ) -> Self { + __impl::build_and_append_join_clause(self, JoinKind::Left, join_table, left, right) + } + + fn inner_join( + self, + join_table: impl Into>, + left: impl Into>, + right: impl Into>, + ) -> Self { + __impl::build_and_append_join_clause(self, JoinKind::Inner, join_table, left, right) + } + + fn right_join( + self, + join_table: impl Into>, + left: impl Into>, + right: impl Into>, + ) -> Self { + __impl::build_and_append_join_clause(self, JoinKind::Right, join_table, left, right) + } + + fn full_join( + self, + join_table: impl Into>, + left: impl Into>, + right: impl Into>, + ) -> Self { + __impl::build_and_append_join_clause(self, JoinKind::Full, join_table, left, right) + } + + fn order_by>>( + mut self, + order_by: Z, + desc: bool, + ) -> Self { + self._inner.ast.order_by = Some(OrderByClause::new(order_by, desc)); + self + } +} + +impl<'a> QueryBuilderOps<'a> for SelectQueryBuilder<'a> { + #[inline(always)] + fn build(self) -> Result, Box> { + self._inner.build() + } + + #[inline] + fn r#where>>(mut self, column_name: I, operator: Operator) -> Self { + self._inner.r#where(column_name, operator); + self + } + + #[inline] + fn where_value(mut self, r#where: &'a Z, op: Operator) -> Self { + self._inner.where_value(r#where, op); + self + } + + #[inline] + fn and(mut self, column: &'a Z, op: Operator) -> Self { + self._inner.and(column, op); + self + } + + #[inline] + fn and_values_in<'b, Z, Q>( + mut self, + r#and: Z, + values: &'a [Q], + ) -> Result> + where + Z: FieldIdentifier, + Q: QueryParameter, + Self: Sized, + { + self._inner.and_values_in(r#and, values)?; + Ok(self) + } + + #[inline] + fn or_values_in<'b, Z, Q>( + mut self, + r#or: Z, + values: &'a [Q], + ) -> Result> + where + Z: FieldIdentifier, + Q: QueryParameter, + Self: Sized, + { + self._inner.or_values_in(r#or, values)?; + Ok(self) + } + + #[inline] + fn or(mut self, column: &'a Z, op: Operator) -> Self { + self._inner.or(column, op); + self + } +} + +mod __impl { + use crate::query::operators::Operator; + use crate::query::querybuilder::SelectQueryBuilder; + use crate::query::querybuilder::syntax::column::ColumnRef; + use crate::query::querybuilder::syntax::join::{JoinClause, JoinKind}; + use crate::query::querybuilder::syntax::table_metadata::TableMetadata; + + pub(crate) fn build_and_append_join_clause<'a>( + mut builder: SelectQueryBuilder<'a>, + join_kind: JoinKind, + target_table: impl Into>, + left: impl Into>, + right: impl Into>, + ) -> SelectQueryBuilder<'a> { + let join_clause = build_join_clause(join_kind, target_table, left, right); + builder._inner.ast.joins.push(join_clause); + builder + } + + fn build_join_clause<'a>( + kind: JoinKind, + target_table: impl Into>, + left: impl Into>, + right: impl Into>, + ) -> JoinClause<'a> { + JoinClause { + kind, + target_table: target_table.into(), + left: left.into(), + operator: Operator::Eq, + right: right.into(), + } + } +} diff --git a/canyon_core/src/query/querybuilder/types/update.rs b/canyon_core/src/query/querybuilder/types/update.rs new file mode 100644 index 00000000..723e4453 --- /dev/null +++ b/canyon_core/src/query/querybuilder/types/update.rs @@ -0,0 +1,168 @@ +use crate::{ + connection::database_type::DatabaseType, + query::{ + bounds::{FieldIdentifier, FieldValueIdentifier}, + operators::Operator, + parameters::QueryParameter, + query::Query, + querybuilder::{ + QueryBuilder, QueryBuilderOps, UpdateQueryBuilderOps, + syntax::{ast::update::UpdateAst, column::ColumnRef}, + types::TableMetadata, + }, + }, +}; +use std::error::Error; + +/// Fluent builder for `UPDATE` statements +pub struct UpdateQueryBuilder<'a> { + pub(crate) _inner: QueryBuilder<'a, UpdateAst<'a>>, +} +impl<'a> UpdateQueryBuilder<'a> { + /// Creates an update builder whose database dialect will be resolved later. + pub fn new( + table_schema_data: impl Into>, + database_type: DatabaseType, + ) -> Self { + Self { + _inner: QueryBuilder::new(table_schema_data, UpdateAst::new(), database_type), + } + } + /// Creates an update builder for a specific database dialect. + pub fn new_for(table_schema_data: TableMetadata<'a>, database_type: DatabaseType) -> Self { + Self { + _inner: QueryBuilder::new_querybuilder( + table_schema_data, + UpdateAst::new(), + database_type, + ), + } + } +} + +impl<'a> UpdateQueryBuilderOps<'a> for UpdateQueryBuilder<'a> { + fn set>>( + mut self, + columns: Vec, + ) -> Result> + where + Self: Sized, + { + __validators::set_clause_values_not_empty(&columns)?; + self._inner.ast.columns = columns.into_iter().map(Into::into).collect(); + Ok(self) + } + + fn set_values( + mut self, + columns: &'a [(Z, Q)], + ) -> Result> + where + Z: FieldIdentifier + Into> + Clone, + Q: QueryParameter, + { + __validators::set_clause_not_already_present(&self)?; + __validators::set_clause_values_not_empty(columns)?; + self._inner.ast.columns = columns + .iter() + .map(|(column, _)| column.clone().into()) + .collect(); + for (_, value) in columns { + self._inner.params.push(value as &dyn QueryParameter); + } + Ok(self) + } +} + +impl<'a> QueryBuilderOps<'a> for UpdateQueryBuilder<'a> { + #[inline(always)] + fn build(self) -> Result, Box> { + self._inner.build() + } + #[inline] + fn r#where>>(mut self, column_name: I, operator: Operator) -> Self { + self._inner.r#where(column_name, operator); + self + } + + #[inline] + fn where_value(mut self, r#where: &'a Z, op: Operator) -> Self { + self._inner.where_value(r#where, op); + self + } + + #[inline] + fn and(mut self, column: &'a Z, op: Operator) -> Self { + self._inner.and(column, op); + self + } + #[inline] + fn and_values_in<'b, Z, Q>( + mut self, + r#and: Z, + values: &'a [Q], + ) -> Result> + where + Z: FieldIdentifier, + Q: QueryParameter, + { + self._inner.and_values_in(r#and, values)?; + Ok(self) + } + + #[inline] + fn or_values_in<'b, Z, Q>( + mut self, + r#or: Z, + values: &'a [Q], + ) -> Result> + where + Z: FieldIdentifier, + Q: QueryParameter, + { + self._inner.or_values_in(r#or, values)?; + Ok(self) + } + + #[inline] + fn or(mut self, column: &'a Z, op: Operator) -> Self { + self._inner.or(column, op); + self + } +} + +mod __validators { + use crate::query::querybuilder::UpdateQueryBuilder; + use std::error::Error; + use std::io::ErrorKind; + + /// Prevents `set_values` from replacing a previously configured `SET` clause. + pub(super) fn set_clause_not_already_present<'a>( + builder: &UpdateQueryBuilder<'a>, + ) -> Result<(), Box> { + if !builder._inner.ast.columns.is_empty() { + return Err(std::io::Error::new( + // TODO: CanyonError + ErrorKind::Unsupported, + "SET clause already present", + ) + .into()); + } + Ok(()) + } + + /// Rejects update statements that would produce an empty `SET` clause. + pub(super) fn set_clause_values_not_empty( + values: &[T], + ) -> Result<(), Box> { + if values.is_empty() { + return Err(std::io::Error::new( + // TODO: CanyonError + ErrorKind::Unsupported, + "Empty SET clause", + ) + .into()); + } + Ok(()) + } +} diff --git a/canyon_core/src/row.rs b/canyon_core/src/row.rs new file mode 100644 index 00000000..bbc3eea4 --- /dev/null +++ b/canyon_core/src/row.rs @@ -0,0 +1,185 @@ +#![allow(unused_imports)] + +#[cfg(feature = "mysql")] +use mysql_async::{self}; +#[cfg(feature = "mssql")] +use tiberius::{self}; +#[cfg(feature = "postgres")] +use tokio_postgres::{self}; + +use crate::column::{Column, ColumnType}; +use std::{any::Any, borrow::Cow}; + +/// Generic abstraction to represent any of the Row types +/// from the client crates +pub trait Row { + fn as_any(&self) -> &dyn Any; +} + +#[cfg(feature = "postgres")] +impl Row for tokio_postgres::Row { + fn as_any(&self) -> &dyn Any { + self + } +} + +#[cfg(feature = "mssql")] +impl Row for tiberius::Row { + fn as_any(&self) -> &dyn Any { + self + } +} + +#[cfg(feature = "mysql")] +impl Row for mysql_async::Row { + fn as_any(&self) -> &dyn Any { + self + } +} + +pub trait RowOperations { + #[cfg(feature = "postgres")] + fn get_postgres<'a, Output>(&'a self, col_name: &'a str) -> Output + where + Output: tokio_postgres::types::FromSql<'a>; + #[cfg(feature = "mssql")] + fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output + where + Output: tiberius::FromSql<'a>; + #[cfg(feature = "mysql")] + fn get_mysql<'a, Output>(&'a self, col_name: &'a str) -> Output + where + Output: mysql_async::prelude::FromValue; + + #[cfg(feature = "postgres")] + fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option + where + Output: tokio_postgres::types::FromSql<'a>; + #[cfg(feature = "mssql")] + fn get_mssql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option + where + Output: tiberius::FromSql<'a>; + + #[cfg(feature = "mysql")] + fn get_mysql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option + where + Output: mysql_async::prelude::FromValue; + + fn columns(&self) -> Vec>; +} + +impl RowOperations for &dyn Row { + #[cfg(feature = "postgres")] + fn get_postgres<'a, Output>(&'a self, col_name: &'a str) -> Output + where + Output: tokio_postgres::types::FromSql<'a>, + { + if let Some(row) = self.as_any().downcast_ref::() { + return row.get::<&str, Output>(col_name); + }; + panic!() // TODO into result and propagate + } + #[cfg(feature = "mssql")] + fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output + where + Output: tiberius::FromSql<'a>, + { + if let Some(row) = self.as_any().downcast_ref::() { + return row + .get::(col_name) + .expect("Failed to obtain a row in the MSSQL migrations"); + }; + panic!() // TODO into result and propagate + } + + #[cfg(feature = "mysql")] + fn get_mysql<'a, Output>(&'a self, col_name: &'a str) -> Output + where + Output: mysql_async::prelude::FromValue, + { + self.get_mysql_opt(col_name) + .expect("Failed to obtain a column in the MySql") + } + + #[cfg(feature = "postgres")] + fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option + where + Output: tokio_postgres::types::FromSql<'a>, + { + if let Some(row) = self.as_any().downcast_ref::() { + return row.get::<&str, Option>(col_name); + }; + panic!() // TODO into result and propagate + } + + #[cfg(feature = "mssql")] + fn get_mssql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option + where + Output: tiberius::FromSql<'a>, + { + if let Some(row) = self.as_any().downcast_ref::() { + return row.get::(col_name); + }; + panic!() // TODO into result and propagate + } + #[cfg(feature = "mysql")] + fn get_mysql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option + where + Output: mysql_async::prelude::FromValue, + { + if let Some(row) = self.as_any().downcast_ref::() { + return row.get::(col_name); + }; + panic!() // TODO into result and propagate + } + + fn columns(&self) -> Vec> { + let mut cols = vec![]; + + #[cfg(feature = "postgres")] + { + if self.as_any().is::() { + self.as_any() + .downcast_ref::() + .expect("Not a tokio postgres Row for column") + .columns() + .iter() + .for_each(|c| { + cols.push(Column { + name: std::borrow::Cow::from(c.name()), + type_: crate::column::ColumnType::Postgres(c.type_().to_owned()), + }) + }) + } + } + #[cfg(feature = "mssql")] + { + if self.as_any().is::() { + self.as_any() + .downcast_ref::() + .expect("Not a Tiberius Row for column") + .columns() + .iter() + .for_each(|c| { + cols.push(Column { + name: Cow::from(c.name()), + type_: ColumnType::SqlServer(c.column_type()), + }) + }) + }; + } + #[cfg(feature = "mysql")] + { + if let Some(mysql_row) = self.as_any().downcast_ref::() { + mysql_row.columns_ref().iter().for_each(|c| { + cols.push(Column { + name: c.name_str(), + type_: ColumnType::MySQL(c.column_type()), + }) + }) + } + } + + cols + } +} diff --git a/canyon_core/src/rows.rs b/canyon_core/src/rows.rs new file mode 100644 index 00000000..14d3d534 --- /dev/null +++ b/canyon_core/src/rows.rs @@ -0,0 +1,215 @@ +#![allow(unreachable_patterns)] + +//! The rows module of Canyon-SQL. +//! +//! This module defines the `CanyonRows` enum, which wraps database query results for supported +//! databases. It also provides traits and utilities for mapping rows to user-defined types. + +#[cfg(feature = "mysql")] +use mysql_async::{self}; +#[cfg(feature = "mssql")] +use tiberius::{self}; +#[cfg(feature = "postgres")] +use tokio_postgres::{self}; + +use crate::mapper::RowMapper; +use crate::row::Row; + +/// Lightweight wrapper over the collection of results of the different crates +/// supported by Canyon-SQL. +/// +/// Even tho the wrapping seems meaningless, this allows us to provide internal +/// operations that are too difficult or too ugly to implement in the macros that +/// will call the query method of Crud. +#[derive(Debug)] +pub enum CanyonRows { + #[cfg(feature = "postgres")] + Postgres(Vec), + #[cfg(feature = "mssql")] + Tiberius(Vec), + #[cfg(feature = "mysql")] + MySQL(Vec), +} + +impl CanyonRows { + #[cfg(feature = "postgres")] + pub fn get_postgres_rows(&self) -> &Vec { + match self { + Self::Postgres(v) => v, + _ => panic!("This branch will never ever should be reachable"), + } + } + + #[cfg(feature = "mssql")] + pub fn get_tiberius_rows(&self) -> &Vec { + match self { + Self::Tiberius(v) => v, + _ => panic!("This branch will never ever should be reachable"), + } + } + + #[cfg(feature = "mysql")] + pub fn get_mysql_rows(&self) -> &Vec { + match self { + Self::MySQL(v) => v, + _ => panic!("This branch will never ever should be reachable"), + } + } + + /// Returns the entity at the given index for the returned rows + /// + /// This is just a wrapper get operation over the [Vec] get operation + pub fn get_row_at(&self, index: usize) -> Option<&dyn Row> { + match self { + #[cfg(feature = "postgres")] + Self::Postgres(v) => v.get(index).map(|inner| inner as &dyn Row), + #[cfg(feature = "mssql")] + Self::Tiberius(v) => v.get(index).map(|inner| inner as &dyn Row), + #[cfg(feature = "mysql")] + Self::MySQL(v) => v.get(index).map(|inner| inner as &dyn Row), + } + } + + pub fn first_row>(&self) -> Option { + let row = match self { + #[cfg(feature = "postgres")] + Self::Postgres(v) => v.first().map(|r| T::deserialize_postgresql(r)), + #[cfg(feature = "mssql")] + Self::Tiberius(v) => v.first().map(|r| T::deserialize_sqlserver(r)), + #[cfg(feature = "mysql")] + Self::MySQL(v) => v.first().map(|r| T::deserialize_mysql(r)), + }; + + row?.ok() + } + + /// Returns the number of elements present on the wrapped collection + pub fn len(&self) -> usize { + match self { + #[cfg(feature = "postgres")] + Self::Postgres(v) => v.len(), + #[cfg(feature = "mssql")] + Self::Tiberius(v) => v.len(), + #[cfg(feature = "mysql")] + Self::MySQL(v) => v.len(), + } + } + + /// Returns true whenever the wrapped collection of Rows does not contains any elements + pub fn is_empty(&self) -> bool { + match self { + #[cfg(feature = "postgres")] + Self::Postgres(v) => v.is_empty(), + #[cfg(feature = "mssql")] + Self::Tiberius(v) => v.is_empty(), + #[cfg(feature = "mysql")] + Self::MySQL(v) => v.is_empty(), + } + } +} + +pub trait FromSql<'a>: + __backend_from_sql::PostgresFromSql<'a> + + __backend_from_sql::MySqlFromSql + + __backend_from_sql::MsSqlFromSql<'a> +{ +} + +impl<'a, T> FromSql<'a> for T where + T: __backend_from_sql::PostgresFromSql<'a> + + __backend_from_sql::MySqlFromSql + + __backend_from_sql::MsSqlFromSql<'a> +{ +} + +pub trait FromSqlOwnedValue: + __backend_from_sql_owned::PostgresFromSqlOwned + + __backend_from_sql_owned::MySqlFromSqlOwned + + __backend_from_sql_owned::MsSqlFromSqlOwned +{ +} + +impl FromSqlOwnedValue for T where + T: __backend_from_sql_owned::PostgresFromSqlOwned + + __backend_from_sql_owned::MySqlFromSqlOwned + + __backend_from_sql_owned::MsSqlFromSqlOwned +{ +} + +#[doc(hidden)] +pub mod __backend_from_sql { + #[cfg(feature = "postgres")] + pub trait PostgresFromSql<'a>: tokio_postgres::types::FromSql<'a> {} + + #[cfg(feature = "postgres")] + impl<'a, T> PostgresFromSql<'a> for T where T: tokio_postgres::types::FromSql<'a> {} + + #[cfg(not(feature = "postgres"))] + pub trait PostgresFromSql<'a> {} + + #[cfg(not(feature = "postgres"))] + impl<'a, T> PostgresFromSql<'a> for T {} + + #[cfg(feature = "mysql")] + pub trait MySqlFromSql: mysql_async::prelude::FromValue {} + + #[cfg(feature = "mysql")] + impl MySqlFromSql for T where T: mysql_async::prelude::FromValue {} + + #[cfg(not(feature = "mysql"))] + pub trait MySqlFromSql {} + + #[cfg(not(feature = "mysql"))] + impl MySqlFromSql for T {} + + #[cfg(feature = "mssql")] + pub trait MsSqlFromSql<'a>: tiberius::FromSql<'a> {} + + #[cfg(feature = "mssql")] + impl<'a, T> MsSqlFromSql<'a> for T where T: tiberius::FromSql<'a> {} + + #[cfg(not(feature = "mssql"))] + pub trait MsSqlFromSql<'a> {} + + #[cfg(not(feature = "mssql"))] + impl<'a, T> MsSqlFromSql<'a> for T {} +} + +#[doc(hidden)] +pub mod __backend_from_sql_owned { + #[cfg(feature = "postgres")] + pub trait PostgresFromSqlOwned: tokio_postgres::types::FromSqlOwned {} + + #[cfg(feature = "postgres")] + impl PostgresFromSqlOwned for T where T: tokio_postgres::types::FromSqlOwned {} + + #[cfg(not(feature = "postgres"))] + pub trait PostgresFromSqlOwned {} + + #[cfg(not(feature = "postgres"))] + impl PostgresFromSqlOwned for T {} + + #[cfg(feature = "mysql")] + pub trait MySqlFromSqlOwned: mysql_async::prelude::FromValue {} + + #[cfg(feature = "mysql")] + impl MySqlFromSqlOwned for T where T: mysql_async::prelude::FromValue {} + + #[cfg(not(feature = "mysql"))] + pub trait MySqlFromSqlOwned {} + + #[cfg(not(feature = "mysql"))] + impl MySqlFromSqlOwned for T {} + + #[cfg(feature = "mssql")] + pub trait MsSqlFromSqlOwned: tiberius::FromSqlOwned {} + + #[cfg(feature = "mssql")] + impl MsSqlFromSqlOwned for T where T: tiberius::FromSqlOwned {} + + #[cfg(not(feature = "mssql"))] + pub trait MsSqlFromSqlOwned {} + + #[cfg(not(feature = "mssql"))] + impl MsSqlFromSqlOwned for T {} +} diff --git a/canyon_core/src/transaction.rs b/canyon_core/src/transaction.rs new file mode 100644 index 00000000..ec98b778 --- /dev/null +++ b/canyon_core/src/transaction.rs @@ -0,0 +1,108 @@ +use crate::connection::contracts::DbConnection; +use crate::mapper::RowMapper; +use crate::rows::FromSqlOwnedValue; +use crate::{query::parameters::QueryParameter, rows::CanyonRows}; +use std::error::Error; +use std::future::Future; + +/// The `Transaction` trait serves as a proxy for types implementing CRUD operations. +/// +/// This trait provides a set of static methods that mirror the functionality of CRUD operations, +/// allowing implementors to be coerced into `<#ty as Transaction>::...` usage patterns. +/// It is primarily used by the generated macros of `CrudOperations` to simplify interaction +/// with database entities by abstracting common operations such as querying rows, executing +/// statements, and retrieving single results. +/// +/// # Purpose +/// The `Transaction` trait is typically used to provide a unified interface for CRUD operations +/// on database entities. It enables developers to work with any type that implements the required +/// CRUD traits, abstracting away the underlying database connection details. +/// +/// # Features +/// - Acts as a proxy for CRUD operations. +/// - Provides static methods for common database entity operations. +/// - Simplifies interaction with database entities. +/// +/// # Examples +/// ```ignore +/// async fn perform_query(entity: E) { +/// let result = ::query("SELECT * FROM users", &[], entity).await; +/// match result { +/// Ok(rows) => println!("Retrieved {} rows", rows.len()), +/// Err(e) => eprintln!("Error: {}", e), +/// } +/// } +/// ``` +/// +/// # Methods +/// - `query`: Executes a query and retrieves multiple rows mapped to a user-defined type. +/// - `query_one`: Executes a query and retrieves a single row mapped to a user-defined type. +/// - `query_one_for`: Executes a query and retrieves a single value of a specific type. +/// - `query_rows`: Executes a query and retrieves the raw rows wrapped in `CanyonRows`. +/// - `execute`: Executes a SQL statement and returns the number of affected rows. +pub trait Transaction { + fn query( + stmt: S, + params: &[&dyn QueryParameter], + input: impl DbConnection + Send, + ) -> impl Future, Box>> + where + S: AsRef + Send, + R: RowMapper, + Vec: FromIterator<::Output>, + { + async move { input.query(stmt, params).await } + } + + fn query_one<'a, S, Z, R>( + stmt: S, + params: Z, + input: impl DbConnection + Send + 'a, + ) -> impl Future, Box>> + Send + where + S: AsRef + Send + 'a, + Z: AsRef<[&'a dyn QueryParameter]> + Send, + R: RowMapper, + { + async move { input.query_one::(stmt.as_ref(), params.as_ref()).await } + } + + fn query_one_for<'a, S, Z, F: FromSqlOwnedValue>( + stmt: S, + params: Z, + input: impl DbConnection + Send + 'a, + ) -> impl Future>> + Send + where + S: AsRef + Send + 'a, + Z: AsRef<[&'a dyn QueryParameter]> + Send + 'a, + { + async move { input.query_one_for(stmt.as_ref(), params.as_ref()).await } + } + + /// Performs a query against the targeted database by the selected or + /// the defaulted datasource, wrapping the resultant collection of entities + /// in [`super::rows::CanyonRows`] + fn query_rows<'a, S, Z>( + stmt: S, + params: Z, + input: impl DbConnection + Send + 'a, + ) -> impl Future>> + Send + where + S: AsRef + Send + 'a, + Z: AsRef<[&'a dyn QueryParameter]> + Send + 'a, + { + async move { input.query_rows(stmt.as_ref(), params.as_ref()).await } + } + + fn execute<'a, S, Z>( + stmt: S, + params: Z, + input: impl DbConnection + Send + 'a, + ) -> impl Future>> + Send + where + S: AsRef + Send + 'a, + Z: AsRef<[&'a dyn QueryParameter]> + Send + 'a, + { + async move { input.execute(stmt.as_ref(), params.as_ref()).await } + } +} diff --git a/canyon_crud/Cargo.toml b/canyon_crud/Cargo.toml index dfdd3ddb..05eca2d3 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -10,19 +10,16 @@ license.workspace = true description.workspace = true [dependencies] +canyon_core = { workspace = true } + tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } mysql_async = { workspace = true, optional = true } mysql_common = { workspace = true, optional = true } chrono = { workspace = true } -async-trait = { workspace = true } - -canyon_connection = { workspace = true } - -regex = { workspace = true } [features] -postgres = ["tokio-postgres", "canyon_connection/postgres"] -mssql = ["tiberius", "canyon_connection/mssql"] -mysql = ["mysql_async","mysql_common", "canyon_connection/mysql"] +postgres = ["tokio-postgres", "canyon_core/postgres"] +mssql = ["tiberius", "canyon_core/mssql"] +mysql = ["mysql_async","mysql_common", "canyon_core/mysql"] diff --git a/canyon_crud/src/bounds.rs b/canyon_crud/src/bounds.rs deleted file mode 100644 index 27ffb97f..00000000 --- a/canyon_crud/src/bounds.rs +++ /dev/null @@ -1,875 +0,0 @@ -use crate::{ - crud::{CrudOperations, Transaction}, - mapper::RowMapper, -}; -#[cfg(feature = "mysql")] -use canyon_connection::mysql_async::{self, prelude::ToValue}; -#[cfg(feature = "mssql")] -use canyon_connection::tiberius::{self, ColumnData, IntoSql}; -#[cfg(feature = "postgres")] -use canyon_connection::tokio_postgres::{self, types::ToSql}; - -use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc}; - -use std::{any::Any, borrow::Cow}; - -/// Created for retrieve the field's name of a field of a struct, giving -/// the Canyon's autogenerated enum with the variants that maps this -/// fields. -/// -/// ``` -/// pub struct Struct<'a> { -/// pub some_field: &'a str -/// } -/// -/// // Autogenerated enum -/// #[derive(Debug)] -/// #[allow(non_camel_case_types)] -/// pub enum StructField { -/// some_field -/// } -/// ``` -/// So, to retrieve the field's name, something like this w'd be used on some part -/// of the Canyon's Manager crate, to wire the necessary code to pass the field -/// name, retrieved from the enum variant, to a called. -/// -/// // Something like: -/// `let struct_field_name_from_variant = StructField::some_field.field_name_as_str();` -pub trait FieldIdentifier -where - T: Transaction + CrudOperations + RowMapper, -{ - fn as_str(&self) -> &'static str; -} - -/// Represents some kind of introspection to make the implementors -/// able to retrieve a value inside some variant of an associated enum type. -/// and convert it to a tuple struct formed by the column name as an String, -/// and the dynamic value of the [`QueryParameter<'_>`] trait object contained -/// inside the variant requested, -/// enabling a conversion of that value into something -/// that can be part of an SQL query. -/// -/// -/// Ex: -/// `SELECT * FROM some_table WHERE id = 2` -/// -/// That '2' it's extracted from some enum that implements [`FieldValueIdentifier`], -/// where usually the variant w'd be something like: -/// -/// ``` -/// pub enum Enum { -/// IntVariant(i32) -/// } -/// ``` -pub trait FieldValueIdentifier<'a, T> -where - T: Transaction + CrudOperations + RowMapper, -{ - fn value(self) -> (&'static str, &'a dyn QueryParameter<'a>); -} - -/// Bounds to some type T in order to make it callable over some fn parameter T -/// -/// Represents the ability of an struct to be considered as candidate to perform -/// actions over it as it holds the 'parent' side of a foreign key relation. -/// -/// Usually, it's used on the Canyon macros to retrieve the column that -/// this side of the relation it's representing -pub trait ForeignKeyable { - /// Retrieves the field related to the column passed in - fn get_fk_column(&self, column: &str) -> Option<&dyn QueryParameter<'_>>; -} - -/// Generic abstraction to represent any of the Row types -/// from the client crates -pub trait Row { - fn as_any(&self) -> &dyn Any; -} - -#[cfg(feature = "postgres")] -impl Row for tokio_postgres::Row { - fn as_any(&self) -> &dyn Any { - self - } -} - -#[cfg(feature = "mssql")] -impl Row for tiberius::Row { - fn as_any(&self) -> &dyn Any { - self - } -} - -#[cfg(feature = "mysql")] -impl Row for mysql_async::Row { - fn as_any(&self) -> &dyn Any { - self - } -} - -/// Generic abstraction for hold a Column type that will be one of the Column -/// types present in the dependent crates -// #[derive(Copy, Clone)] -pub struct Column<'a> { - name: Cow<'a, str>, - type_: ColumnType, -} -impl<'a> Column<'a> { - pub fn name(&self) -> &str { - &self.name - } - pub fn column_type(&self) -> &ColumnType { - &self.type_ - } - // pub fn type_(&'a self) -> &'_ dyn Type { - // match (*self).type_ { - // #[cfg(feature = "postgres")] ColumnType::Postgres(v) => v as &'a dyn Type, - // #[cfg(feature = "mssql")] ColumnType::SqlServer(v) => v as &'a dyn Type, - // } - // } -} - -pub trait Type { - fn as_any(&self) -> &dyn Any; -} -#[cfg(feature = "postgres")] -impl Type for tokio_postgres::types::Type { - fn as_any(&self) -> &dyn Any { - self - } -} -#[cfg(feature = "mssql")] -impl Type for tiberius::ColumnType { - fn as_any(&self) -> &dyn Any { - self - } -} -#[cfg(feature = "mysql")] -impl Type for mysql_async::consts::ColumnType { - fn as_any(&self) -> &dyn Any { - self - } -} - -/// Wrapper over the dependencies Column's types -pub enum ColumnType { - #[cfg(feature = "postgres")] - Postgres(tokio_postgres::types::Type), - #[cfg(feature = "mssql")] - SqlServer(tiberius::ColumnType), - #[cfg(feature = "mysql")] - MySQL(mysql_async::consts::ColumnType), -} - -pub trait RowOperations { - #[cfg(feature = "postgres")] - fn get_postgres<'a, Output>(&'a self, col_name: &'a str) -> Output - where - Output: tokio_postgres::types::FromSql<'a>; - #[cfg(feature = "mssql")] - fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output - where - Output: tiberius::FromSql<'a>; - #[cfg(feature = "mysql")] - fn get_mysql<'a, Output>(&'a self, col_name: &'a str) -> Output - where - Output: mysql_async::prelude::FromValue; - - #[cfg(feature = "postgres")] - fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option - where - Output: tokio_postgres::types::FromSql<'a>; - #[cfg(feature = "mssql")] - fn get_mssql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option - where - Output: tiberius::FromSql<'a>; - - #[cfg(feature = "mysql")] - fn get_mysql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option - where - Output: mysql_async::prelude::FromValue; - - fn columns(&self) -> Vec; -} - -impl RowOperations for &dyn Row { - #[cfg(feature = "postgres")] - fn get_postgres<'a, Output>(&'a self, col_name: &'a str) -> Output - where - Output: tokio_postgres::types::FromSql<'a>, - { - if let Some(row) = self.as_any().downcast_ref::() { - return row.get::<&str, Output>(col_name); - }; - panic!() // TODO into result and propagate - } - #[cfg(feature = "mssql")] - fn get_mssql<'a, Output>(&'a self, col_name: &'a str) -> Output - where - Output: tiberius::FromSql<'a>, - { - if let Some(row) = self.as_any().downcast_ref::() { - return row - .get::(col_name) - .expect("Failed to obtain a row in the MSSQL migrations"); - }; - panic!() // TODO into result and propagate - } - - #[cfg(feature = "mysql")] - fn get_mysql<'a, Output>(&'a self, col_name: &'a str) -> Output - where - Output: mysql_async::prelude::FromValue, - { - self.get_mysql_opt(col_name) - .expect("Failed to obtain a column in the MySql") - } - - #[cfg(feature = "postgres")] - fn get_postgres_opt<'a, Output>(&'a self, col_name: &'a str) -> Option - where - Output: tokio_postgres::types::FromSql<'a>, - { - if let Some(row) = self.as_any().downcast_ref::() { - return row.get::<&str, Option>(col_name); - }; - panic!() // TODO into result and propagate - } - - #[cfg(feature = "mssql")] - fn get_mssql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option - where - Output: tiberius::FromSql<'a>, - { - if let Some(row) = self.as_any().downcast_ref::() { - return row.get::(col_name); - }; - panic!() // TODO into result and propagate - } - #[cfg(feature = "mysql")] - fn get_mysql_opt<'a, Output>(&'a self, col_name: &'a str) -> Option - where - Output: mysql_async::prelude::FromValue, - { - if let Some(row) = self.as_any().downcast_ref::() { - return row.get::(col_name); - }; - panic!() // TODO into result and propagate - } - - fn columns(&self) -> Vec { - let mut cols = vec![]; - - #[cfg(feature = "postgres")] - { - if self.as_any().is::() { - self.as_any() - .downcast_ref::() - .expect("Not a tokio postgres Row for column") - .columns() - .iter() - .for_each(|c| { - cols.push(Column { - name: Cow::from(c.name()), - type_: ColumnType::Postgres(c.type_().to_owned()), - }) - }) - } - } - #[cfg(feature = "mssql")] - { - if self.as_any().is::() { - self.as_any() - .downcast_ref::() - .expect("Not a Tiberius Row for column") - .columns() - .iter() - .for_each(|c| { - cols.push(Column { - name: Cow::from(c.name()), - type_: ColumnType::SqlServer(c.column_type()), - }) - }) - }; - } - #[cfg(feature = "mysql")] - { - if let Some(mysql_row) = self.as_any().downcast_ref::() { - mysql_row.columns_ref().iter().for_each(|c| { - cols.push(Column { - name: c.name_str(), - type_: ColumnType::MySQL(c.column_type()), - }) - }) - } - } - - cols - } -} - -/// Defines a trait for represent type bounds against the allowed -/// data types supported by Canyon to be used as query parameters. -pub trait QueryParameter<'a>: std::fmt::Debug + Sync + Send { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync); - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_>; - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue; -} - -/// The implementation of the [`canyon_connection::tiberius`] [`IntoSql`] for the -/// query parameters. -/// -/// This implementation is necessary because of the generic amplitude -/// of the arguments of the [`Transaction::query`], that should work with -/// a collection of [`QueryParameter<'a>`], in order to allow a workflow -/// that is not dependent of the specific type of the argument that holds -/// the query parameters of the database connectors -#[cfg(feature = "mssql")] -impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { - fn into_sql(self) -> ColumnData<'a> { - self.as_sqlserver_param() - } -} - -//TODO Pending to review and see if it is necessary to apply something similar to the previous implementation. - -impl<'a> QueryParameter<'a> for bool { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::Bit(Some(*self)) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn ToValue { - self - } -} -impl<'a> QueryParameter<'a> for i16 { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I16(Some(*self)) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for &i16 { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I16(Some(**self)) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I16(*self) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option<&i16> { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I16(Some(*self.unwrap())) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for i32 { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I32(Some(*self)) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for &i32 { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I32(Some(**self)) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I32(*self) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option<&i32> { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I32(Some(*self.unwrap())) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for f32 { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F32(Some(*self)) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for &f32 { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F32(Some(**self)) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F32(*self) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option<&f32> { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F32(Some( - *self.expect("Error on an f32 value on QueryParameter<'_>"), - )) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for f64 { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F64(Some(*self)) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for &f64 { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F64(Some(**self)) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F64(*self) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option<&f64> { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F64(Some( - *self.expect("Error on an f64 value on QueryParameter<'_>"), - )) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for i64 { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I64(Some(*self)) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for &i64 { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I64(Some(**self)) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I64(*self) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option<&i64> { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I64(Some(*self.unwrap())) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for String { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::String(Some(std::borrow::Cow::Owned(self.to_owned()))) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for &String { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::String(Some(std::borrow::Cow::Borrowed(self))) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - match self { - Some(string) => ColumnData::String(Some(std::borrow::Cow::Owned(string.to_owned()))), - None => ColumnData::String(None), - } - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option<&String> { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - match self { - Some(string) => ColumnData::String(Some(std::borrow::Cow::Borrowed(string))), - None => ColumnData::String(None), - } - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for &'_ str { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::String(Some(std::borrow::Cow::Borrowed(*self))) - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option<&'_ str> { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - match *self { - Some(str) => ColumnData::String(Some(std::borrow::Cow::Borrowed(str))), - None => ColumnData::String(None), - } - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for NaiveDate { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for NaiveTime { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for NaiveDateTime { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} -impl<'a> QueryParameter<'a> for Option { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - self - } -} - -//TODO pending -impl<'a> QueryParameter<'a> for DateTime { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - todo!() - } -} - -impl<'a> QueryParameter<'a> for Option> { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - todo!() - } -} - -impl<'a> QueryParameter<'a> for DateTime { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - todo!() - } -} - -impl<'a> QueryParameter<'a> for Option> { - #[cfg(feature = "postgres")] - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - #[cfg(feature = "mssql")] - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } - #[cfg(feature = "mysql")] - fn as_mysql_param(&self) -> &dyn mysql_async::prelude::ToValue { - todo!() - } -} diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index 981c24f1..b6b8a1ea 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -1,331 +1,113 @@ -use async_trait::async_trait; -use std::fmt::Display; - -use canyon_connection::canyon_database_connector::DatabaseConnection; -use canyon_connection::{get_database_connection, CACHED_DATABASE_CONN}; - -use crate::bounds::QueryParameter; -use crate::mapper::RowMapper; -use crate::query_elements::query_builder::{ - DeleteQueryBuilder, SelectQueryBuilder, UpdateQueryBuilder, +use canyon_core::{ + connection::{contracts::DbConnection, database_type::DatabaseType}, + mapper::RowMapper, + query::{ + parameters::QueryParameter, + querybuilder::{DeleteQueryBuilder, SelectQueryBuilder, UpdateQueryBuilder}, + }, }; -use crate::rows::CanyonRows; - -#[cfg(feature = "mysql")] -pub const DETECT_PARAMS_IN_QUERY: &str = r"\$([\d])+"; -#[cfg(feature = "mysql")] -pub const DETECT_QUOTE_IN_QUERY: &str = r#"\"|\\"#; - -/// This traits defines and implements a query against a database given -/// an statement `stmt` and the params to pass the to the client. -/// -/// Returns [`std::result::Result`] of [`CanyonRows`], which is the core Canyon type to wrap -/// the result of the query provide automatic mappings and deserialization -#[async_trait] -pub trait Transaction { - /// Performs a query against the targeted database by the selected or - /// the defaulted datasource, wrapping the resultant collection of entities - /// in [`super::rows::CanyonRows`] - async fn query<'a, S, Z>( - stmt: S, - params: Z, - datasource_name: &'a str, - ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> - where - S: AsRef + Display + Sync + Send + 'a, - Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, - { - let mut guarded_cache = CACHED_DATABASE_CONN.lock().await; - let database_conn = get_database_connection(datasource_name, &mut guarded_cache); - - match *database_conn { - #[cfg(feature = "postgres")] - DatabaseConnection::Postgres(_) => { - postgres_query_launcher::launch::( - database_conn, - stmt.to_string(), - params.as_ref(), - ) - .await - } - #[cfg(feature = "mssql")] - DatabaseConnection::SqlServer(_) => { - sqlserver_query_launcher::launch::( - database_conn, - &mut stmt.to_string(), - params, - ) - .await - } - #[cfg(feature = "mysql")] - DatabaseConnection::MySQL(_) => { - mysql_query_launcher::launch::(database_conn, stmt.to_string(), params.as_ref()) - .await - } - } - } -} +use std::{error::Error, future::Future}; -/// *CrudOperations* it's the core part of Canyon-SQL. -/// -/// Here it's defined and implemented every CRUD operation -/// that the user has available, just by deriving the `CanyonCrud` -/// derive macro when a struct contains the annotation. -/// -/// Also, this traits needs that the type T over what it's generified -/// to implement certain types in order to work correctly. -/// -/// The most notorious one it's the [`RowMapper`] one, which allows -/// Canyon to directly maps database results into structs. -/// -/// See it's definition and docs to see the implementations. -/// Also, you can find the written macro-code that performs the auto-mapping -/// in the *canyon_sql_root::canyon_macros* crates, on the root of this project. -#[async_trait] -pub trait CrudOperations: Transaction +pub trait ReadOperations: Send where - T: CrudOperations + RowMapper, + R: RowMapper, + Vec: FromIterator, { - async fn find_all<'a>() -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>>; + fn find_all() -> impl Future, Box>> + Send; - async fn find_all_datasource<'a>( - datasource_name: &'a str, - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>>; - - async fn find_all_unchecked<'a>() -> Vec; - - async fn find_all_unchecked_datasource<'a>(datasource_name: &'a str) -> Vec; + fn find_all_with<'connection, I>( + input: I, + ) -> impl Future, Box>> + Send + where + I: DbConnection + Send + 'connection; - fn select_query<'a>() -> SelectQueryBuilder<'a, T>; + fn select_query<'a>() -> Result, Box>; - fn select_query_datasource(datasource_name: &str) -> SelectQueryBuilder<'_, T>; + fn select_query_with<'a>( + database_type: DatabaseType, + ) -> Result, Box>; - async fn count() -> Result>; + fn count() -> impl Future>> + Send; - async fn count_datasource<'a>( - datasource_name: &'a str, - ) -> Result>; + fn count_with<'connection, I>( + input: I, + ) -> impl Future>> + Send + where + I: DbConnection + Send + 'connection; - async fn find_by_pk<'a>( - value: &'a dyn QueryParameter<'a>, - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>>; + fn find_by_pk<'value, 'error>( + value: &'value dyn QueryParameter, + ) -> impl Future, Box>> + Send; - async fn find_by_pk_datasource<'a>( - value: &'a dyn QueryParameter<'a>, - datasource_name: &'a str, - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>>; + fn find_by_pk_with<'value, 'error, I>( + value: &'value dyn QueryParameter, + input: I, + ) -> impl Future, Box>> + Send + where + I: DbConnection + Send + 'value; +} - async fn insert<'a>(&mut self) -> Result<(), Box>; +pub trait InsertOperations: Send { + fn insert<'entity, 'error>( + &'entity mut self, + ) -> impl Future>> + Send; - async fn insert_datasource<'a>( + fn insert_with<'connection, I>( &mut self, - datasource_name: &'a str, - ) -> Result<(), Box>; - - async fn multi_insert<'a>( - instances: &'a mut [&'a mut T], - ) -> Result<(), Box<(dyn std::error::Error + Send + Sync + 'static)>>; - - async fn multi_insert_datasource<'a>( - instances: &'a mut [&'a mut T], - datasource_name: &'a str, - ) -> Result<(), Box<(dyn std::error::Error + Send + Sync + 'static)>>; - - async fn update(&self) -> Result<(), Box>; - - async fn update_datasource<'a>( - &self, - datasource_name: &'a str, - ) -> Result<(), Box>; - - fn update_query<'a>() -> UpdateQueryBuilder<'a, T>; - - fn update_query_datasource(datasource_name: &str) -> UpdateQueryBuilder<'_, T>; - - async fn delete(&self) -> Result<(), Box>; - - async fn delete_datasource<'a>( - &self, - datasource_name: &'a str, - ) -> Result<(), Box>; - - fn delete_query<'a>() -> DeleteQueryBuilder<'a, T>; - - fn delete_query_datasource(datasource_name: &str) -> DeleteQueryBuilder<'_, T>; + input: I, + ) -> impl Future>> + Send + where + I: DbConnection + Send + 'connection; } -#[cfg(feature = "postgres")] -mod postgres_query_launcher { - use canyon_connection::canyon_database_connector::DatabaseConnection; - - use crate::bounds::QueryParameter; - use crate::rows::CanyonRows; +pub trait UpdateOperations: Send { + fn update(&self) -> impl Future>> + Send; - pub async fn launch<'a, T>( - db_conn: &DatabaseConnection, - stmt: String, - params: &'a [&'_ dyn QueryParameter<'_>], - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> { - let mut m_params = Vec::new(); - for param in params { - m_params.push(param.as_postgres_param()); - } + fn update_with<'connection, I>( + &self, + input: I, + ) -> impl Future>> + Send + where + I: DbConnection + Send + 'connection; - let r = db_conn - .postgres_connection() - .client - .query(&stmt, m_params.as_slice()) - .await?; + fn update_query<'canyon, 'err>() + -> Result, Box> + where + 'canyon: 'err; - Ok(CanyonRows::Postgres(r)) - } + fn update_query_with<'a>(database_type: DatabaseType) -> UpdateQueryBuilder<'a>; } -#[cfg(feature = "mssql")] -mod sqlserver_query_launcher { - use crate::rows::CanyonRows; - use crate::{ - bounds::QueryParameter, - canyon_connection::{canyon_database_connector::DatabaseConnection, tiberius::Query}, - }; +pub trait DeleteOperations: Send { + fn delete(&self) -> impl Future>> + Send; - pub async fn launch<'a, T, Z>( - db_conn: &mut DatabaseConnection, - stmt: &mut String, - params: Z, - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> + fn delete_with<'connection, 'error, I>( + &self, + input: I, + ) -> impl Future>> + Send where - Z: AsRef<[&'a dyn QueryParameter<'a>]> + Sync + Send + 'a, - { - // Re-generate de insert statement to adequate it to the SQL SERVER syntax to retrieve the PK value(s) after insert - if stmt.contains("RETURNING") { - let c = stmt.clone(); - let temp = c.split_once("RETURNING").unwrap(); - let temp2 = temp.0.split_once("VALUES").unwrap(); - - *stmt = format!( - "{} OUTPUT inserted.{} VALUES {}", - temp2.0.trim(), - temp.1.trim(), - temp2.1.trim() - ); - } + I: DbConnection + Send + 'connection; - let mut mssql_query = Query::new(stmt.to_owned().replace('$', "@P")); - params - .as_ref() - .iter() - .for_each(|param| mssql_query.bind(*param)); - - let _results = mssql_query - .query(db_conn.sqlserver_connection().client) - .await? - .into_results() - .await?; + fn delete_query<'canyon, 'err>() + -> Result, Box> + where + 'canyon: 'err; - Ok(CanyonRows::Tiberius( - _results.into_iter().flatten().collect(), - )) - } + fn delete_query_with<'a>(database_type: DatabaseType) -> DeleteQueryBuilder<'a>; } -#[cfg(feature = "mysql")] -mod mysql_query_launcher { - use std::sync::Arc; - - use mysql_async::prelude::Query; - use mysql_async::QueryWithParams; - use mysql_async::Value; - - use canyon_connection::canyon_database_connector::DatabaseConnection; - - use crate::bounds::QueryParameter; - use crate::rows::CanyonRows; - use mysql_async::Row; - use mysql_common::constants::ColumnType; - use mysql_common::row; - - use super::reorder_params; - use crate::crud::{DETECT_PARAMS_IN_QUERY, DETECT_QUOTE_IN_QUERY}; - use regex::Regex; - - pub async fn launch<'a, T>( - db_conn: &DatabaseConnection, - stmt: String, - params: &'a [&'_ dyn QueryParameter<'_>], - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> { - let mysql_connection = db_conn.mysql_connection().client.get_conn().await?; - - let stmt_with_escape_characters = regex::escape(&stmt); - let query_string = - Regex::new(DETECT_PARAMS_IN_QUERY)?.replace_all(&stmt_with_escape_characters, "?"); - - let mut query_string = Regex::new(DETECT_QUOTE_IN_QUERY)? - .replace_all(&query_string, "") - .to_string(); - - let mut is_insert = false; - if let Some(index_start_clausule_returning) = query_string.find(" RETURNING") { - query_string.truncate(index_start_clausule_returning); - is_insert = true; - } - - let params_query: Vec = - reorder_params(&stmt, params, |f| f.as_mysql_param().to_value()); - - let query_with_params = QueryWithParams { - query: query_string, - params: params_query, - }; - - let mut query_result = query_with_params - .run(mysql_connection) - .await - .expect("Error executing query in mysql"); - - let result_rows = if is_insert { - let last_insert = query_result - .last_insert_id() - .map(Value::UInt) - .expect("Error getting pk id in insert"); - - vec![row::new_row( - vec![last_insert], - Arc::new([mysql_async::Column::new(ColumnType::MYSQL_TYPE_UNKNOWN)]), - )] - } else { - query_result - .collect::() - .await - .expect("Error resolved trait FromRow in mysql") - }; - - Ok(CanyonRows::MySQL(result_rows)) - } +pub trait CrudOperations: + ReadOperations + InsertOperations + UpdateOperations + DeleteOperations +where + R: RowMapper, + Vec: FromIterator, +{ } -#[cfg(feature = "mysql")] -fn reorder_params( - stmt: &str, - params: &[&'_ dyn QueryParameter<'_>], - fn_parser: impl Fn(&&dyn QueryParameter<'_>) -> T, -) -> Vec { - let mut ordered_params = vec![]; - let rg = regex::Regex::new(DETECT_PARAMS_IN_QUERY) - .expect("Error create regex with detect params pattern expression"); - - for positional_param in rg.find_iter(stmt) { - let pp: &str = positional_param.as_str(); - let pp_index = pp[1..] // param $1 -> get 1 - .parse::() - .expect("Error parse mapped parameter to usized.") - - 1; - - let element = params - .get(pp_index) - .expect("Error obtaining the element of the mapping against parameters."); - ordered_params.push(fn_parser(element)); - } - - ordered_params +impl CrudOperations for T +where + T: ReadOperations + InsertOperations + UpdateOperations + DeleteOperations, + R: RowMapper, + Vec: FromIterator, +{ } diff --git a/canyon_crud/src/entity.rs b/canyon_crud/src/entity.rs new file mode 100644 index 00000000..ce6b57dc --- /dev/null +++ b/canyon_crud/src/entity.rs @@ -0,0 +1,52 @@ +use canyon_core::connection::contracts::DbConnection; +use canyon_core::mapper::RowMapper; +use canyon_core::query::bounds::EntityRuntimeInfo; +use std::error::Error; + +/// CRUD operations over an entity supplied to the operation. +/// +/// It is intended for repository adapters and layered architectures where the persistence type is +/// not the entity being persisted. +pub trait EntityCrudOperations: Send { + fn insert_entity<'entity, 'error, T>( + entity: &'entity mut T, + ) -> impl Future>> + where + T: RowMapper + EntityRuntimeInfo + Sync + 'entity; + + fn insert_entity_with<'entity, 'error, T, I>( + entity: &'entity mut T, + input: I, + ) -> impl Future>> + where + T: RowMapper + EntityRuntimeInfo + Sync + 'entity, + I: DbConnection + Send + 'entity; + + fn update_entity<'entity, 'error, T>( + entity: &'entity T, + ) -> impl Future>> + where + T: RowMapper + EntityRuntimeInfo + Sync + 'entity; + + fn update_entity_with<'entity, 'error, T, I>( + entity: &'entity T, + input: I, + ) -> impl Future>> + where + T: RowMapper + EntityRuntimeInfo + Sync + 'entity, + I: DbConnection + Send + 'entity; + + fn delete_entity<'entity, 'error, T>( + entity: &'entity T, + ) -> impl Future>> + where + T: RowMapper + EntityRuntimeInfo + Sync + 'entity; + + fn delete_entity_with<'entity, 'error, T, I>( + entity: &'entity T, + input: I, + ) -> impl Future>> + where + T: RowMapper + EntityRuntimeInfo + Sync + 'entity, + I: DbConnection + Send + 'entity; +} diff --git a/canyon_crud/src/lib.rs b/canyon_crud/src/lib.rs index cea474cb..401734a7 100644 --- a/canyon_crud/src/lib.rs +++ b/canyon_crud/src/lib.rs @@ -1,13 +1,7 @@ -pub extern crate async_trait; -extern crate canyon_connection; - -pub mod bounds; pub mod crud; -pub mod mapper; -pub mod query_elements; -pub mod rows; +pub mod entity; -pub use query_elements::operators::*; +pub use canyon_core::query::operators::*; -pub use canyon_connection::{canyon_database_connector::DatabaseType, datasources::*}; +pub use canyon_core::connection::{database_type::DatabaseType, datasources::*}; pub use chrono; diff --git a/canyon_crud/src/mapper.rs b/canyon_crud/src/mapper.rs deleted file mode 100644 index 252df1ce..00000000 --- a/canyon_crud/src/mapper.rs +++ /dev/null @@ -1,20 +0,0 @@ -#[cfg(feature = "mysql")] -use canyon_connection::mysql_async; -#[cfg(feature = "mssql")] -use canyon_connection::tiberius; -#[cfg(feature = "postgres")] -use canyon_connection::tokio_postgres; - -use crate::crud::Transaction; - -/// Declares functions that takes care to deserialize data incoming -/// from some supported database in Canyon-SQL into a user's defined -/// type `T` -pub trait RowMapper>: Sized { - #[cfg(feature = "postgres")] - fn deserialize_postgresql(row: &tokio_postgres::Row) -> T; - #[cfg(feature = "mssql")] - fn deserialize_sqlserver(row: &tiberius::Row) -> T; - #[cfg(feature = "mysql")] - fn deserialize_mysql(row: &mysql_async::Row) -> T; -} diff --git a/canyon_crud/src/query_elements/mod.rs b/canyon_crud/src/query_elements/mod.rs deleted file mode 100644 index e319d4a4..00000000 --- a/canyon_crud/src/query_elements/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod operators; -pub mod query; -pub mod query_builder; diff --git a/canyon_crud/src/query_elements/operators.rs b/canyon_crud/src/query_elements/operators.rs deleted file mode 100644 index 015ced03..00000000 --- a/canyon_crud/src/query_elements/operators.rs +++ /dev/null @@ -1,69 +0,0 @@ -use canyon_connection::canyon_database_connector::DatabaseType; - -pub trait Operator { - fn as_str(&self, placeholder_counter: usize, datasource_type: &DatabaseType) -> String; -} - -/// Enumerated type for represent the comparison operations -/// in SQL sentences -pub enum Comp { - /// Operator "=" equals - Eq, - /// Operator "!=" not equals - Neq, - /// Operator ">" greater than value - Gt, - /// Operator ">=" greater or equals than value - GtEq, - /// Operator "<" less than value - Lt, - /// Operator "=<" less or equals than value - LtEq, -} - -impl Operator for Comp { - fn as_str(&self, placeholder_counter: usize, _datasource_type: &DatabaseType) -> String { - match *self { - Self::Eq => format!(" = ${placeholder_counter}"), - Self::Neq => format!(" <> ${placeholder_counter}"), - Self::Gt => format!(" > ${placeholder_counter}"), - Self::GtEq => format!(" >= ${placeholder_counter}"), - Self::Lt => format!(" < ${placeholder_counter}"), - Self::LtEq => format!(" <= ${placeholder_counter}"), - } - } -} - -pub enum Like { - /// Operator "LIKE" as '%pattern%' - Full, - /// Operator "LIKE" as '%pattern' - Left, - /// Operator "LIKE" as 'pattern%' - Right, -} - -impl Operator for Like { - fn as_str(&self, placeholder_counter: usize, datasource_type: &DatabaseType) -> String { - let type_data_to_cast_str = match datasource_type { - #[cfg(feature = "postgres")] - DatabaseType::PostgreSql => "VARCHAR", - #[cfg(feature = "mssql")] - DatabaseType::SqlServer => "VARCHAR", - #[cfg(feature = "mysql")] - DatabaseType::MySQL => "CHAR", - }; - - match *self { - Like::Full => { - format!(" LIKE CONCAT('%', CAST(${placeholder_counter} AS {type_data_to_cast_str}) ,'%')") - } - Like::Left => format!( - " LIKE CONCAT('%', CAST(${placeholder_counter} AS {type_data_to_cast_str}))" - ), - Like::Right => format!( - " LIKE CONCAT(CAST(${placeholder_counter} AS {type_data_to_cast_str}) ,'%')" - ), - } - } -} diff --git a/canyon_crud/src/query_elements/query.rs b/canyon_crud/src/query_elements/query.rs deleted file mode 100644 index 3923d3b6..00000000 --- a/canyon_crud/src/query_elements/query.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::{fmt::Debug, marker::PhantomData}; - -use crate::{ - bounds::QueryParameter, - crud::{CrudOperations, Transaction}, - mapper::RowMapper, -}; - -/// Holds a sql sentence details -#[derive(Debug, Clone)] -pub struct Query<'a, T: CrudOperations + Transaction + RowMapper> { - pub sql: String, - pub params: Vec<&'a dyn QueryParameter<'a>>, - marker: PhantomData, -} - -impl<'a, T> Query<'a, T> -where - T: CrudOperations + Transaction + RowMapper, -{ - pub fn new(sql: String) -> Query<'a, T> { - Self { - sql, - params: vec![], - marker: PhantomData, - } - } -} diff --git a/canyon_crud/src/query_elements/query_builder.rs b/canyon_crud/src/query_elements/query_builder.rs deleted file mode 100644 index 4d56401a..00000000 --- a/canyon_crud/src/query_elements/query_builder.rs +++ /dev/null @@ -1,687 +0,0 @@ -use std::fmt::Debug; - -use canyon_connection::{ - canyon_database_connector::DatabaseType, get_database_config, DATASOURCES, -}; - -use crate::{ - bounds::{FieldIdentifier, FieldValueIdentifier, QueryParameter}, - crud::{CrudOperations, Transaction}, - mapper::RowMapper, - query_elements::query::Query, - Operator, -}; - -/// Contains the elements that makes part of the formal declaration -/// of the behaviour of the Canyon-SQL QueryBuilder -pub mod ops { - pub use super::*; - - /// The [`QueryBuilder`] trait is the root of a kind of hierarchy - /// on more specific [`super::QueryBuilder`], that are: - /// - /// * [`super::SelectQueryBuilder`] - /// * [`super::UpdateQueryBuilder`] - /// * [`super::DeleteQueryBuilder`] - /// - /// This trait provides the formal declaration of the behaviour that the - /// implementors must provide in their public interfaces, groping - /// the common elements between every element down in that - /// hierarchy. - /// - /// For example, the [`super::QueryBuilder`] type holds the data - /// necessary for track the SQL sentence while it's being generated - /// thought the fluent builder, and provides the behaviour of - /// the common elements defined in this trait. - /// - /// The more concrete types represents a wrapper over a raw - /// [`super::QueryBuilder`], offering all the elements declared - /// in this trait in its public interface, and which implementation - /// only consists of call the same method on the wrapped - /// [`super::QueryBuilder`]. - /// - /// This allows us to declare in their public interface their - /// specific operations, like, for example, join operations - /// on the [`super::SelectQueryBuilder`], and the usage - /// of the `SET` clause on a [`super::UpdateQueryBuilder`], - /// without mixing types or convoluting everything into - /// just one type. - pub trait QueryBuilder<'a, T> - where - T: CrudOperations + Transaction + RowMapper, - { - /// Returns a read-only reference to the underlying SQL sentence, - /// with the same lifetime as self - fn read_sql(&'a self) -> &'a str; - - /// Public interface for append the content of an slice to the end of - /// the underlying SQL sentece. - /// - /// This mutator will allow the user to wire SQL code to the already - /// generated one - /// - /// * `sql` - The [`&str`] to be wired in the SQL - fn push_sql(&mut self, sql: &str); - - /// Generates a `WHERE` SQL clause for constraint the query. - /// - /// * `column` - A [`FieldValueIdentifier`] that will provide the target - /// column name and the value for the filter - /// * `op` - Any element that implements [`Operator`] for create the comparison - /// or equality binary operator - fn r#where>( - &mut self, - column: Z, - op: impl Operator, - ) -> &mut Self - where - T: Debug + CrudOperations + Transaction + RowMapper; - - /// Generates an `AND` SQL clause for constraint the query. - /// - /// * `column` - A [`FieldValueIdentifier`] that will provide the target - /// column name and the value for the filter - /// * `op` - Any element that implements [`Operator`] for create the comparison - /// or equality binary operator - fn and>( - &mut self, - column: Z, - op: impl Operator, - ) -> &mut Self; - - /// Generates an `AND` SQL clause for constraint the query that will create - /// the filter in conjunction with an `IN` operator that will ac - /// - /// * `column` - A [`FieldIdentifier`] that will provide the target - /// column name for the filter, based on the variant that represents - /// the field name that maps the targeted column name - /// * `values` - An array of [`QueryParameter`] with the values to filter - /// inside the `IN` operator - fn and_values_in(&mut self, column: Z, values: &'a [Q]) -> &mut Self - where - Z: FieldIdentifier, - Q: QueryParameter<'a>; - - /// Generates an `OR` SQL clause for constraint the query that will create - /// the filter in conjunction with an `IN` operator that will ac - /// - /// * `column` - A [`FieldIdentifier`] that will provide the target - /// column name for the filter, based on the variant that represents - /// the field name that maps the targeted column name - /// * `values` - An array of [`QueryParameter`] with the values to filter - /// inside the `IN` operator - fn or_values_in(&mut self, r#or: Z, values: &'a [Q]) -> &mut Self - where - Z: FieldIdentifier, - Q: QueryParameter<'a>; - - /// Generates an `OR` SQL clause for constraint the query. - /// - /// * `column` - A [`FieldValueIdentifier`] that will provide the target - /// column name and the value for the filter - /// * `op` - Any element that implements [`Operator`] for create the comparison - /// or equality binary operator - fn or>(&mut self, column: Z, op: impl Operator) - -> &mut Self; - - /// Generates a `ORDER BY` SQL clause for constraint the query. - /// - /// * `order_by` - A [`FieldIdentifier`] that will provide the target column name - /// * `desc` - a boolean indicating if the generated `ORDER_BY` must be in ascending or descending order - fn order_by>(&mut self, order_by: Z, desc: bool) -> &mut Self; - } -} - -/// Type for construct more complex queries than the classical CRUD ones. -#[derive(Debug, Clone)] -pub struct QueryBuilder<'a, T> -where - T: CrudOperations + Transaction + RowMapper, -{ - query: Query<'a, T>, - datasource_name: &'a str, - datasource_type: DatabaseType, -} - -unsafe impl<'a, T> Send for QueryBuilder<'a, T> where - T: CrudOperations + Transaction + RowMapper -{ -} -unsafe impl<'a, T> Sync for QueryBuilder<'a, T> where - T: CrudOperations + Transaction + RowMapper -{ -} - -impl<'a, T> QueryBuilder<'a, T> -where - T: CrudOperations + Transaction + RowMapper, -{ - /// Returns a new instance of the [`QueryBuilder`] - pub fn new(query: Query<'a, T>, datasource_name: &'a str) -> Self { - Self { - query, - datasource_name, - datasource_type: DatabaseType::from( - &get_database_config(datasource_name, &DATASOURCES).auth, - ), - } - } - - /// Launches the generated query against the database targeted - /// by the selected datasource - pub async fn query( - &'a mut self, - ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> { - self.query.sql.push(';'); - - Ok(T::query( - self.query.sql.clone(), - self.query.params.to_vec(), - self.datasource_name, - ) - .await? - .into_results::()) - } - - pub fn r#where>(&mut self, r#where: Z, op: impl Operator) { - let (column_name, value) = r#where.value(); - - let where_ = String::from(" WHERE ") - + column_name - + &op.as_str(self.query.params.len() + 1, &self.datasource_type); - - self.query.sql.push_str(&where_); - self.query.params.push(value); - } - - pub fn and>(&mut self, r#and: Z, op: impl Operator) { - let (column_name, value) = r#and.value(); - - let and_ = String::from(" AND ") - + column_name - + &op.as_str(self.query.params.len() + 1, &self.datasource_type); - - self.query.sql.push_str(&and_); - self.query.params.push(value); - } - - pub fn or>(&mut self, r#and: Z, op: impl Operator) { - let (column_name, value) = r#and.value(); - - let and_ = String::from(" OR ") - + column_name - + &op.as_str(self.query.params.len() + 1, &self.datasource_type); - - self.query.sql.push_str(&and_); - self.query.params.push(value); - } - - pub fn and_values_in(&mut self, r#and: Z, values: &'a [Q]) - where - Z: FieldIdentifier, - Q: QueryParameter<'a>, - { - if values.is_empty() { - return; - } - - self.query - .sql - .push_str(&format!(" AND {} IN (", r#and.as_str())); - - let mut counter = 1; - values.iter().for_each(|qp| { - if values.len() != counter { - self.query - .sql - .push_str(&format!("${}, ", self.query.params.len())); - counter += 1; - } else { - self.query - .sql - .push_str(&format!("${}", self.query.params.len())); - } - self.query.params.push(qp) - }); - - self.query.sql.push(')') - } - - fn or_values_in(&mut self, r#or: Z, values: &'a [Q]) - where - Z: FieldIdentifier, - Q: QueryParameter<'a>, - { - if values.is_empty() { - return; - } - - self.query - .sql - .push_str(&format!(" OR {} IN (", r#or.as_str())); - - let mut counter = 1; - values.iter().for_each(|qp| { - if values.len() != counter { - self.query - .sql - .push_str(&format!("${}, ", self.query.params.len())); - counter += 1; - } else { - self.query - .sql - .push_str(&format!("${}", self.query.params.len())); - } - self.query.params.push(qp) - }); - - self.query.sql.push(')') - } - - #[inline] - pub fn order_by>(&mut self, order_by: Z, desc: bool) { - self.query.sql.push_str( - &(format!( - " ORDER BY {}{}", - order_by.as_str(), - if desc { " DESC " } else { "" } - )), - ); - } -} - -#[derive(Debug, Clone)] -pub struct SelectQueryBuilder<'a, T> -where - T: CrudOperations + Transaction + RowMapper, -{ - _inner: QueryBuilder<'a, T>, -} - -impl<'a, T> SelectQueryBuilder<'a, T> -where - T: CrudOperations + Transaction + RowMapper, -{ - /// Generates a new public instance of the [`SelectQueryBuilder`] - pub fn new(table_schema_data: &str, datasource_name: &'a str) -> Self { - Self { - _inner: QueryBuilder::::new( - Query::new(format!("SELECT * FROM {table_schema_data}")), - datasource_name, - ), - } - } - - /// Launches the generated query to the database pointed by the - /// selected datasource - #[inline] - pub async fn query( - &'a mut self, - ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> { - self._inner.query().await - } - - /// Adds a *LEFT JOIN* SQL statement to the underlying - /// [`Query`] held by the [`QueryBuilder`], where: - /// - /// * `join_table` - The table target of the join operation - /// * `col1` - The left side of the ON operator for the join - /// * `col2` - The right side of the ON operator for the join - /// - /// > Note: The order on the column parameters is irrelevant - pub fn left_join(&mut self, join_table: &str, col1: &str, col2: &str) -> &mut Self { - self._inner - .query - .sql - .push_str(&format!(" LEFT JOIN {join_table} ON {col1} = {col2}")); - self - } - - /// Adds a *RIGHT JOIN* SQL statement to the underlying - /// [`Query`] held by the [`QueryBuilder`], where: - /// - /// * `join_table` - The table target of the join operation - /// * `col1` - The left side of the ON operator for the join - /// * `col2` - The right side of the ON operator for the join - /// - /// > Note: The order on the column parameters is irrelevant - pub fn inner_join(&mut self, join_table: &str, col1: &str, col2: &str) -> &mut Self { - self._inner - .query - .sql - .push_str(&format!(" INNER JOIN {join_table} ON {col1} = {col2}")); - self - } - - /// Adds a *RIGHT JOIN* SQL statement to the underlying - /// [`Query`] held by the [`QueryBuilder`], where: - /// - /// * `join_table` - The table target of the join operation - /// * `col1` - The left side of the ON operator for the join - /// * `col2` - The right side of the ON operator for the join - /// - /// > Note: The order on the column parameters is irrelevant - pub fn right_join(&mut self, join_table: &str, col1: &str, col2: &str) -> &mut Self { - self._inner - .query - .sql - .push_str(&format!(" RIGHT JOIN {join_table} ON {col1} = {col2}")); - self - } - - /// Adds a *FULL JOIN* SQL statement to the underlying - /// [`Query`] held by the [`QueryBuilder`], where: - /// - /// * `join_table` - The table target of the join operation - /// * `col1` - The left side of the ON operator for the join - /// * `col2` - The right side of the ON operator for the join - /// - /// > Note: The order on the column parameters is irrelevant - pub fn full_join(&mut self, join_table: &str, col1: &str, col2: &str) -> &mut Self { - self._inner - .query - .sql - .push_str(&format!(" FULL JOIN {join_table} ON {col1} = {col2}")); - self - } -} - -impl<'a, T> ops::QueryBuilder<'a, T> for SelectQueryBuilder<'a, T> -where - T: Debug + CrudOperations + Transaction + RowMapper + Send, -{ - #[inline] - fn read_sql(&'a self) -> &'a str { - self._inner.query.sql.as_str() - } - - #[inline(always)] - fn push_sql(&mut self, sql: &str) { - self._inner.query.sql.push_str(sql); - } - - #[inline] - fn r#where>( - &mut self, - r#where: Z, - op: impl Operator, - ) -> &mut Self { - self._inner.r#where(r#where, op); - self - } - - #[inline] - fn and>(&mut self, column: Z, op: impl Operator) -> &mut Self { - self._inner.and(column, op); - self - } - - #[inline] - fn and_values_in(&mut self, r#and: Z, values: &'a [Q]) -> &mut Self - where - Z: FieldIdentifier, - Q: QueryParameter<'a>, - { - self._inner.and_values_in(and, values); - self - } - - #[inline] - fn or_values_in(&mut self, r#and: Z, values: &'a [Q]) -> &mut Self - where - Z: FieldIdentifier, - Q: QueryParameter<'a>, - { - self._inner.or_values_in(and, values); - self - } - - #[inline] - fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { - self._inner.or(column, op); - self - } - - #[inline] - fn order_by>(&mut self, order_by: Z, desc: bool) -> &mut Self { - self._inner.order_by(order_by, desc); - self - } -} - -/// Contains the specific database operations of the *UPDATE* SQL statements. -/// -/// * `set` - To construct a new `SET` clause to determine the columns to -/// update with the provided values -#[derive(Debug, Clone)] -pub struct UpdateQueryBuilder<'a, T> -where - T: CrudOperations + Transaction + RowMapper, -{ - _inner: QueryBuilder<'a, T>, -} - -impl<'a, T> UpdateQueryBuilder<'a, T> -where - T: CrudOperations + Transaction + RowMapper, -{ - /// Generates a new public instance of the [`UpdateQueryBuilder`] - pub fn new(table_schema_data: &str, datasource_name: &'a str) -> Self { - Self { - _inner: QueryBuilder::::new( - Query::new(format!("UPDATE {table_schema_data}")), - datasource_name, - ), - } - } - - /// Launches the generated query to the database pointed by the - /// selected datasource - #[inline] - pub async fn query( - &'a mut self, - ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> { - self._inner.query().await - } - - /// Creates an SQL `SET` clause to especify the columns that must be updated in the sentence - pub fn set(&mut self, columns: &'a [(Z, Q)]) -> &mut Self - where - Z: FieldIdentifier + Clone, - Q: QueryParameter<'a>, - { - if columns.is_empty() { - return self; - } - if self._inner.query.sql.contains("SET") { - panic!( - "\n{}", - String::from("\t[PANIC!] - Don't use chained calls of the .set(...) method. ") - + "\n\tPass all the values in a unique call within the 'columns' " - + "array of tuples parameter\n" - ) - } - - let mut set_clause = String::new(); - set_clause.push_str(" SET "); - - for (idx, column) in columns.iter().enumerate() { - set_clause.push_str(&format!( - "{} = ${}", - column.0.as_str(), - self._inner.query.params.len() + 1 - )); - - if idx < columns.len() - 1 { - set_clause.push_str(", "); - } - self._inner.query.params.push(&column.1); - } - - self._inner.query.sql.push_str(&set_clause); - self - } -} - -impl<'a, T> ops::QueryBuilder<'a, T> for UpdateQueryBuilder<'a, T> -where - T: Debug + CrudOperations + Transaction + RowMapper + Send, -{ - #[inline] - fn read_sql(&'a self) -> &'a str { - self._inner.query.sql.as_str() - } - - #[inline(always)] - fn push_sql(&mut self, sql: &str) { - self._inner.query.sql.push_str(sql); - } - - #[inline] - fn r#where>( - &mut self, - r#where: Z, - op: impl Operator, - ) -> &mut Self { - self._inner.r#where(r#where, op); - self - } - - #[inline] - fn and>(&mut self, column: Z, op: impl Operator) -> &mut Self { - self._inner.and(column, op); - self - } - - #[inline] - fn and_values_in(&mut self, r#and: Z, values: &'a [Q]) -> &mut Self - where - Z: FieldIdentifier, - Q: QueryParameter<'a>, - { - self._inner.and_values_in(and, values); - self - } - - #[inline] - fn or_values_in(&mut self, r#or: Z, values: &'a [Q]) -> &mut Self - where - Z: FieldIdentifier, - Q: QueryParameter<'a>, - { - self._inner.or_values_in(or, values); - self - } - - #[inline] - fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { - self._inner.or(column, op); - self - } - - #[inline] - fn order_by>(&mut self, order_by: Z, desc: bool) -> &mut Self { - self._inner.order_by(order_by, desc); - self - } -} - -/// Contains the specific database operations associated with the -/// *DELETE* SQL statements. -/// -/// * `set` - To construct a new `SET` clause to determine the columns to -/// update with the provided values -#[derive(Debug, Clone)] -pub struct DeleteQueryBuilder<'a, T> -where - T: CrudOperations + Transaction + RowMapper, -{ - _inner: QueryBuilder<'a, T>, -} - -impl<'a, T> DeleteQueryBuilder<'a, T> -where - T: CrudOperations + Transaction + RowMapper, -{ - /// Generates a new public instance of the [`DeleteQueryBuilder`] - pub fn new(table_schema_data: &str, datasource_name: &'a str) -> Self { - Self { - _inner: QueryBuilder::::new( - Query::new(format!("DELETE FROM {table_schema_data}")), - datasource_name, - ), - } - } - - /// Launches the generated query to the database pointed by the - /// selected datasource - #[inline] - pub async fn query( - &'a mut self, - ) -> Result, Box<(dyn std::error::Error + Sync + Send + 'static)>> { - self._inner.query().await - } -} - -impl<'a, T> ops::QueryBuilder<'a, T> for DeleteQueryBuilder<'a, T> -where - T: Debug + CrudOperations + Transaction + RowMapper + Send, -{ - #[inline] - fn read_sql(&'a self) -> &'a str { - self._inner.query.sql.as_str() - } - - #[inline(always)] - fn push_sql(&mut self, sql: &str) { - self._inner.query.sql.push_str(sql); - } - - #[inline] - fn r#where>( - &mut self, - r#where: Z, - op: impl Operator, - ) -> &mut Self { - self._inner.r#where(r#where, op); - self - } - - #[inline] - fn and>(&mut self, column: Z, op: impl Operator) -> &mut Self { - self._inner.and(column, op); - self - } - - #[inline] - fn and_values_in(&mut self, r#and: Z, values: &'a [Q]) -> &mut Self - where - Z: FieldIdentifier, - Q: QueryParameter<'a>, - { - self._inner.or_values_in(and, values); - self - } - - #[inline] - fn or_values_in(&mut self, r#or: Z, values: &'a [Q]) -> &mut Self - where - Z: FieldIdentifier, - Q: QueryParameter<'a>, - { - self._inner.or_values_in(or, values); - self - } - - #[inline] - fn or>(&mut self, column: Z, op: impl Operator) -> &mut Self { - self._inner.or(column, op); - self - } - - #[inline] - fn order_by>(&mut self, order_by: Z, desc: bool) -> &mut Self { - self._inner.order_by(order_by, desc); - self - } -} diff --git a/canyon_crud/src/rows.rs b/canyon_crud/src/rows.rs deleted file mode 100644 index 517592a6..00000000 --- a/canyon_crud/src/rows.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::crud::Transaction; -use crate::mapper::RowMapper; -use std::marker::PhantomData; - -/// Lightweight wrapper over the collection of results of the different crates -/// supported by Canyon-SQL. -/// -/// Even tho the wrapping seems meaningless, this allows us to provide internal -/// operations that are too difficult or to ugly to implement in the macros that -/// will call the query method of Crud. -pub enum CanyonRows { - #[cfg(feature = "postgres")] - Postgres(Vec), - #[cfg(feature = "mssql")] - Tiberius(Vec), - #[cfg(feature = "mysql")] - MySQL(Vec), - - UnusableTypeMarker(PhantomData), -} - -impl CanyonRows { - #[cfg(feature = "postgres")] - pub fn get_postgres_rows(&self) -> &Vec { - match self { - Self::Postgres(v) => v, - _ => panic!("This branch will never ever should be reachable"), - } - } - - #[cfg(feature = "mssql")] - pub fn get_tiberius_rows(&self) -> &Vec { - match self { - Self::Tiberius(v) => v, - _ => panic!("This branch will never ever should be reachable"), - } - } - - #[cfg(feature = "mysql")] - pub fn get_mysql_rows(&self) -> &Vec { - match self { - Self::MySQL(v) => v, - _ => panic!("This branch will never ever should be reachable"), - } - } - - /// Consumes `self` and returns the wrapped [`std::vec::Vec`] with the instances of T - pub fn into_results>(self) -> Vec - where - T: Transaction, - { - match self { - #[cfg(feature = "postgres")] - Self::Postgres(v) => v.iter().map(|row| Z::deserialize_postgresql(row)).collect(), - #[cfg(feature = "mssql")] - Self::Tiberius(v) => v.iter().map(|row| Z::deserialize_sqlserver(row)).collect(), - #[cfg(feature = "mysql")] - Self::MySQL(v) => v.iter().map(|row| Z::deserialize_mysql(row)).collect(), - _ => panic!("This branch will never ever should be reachable"), - } - } - - /// Returns the number of elements present on the wrapped collection - pub fn len(&self) -> usize { - match self { - #[cfg(feature = "postgres")] - Self::Postgres(v) => v.len(), - #[cfg(feature = "mssql")] - Self::Tiberius(v) => v.len(), - #[cfg(feature = "mysql")] - Self::MySQL(v) => v.len(), - _ => panic!("This branch will never ever should be reachable"), - } - } - - /// Returns true whenever the wrapped collection of Rows does not contains any elements - pub fn is_empty(&self) -> bool { - match self { - #[cfg(feature = "postgres")] - Self::Postgres(v) => v.is_empty(), - #[cfg(feature = "mssql")] - Self::Tiberius(v) => v.is_empty(), - #[cfg(feature = "mysql")] - Self::MySQL(v) => v.is_empty(), - _ => panic!("This branch will never ever should be reachable"), - } - } -} diff --git a/canyon_entities/Cargo.toml b/canyon_entities/Cargo.toml index 374e2e98..8f2ce210 100644 --- a/canyon_entities/Cargo.toml +++ b/canyon_entities/Cargo.toml @@ -10,8 +10,7 @@ license.workspace = true description.workspace = true [dependencies] -regex = { workspace = true } partialdebug = { workspace = true } quote = { workspace = true } proc-macro2 = { workspace = true } -syn = { version = "1.0.86", features = ["full", "parsing"] } # TODO Pending to refactor and upgrade +syn = { version = "2.0.117", features = ["full", "parsing"] } # TODO Pending to refactor and upgrade diff --git a/canyon_entities/src/entity.rs b/canyon_entities/src/entity.rs index 8604d0e8..b387ebf8 100644 --- a/canyon_entities/src/entity.rs +++ b/canyon_entities/src/entity.rs @@ -1,10 +1,10 @@ use partialdebug::placeholder::PartialDebug; -use proc_macro2::{Ident, TokenStream}; +use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use std::convert::TryFrom; use syn::{ + Attribute, Generics, ItemStruct, LitStr, Visibility, parse::{Parse, ParseBuffer}, - Attribute, Generics, ItemStruct, Visibility, }; use super::entity_fields::EntityField; @@ -44,13 +44,14 @@ impl CanyonEntity { /// which this enum is related to. /// /// Makes a variant `#field_name(#ty)` where `#ty` it's a trait object - /// of type [`canyon_crud::bounds::QueryParameter`] + /// of type `canyon_core::QueryParameter` TODO: correct the comment when refactored pub fn get_fields_as_enum_variants_with_value(&self) -> Vec { self.fields .iter() .map(|f| { let field_name = &f.name; - quote! { #field_name(&'a dyn canyon_sql::crud::bounds::QueryParameter<'a>) } + let field_ty = &f.field_type; + quote! { #field_name(#field_ty) } }) .collect::>() } @@ -69,30 +70,51 @@ impl CanyonEntity { .collect::>() } - /// Generates an implementation of the match pattern to find whatever variant - /// is being requested when the method `.field_name_as_str(self)` it's invoked over some - /// instance that implements the `canyon_sql_root::crud::bounds::FieldIdentifier` trait - pub fn create_match_arm_for_get_variant_as_string( + pub fn create_match_arm_for_table_and_column_name( &self, enum_name: &Ident, + db_table_name: &str, ) -> Vec { self.fields .iter() .map(|f| { let field_name = &f.name; - let field_name_as_string = f.name.to_string(); + let full_name = format!("{}.{}", db_table_name, f.name); + let full_name_lit = LitStr::new(&full_name, Span::call_site()); quote! { - #enum_name::#field_name => #field_name_as_string.to_string() + #enum_name::#field_name => #full_name_lit } }) - .collect::>() + .collect() + } + + pub fn create_match_arm_for_column_ref( + &self, + enum_name: &Ident, + db_table_name: &str, + ) -> Vec { + self.fields + .iter() + .map(|f| { + let field_name = &f.name; + let field_name_as_str = f.name.to_string(); + + quote! { + #enum_name::#field_name => canyon_sql::query::ColumnRef { + table: Some(std::borrow::Cow::Borrowed(#db_table_name)), + column: std::borrow::Cow::from(#field_name_as_str), + alias: None + } + } + }) + .collect() } /// Generates an implementation of the match pattern to find whatever variant - /// is being requested when the method `.value()` it's invoked over some - /// instance that implements the `canyon_sql_root::crud::bounds::FieldValueIdentifier` trait - pub fn create_match_arm_for_relate_fields_with_values( + /// is being requested when the method `.field_name_as_str(self)` it's invoked over some + /// instance that implements the `canyon_sql_root::crud::bounds::FieldIdentifier` trait + pub fn create_match_arm_for_get_variant_as_string( &self, enum_name: &Ident, ) -> Vec { @@ -103,7 +125,7 @@ impl CanyonEntity { let field_name_as_string = f.name.to_string(); quote! { - #enum_name::#field_name(v) => (#field_name_as_string, v) + #enum_name::#field_name => #field_name_as_string.to_string() } }) .collect::>() diff --git a/canyon_entities/src/field_annotation.rs b/canyon_entities/src/field_annotation.rs index 8c01615d..1ada2840 100644 --- a/canyon_entities/src/field_annotation.rs +++ b/canyon_entities/src/field_annotation.rs @@ -1,9 +1,9 @@ use proc_macro2::Ident; -use std::{collections::HashMap, convert::TryFrom}; -use syn::{punctuated::Punctuated, Attribute, MetaNameValue, Token}; +use std::convert::TryFrom; +use syn::{Attribute, Expr, Lit, MetaNameValue, Token, punctuated::Punctuated}; /// The available annotations for a field that belongs to any struct -/// annotaded with `#[canyon_entity]` +/// annotated with `#[canyon_entity]`. #[derive(Debug, Clone)] pub enum EntityFieldAnnotation { PrimaryKey(bool), @@ -11,8 +11,8 @@ pub enum EntityFieldAnnotation { } impl EntityFieldAnnotation { - /// Returns the data of the [`EntityFieldAnnotation`] in a understandable format for - /// operations that requires character matching + /// Returns the data of the [`EntityFieldAnnotation`] in an understandable format for + /// operations that require character matching. pub fn get_as_string(&self) -> String { match self { Self::PrimaryKey(autoincremental) => { @@ -24,107 +24,66 @@ impl EntityFieldAnnotation { } } - /// Retrieves the user defined data in the #[primary_key] attribute - fn primary_key_parser( + fn parse_primary_key( ident: &Ident, - attr_args: &Result, syn::Error>, + args: syn::Result>, ) -> syn::Result { - match attr_args { - Ok(name_value) => { - let mut data: HashMap = HashMap::new(); - for nv in name_value { - // The identifier - let attr_value_ident = nv.path.get_ident().unwrap().to_string(); - // The value after the Token[=] - let attr_value = match &nv.lit { - // Error if the token is not a boolean literal - syn::Lit::Bool(v) => v.value(), - _ => { - return Err(syn::Error::new_spanned( - nv.path.clone(), - format!( - "Only bool literals are supported for the `{}` attribute", - &attr_value_ident - ), - )) - } - }; - data.insert(attr_value_ident, attr_value); - } + let Ok(args) = args else { + return Ok(Self::PrimaryKey(true)); + }; + + let mut autoincremental = None; - Ok(EntityFieldAnnotation::PrimaryKey( - match data.get("autoincremental") { - Some(aut) => aut.to_owned(), - None => { - // TODO En vez de error, false para default - return Err(syn::Error::new_spanned( - ident, - "Missed `autoincremental` argument on the Primary Key annotation" - .to_string(), - )); - } - }, - )) + for arg in &args { + match arg_key(arg)?.as_str() { + "autoincremental" => { + autoincremental = Some(parse_bool_value(arg)?); + } + unknown => return Err(unknown_argument(arg, unknown)), } - Err(_) => Ok(EntityFieldAnnotation::PrimaryKey(true)), } + + autoincremental.map(Self::PrimaryKey).ok_or_else(|| { + syn::Error::new_spanned( + ident, + "Missing `autoincremental` argument on the Primary Key annotation", + ) + }) } - fn foreign_key_parser( + fn parse_foreign_key( ident: &Ident, - attr_args: &Result, syn::Error>, + args: syn::Result>, ) -> syn::Result { - match attr_args { - Ok(name_value) => { - let mut data: HashMap = HashMap::new(); - - for nv in name_value { - // The identifier - let attr_value_ident = nv.path.get_ident().unwrap().to_string(); - // The value after the Token[=] - let attr_value = match &nv.lit { - // Error if the token is not a string literal - // TODO Implement the option (or change it to) to use a Rust Ident instead a Str Lit - syn::Lit::Str(v) => v.value(), - _ => { - return Err( - syn::Error::new_spanned( - nv.path.clone(), - format!("Only string literals are supported for the `{attr_value_ident}` attribute") - ) - ) - } - }; - data.insert(attr_value_ident, attr_value); - } + let args = args.map_err(|error| { + syn::Error::new_spanned(ident, format!("Error generating the Foreign Key: {error}")) + })?; - Ok(EntityFieldAnnotation::ForeignKey( - match data.get("table") { - Some(table) => table.to_owned(), - None => { - return Err(syn::Error::new_spanned( - ident, - "Missed `table` argument on the Foreign Key annotation".to_string(), - )) - } - }, - match data.get("column") { - Some(table) => table.to_owned(), - None => { - return Err(syn::Error::new_spanned( - ident, - "Missed `column` argument on the Foreign Key annotation" - .to_string(), - )) - } - }, - )) + let mut table = None; + let mut column = None; + + for arg in &args { + match arg_key(arg)?.as_str() { + "table" => table = Some(parse_string_value(arg)?), + "column" => column = Some(parse_string_value(arg)?), + unknown => return Err(unknown_argument(arg, unknown)), } - Err(_) => Err(syn::Error::new_spanned( - ident, - "Error generating the Foreign Key".to_string(), - )), } + + Ok(Self::ForeignKey( + table.ok_or_else(|| { + syn::Error::new_spanned( + ident, + "Missing `table` argument on the Foreign Key annotation", + ) + })?, + column.ok_or_else(|| { + syn::Error::new_spanned( + ident, + "Missing `column` argument on the Foreign Key annotation", + ) + })?, + )) } } @@ -132,19 +91,309 @@ impl TryFrom<&&Attribute> for EntityFieldAnnotation { type Error = syn::Error; fn try_from(attribute: &&Attribute) -> Result { - let ident = attribute.path.segments[0].ident.clone(); - let name_values: Result, syn::Error> = - attribute.parse_args_with(Punctuated::parse_terminated); - - Ok(match ident.to_string().as_str() { - "primary_key" => EntityFieldAnnotation::primary_key_parser(&ident, &name_values)?, - "foreign_key" => EntityFieldAnnotation::foreign_key_parser(&ident, &name_values)?, - _ => { - return Err(syn::Error::new_spanned( - ident.clone(), - format!("Unknown attribute `{}`", &ident), - )) - } + let ident = attribute + .path() + .get_ident() + .ok_or_else(|| syn::Error::new_spanned(attribute.path(), "Expected attribute ident"))?; + + let args = + attribute.parse_args_with(Punctuated::::parse_terminated); + + match ident.to_string().as_str() { + "primary_key" => Self::parse_primary_key(ident, args), + "foreign_key" => Self::parse_foreign_key(ident, args), + _ => Err(syn::Error::new_spanned( + ident, + format!("Unknown attribute `{ident}`"), + )), + } + } +} + +fn arg_key(arg: &MetaNameValue) -> syn::Result { + arg.path + .get_ident() + .map(ToString::to_string) + .ok_or_else(|| syn::Error::new_spanned(&arg.path, "Expected argument ident")) +} + +fn parse_string_value(arg: &MetaNameValue) -> syn::Result { + match &arg.value { + Expr::Lit(expr_lit) => match &expr_lit.lit { + Lit::Str(lit) => Ok(lit.value()), + _ => Err(syn::Error::new_spanned( + &arg.value, + "Expected string literal", + )), + }, + _ => Err(syn::Error::new_spanned( + &arg.value, + "Expected literal expression", + )), + } +} + +fn parse_bool_value(arg: &MetaNameValue) -> syn::Result { + parse_string_value(arg).and_then(|value| { + value.parse::().map_err(|_| { + syn::Error::new_spanned( + &arg.value, + format!("Expected boolean string literal, found `{value}`"), + ) }) + }) +} + +fn unknown_argument(arg: &MetaNameValue, ident: &str) -> syn::Error { + syn::Error::new_spanned(&arg.path, format!("Unknown annotation argument `{ident}`")) +} + +#[cfg(test)] +mod tests { + use super::*; + use syn::{Attribute, Field, parse_quote}; + + fn annotation_from(attribute: &Attribute) -> syn::Result { + EntityFieldAnnotation::try_from(&attribute) + } + + fn field_attribute(field: &Field) -> &Attribute { + field + .attrs + .first() + .expect("test field must have one attribute") + } + + #[test] + fn parses_primary_key_without_arguments_as_autoincremental() { + let field: Field = parse_quote! { + #[primary_key] + id: i32 + }; + + let annotation = annotation_from(field_attribute(&field)).unwrap(); + + assert!(matches!( + annotation, + EntityFieldAnnotation::PrimaryKey(true) + )); + } + + #[test] + fn parses_primary_key_with_autoincremental_enabled() { + let field: Field = parse_quote! { + #[primary_key(autoincremental = "true")] + id: i32 + }; + + let annotation = annotation_from(field_attribute(&field)).unwrap(); + + assert!(matches!( + annotation, + EntityFieldAnnotation::PrimaryKey(true) + )); + } + + #[test] + fn parses_primary_key_with_autoincremental_disabled() { + let field: Field = parse_quote! { + #[primary_key(autoincremental = "false")] + id: i32 + }; + + let annotation = annotation_from(field_attribute(&field)).unwrap(); + + assert!(matches!( + annotation, + EntityFieldAnnotation::PrimaryKey(false) + )); + } + + #[test] + fn rejects_primary_key_with_unknown_argument() { + let field: Field = parse_quote! { + #[primary_key(foo = "true")] + id: i32 + }; + + let error = annotation_from(field_attribute(&field)).unwrap_err(); + + assert!( + error + .to_string() + .contains("Unknown annotation argument `foo`") + ); + } + + #[test] + fn rejects_primary_key_with_non_boolean_value() { + let field: Field = parse_quote! { + #[primary_key(autoincremental = "yes")] + id: i32 + }; + + let error = annotation_from(field_attribute(&field)).unwrap_err(); + + assert!( + error + .to_string() + .contains("Expected boolean string literal, found `yes`") + ); + } + + #[test] + fn rejects_primary_key_with_non_string_literal_value() { + let field: Field = parse_quote! { + #[primary_key(autoincremental = true)] + id: i32 + }; + + let error = annotation_from(field_attribute(&field)).unwrap_err(); + + assert!(error.to_string().contains("Expected string literal")); + } + + #[test] + fn parses_foreign_key() { + let field: Field = parse_quote! { + #[foreign_key(table = "users", column = "id")] + user_id: i32 + }; + + let annotation = annotation_from(field_attribute(&field)).unwrap(); + + match annotation { + EntityFieldAnnotation::ForeignKey(table, column) => { + assert_eq!(table, "users"); + assert_eq!(column, "id"); + } + EntityFieldAnnotation::PrimaryKey(_) => panic!("expected foreign key annotation"), + } + } + + #[test] + fn parses_foreign_key_arguments_in_any_order() { + let field: Field = parse_quote! { + #[foreign_key(column = "id", table = "users")] + user_id: i32 + }; + + let annotation = annotation_from(field_attribute(&field)).unwrap(); + + match annotation { + EntityFieldAnnotation::ForeignKey(table, column) => { + assert_eq!(table, "users"); + assert_eq!(column, "id"); + } + EntityFieldAnnotation::PrimaryKey(_) => panic!("expected foreign key annotation"), + } + } + + #[test] + fn rejects_foreign_key_without_arguments() { + let field: Field = parse_quote! { + #[foreign_key] + user_id: i32 + }; + + let error = annotation_from(field_attribute(&field)).unwrap_err(); + + assert!( + error + .to_string() + .contains("Error generating the Foreign Key") + ); + } + + #[test] + fn rejects_foreign_key_with_missing_table_argument() { + let field: Field = parse_quote! { + #[foreign_key(column = "id")] + user_id: i32 + }; + + let error = annotation_from(field_attribute(&field)).unwrap_err(); + + assert!( + error + .to_string() + .contains("Missing `table` argument on the Foreign Key annotation") + ); + } + + #[test] + fn rejects_foreign_key_with_missing_column_argument() { + let field: Field = parse_quote! { + #[foreign_key(table = "users")] + user_id: i32 + }; + + let error = annotation_from(field_attribute(&field)).unwrap_err(); + + assert!( + error + .to_string() + .contains("Missing `column` argument on the Foreign Key annotation") + ); + } + + #[test] + fn rejects_foreign_key_with_unknown_argument() { + let field: Field = parse_quote! { + #[foreign_key(table = "users", column = "id", cascade = "true")] + user_id: i32 + }; + + let error = annotation_from(field_attribute(&field)).unwrap_err(); + + assert!( + error + .to_string() + .contains("Unknown annotation argument `cascade`") + ); + } + + #[test] + fn rejects_foreign_key_with_non_string_table_value() { + let field: Field = parse_quote! { + #[foreign_key(table = users, column = "id")] + user_id: i32 + }; + + let error = annotation_from(field_attribute(&field)).unwrap_err(); + + assert!(error.to_string().contains("Expected literal expression")); + } + + #[test] + fn rejects_unknown_attribute() { + let field: Field = parse_quote! { + #[indexed] + id: i32 + }; + + let error = annotation_from(field_attribute(&field)).unwrap_err(); + + assert!(error.to_string().contains("Unknown attribute `indexed`")); + } + + #[test] + fn formats_primary_key_annotation_as_string() { + let annotation = EntityFieldAnnotation::PrimaryKey(true); + + assert_eq!( + annotation.get_as_string(), + "Annotation: PrimaryKey, Autoincremental: true" + ); + } + + #[test] + fn formats_foreign_key_annotation_as_string() { + let annotation = EntityFieldAnnotation::ForeignKey("users".into(), "id".into()); + + assert_eq!( + annotation.get_as_string(), + "Annotation: ForeignKey, Table: users, Column: id" + ); } } diff --git a/canyon_entities/src/helpers.rs b/canyon_entities/src/helpers.rs new file mode 100644 index 00000000..b1eb2d7d --- /dev/null +++ b/canyon_entities/src/helpers.rs @@ -0,0 +1,88 @@ +use proc_macro2::{Ident, Span}; + +/// Autogenerates a default table name for an entity given their struct name +/// TODO: This is duplicated from the macro's crate. We should be able to join both crates in +/// one later, but now, for developing purposes, we need to maintain here for a while this here +pub fn default_database_table_name_from_entity_name(ty: &str) -> String { + let mut table_name: String = String::new(); + + let mut index = 0; + for char in ty.chars() { + if index < 1 { + table_name.push(char.to_ascii_lowercase()); + index += 1; + } else { + match char { + n if n.is_ascii_uppercase() => { + table_name.push('_'); + table_name.push(n.to_ascii_lowercase()); + } + _ => table_name.push(char), + } + } + } + + table_name +} + +/// Parses the content of a &str to get the related identifier of a type +pub fn database_table_name_to_struct_ident(name: &str) -> Ident { + let mut struct_name: String = String::new(); + + let mut first_iteration = true; + let mut previous_was_underscore = false; + + for char in name.chars() { + if first_iteration { + struct_name.push(char.to_ascii_uppercase()); + first_iteration = false; + } else { + match char { + '_' => { + previous_was_underscore = true; + } + char if char.is_ascii_lowercase() => { + if previous_was_underscore { + struct_name.push(char.to_ascii_lowercase()) + } else { + struct_name.push(char) + } + } + _ => panic!("Detected wrong format or broken convention for database table names"), + } + } + } + + Ident::new(&struct_name, Span::call_site()) +} + +#[cfg(test)] +mod default_table_name_from_entity_name_tests { + use crate::helpers::default_database_table_name_from_entity_name; + + #[test] + #[cfg(not(target_env = "msvc"))] + fn test_entity_database_name_defaulter() { + assert_eq!( + default_database_table_name_from_entity_name("League"), + "league".to_owned() + ); + assert_eq!( + default_database_table_name_from_entity_name("MajorLeague"), + "major_league".to_owned() + ); + assert_eq!( + default_database_table_name_from_entity_name("MajorLeagueTournament"), + "major_league_tournament".to_owned() + ); + + assert_ne!( + default_database_table_name_from_entity_name("MajorLeague"), + "majorleague".to_owned() + ); + assert_ne!( + default_database_table_name_from_entity_name("MajorLeague"), + "MajorLeague".to_owned() + ); + } +} diff --git a/canyon_entities/src/lib.rs b/canyon_entities/src/lib.rs index 8b3abd6c..9aebeab0 100644 --- a/canyon_entities/src/lib.rs +++ b/canyon_entities/src/lib.rs @@ -4,6 +4,7 @@ use std::sync::Mutex; pub mod entity; pub mod entity_fields; pub mod field_annotation; +pub mod helpers; pub mod manager_builder; pub mod register_types; diff --git a/canyon_entities/src/manager_builder.rs b/canyon_entities/src/manager_builder.rs index d717909f..60999297 100644 --- a/canyon_entities/src/manager_builder.rs +++ b/canyon_entities/src/manager_builder.rs @@ -1,9 +1,9 @@ +use super::entity::CanyonEntity; +use crate::helpers; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use syn::{Attribute, Generics, Visibility}; -use super::entity::CanyonEntity; - /// Builds the TokenStream that contains the user defined struct pub fn generate_user_struct(canyon_entity: &CanyonEntity) -> TokenStream { let fields = &canyon_entity.get_attrs_as_token_stream(); @@ -21,31 +21,98 @@ pub fn generate_user_struct(canyon_entity: &CanyonEntity) -> TokenStream { } } +pub fn generated_enum_type_for_struct_data(canyon_entity: &CanyonEntity) -> TokenStream { + let struct_name = canyon_entity.struct_name.to_string(); + let enum_name = Ident::new(&(String::from(&struct_name) + "Table"), Span::call_site()); + let db_target_table_name = helpers::default_database_table_name_from_entity_name(&struct_name); // TODO: same as the other to-do, we need some way of know what's the db name if it's changed in the canyon_entity macro + + let generics = &canyon_entity.generics; + let visibility = &canyon_entity.vis; + + quote! { + /// Auto-generated enum to represent compile-time metadata + /// about a Canyon entity type. + /// + /// The enum is named by appending `Table` to the struct name and contains + /// variants for retrieving metadata associated with the entity. Currently, + /// it includes: + /// + /// - `name`: The struct's identifier as a string. + /// - `DbName`: The name of the database table derived from the struct's name, + /// but adapted to the `snake_case` convention, which is the standard adopted + /// by Canyon these early days to transform type Idents into table names + /// + /// This enum implements the `EntityTable` trait, providing the `table_name` method, + /// which is useful in code that needs to retrieve such metadata dynamically while + /// keeping strong typing and avoiding magic strings. + /// + /// # Example + /// ``` + /// pub struct League { + /// id: i32, + /// name: String, + /// } + /// + /// // This is the auto-generated by Canyon with the `Fields` macro + /// pub enum LeagueTable { + /// Name, + /// DbName + /// } + /// + /// assert_eq!(LeagueTable::Name.to_string(), "League"); + /// assert_eq!(LeagueTable::DbName.to_string(), "league"); + /// ``` + #[derive(Clone)] + #visibility enum #enum_name #generics { + Name, + DbName + } + + impl #generics std::fmt::Display for #enum_name #generics { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.table_name()) + } + } + + impl canyon_sql::query::bounds::EntityTable for #generics #enum_name #generics { + fn table_name<'a>(&self) -> &'a str { + match *self { + #enum_name::Name => #struct_name, + #enum_name::DbName => #db_target_table_name, + } + } + } + } +} + /// Auto-generated enum to represent every field of the related type /// as a variant of an enum that it's named with the concatenation /// of the type identifier + Field /// /// The idea it's to have a representation of the field name as an enum -/// variant, avoiding to let the user passing around Strings and instead, +/// variant, letting the user passing around Strings and instead, /// passing variants of a concrete enumeration type, that when required, /// will be called though macro code to obtain the &str representation /// of the field name. pub fn generate_enum_with_fields(canyon_entity: &CanyonEntity) -> TokenStream { - let ty = &canyon_entity.struct_name; let struct_name = canyon_entity.struct_name.to_string(); + let db_target_table_name = helpers::default_database_table_name_from_entity_name(&struct_name); // TODO: this could be a bug, because the macros may let the user change the target table name, so it won't be accurate here + let enum_name = Ident::new((struct_name + "Field").as_str(), Span::call_site()); let fields_names = &canyon_entity.get_fields_as_enum_variants(); let match_arms_str = &canyon_entity.create_match_arm_for_get_variant_as_str(&enum_name); + let match_arms_column_ref = + &canyon_entity.create_match_arm_for_column_ref(&enum_name, &db_target_table_name); let visibility = &canyon_entity.vis; let generics = &canyon_entity.generics; quote! { - #[derive(Clone, Debug)] #[allow(non_camel_case_types)] #[allow(unused_variables)] #[allow(dead_code)] + #[derive(Clone)] /// Auto-generated enum to represent every field of the related type /// as a variant of an enum that it's named with the concatenation /// of the type identifier + Field @@ -77,7 +144,20 @@ pub fn generate_enum_with_fields(canyon_entity: &CanyonEntity) -> TokenStream { #(#fields_names),* } - impl #generics canyon_sql::crud::bounds::FieldIdentifier<#ty> for #generics #enum_name #generics { + impl #generics std::fmt::Display for #enum_name #generics { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } + } + + impl #generics canyon_sql::query::bounds::FieldIdentifier for #generics #enum_name #generics { + #[inline(always)] + fn as_column_ref(&self) -> canyon_sql::query::ColumnRef<'static> { + match self { + #(#match_arms_column_ref),* + } + } + fn as_str(&self) -> &'static str { match *self { #(#match_arms_str),* @@ -93,20 +173,26 @@ pub fn generate_enum_with_fields(canyon_entity: &CanyonEntity) -> TokenStream { /// The type of the inner value `(Enum::Variant(SomeType))` is the same /// that the field that the variant represents pub fn generate_enum_with_fields_values(canyon_entity: &CanyonEntity) -> TokenStream { - let ty = &canyon_entity.struct_name; let struct_name = canyon_entity.struct_name.to_string(); + let db_target_table_name = helpers::default_database_table_name_from_entity_name(&struct_name); // TODO: this could be a bug, because the macros may let the user change the target table name, so it won't be accurate here let enum_name = Ident::new((struct_name + "FieldValue").as_str(), Span::call_site()); let fields_names = &canyon_entity.get_fields_as_enum_variants_with_value(); - let match_arms = &canyon_entity.create_match_arm_for_relate_fields_with_values(&enum_name); - let visibility = &canyon_entity.vis; + let column_match_arms = __detail::create_column_name_match_arms_for_enum_variants( + canyon_entity, + &enum_name, + &db_target_table_name, + ); + let value_match_arms = + __detail::create_value_match_arms_for_enum_variants(canyon_entity, &enum_name); + quote! { - #[derive(Debug)] #[allow(non_camel_case_types)] #[allow(unused_variables)] #[allow(dead_code)] + #[derive(Clone)] /// Auto-generated enumeration to represent each field of the related /// type as a variant, which can support and contain a value of the field data type. /// @@ -121,20 +207,62 @@ pub fn generate_enum_with_fields_values(canyon_entity: &CanyonEntity) -> TokenSt /// #[allow(non_camel_case_types)] /// pub enum LeagueFieldValue { /// id(i32), - /// name(String) + /// name(String), /// opt(Option) /// } /// ``` - #visibility enum #enum_name<'a> { + #visibility enum #enum_name { #(#fields_names),* } - impl<'a> canyon_sql::crud::bounds::FieldValueIdentifier<'a, #ty> for #enum_name<'a> { - fn value(self) -> (&'static str, &'a dyn QueryParameter<'a>) { + impl canyon_sql::query::bounds::FieldValueIdentifier for #enum_name { + fn column(&self) -> canyon_sql::query::ColumnRef<'static> { + match self { + #(#column_match_arms),* + } + } + fn value(&self) -> &dyn canyon_sql::query::QueryParameter { match self { - #(#match_arms),* + #(#value_match_arms),* } } } } } + +mod __detail { + use crate::entity::CanyonEntity; + use proc_macro2::{Ident, TokenStream}; + use quote::quote; + + pub(crate) fn create_column_name_match_arms_for_enum_variants<'a>( + entity: &'a CanyonEntity, + enum_ident: &'a Ident, + db_table_name: &'a str, + ) -> impl Iterator + 'a { + entity.fields.iter().map(move |f| { + let field_ident = &f.name; + let field_name = field_ident.to_string(); + + quote! { + #enum_ident::#field_ident(_) => canyon_sql::query::ColumnRef { + table: Some(std::borrow::Cow::Borrowed(#db_table_name)), + column: std::borrow::Cow::from(#field_name), + alias: None + } + } + }) + } + + pub(crate) fn create_value_match_arms_for_enum_variants<'a>( + entity: &'a CanyonEntity, + enum_ident: &'a Ident, + ) -> impl Iterator + 'a { + entity.fields.iter().map(move |f| { + let field_ident = &f.name; + quote! { + #enum_ident::#field_ident(v) => v as &dyn canyon_sql::query::QueryParameter + } + }) + } +} diff --git a/canyon_macros/Cargo.toml b/canyon_macros/Cargo.toml index 8b8a2852..58c31ebe 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -13,20 +13,36 @@ description.workspace = true proc-macro = true [dependencies] -syn = { version = "1.0.109", features = ["full"] } # TODO Pending to upgrade and refactor +syn = { version = "2.0.117", features = ["full", "parsing"] } # TODO Pending to upgrade and refactor quote = { workspace = true } proc-macro2 = { workspace = true } -futures = { workspace = true } -tokio = { workspace = true } +regex = { workspace = true } -canyon_connection = { workspace = true } +canyon_core = { workspace = true } canyon_crud = { workspace = true } canyon_entities = { workspace = true } canyon_migrations = { workspace = true, optional = true } [features] -postgres = ["canyon_connection/postgres", "canyon_crud/postgres", "canyon_migrations/postgres"] -mssql = ["canyon_connection/mssql", "canyon_crud/mssql", "canyon_migrations/mssql"] -mysql = ["canyon_connection/mysql", "canyon_crud/mysql", "canyon_migrations/mysql"] +postgres = [ + "canyon_core/postgres", + "canyon_crud/postgres", + "canyon_migrations?/postgres", +] + +mssql = [ + "canyon_core/mssql", + "canyon_crud/mssql", + "canyon_migrations?/mssql", +] + +mysql = [ + "canyon_core/mysql", + "canyon_crud/mysql", + "canyon_migrations?/mysql", +] + +migrations = [ + "dep:canyon_migrations", +] -migrations = ["canyon_migrations"] diff --git a/canyon_macros/src/canyon_entity_macro.rs b/canyon_macros/src/canyon_entity_macro.rs index 483f8f8e..a5a0afe5 100644 --- a/canyon_macros/src/canyon_entity_macro.rs +++ b/canyon_macros/src/canyon_entity_macro.rs @@ -1,75 +1,152 @@ +use crate::utils::helpers; +use canyon_entities::CANYON_REGISTER_ENTITIES; +use canyon_entities::entity::CanyonEntity; +use canyon_entities::entity_fields::EntityField; +use canyon_entities::manager_builder::generate_user_struct; +use canyon_entities::register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}; +use proc_macro::TokenStream as CompilerTokenStream; use proc_macro2::{Span, TokenStream}; -use syn::NestedMeta; - -pub(crate) fn parse_canyon_entity_proc_macro_attr( - attrs: Vec, -) -> ( - Option<&'static str>, - Option<&'static str>, - Option, -) { - let mut table_name: Option<&str> = None; - let mut schema_name: Option<&str> = None; - - let mut parsing_attribute_error: Option = None; - - // The parse of the available options to configure the Canyon Entity - for element in attrs { - match element { - syn::NestedMeta::Meta(m) => { - match m { - syn::Meta::NameValue(nv) => { - let attr_arg_ident = nv - .path - .get_ident() - .expect("Something went wrong parsing the `table_name` argument") - .to_string(); - - if &attr_arg_ident == "table_name" || &attr_arg_ident == "schema" { - match nv.lit { - syn::Lit::Str(ref l) => { - if &attr_arg_ident == "table_name" { - table_name = Some(Box::leak(l.value().into_boxed_str())) - } else { - schema_name = Some(Box::leak(l.value().into_boxed_str())) - } - } - _ => { - parsing_attribute_error = Some(syn::Error::new( - Span::call_site(), - "Only string literals are valid values for the attributes" - ).into_compile_error()); - } - } - } else { - parsing_attribute_error = Some( - syn::Error::new( - Span::call_site(), - format!( - "Argument: `{:?}` are not allowed in the canyon_macro attr", - &attr_arg_ident - ), - ) - .into_compile_error(), - ); - } - } - _ => { - parsing_attribute_error = Some(syn::Error::new( - Span::call_site(), - "Only argument identifiers with a value after an `=` sign are allowed on the `canyon_macros::canyon_entity` proc macro" - ).into_compile_error()); - } - } - } - syn::NestedMeta::Lit(_) => { - parsing_attribute_error = Some(syn::Error::new( - Span::call_site(), - "No literal values allowed on the `canyon_macros::canyon_entity` proc macro" - ).into_compile_error()); - } +use quote::quote; +use syn::punctuated::Punctuated; +use syn::{Expr, Lit, Meta, Token}; + +pub type CanyonEntityAttributeArgs = Punctuated; + +pub fn generate_canyon_entity_tokens( + attrs: CanyonEntityAttributeArgs, + input: CompilerTokenStream, +) -> TokenStream { + let parsed_attrs = parse_canyon_entity_proc_macro_attr(attrs); + + let entity = match syn::parse::(input) { + Ok(entity) => entity, + Err(error) => return error.into_compile_error(), + }; + + let generated_user_struct = generate_user_struct(&entity); + let register_entity = + build_register_entity(&entity, parsed_attrs.table_name, parsed_attrs.schema_name); + + CANYON_REGISTER_ENTITIES + .lock() + .expect("Error acquiring Mutex guard on Canyon Entity macro") + .push(register_entity); + + if let Some(error) = parsed_attrs.error { + quote! { + #error + #generated_user_struct + } + } else { + quote! { + #generated_user_struct + } + } +} + +fn build_register_entity<'a>( + entity: &CanyonEntity, + table_name: Option<&'static str>, + schema_name: Option<&'static str>, +) -> CanyonRegisterEntity<'a> { + let entity_name = leak_string(entity.struct_name.to_string()); + + CanyonRegisterEntity { + entity_name, + entity_db_table_name: table_name.unwrap_or_else(|| { + leak_string(helpers::default_database_table_name_from_entity_name( + entity_name, + )) + }), + user_schema_name: schema_name, + entity_fields: entity + .fields + .iter() + .map(build_register_entity_field) + .collect(), + } +} + +fn build_register_entity_field(field: &EntityField) -> CanyonRegisterEntityField { + CanyonRegisterEntityField { + field_name: field.name.to_string(), + field_type: field.get_field_type_as_string().replace(' ', ""), + annotations: field + .attributes + .iter() + .map(|attr| attr.get_as_string()) + .collect(), + } +} + +#[derive(Default)] +struct ParsedCanyonEntityAttrs { + table_name: Option<&'static str>, + schema_name: Option<&'static str>, + error: Option, +} + +fn parse_canyon_entity_proc_macro_attr( + attrs: CanyonEntityAttributeArgs, +) -> ParsedCanyonEntityAttrs { + let mut parsed = ParsedCanyonEntityAttrs::default(); + + for meta in attrs { + if let Err(error) = parse_canyon_entity_meta(meta, &mut parsed) { + parsed.error = Some(error.into_compile_error()); } } - (table_name, schema_name, parsing_attribute_error) + parsed +} + +fn parse_canyon_entity_meta(meta: Meta, parsed: &mut ParsedCanyonEntityAttrs) -> syn::Result<()> { + let Meta::NameValue(name_value) = meta else { + return Err(syn::Error::new( + Span::call_site(), + "Only argument identifiers with a value after an `=` sign are allowed on the `canyon_macros::canyon_entity` proc macro", + )); + }; + + let ident = name_value.path.get_ident().ok_or_else(|| { + syn::Error::new_spanned( + &name_value.path, + "Only simple identifiers are valid keys for `canyon_entity` attribute arguments", + ) + })?; + + let value = parse_string_literal(&name_value.value)?; + + match ident.to_string().as_str() { + "table_name" => parsed.table_name = Some(leak_string(value)), + "schema" => parsed.schema_name = Some(leak_string(value)), + _ => { + return Err(syn::Error::new_spanned( + ident, + format!("Argument `{ident}` is not allowed in the `canyon_entity` macro attribute"), + )); + } + } + + Ok(()) +} + +fn parse_string_literal(expr: &Expr) -> syn::Result { + match expr { + Expr::Lit(expr_lit) => match &expr_lit.lit { + Lit::Str(value) => Ok(value.value()), + _ => Err(syn::Error::new_spanned( + expr, + "Only string literals are valid values for the attributes", + )), + }, + _ => Err(syn::Error::new_spanned( + expr, + "Only literal expressions are valid values for the attributes", + )), + } +} + +fn leak_string(value: String) -> &'static str { + Box::leak(value.into_boxed_str()) } diff --git a/canyon_macros/src/canyon_macro.rs b/canyon_macros/src/canyon_macro.rs index 95379581..b005e10a 100644 --- a/canyon_macros/src/canyon_macro.rs +++ b/canyon_macros/src/canyon_macro.rs @@ -1,15 +1,18 @@ //! Provides helpers to build the `#[canyon_macros::canyon]` procedural like attribute macro #![cfg(feature = "migrations")] -use canyon_connection::CANYON_TOKIO_RUNTIME; +use canyon_core::connection::get_canyon_tokio_runtime; use canyon_migrations::migrations::handler::Migrations; use canyon_migrations::{CM_QUERIES_TO_EXECUTE, QUERIES_TO_EXECUTE}; use proc_macro2::TokenStream; use quote::quote; pub fn main_with_queries() -> TokenStream { - CANYON_TOKIO_RUNTIME.block_on(async { - canyon_connection::init_connections_cache().await; + // TODO: migrations on main instead of main_with_queries + get_canyon_tokio_runtime().block_on(async { + canyon_core::canyon::Canyon::init() + .await + .expect("Error initializing the connections POOL"); Migrations::migrate().await; }); @@ -26,15 +29,29 @@ pub fn main_with_queries() -> TokenStream { /// Creates a TokenScream that is used to load the data generated at compile-time /// by the `CanyonManaged` macros again on the queries register fn wire_queries_to_execute(canyon_manager_tokens: &mut Vec) { - let cm_data = CM_QUERIES_TO_EXECUTE.lock().unwrap(); - let data = QUERIES_TO_EXECUTE.lock().unwrap(); + let data_to_wire = if let Some(mutex) = QUERIES_TO_EXECUTE.get() { + let queries = mutex.lock().expect("QUERIES_TO_EXECUTE poisoned"); + queries + .iter() + .map(|(key, value)| { + quote! { hm.insert(#key, vec![#(#value),*]); } + }) + .collect::>() + } else { + vec![] + }; - let cm_data_to_wire = cm_data.iter().map(|(key, value)| { - quote! { cm_hm.insert(#key, vec![#(#value),*]); } - }); - let data_to_wire = data.iter().map(|(key, value)| { - quote! { hm.insert(#key, vec![#(#value),*]); } - }); + let cm_data_to_wire = if let Some(mutex) = CM_QUERIES_TO_EXECUTE.get() { + let cm_queries = mutex.lock().expect("CM_QUERIES_TO_EXECUTE poisoned"); + cm_queries + .iter() + .map(|(key, value)| { + quote! { cm_hm.insert(#key, vec![#(#value),*]); } + }) + .collect::>() + } else { + vec![] + }; let tokens = quote! { use std::collections::HashMap; @@ -50,5 +67,5 @@ fn wire_queries_to_execute(canyon_manager_tokens: &mut Vec) { MigrationsProcessor::from_query_register(&hm).await; }; - canyon_manager_tokens.push(tokens) + canyon_manager_tokens.push(tokens); } diff --git a/canyon_macros/src/canyon_mapper_macro.rs b/canyon_macros/src/canyon_mapper_macro.rs new file mode 100644 index 00000000..084de745 --- /dev/null +++ b/canyon_macros/src/canyon_mapper_macro.rs @@ -0,0 +1,406 @@ +#![allow(unused_imports)] + +use proc_macro::TokenStream as CompilerTokenStream; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use regex::Regex; +use syn::{DeriveInput, Type, Visibility}; + +use crate::utils::macro_tokens::MacroTokens; +use canyon_core::connection::database_type::DatabaseType; + +#[cfg(feature = "mssql")] +use quote::ToTokens; + +use crate::MacroResult; + +#[cfg(feature = "mssql")] +const BY_VALUE_CONVERSION_TARGETS: [&str; 1] = ["String"]; + +pub fn canyon_mapper_tokens(input: CompilerTokenStream) -> MacroResult { + let ast = syn::parse::(input)?; + let macro_data = MacroTokens::new(&ast)?; + + Ok(canyon_mapper_impl_tokens(macro_data)) +} + +/// Generates the [`canyon_sql::core::RowMapper`] and +/// [`canyon_sql::query::bounds::EntityRuntimeInfo`] implementations for an +/// entity annotated with `CanyonMapper`. +fn canyon_mapper_impl_tokens(ast: MacroTokens) -> TokenStream { + let ty = ast.ty; + let ty_str = ty.to_string(); + let fields = ast.fields(); + let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + + let mut mapper_methods = TokenStream::new(); + + #[cfg(feature = "postgres")] + { + let field_mappings = create_postgres_fields_mapping(&ty_str, &fields); + + mapper_methods.extend(quote! { + fn deserialize_postgresql( + row: &canyon_sql::db_clients::tokio_postgres::Row, + ) -> Result> { + Ok(Self { + #(#field_mappings),* + }) + } + }); + } + + #[cfg(feature = "mssql")] + { + let field_mappings = create_sqlserver_fields_mapping(&ty_str, &fields); + + mapper_methods.extend(quote! { + fn deserialize_sqlserver( + row: &canyon_sql::db_clients::tiberius::Row, + ) -> Result> { + Ok(Self { + #(#field_mappings),* + }) + } + }); + } + + #[cfg(feature = "mysql")] + { + let field_mappings = create_mysql_fields_mapping(&ty_str, &fields); + + mapper_methods.extend(quote! { + fn deserialize_mysql( + row: &canyon_sql::db_clients::mysql_async::Row, + ) -> Result> { + Ok(Self { + #(#field_mappings),* + }) + } + }); + } + + let entity_runtime_info = __details::entity_runtime_info_macro::tokens(&ast); + + quote! { + use crate::canyon_sql::crud::CrudOperations; + + impl #impl_generics canyon_sql::core::RowMapper + for #ty #ty_generics + #where_clause + { + type Output = #ty; + + #mapper_methods + } + + #entity_runtime_info + } +} + +#[cfg(feature = "postgres")] +fn create_postgres_fields_mapping<'a>( + entity_name: &'a str, + fields: &'a [(Visibility, Ident, Type)], +) -> impl Iterator + use<'a> { + fields.iter().map(move |(_, ident, field_type)| { + let column_name = ident.to_string(); + let error = + create_row_mapper_error_extracting_row(ident, entity_name, DatabaseType::PostgreSql); + + quote! { + #ident: row + .try_get::<&str, #field_type>(#column_name) + .map_err(|_| #error)? + } + }) +} + +#[cfg(feature = "mysql")] +fn create_mysql_fields_mapping<'a>( + entity_name: &'a str, + fields: &'a [(Visibility, Ident, Type)], +) -> impl Iterator + use<'a> { + fields.iter().map(move |(_, ident, _)| { + let column_name = ident.to_string(); + let error = create_row_mapper_error_extracting_row(ident, entity_name, DatabaseType::MySQL); + + quote! { + #ident: row + .get_opt(#column_name) + .ok_or_else(|| #error)?? + } + }) +} + +#[cfg(feature = "mssql")] +fn create_sqlserver_fields_mapping<'a>( + entity_name: &'a str, + fields: &'a [(Visibility, Ident, Type)], +) -> impl Iterator + use<'a> { + fields.iter().map(move |(_, ident, field_type)| { + let column_name = ident.to_string(); + let error = + create_row_mapper_error_extracting_row(ident, entity_name, DatabaseType::SqlServer); + + let target_type = get_field_type_as_string(field_type); + let deserialization = + create_tiberius_field_deserialization(&target_type, &column_name, error); + + quote! { + #ident: #deserialization + } + }) +} + +/// Builds the conversion required by Tiberius for fields whose borrowed SQL +/// representation differs from the entity's owned Rust type. +/// +/// In particular, `String` fields are read as `&str` and then converted into +/// owned values. +#[cfg(feature = "mssql")] +fn create_tiberius_field_deserialization( + target_type: &str, + column_name: &str, + error: String, +) -> TokenStream { + let is_optional = target_type.contains("Option"); + + let require_value = if is_optional { + quote! {} + } else { + quote! { .ok_or_else(|| #error)? } + }; + + let deserializing_type = get_deserializing_type(target_type); + + let convert_to_owned = if BY_VALUE_CONVERSION_TARGETS + .iter() + .any(|candidate| target_type.contains(candidate)) + { + if is_optional { + quote! { .map(ToOwned::to_owned) } + } else { + quote! { .to_owned() } + } + } else { + quote! {} + }; + + quote! { + row.get::<#deserializing_type, &str>(#column_name) + #require_value + #convert_to_owned + } +} + +#[cfg(feature = "mssql")] +fn extract_deserializing_type_name(target_type: &str) -> String { + static TYPE_REGEX: std::sync::OnceLock = std::sync::OnceLock::new(); + + let regex = TYPE_REGEX.get_or_init(|| { + Regex::new(r"(?:Option\s*<\s*)?(?P&?\w+)(?:\s*>)?") + .expect("the Tiberius type extraction regex must be valid") + }); + + regex + .captures(target_type) + .map(|captures| captures["type"].to_owned()) + .unwrap_or_else(|| { + panic!("Unable to determine the SQL Server deserialization type for `{target_type}`") + }) +} + +#[cfg(feature = "mssql")] +fn get_deserializing_type(target_type: &str) -> TokenStream { + let extracted_type = extract_deserializing_type_name(target_type); + + if BY_VALUE_CONVERSION_TARGETS.contains(&extracted_type.as_str()) { + quote! { &str } + } else if extracted_type.contains("Date") || extracted_type.contains("Time") { + let ident = Ident::new(&extracted_type, Span::call_site()); + quote! { canyon_sql::date_time::#ident } + } else { + let ident = Ident::new(&extracted_type, Span::call_site()); + quote! { #ident } + } +} + +#[cfg(feature = "mssql")] +fn get_field_type_as_string(field_type: &Type) -> String { + field_type.to_token_stream().to_string() +} + +fn create_row_mapper_error_extracting_row( + field_ident: &Ident, + entity_name: &str, + database_type: DatabaseType, +) -> String { + std::io::Error::other(format!( + "Failed to retrieve field `{field_ident}` for entity `{entity_name}` using {database_type}" + )) + .to_string() +} + +#[cfg(all(test, feature = "mssql"))] +mod mapper_macro_tests { + use super::{extract_deserializing_type_name, get_deserializing_type}; + + #[test] + fn extracts_the_inner_tiberius_deserialization_type_name() { + assert_eq!("String", extract_deserializing_type_name("String")); + assert_eq!("String", extract_deserializing_type_name("Option")); + assert_eq!("i64", extract_deserializing_type_name("i64")); + assert_eq!("DateTime", extract_deserializing_type_name("DateTime")); + assert_eq!( + "NaiveDateTime", + extract_deserializing_type_name("NaiveDateTime") + ); + } + + #[test] + fn maps_canyon_types_to_tiberius_deserialization_tokens() { + assert_eq!("& str", get_deserializing_type("String").to_string()); + assert_eq!( + "& str", + get_deserializing_type("Option").to_string() + ); + assert_eq!("i64", get_deserializing_type("i64").to_string()); + + assert_eq!( + "canyon_sql :: date_time :: DateTime", + get_deserializing_type("DateTime").to_string() + ); + assert_eq!( + "canyon_sql :: date_time :: NaiveDateTime", + get_deserializing_type("NaiveDateTime").to_string() + ); + } +} + +mod __details { + use super::*; + + pub(crate) mod entity_runtime_info_macro { + use super::*; + use crate::utils::helpers; + + /// Generates runtime field access for CRUD operations. + /// + /// Insertable fields deliberately exclude the primary key. Primary-key + /// metadata and access are exposed separately so callers can handle + /// entities with and without generated keys. + pub(crate) fn tokens(ast: &MacroTokens) -> TokenStream { + let ty = ast.ty; + let ty_str = ty.to_string(); + let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + + let primary_key = ast.get_primary_key_field_annotation(); + let primary_key_ident = primary_key.map(|field| field.ident); + let primary_key_type = primary_key.map(|field| field.ty); + + let insertable_fields = ast.get_fields_idents_skipping_pk().collect::>(); + let field_values = insertable_fields + .iter() + .map(|ident| quote! { &self.#ident }); + + let field_columns = helpers::get_struct_fields_as_column_ref_token_stream(ast, true); + + let primary_key_name = primary_key_name_tokens(ast); + let primary_key_value = primary_key_value_tokens(&primary_key_ident); + let primary_key_type = primary_key_associated_type_tokens(&primary_key_type); + let set_primary_key = set_primary_key_method_tokens(&primary_key_ident); + + quote! { + impl #impl_generics canyon_sql::query::bounds::EntityRuntimeInfo for #ty #ty_generics #where_clause { + type PrimaryKey = #primary_key_type; + + fn field_values( + &self, + ) -> Vec<&dyn canyon_sql::query::QueryParameter> { + vec![#(#field_values),*] + } + + fn field_columns( + ) -> Vec> { + #field_columns.collect() + } + + fn primary_key_name() -> Option<&'static str> { + #primary_key_name + } + + fn primary_key_value( + &self, + ) -> Option<&dyn canyon_sql::query::QueryParameter> { + #primary_key_value + } + + fn set_primary_key( + &mut self, + value: Self::PrimaryKey, + ) -> Result< + (), + Box, + > { + #set_primary_key + } + + fn primary_key_column() -> Option> { + Self::primary_key_name() + .map(|pk| canyon_sql::query::ColumnRef::new(#ty_str, pk)) + } + } + } + } + } + + fn primary_key_name_tokens(ast: &MacroTokens) -> TokenStream { + match ast.get_primary_key_annotation() { + Some(primary_key) => quote! { Some(#primary_key) }, + None => quote! { None }, + } + } + + fn primary_key_value_tokens(primary_key_ident: &Option<&Ident>) -> TokenStream { + match primary_key_ident { + Some(ident) => { + quote! { + Some(&self.#ident as &dyn canyon_sql::query::QueryParameter) + } + } + None => quote! { None }, + } + } + + fn primary_key_associated_type_tokens(primary_key_type: &Option<&Type>) -> TokenStream { + match primary_key_type { + Some(primary_key_type) => quote! { #primary_key_type }, + + // The associated type remains mandatory even for entities without a + // primary key. It is never consumed because `primary_key_value` + // returns `None` and `set_primary_key` returns an error. + None => quote! { i64 }, + } + } + + fn set_primary_key_method_tokens(primary_key_ident: &Option<&Ident>) -> TokenStream { + match primary_key_ident { + Some(ident) => { + quote! { + self.#ident = value.into(); + Ok(()) + } + } + None => { + quote! { + Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "No primary key field is defined for this entity", + ) + .into()) + } + } + } + } +} diff --git a/canyon_macros/src/canyon_tokio_test.rs b/canyon_macros/src/canyon_tokio_test.rs new file mode 100644 index 00000000..13835262 --- /dev/null +++ b/canyon_macros/src/canyon_tokio_test.rs @@ -0,0 +1,36 @@ +use crate::{MacroResult, utils::function_parser::FunctionParser}; +use proc_macro::TokenStream; +use quote::quote; + +pub(crate) fn generate_canyon_tokio_test_tokens(input: TokenStream) -> MacroResult { + let function = syn::parse::(input)?; + + let visibility = function.vis; + let signature = function.sig; + let body = function.block.stmts; + let attributes = function.attrs; + + Ok(quote! { + #[test] + #(#attributes)* + #visibility #signature { + canyon_sql::runtime::get_canyon_tokio_runtime() + .handle() + .block_on(async { + canyon_sql::core::Canyon::init() + .await + .expect("error initializing Canyon's connection pools"); + + async { + { + #(#body)* + } + + Ok::<(), Box>(()) + } + .await + .expect("error executing the `canyon_tokio_test` body"); + }) + } + }) +} diff --git a/canyon_macros/src/foreignkeyable_macro.rs b/canyon_macros/src/foreignkeyable_macro.rs new file mode 100644 index 00000000..c34558c1 --- /dev/null +++ b/canyon_macros/src/foreignkeyable_macro.rs @@ -0,0 +1,55 @@ +use crate::MacroResult; +use crate::utils::helpers::filter_fields; +use proc_macro::TokenStream as CompilerTokenStream; +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +pub fn foreignkeyable_tokens(input: CompilerTokenStream) -> MacroResult { + let ast = syn::parse::(input)?; + Ok(foreignkeyable_impl_tokens(ast)) +} + +fn foreignkeyable_impl_tokens(ast: DeriveInput) -> TokenStream { + let ty = ast.ident; + + // Recovers the identifiers of the structs members + let fields = filter_fields(match ast.data { + syn::Data::Struct(ref s) => &s.fields, + _ => { + return syn::Error::new(ty.span(), "ForeignKeyable only works with Structs") + .to_compile_error(); + } + }); + + let field_idents = fields.iter().map(|(_vis, ident)| { + let i = ident.to_string(); + quote! { + #i => Some(&self.#ident as &dyn canyon_sql::query::QueryParameter) + } + }); + let field_idents_cloned = field_idents.clone(); + + quote! { + /// Implementation of the trait `ForeignKeyable` for the type + /// calling this derive proc macro + impl canyon_sql::query::bounds::ForeignKeyable for #ty { + fn foreign_key_value(&self, column: &str) -> Option<&dyn canyon_sql::query::QueryParameter> { + match column { + #(#field_idents),*, + _ => None + } + } + } + /// Implementation of the trait `ForeignKeyable` for a reference of this type + /// calling this derive proc macro + impl canyon_sql::query::bounds::ForeignKeyable<&Self> for &#ty { + fn foreign_key_value(&self, column: &str) -> Option<&dyn canyon_sql::query::QueryParameter> { + match column { + #(#field_idents_cloned),*, + _ => None + } + } + } + } +} diff --git a/canyon_macros/src/lib.rs b/canyon_macros/src/lib.rs index bd9cff0f..28bd4e8e 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -1,632 +1,226 @@ extern crate proc_macro; +extern crate regex; -mod canyon_entity_macro; #[cfg(feature = "migrations")] use canyon_macro::main_with_queries; - +#[cfg(feature = "migrations")] mod canyon_macro; + +mod canyon_entity_macro; + +mod canyon_mapper_macro; +mod canyon_tokio_test; +mod foreignkeyable_macro; mod query_operations; mod utils; -use canyon_entity_macro::parse_canyon_entity_proc_macro_attr; use proc_macro::TokenStream as CompilerTokenStream; -use proc_macro2::{Ident, TokenStream}; +use proc_macro2::TokenStream; use quote::quote; -use syn::{DeriveInput, Fields, Type, Visibility}; - -use query_operations::{ - delete::{generate_delete_query_tokens, generate_delete_tokens}, - insert::{generate_insert_tokens, generate_multiple_insert_tokens}, - select::{ - generate_count_tokens, generate_find_all_query_tokens, generate_find_all_tokens, - generate_find_all_unchecked_tokens, generate_find_by_foreign_key_tokens, - generate_find_by_pk_tokens, generate_find_by_reverse_foreign_key_tokens, +use syn::{DeriveInput, Error, parse_macro_input}; + +use crate::{ + canyon_entity_macro::{CanyonEntityAttributeArgs, generate_canyon_entity_tokens}, + canyon_mapper_macro::canyon_mapper_tokens, + canyon_tokio_test::generate_canyon_tokio_test_tokens, + foreignkeyable_macro::foreignkeyable_tokens, + query_operations::{ + impl_crud_entity_operations_trait_for_struct, impl_crud_operations_trait_for_struct, + impl_delete_operations_trait_for_struct, impl_insert_operations_trait_for_struct, + impl_read_operations_trait_for_struct, impl_update_operations_trait_for_struct, }, - update::{generate_update_query_tokens, generate_update_tokens}, + utils::{function_parser::FunctionParser, helpers, macro_tokens::MacroTokens}, }; -use utils::{function_parser::FunctionParser, helpers, macro_tokens::MacroTokens}; use canyon_entities::{ entity::CanyonEntity, manager_builder::{ - generate_enum_with_fields, generate_enum_with_fields_values, generate_user_struct, + generate_enum_with_fields, generate_enum_with_fields_values, + generated_enum_type_for_struct_data, }, - register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}, - CANYON_REGISTER_ENTITIES, }; -/// Macro for handling the entry point to the program. -/// -/// Avoids the user to write the tokio proc_attribute and -/// the async modifier to the main fn() +type MacroResult = syn::Result; + +type OperationsGenerator = for<'a> fn(&MacroTokens<'a>, &str) -> MacroResult; + +fn derive_operations( + input: CompilerTokenStream, + generator: OperationsGenerator, +) -> CompilerTokenStream { + derive_operations_tokens(input, generator) + .unwrap_or_else(Error::into_compile_error) + .into() +} + +fn derive_operations_tokens( + input: CompilerTokenStream, + generator: OperationsGenerator, +) -> MacroResult { + let ast = syn::parse::(input)?; + let macro_data = MacroTokens::new(&ast)?; + + let table_schema_data = helpers::table_schema_parser(¯o_data).map_err(|tokens| { + Error::new_spanned(tokens, "failed to parse Canyon table and schema metadata") + })?; + + generator(¯o_data, &table_schema_data.sql()) +} + +/// Canyon's application entry point. /// -/// Also, takes care about wire the necessary code that Canyon's need -/// to run in order to check the provided code and in order to perform -/// the necessary operations for the migrations +/// Initializes Canyon inside its Tokio runtime before executing the user's +/// `main` body and, when enabled, runs the generated migration setup. #[proc_macro_attribute] pub fn main(_meta: CompilerTokenStream, input: CompilerTokenStream) -> CompilerTokenStream { - let func_res = syn::parse::(input); - if func_res.is_err() { - return quote! { fn main() {} }.into(); + let function = parse_macro_input!(input as FunctionParser); + + if function.sig.ident != "main" { + return Error::new( + function.sig.ident.span(), + "the #[canyon::main] attribute can only be applied to `fn main()`", + ) + .into_compile_error() + .into(); } - // TODO check if the `canyon` macro it's attached only to main? - let func = func_res.ok().unwrap(); - let sign = func.sig; - let body = func.block.stmts; + let signature = function.sig; + let visibility = function.vis; + let attributes = function.attrs; + let body = function.block.stmts; #[allow(unused_mut, unused_assignments)] let mut migrations_tokens = quote! {}; + #[cfg(feature = "migrations")] { migrations_tokens = main_with_queries(); } - // The final code wired in main() quote! { - #sign { - canyon_sql::runtime::CANYON_TOKIO_RUNTIME + #(#attributes)* + #visibility #signature { + canyon_sql::runtime::get_canyon_tokio_runtime() .handle() - .block_on( async { - canyon_sql::runtime::init_connections_cache().await; + .block_on(async { + canyon_sql::core::Canyon::init() + .await + .expect( + "error initializing Canyon's connection pools", + ); + #migrations_tokens #(#body)* - } - ) + }) } } .into() } +/// Runs a test function inside Canyon's Tokio runtime. #[proc_macro_attribute] -/// Wraps the [`test`] proc macro in a convenient way to run tests within -/// the tokio's current reactor pub fn canyon_tokio_test( _meta: CompilerTokenStream, input: CompilerTokenStream, ) -> CompilerTokenStream { - let func_res = syn::parse::(input); - if func_res.is_err() { - quote! { fn non_valid_test_fn() {} }.into() - } else { - let func = func_res.ok().unwrap(); - let sign = func.sig; - let body = func.block.stmts; - let attrs = func.attrs; - - quote! { - #[test] - #(#attrs)* - #sign { - canyon_sql::runtime::CANYON_TOKIO_RUNTIME - .handle() - .block_on( async { - canyon_sql::runtime::init_connections_cache().await; - #(#body)* - }); - } - } + generate_canyon_tokio_test_tokens(input) + .unwrap_or_else(Error::into_compile_error) .into() - } } -/// Generates the enums that contains the `TypeFields` and `TypeFieldsValues` -/// that the query-builder requires for construct its queries -#[proc_macro_derive(Fields)] -pub fn querybuilder_fields(input: CompilerTokenStream) -> CompilerTokenStream { - let entity_res = syn::parse::(input); - - if entity_res.is_err() { - return entity_res - .expect_err("Unexpected error parsing the struct") - .into_compile_error() - .into(); - } +/// Registers the table metadata and runtime field information required by +/// Canyon. +#[proc_macro_attribute] +pub fn canyon_entity(meta: CompilerTokenStream, input: CompilerTokenStream) -> CompilerTokenStream { + let attributes = parse_macro_input!( + meta with CanyonEntityAttributeArgs::parse_terminated + ); - // No errors detected on the parsing, so we can safely unwrap the parse result - let entity = entity_res.expect("Unexpected error parsing the struct"); - let _generated_enum_type_for_fields = generate_enum_with_fields(&entity); - let _generated_enum_type_for_fields_values = generate_enum_with_fields_values(&entity); - quote! { - use canyon_sql::crud::bounds::QueryParameter; - #_generated_enum_type_for_fields - #_generated_enum_type_for_fields_values - } - .into() + generate_canyon_entity_tokens(attributes, input).into() } -/// Takes data from the struct annotated with the `canyon_entity` macro to fill the Canyon Register -/// where lives the data that Canyon needs to work. +/// Derives Canyon's complete static CRUD API. /// -/// Also, it's the responsible of generate the tokens for all the `Crud` methods available over -/// your type -#[proc_macro_attribute] -pub fn canyon_entity( - _meta: CompilerTokenStream, - input: CompilerTokenStream, -) -> CompilerTokenStream { - let attrs = syn::parse_macro_input!(_meta as syn::AttributeArgs); - - let (table_name, schema_name, parsing_attribute_error) = - parse_canyon_entity_proc_macro_attr(attrs); - - let entity_res = syn::parse::(input); - - if entity_res.is_err() { - return entity_res - .expect_err("Unexpected error parsing the struct") - .into_compile_error() - .into(); - } - - // No errors detected on the parsing, so we can safely unwrap the parse result - let entity = entity_res.expect("Unexpected error parsing the struct"); - // Generate the bits of code that we should give back to the compiler - let generated_user_struct = generate_user_struct(&entity); - - // The identifier of the entities - let mut new_entity = CanyonRegisterEntity::default(); - let e = Box::leak(entity.struct_name.to_string().into_boxed_str()); - new_entity.entity_name = e; - new_entity.entity_db_table_name = table_name.unwrap_or(Box::leak( - helpers::default_database_table_name_from_entity_name(e).into_boxed_str(), - )); - new_entity.user_schema_name = schema_name; - - // The entity fields - for field in entity.fields.iter() { - let mut new_entity_field = CanyonRegisterEntityField { - field_name: field.name.to_string(), - field_type: field.get_field_type_as_string().replace(' ', ""), - ..Default::default() - }; - - field - .attributes - .iter() - .for_each(|attr| new_entity_field.annotations.push(attr.get_as_string())); - - new_entity.entity_fields.push(new_entity_field); - } - - // Fill the register with the data of the attached struct - CANYON_REGISTER_ENTITIES - .lock() - .expect("Error acquiring Mutex guard on Canyon Entity macro") - .push(new_entity); - - // Assemble everything - let tokens = quote! { - #generated_user_struct - }; - - // Pass the result back to the compiler - if let Some(macro_error) = parsing_attribute_error { - quote! { - #macro_error - #generated_user_struct - } - .into() - } else { - tokens.into() - } +/// This convenience derive generates the read, insert, update and delete +/// implementations for the annotated type. +#[proc_macro_derive(CanyonCrud, attributes(canyon_crud))] +pub fn canyon_crud(input: CompilerTokenStream) -> CompilerTokenStream { + derive_operations(input, impl_crud_operations_trait_for_struct) } -/// Allows the implementors to auto-derive the `CrudOperations` trait, which defines the methods -/// that will perform the database communication and the implementation of the queries for every -/// type, as defined in the `CrudOperations` + `Transaction` traits. -#[proc_macro_derive(CanyonCrud)] -pub fn crud_operations(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - // Construct a representation of Rust code as a syntax tree - // that we can manipulate - - // Calls the helper struct to build the tokens that generates the final CRUD methods - let ast: DeriveInput = - syn::parse(input).expect("Error parsing `Canyon Entity for generate the CRUD methods"); - let macro_data = MacroTokens::new(&ast); - - let table_name_res = helpers::table_schema_parser(¯o_data); - - let table_schema_data = if let Err(err) = table_name_res { - return err.into(); - } else { - table_name_res.ok().unwrap() - }; - - // Build the trait implementation - impl_crud_operations_trait_for_struct(¯o_data, table_schema_data) +/// Derives read operations for the annotated type. +/// +/// This includes operations such as `find_all`, `find_by_pk`, `count` and +/// `select_query`. +#[proc_macro_derive(CanyonRead, attributes(canyon_crud))] +pub fn canyon_read(input: CompilerTokenStream) -> CompilerTokenStream { + derive_operations(input, impl_read_operations_trait_for_struct) } -fn impl_crud_operations_trait_for_struct( - macro_data: &MacroTokens<'_>, - table_schema_data: String, -) -> proc_macro::TokenStream { - let ty = macro_data.ty; - - // Builds the find_all() query - let _find_all_unchecked_tokens = - generate_find_all_unchecked_tokens(macro_data, &table_schema_data); - // Builds the find_all_result() query - let _find_all_tokens = generate_find_all_tokens(macro_data, &table_schema_data); - // Builds the find_all_query() query as a QueryBuilder - let _find_all_query_tokens = generate_find_all_query_tokens(macro_data, &table_schema_data); - - // Builds a COUNT(*) query over some table - let _count_tokens = generate_count_tokens(macro_data, &table_schema_data); - - // Builds the find_by_pk() query - let _find_by_pk_tokens = generate_find_by_pk_tokens(macro_data, &table_schema_data); - - // Builds the insert() query - let _insert_tokens = generate_insert_tokens(macro_data, &table_schema_data); - // Builds the insert_multi() query - let _insert_multi_tokens = generate_multiple_insert_tokens(macro_data, &table_schema_data); - - // Builds the update() queries - let _update_tokens = generate_update_tokens(macro_data, &table_schema_data); - // Builds the update() query as a QueryBuilder - let _update_query_tokens = generate_update_query_tokens(macro_data, &table_schema_data); - - // Builds the delete() queries - let _delete_tokens = generate_delete_tokens(macro_data, &table_schema_data); - - // Builds the delete() query as a QueryBuilder - let _delete_query_tokens = generate_delete_query_tokens(macro_data, &table_schema_data); - - // Search by foreign (d) key as Vec, cause Canyon supports multiple fields having FK annotation - let _search_by_fk_tokens: Vec<(TokenStream, TokenStream)> = - generate_find_by_foreign_key_tokens(macro_data); - let fk_method_signatures = _search_by_fk_tokens.iter().map(|(sign, _)| sign); - let fk_method_implementations = _search_by_fk_tokens.iter().map(|(_, m_impl)| m_impl); - - // The tokens for generating the methods that enable Canyon to retrieve the child entities that are of T type - // given a parent entity U: ForeignKeyable, as an associated function for the child type (T) - let _search_by_revese_fk_tokens: Vec<(TokenStream, TokenStream)> = - generate_find_by_reverse_foreign_key_tokens(macro_data, &table_schema_data); - let rev_fk_method_signatures = _search_by_revese_fk_tokens.iter().map(|(sign, _)| sign); - let rev_fk_method_implementations = - _search_by_revese_fk_tokens.iter().map(|(_, m_impl)| m_impl); - - // The autogenerated name for the trait that holds the fk and rev fk searches - let fk_trait_ident = Ident::new( - &format!("{}FkOperations", &ty.to_string()), - proc_macro2::Span::call_site(), - ); +/// Derives insert operations for instances of the annotated type. +#[proc_macro_derive(CanyonInsert, attributes(canyon_crud))] +pub fn canyon_insert(input: CompilerTokenStream) -> CompilerTokenStream { + derive_operations(input, impl_insert_operations_trait_for_struct) +} - let crud_operations_tokens = quote! { - // The find_all_result impl - #_find_all_tokens - - // The find_all impl - #_find_all_unchecked_tokens - - // The find_all_query impl - #_find_all_query_tokens - - // The COUNT(*) impl - #_count_tokens - - // The find_by_pk impl - #_find_by_pk_tokens - - // The insert impl - #_insert_tokens - - // The insert of multiple entities impl - #_insert_multi_tokens - - // The update impl - #_update_tokens - - // The update as a querybuilder impl - #_update_query_tokens - - // The delete impl - #_delete_tokens - - // The delete as querybuilder impl - #_delete_query_tokens - }; - - let tokens = if !_search_by_fk_tokens.is_empty() { - quote! { - #[canyon_sql::macros::async_trait] - impl canyon_sql::crud::CrudOperations<#ty> for #ty { - #crud_operations_tokens - } - - impl canyon_sql::crud::Transaction<#ty> for #ty {} - - /// Hidden trait for generate the foreign key operations available - /// in Canyon without have to define them before hand in CrudOperations - /// because it's just impossible with the actual system (where the methods - /// are generated dynamically based on some properties of the `foreign_key` - /// annotation) - #[canyon_sql::macros::async_trait] - pub trait #fk_trait_ident<#ty> { - #(#fk_method_signatures)* - #(#rev_fk_method_signatures)* - } - #[canyon_sql::macros::async_trait] - impl #fk_trait_ident<#ty> for #ty - where #ty: - std::fmt::Debug + - canyon_sql::crud::CrudOperations<#ty> + - canyon_sql::crud::RowMapper<#ty> - { - #(#fk_method_implementations)* - #(#rev_fk_method_implementations)* - } - } - } else { - quote! { - #[canyon_sql::macros::async_trait] - impl canyon_sql::crud::CrudOperations<#ty> for #ty { - #crud_operations_tokens - } - - impl canyon_sql::crud::Transaction<#ty> for #ty {} - } - }; +/// Derives update operations for instances of the annotated type. +#[proc_macro_derive(CanyonUpdate, attributes(canyon_crud))] +pub fn canyon_update(input: CompilerTokenStream) -> CompilerTokenStream { + derive_operations(input, impl_update_operations_trait_for_struct) +} - tokens.into() +/// Derives delete operations for instances of the annotated type. +#[proc_macro_derive(CanyonDelete, attributes(canyon_crud))] +pub fn canyon_delete(input: CompilerTokenStream) -> CompilerTokenStream { + derive_operations(input, impl_delete_operations_trait_for_struct) } -/// proc-macro for annotate struct fields that holds a foreign key relation. +/// Derives the separate runtime entity CRUD API. /// -/// So basically, if you have some `ForeignKey` attribute, annotate the parent -/// struct (where the ForeignKey table property points) with this macro -/// to make it able to work with compound table relations -#[proc_macro_derive(ForeignKeyable)] -pub fn implement_foreignkeyable_for_type( - input: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - // Gets the data from the AST - let ast: DeriveInput = syn::parse(input).unwrap(); - let ty = ast.ident; - - // Recovers the identifiers of the structs members - let fields = filter_fields(match ast.data { - syn::Data::Struct(ref s) => &s.fields, - _ => { - return syn::Error::new(ty.span(), "ForeignKeyable only works with Structs") - .to_compile_error() - .into() - } - }); - - let field_idents = fields.iter().map(|(_vis, ident)| { - let i = ident.to_string(); - quote! { - #i => Some(&self.#ident as &dyn canyon_sql::crud::bounds::QueryParameter<'_>) - } - }); - let field_idents_cloned = field_idents.clone(); +/// This is intended for repository adapters whose generated operations receive +/// the entity to persist as an argument instead of operating on `self`. +#[proc_macro_derive(CanyonEntityCrud, attributes(canyon_crud))] +pub fn canyon_entity_crud(input: CompilerTokenStream) -> CompilerTokenStream { + derive_operations(input, impl_crud_entity_operations_trait_for_struct) +} - quote! { - /// Implementation of the trait `ForeignKeyable` for the type - /// calling this derive proc macro - impl canyon_sql::crud::bounds::ForeignKeyable for #ty { - fn get_fk_column(&self, column: &str) -> Option<&dyn canyon_sql::crud::bounds::QueryParameter<'_>> { - match column { - #(#field_idents),*, - _ => None - } - } - } - /// Implementation of the trait `ForeignKeyable` for a reference of this type - /// calling this derive proc macro - impl canyon_sql::crud::bounds::ForeignKeyable<&Self> for &#ty { - fn get_fk_column<'a>(&self, column: &'a str) -> Option<&dyn canyon_sql::crud::bounds::QueryParameter<'_>> { - match column { - #(#field_idents_cloned),*, - _ => None - } - } - } - }.into() +/// Derives the metadata required to navigate foreign-key relationships. +#[proc_macro_derive(ForeignKeyable)] +pub fn implement_foreignkeyable_for_type(input: CompilerTokenStream) -> CompilerTokenStream { + foreignkeyable_tokens(input) + .unwrap_or_else(Error::into_compile_error) + .into() } +/// Derives database-row deserialization for the annotated type. #[proc_macro_derive(CanyonMapper)] -pub fn implement_row_mapper_for_type(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - // Gets the data from the AST - let ast: DeriveInput = syn::parse(input).unwrap(); - - // Recovers the identifiers of the structs members - let fields = fields_with_types(match ast.data { - syn::Data::Struct(ref s) => &s.fields, - _ => { - return syn::Error::new(ast.ident.span(), "CanyonMapper only works with Structs") - .to_compile_error() - .into() - } - }); - - // TODO: refactor the code below after the current bugfixes, to conditinally generate - // the required methods and populate the CanyonMapper trait dependencing on the cfg flags - // enabled with a more elegant solution (a fn for feature, for ex) - #[cfg(feature = "postgres")] - // Here it's where the incoming values of the DatabaseResult are wired into a new - // instance, mapping the fields of the type against the columns - let init_field_values = fields.iter().map(|(_vis, ident, _ty)| { - let ident_name = ident.to_string(); - quote! { - #ident: row.try_get(#ident_name) - .expect(format!("Failed to retrieve the {} field", #ident_name).as_ref()) - } - }); - - #[cfg(feature = "mssql")] - let init_field_values_sqlserver = fields.iter().map(|(_vis, ident, ty)| { - let ident_name = ident.to_string(); - - if get_field_type_as_string(ty) == "String" { - quote! { - #ident: row.get::<&str, &str>(#ident_name) - .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) - .to_string() - } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { - #ident: row.get::(#ident_name) - } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { - #ident: row.get::(#ident_name) - } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { - #ident: row.get::(#ident_name) - } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { - #ident: row.get::(#ident_name) - } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { - #ident: row.get::(#ident_name) - } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { - #ident: row.get::<&str, &str>(#ident_name) - .map( |x| x.to_owned() ) - } - } else if get_field_type_as_string(ty) == "NaiveDate" { - quote! { - #ident: row.get::(#ident_name) - .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) - } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { - #ident: row.get::(#ident_name) - } - } else if get_field_type_as_string(ty) == "NaiveTime" { - quote! { - #ident: row.get::(#ident_name) - .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) - } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { - #ident: row.get::(#ident_name) - } - } else if get_field_type_as_string(ty) == "NaiveDateTime" { - quote! { - #ident: row.get::(#ident_name) - .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) - } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { - #ident: row.get::(#ident_name) - } - } else if get_field_type_as_string(ty) == "DateTime" { - quote! { - #ident: row.get::(#ident_name) - .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) - } - } else if get_field_type_as_string(ty).replace(' ', "") == "Option" { - quote! { - #ident: row.get::(#ident_name) - } - } else { - quote! { - #ident: row.get::<#ty, &str>(#ident_name) - .expect(format!("Failed to retrieve the `{}` field", #ident_name).as_ref()) - } - } - }); - - #[cfg(feature = "mysql")] - let init_field_values_mysql = fields.iter().map(|(_vis, ident, _ty)| { - let ident_name = ident.to_string(); - quote! { - #ident: row.get(#ident_name) - .expect(format!("Failed to retrieve the {} field", #ident_name).as_ref()) - } - }); - - // The type of the Struct - let ty = ast.ident; - - let mut impl_methods = quote! {}; // Collect methods conditionally - - #[cfg(feature = "postgres")] - impl_methods.extend(quote! { - fn deserialize_postgresql(row: &canyon_sql::db_clients::tokio_postgres::Row) -> #ty { - Self { - #(#init_field_values),* - } - } - }); - - #[cfg(feature = "mssql")] - impl_methods.extend(quote! { - fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { - Self { - #(#init_field_values_sqlserver),* - } - } - }); - - #[cfg(feature = "mysql")] - impl_methods.extend(quote! { - fn deserialize_mysql(row: &canyon_sql::db_clients::mysql_async::Row) -> #ty { - Self { - #(#init_field_values_mysql),* - } - } - }); - - // Wrap everything in the shared `impl` block - let tokens = quote! { - impl canyon_sql::crud::RowMapper for #ty { - #impl_methods - } - }; - - tokens.into() +pub fn implement_row_mapper_for_type(input: CompilerTokenStream) -> CompilerTokenStream { + canyon_mapper_tokens(input) + .unwrap_or_else(Error::into_compile_error) + .into() } -/// Helper for generate the fields data for the Custom Derives Macros -fn filter_fields(fields: &Fields) -> Vec<(Visibility, Ident)> { - fields - .iter() - .map(|field| (field.vis.clone(), field.ident.as_ref().unwrap().clone())) - .collect::>() +/// Generates the field identifiers used by Canyon's typed query builder. +#[proc_macro_derive(Fields)] +pub fn querybuilder_fields(input: CompilerTokenStream) -> CompilerTokenStream { + querybuilder_fields_tokens(input) + .unwrap_or_else(Error::into_compile_error) + .into() } -fn fields_with_types(fields: &Fields) -> Vec<(Visibility, Ident, Type)> { - fields - .iter() - .map(|field| { - ( - field.vis.clone(), - field.ident.as_ref().unwrap().clone(), - field.ty.clone(), - ) - }) - .collect::>() -} +fn querybuilder_fields_tokens(input: CompilerTokenStream) -> MacroResult { + let entity = syn::parse::(input)?; -#[cfg(feature = "mssql")] -use quote::ToTokens; -#[cfg(feature = "mssql")] -fn get_field_type_as_string(typ: &Type) -> String { - match typ { - Type::Array(type_) => type_.to_token_stream().to_string(), - Type::BareFn(type_) => type_.to_token_stream().to_string(), - Type::Group(type_) => type_.to_token_stream().to_string(), - Type::ImplTrait(type_) => type_.to_token_stream().to_string(), - Type::Infer(type_) => type_.to_token_stream().to_string(), - Type::Macro(type_) => type_.to_token_stream().to_string(), - Type::Never(type_) => type_.to_token_stream().to_string(), - Type::Paren(type_) => type_.to_token_stream().to_string(), - Type::Path(type_) => type_.to_token_stream().to_string(), - Type::Ptr(type_) => type_.to_token_stream().to_string(), - Type::Reference(type_) => type_.to_token_stream().to_string(), - Type::Slice(type_) => type_.to_token_stream().to_string(), - Type::TraitObject(type_) => type_.to_token_stream().to_string(), - Type::Tuple(type_) => type_.to_token_stream().to_string(), - Type::Verbatim(type_) => type_.to_token_stream().to_string(), - _ => "".to_owned(), - } + let struct_metadata = generated_enum_type_for_struct_data(&entity); + let fields = generate_enum_with_fields(&entity); + let field_values = generate_enum_with_fields_values(&entity); + + Ok(quote! { + use canyon_sql::query::bounds::EntityTable; + use canyon_sql::query::bounds::FieldIdentifier; + + #struct_metadata + #fields + #field_values + }) } diff --git a/canyon_macros/src/query_operations/consts.rs b/canyon_macros/src/query_operations/consts.rs new file mode 100644 index 00000000..2a74a1bd --- /dev/null +++ b/canyon_macros/src/query_operations/consts.rs @@ -0,0 +1,77 @@ +#![allow(dead_code)] + +use std::cell::RefCell; + +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{Ident, Type}; + +pub const UNAVAILABLE_CRUD_OP_ON_INSTANCE: &str = "Operation is unavailable. T doesn't contain a #[primary_key]\ + annotation. You must construct the query with the QueryBuilder type\ + (_query method for the CrudOperations implementors"; + +pub(crate) fn generate_no_pk_error() -> TokenStream { + let err_msg = UNAVAILABLE_CRUD_OP_ON_INSTANCE; + quote! { + return Err( + std::io::Error::new( + std::io::ErrorKind::Unsupported, + #err_msg + ).into_inner().unwrap() + ); + } +} + +pub(crate) fn generate_default_db_conn_tokens() -> TokenStream { + quote! { + let default_db_conn = canyon_sql::core::Canyon::instance()? + .get_default_connection()?; + default_db_conn + } +} + +pub(crate) fn generate_default_db_conn_and_type_tokens() -> TokenStream { + quote! { + let default_db_conn = canyon_sql::core::Canyon::instance()? + .get_default_connection()?; + let db_type = default_db_conn.get_database_type()?; + } +} + +thread_local! { + pub static USER_MOCK_TY: RefCell = RefCell::new(Ident::new("User", Span::call_site())); + pub static USER_MOCK_MAPPER_TY: RefCell = RefCell::new(Ident::new("User", Span::call_site())); + pub static VOID_RET_TY: RefCell = RefCell::new({ + let ret_ty: Type = syn::parse_str("()").expect("Failed to parse unit type"); + quote! { #ret_ty } + }); + pub static PK_MOCK_FIELD_VALUE: RefCell = RefCell::new({ + quote! { 1 } + }); +} + +pub const RAW_RET_TY: &str = "Vec < User >"; +pub const RES_RET_TY: &str = + "Result < Vec < User > , Box < (dyn std :: error :: Error + Send + Sync) >>"; +pub const RES_VOID_RET_TY: &str = + "Result < () , Box < (dyn std :: error :: Error + Send + Sync) >>"; +pub const RES_RET_TY_LT: &str = + "Result < Vec < User > , Box < (dyn std :: error :: Error + Send + Sync + 'a) >>"; +pub const RES_VOID_RET_TY_LT: &str = + "Result < () , Box < (dyn std :: error :: Error + Send + Sync + 'a) >>"; +pub const OPT_RET_TY_LT: &str = + "Result < Option < User > , Box < (dyn std :: error :: Error + Send + Sync + 'a) >>"; +pub const I64_RET_TY: &str = "Result < i64 , Box < (dyn std :: error :: Error + Send + Sync) >>"; +pub const I64_RET_TY_LT: &str = + "Result < i64 , Box < (dyn std :: error :: Error + Send + Sync + 'a) >>"; + +pub const MAPS_TO: &str = "into_results :: < User > ()"; +pub const LT_CONSTRAINT: &str = "< 'a "; +pub const INPUT_PARAM: &str = "input : I"; +pub const VALUE_PARAM: &str = "& 'a dyn canyon_sql :: core :: QueryParameter < 'a >"; + +pub const WITH_WHERE_BOUNDS: &str = "where I : canyon_sql :: core :: DbConnection + Send + 'a "; + +pub const FIND_BY_PK_ERR_NO_PK: &str = "You can't use the 'find_by_pk' associated function on a \ + CanyonEntity that does not have a #[primary_key] annotation. \ + If you need to perform an specific search, use the Querybuilder instead."; diff --git a/canyon_macros/src/query_operations/delete.rs b/canyon_macros/src/query_operations/delete.rs deleted file mode 100644 index cabfa37f..00000000 --- a/canyon_macros/src/query_operations/delete.rs +++ /dev/null @@ -1,117 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; - -use crate::utils::macro_tokens::MacroTokens; - -/// Generates the TokenStream for the __delete() CRUD operation -/// returning a result, indicating a possible failure querying the database -pub fn generate_delete_tokens(macro_data: &MacroTokens, table_schema_data: &String) -> TokenStream { - let ty = macro_data.ty; - - let fields = macro_data.get_struct_fields(); - let pk = macro_data.get_primary_key_annotation(); - - if let Some(primary_key) = pk { - let pk_field = fields - .iter() - .find(|f| *f.to_string() == primary_key) - .expect( - "Something really bad happened finding the Ident for the pk field on the delete", - ); - let pk_field_value = - quote! { &self.#pk_field as &dyn canyon_sql::crud::bounds::QueryParameter<'_> }; - - quote! { - /// Deletes from a database entity the row that matches - /// the current instance of a T type, returning a result - /// indicating a possible failure querying the database. - async fn delete(&self) -> Result<(), Box<(dyn std::error::Error + Send + Sync + 'static)>> { - <#ty as canyon_sql::crud::Transaction<#ty>>::query( - format!("DELETE FROM {} WHERE {:?} = $1", #table_schema_data, #primary_key), - &[#pk_field_value], - "" - ).await?; - - Ok(()) - } - - /// Deletes from a database entity the row that matches - /// the current instance of a T type, returning a result - /// indicating a possible failure querying the database with the specified datasource. - async fn delete_datasource<'a>(&self, datasource_name: &'a str) - -> Result<(), Box<(dyn std::error::Error + Send + Sync + 'static)>> - { - <#ty as canyon_sql::crud::Transaction<#ty>>::query( - format!("DELETE FROM {} WHERE {:?} = $1", #table_schema_data, #primary_key), - &[#pk_field_value], - datasource_name - ).await?; - - Ok(()) - } - } - } else { - // Delete operation over an instance isn't available without declaring a primary key. - // The delete querybuilder variant must be used for the case when there's no pk declared - quote! { - async fn delete(&self) - -> Result<(), Box> - { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "You can't use the 'delete' method on a \ - CanyonEntity that does not have a #[primary_key] annotation. \ - If you need to perform an specific search, use the Querybuilder instead." - ).into_inner().unwrap()) - } - - async fn delete_datasource<'a>(&self, datasource_name: &'a str) - -> Result<(), Box> - { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "You can't use the 'delete_datasource' method on a \ - CanyonEntity that does not have a #[primary_key] annotation. \ - If you need to perform an specific search, use the Querybuilder instead." - ).into_inner().unwrap()) - } - } - } -} - -/// Generates the TokenStream for the __delete() CRUD operation as a -/// [`query_elements::query_builder::QueryBuilder<'a, #ty>`] -pub fn generate_delete_query_tokens( - macro_data: &MacroTokens, - table_schema_data: &String, -) -> TokenStream { - let ty = macro_data.ty; - - quote! { - /// Generates a [`canyon_sql::query::DeleteQueryBuilder`] - /// that allows you to customize the query by adding parameters and constrains dynamically. - /// - /// It performs an `DELETE FROM table_name`, where `table_name` it's the name of your - /// entity but converted to the corresponding database convention, - /// unless concrete values are set on the available parameters of the - /// `canyon_macro(table_name = "table_name", schema = "schema")` - fn delete_query<'a>() -> canyon_sql::query::DeleteQueryBuilder<'a, #ty> { - canyon_sql::query::DeleteQueryBuilder::new(#table_schema_data, "") - } - - /// Generates a [`canyon_sql::query::DeleteQueryBuilder`] - /// that allows you to customize the query by adding parameters and constrains dynamically. - /// - /// It performs an `DELETE FROM table_name`, where `table_name` it's the name of your - /// entity but converted to the corresponding database convention, - /// unless concrete values are set on the available parameters of the - /// `canyon_macro(table_name = "table_name", schema = "schema")` - /// - /// The query it's made against the database with the configured datasource - /// described in the configuration file, and selected with the [`&str`] - /// passed as parameter. - fn delete_query_datasource<'a>(datasource_name: &'a str) -> canyon_sql::query::DeleteQueryBuilder<'a, #ty> { - canyon_sql::query::DeleteQueryBuilder::new(#table_schema_data, datasource_name) - } - } -} diff --git a/canyon_macros/src/query_operations/delete/entity.rs b/canyon_macros/src/query_operations/delete/entity.rs new file mode 100644 index 00000000..b1d7becf --- /dev/null +++ b/canyon_macros/src/query_operations/delete/entity.rs @@ -0,0 +1,133 @@ +use proc_macro2::TokenStream; +use quote::quote; + +pub(crate) fn generate_delete_entity_tokens(table_schema_data: &str) -> syn::Result { + let delete_entity_signature = __detail::generate_delete_entity_signature(); + + let delete_entity_with_signature = __detail::generate_delete_entity_with_signature(); + + let delete_entity_body = __detail::generate_delete_entity_body(table_schema_data); + + let delete_entity_with_body = __detail::generate_delete_entity_with_body(table_schema_data); + + Ok(quote! { + #delete_entity_signature { + #delete_entity_body + } + + #delete_entity_with_signature { + #delete_entity_with_body + } + }) +} + +mod __detail { + use proc_macro2::TokenStream; + use quote::quote; + + use crate::query_operations::consts; + + pub(crate) fn generate_delete_entity_body(table_schema_data: &str) -> TokenStream { + let default_db_conn_and_type = consts::generate_default_db_conn_and_type_tokens(); + + let delete_execution = + generate_delete_execution(table_schema_data, quote! { default_db_conn }); + + quote! { + #default_db_conn_and_type + #delete_execution + + Ok(()) + } + } + + pub(crate) fn generate_delete_entity_with_body(table_schema_data: &str) -> TokenStream { + let delete_execution = generate_delete_execution(table_schema_data, quote! { input }); + + quote! { + let db_type = input.get_database_type()?; + + #delete_execution + + Ok(()) + } + } + + fn generate_delete_execution(table_schema_data: &str, connection: TokenStream) -> TokenStream { + quote! { + use canyon_sql::connection::DbConnection; + use canyon_sql::query::querybuilder::{ + DeleteQueryBuilderOps, + QueryBuilderOps, + }; + + let primary_key_name = + + ::primary_key_name() + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Cannot delete an entity without a primary key", + ) + })?; + + let primary_key_value = + + ::primary_key_value(entity) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Cannot delete an entity without a primary-key value", + ) + })?; + + let query = + canyon_sql::query::querybuilder::DeleteQueryBuilder::new( + #table_schema_data, + db_type, + ) + .r#where( + primary_key_name, + canyon_sql::query::operators::Operator::Eq, + ) + .build()?; + + #connection + .execute( + query.as_ref(), + &[primary_key_value], + ) + .await?; + } + } + + pub(crate) fn generate_delete_entity_signature() -> TokenStream { + quote! { + async fn delete_entity<'canyon_lt, 'err_lt, Entity>( + entity: &'canyon_lt Entity, + ) -> Result<(), Box> + where + Entity: canyon_sql::core::RowMapper + + canyon_sql::query::bounds::EntityRuntimeInfo + + Sync + + 'canyon_lt + } + } + + pub(crate) fn generate_delete_entity_with_signature() -> TokenStream { + quote! { + async fn delete_entity_with<'canyon_lt, 'err_lt, Entity, Input>( + entity: &'canyon_lt Entity, + input: Input, + ) -> Result<(), Box> + where + Entity: canyon_sql::core::RowMapper + + canyon_sql::query::bounds::EntityRuntimeInfo + + Sync + + 'canyon_lt, + Input: canyon_sql::connection::DbConnection + + Send + + 'canyon_lt + } + } +} diff --git a/canyon_macros/src/query_operations/delete/method.rs b/canyon_macros/src/query_operations/delete/method.rs new file mode 100644 index 00000000..d719948e --- /dev/null +++ b/canyon_macros/src/query_operations/delete/method.rs @@ -0,0 +1,139 @@ +use crate::utils::macro_tokens::MacroTokens; +use proc_macro2::TokenStream; +use quote::quote; + +pub(crate) fn generate_delete_method_tokens( + macro_data: &MacroTokens, + table_schema_data: &str, +) -> syn::Result { + let mut delete_ops_tokens = TokenStream::new(); + + let pk = macro_data.get_primary_key_field_annotation(); + + if let Some(primary_key) = pk { + let query = __detail::generate_delete_stmt(table_schema_data, primary_key); + let pk_field_value = __detail::get_pk_field_value(primary_key.ident); + + let delete_method_tokens = + __detail::generate_delete_method_tokens(macro_data, &query, &pk_field_value); + let delete_with_method_tokens = + __detail::generate_delete_with_method_tokens(&query, pk_field_value); + + delete_ops_tokens.extend(quote! { + #delete_method_tokens + #delete_with_method_tokens + }); + } else { + __detail::handle_no_primary_key_case(&mut delete_ops_tokens); + } + + Ok(delete_ops_tokens) +} + +mod __detail { + use crate::query_operations::consts; + use crate::{ + query_operations::delete::{__err::generate_no_pk_err, method::__signatures}, + utils::{macro_tokens::MacroTokens, primary_key_attribute::PrimaryKeyAttribute}, + }; + use proc_macro2::{Ident, TokenStream}; + use quote::quote; + + pub(crate) fn generate_delete_stmt( + table_schema_data: &str, + primary_key_attribute: &PrimaryKeyAttribute, + ) -> TokenStream { + let pk_name = &primary_key_attribute.name; + quote! { + canyon_sql::query::querybuilder::DeleteQueryBuilder::new( + #table_schema_data, // TODO: construct a const value + db_type, + ) + .r#where( + #pk_name, + canyon_sql::query::operators::Operator::Eq, + ) + .build()?; + } + } + + pub(crate) fn get_pk_field_value(pk_field: &Ident) -> TokenStream { + quote! { &self.#pk_field as &dyn canyon_sql::query::QueryParameter } + } + + pub(crate) fn generate_delete_method_tokens( + macro_tokens: &MacroTokens, + query: &TokenStream, + pk_field_value: &TokenStream, + ) -> TokenStream { + let ty = macro_tokens.ty; + let (_, ty_generics, _) = macro_tokens.generics.split_for_impl(); + + let delete_signature = __signatures::get_delete_signature(); + let default_db_conn_and_type_tokens = consts::generate_default_db_conn_and_type_tokens(); + + quote! { + #delete_signature { + use canyon_sql::connection::DbConnection; + use canyon_sql::query::querybuilder::{QueryBuilderOps, DeleteQueryBuilderOps}; + + #default_db_conn_and_type_tokens + + let query = #query; + <#ty #ty_generics as canyon_sql::core::Transaction>::execute(query.as_ref(), &[#pk_field_value], default_db_conn).await?; + Ok(()) + } + } + } + + pub(crate) fn generate_delete_with_method_tokens( + query: &TokenStream, + pk_field_value: TokenStream, + ) -> TokenStream { + let delete_with_signature = __signatures::get_delete_with_signature(); + + quote! { + #delete_with_signature { + use canyon_sql::connection::DbConnection; + use canyon_sql::query::querybuilder::{QueryBuilderOps, DeleteQueryBuilderOps}; + + let db_type = input.get_database_type()?; + let query = #query; + input.execute(query.as_ref(), &[#pk_field_value]).await?; + Ok(()) + } + } + } + + // Delete operation over an instance isn't available without declaring a primary key. + // The delete querybuilder variant must be used for the case when there's no pk declared + pub(crate) fn handle_no_primary_key_case(delete_ops_tokens: &mut TokenStream) { + let delete_signature = __signatures::get_delete_signature(); + let delete_with_signature = __signatures::get_delete_with_signature(); + + let no_pk_error = generate_no_pk_err(); + + delete_ops_tokens.extend(quote! { + #delete_signature { #no_pk_error } + #delete_with_signature { #no_pk_error } + }); + } +} + +mod __signatures { + use proc_macro2::TokenStream; + use quote::quote; + + pub(crate) fn get_delete_signature() -> TokenStream { + quote! { + async fn delete(&self) -> Result<(), Box> + } + } + + pub(crate) fn get_delete_with_signature() -> TokenStream { + quote! { + async fn delete_with<'canyon, 'err, I>(&self, input: I) -> Result<(), Box<(dyn std::error::Error + Send + Sync + 'err)>> + where I: canyon_sql::connection::DbConnection + Send + 'canyon + } + } +} diff --git a/canyon_macros/src/query_operations/delete/mod.rs b/canyon_macros/src/query_operations/delete/mod.rs new file mode 100644 index 00000000..b54c74ca --- /dev/null +++ b/canyon_macros/src/query_operations/delete/mod.rs @@ -0,0 +1,52 @@ +mod entity; +mod method; +mod querybuilder; + +use crate::{ + query_operations::delete::{ + entity::generate_delete_entity_tokens as delete_entity_tokens, + method::generate_delete_method_tokens as delete_method_tokens, + querybuilder::generate_delete_querybuilder_tokens, + }, + utils::macro_tokens::MacroTokens, +}; +use proc_macro2::TokenStream; +use quote::quote; + +pub fn generate_delete_method_tokens( + macro_data: &MacroTokens, + table_schema_data: &str, +) -> syn::Result { + let delete_method_ops = delete_method_tokens(macro_data, table_schema_data)?; + let querybuilder_tokens = generate_delete_querybuilder_tokens(table_schema_data); + + Ok(quote! { + #delete_method_ops + #querybuilder_tokens + }) +} + +pub fn generate_delete_entity_tokens(table_schema_data: &str) -> syn::Result { + let entity_tokens = delete_entity_tokens(table_schema_data)?; + + Ok(quote! { + #entity_tokens + }) +} + +mod __err { + use proc_macro2::TokenStream; + use quote::quote; + + pub(crate) fn generate_no_pk_err() -> TokenStream { + quote! { + Err( + std::io::Error::new( + std::io::ErrorKind::Unsupported, + "The type has either zero fields or exactly one that is annotated with #[primary_key].\ + That's makes it ineligibly to be used in the DELETE family of operations." + ).into_inner().unwrap() + ) + } + } +} diff --git a/canyon_macros/src/query_operations/delete/querybuilder.rs b/canyon_macros/src/query_operations/delete/querybuilder.rs new file mode 100644 index 00000000..33983523 --- /dev/null +++ b/canyon_macros/src/query_operations/delete/querybuilder.rs @@ -0,0 +1,38 @@ +use proc_macro2::TokenStream; +use quote::quote; + +/// Generates the TokenStream for the __delete() CRUD operation as a +/// [`query_elements::query_builder::QueryBuilder<'a, #ty>`] +pub(crate) fn generate_delete_querybuilder_tokens(table_schema_data: &str) -> TokenStream { + quote! { + /// Generates a [`canyon_sql::query::querybuilder::DeleteQueryBuilder`] + /// that allows you to customize the query by adding parameters and constrains dynamically. + /// + /// It performs an `DELETE FROM table_name`, where `table_name` it's the name of your + /// entity but converted to the corresponding database convention, + /// unless concrete values are set on the available parameters of the + /// `canyon_macro(table_name = "table_name", schema = "schema")` + fn delete_query<'canyon, 'err>() -> + Result, Box> + where 'canyon: 'err + { + let default_db_type = canyon_sql::core::Canyon::instance()?.get_default_db_type()?; + Ok(canyon_sql::query::querybuilder::DeleteQueryBuilder::new(#table_schema_data, default_db_type)) + } + + /// Generates a [`canyon_sql::query::querybuilder::DeleteQueryBuilder`] + /// that allows you to customize the query by adding parameters and constrains dynamically. + /// + /// It performs an `DELETE FROM table_name`, where `table_name` it's the name of your + /// entity but converted to the corresponding database convention, + /// unless concrete values are set on the available parameters of the + /// `canyon_macro(table_name = "table_name", schema = "schema")` + /// + /// The query it's made against the database with the configured datasource + /// described in the configuration file, selected with the input parameter + fn delete_query_with<'a>(database_type: canyon_sql::connection::DatabaseType) + -> canyon_sql::query::querybuilder::DeleteQueryBuilder<'a> { + canyon_sql::query::querybuilder::DeleteQueryBuilder::new(#table_schema_data, database_type) + } + } +} diff --git a/canyon_macros/src/query_operations/doc_comments.rs b/canyon_macros/src/query_operations/doc_comments.rs new file mode 100644 index 00000000..401e5b5e --- /dev/null +++ b/canyon_macros/src/query_operations/doc_comments.rs @@ -0,0 +1,36 @@ +#![allow(dead_code)] + +pub const SELECT_ALL_BASE_DOC_COMMENT: &str = "Performs a `SELECT * FROM table_name`, where `table_name` it's \ + the name of your entity but converted to the corresponding \ + database convention. P.ej. PostgreSQL prefers table names declared \ + with snake_case identifiers."; + +pub const SELECT_QUERYBUILDER_DOC_COMMENT: &str = "Generates a [`canyon_sql::query::querybuilder::SelectQueryBuilder`] \ + that allows you to customize the query by adding parameters and constrains dynamically. \ + \ + It performs a `SELECT * FROM table_name`, where `table_name` it's the name of your \ + entity but converted to the corresponding database convention, \ + unless concrete values are set on the available parameters of the \ + `canyon_macro => table_name = \"table_name\", schema = \"schema\")`"; + +pub const FIND_BY_PK: &str = "Finds an element on the queried table that matches the \ + value of the field annotated with the `primary_key` attribute, \ + filtering by the column that it's declared as the primary \ + key on the database. \ + \ + *NOTE:* This operation it's only available if the [`CanyonEntity`] contains \ + some field declared as primary key. \ + \ + *returns:* a [`Result, Error>`], wrapping a possible failure \ + querying the database, or, if no errors happens, a success containing \ + and Option with the data found wrapped in the Some(T) variant, \ + or None if the value isn't found on the table."; + +pub const DS_ADVERTISING: &str = "The query it's made against the database with the configured datasource \ + described in the configuration file, and selected with the [`&str`] \ + passed as parameter."; + +pub const DELETE: &str = "Deletes from a database entity the row that matches + the current instance of a T type based on the actual value of the primary + key field, returning a result + indicating a possible failure querying the database."; diff --git a/canyon_macros/src/query_operations/insert.rs b/canyon_macros/src/query_operations/insert.rs deleted file mode 100644 index c6e5e205..00000000 --- a/canyon_macros/src/query_operations/insert.rs +++ /dev/null @@ -1,519 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; - -use crate::utils::macro_tokens::MacroTokens; - -/// Generates the TokenStream for the _insert_result() CRUD operation -pub fn generate_insert_tokens(macro_data: &MacroTokens, table_schema_data: &String) -> TokenStream { - let ty = macro_data.ty; - - // Retrieves the fields of the Struct as a collection of Strings, already parsed - // the condition of remove the primary key if it's present and it's autoincremental - let insert_columns = macro_data.get_column_names_pk_parsed().join(", "); - - // Returns a String with the generic $x placeholder for the query parameters. - let placeholders = macro_data.placeholders_generator(); - - // Retrieves the fields of the Struct - let fields = macro_data.get_struct_fields(); - - let insert_values = fields.iter().map(|ident| { - quote! { &self.#ident } - }); - let insert_values_cloned = insert_values.clone(); - - let primary_key = macro_data.get_primary_key_annotation(); - - let remove_pk_value_from_fn_entry = if let Some(pk_index) = macro_data.get_pk_index() { - quote! { values.remove(#pk_index) } - } else { - quote! {} - }; - - let pk_ident_type = macro_data - ._fields_with_types() - .into_iter() - .find(|(i, _t)| Some(i.to_string()) == primary_key); - let insert_transaction = if let Some(pk_data) = &pk_ident_type { - let pk_ident = &pk_data.0; - let pk_type = &pk_data.1; - - quote! { - #remove_pk_value_from_fn_entry; - - let stmt = format!( - "INSERT INTO {} ({}) VALUES ({}) RETURNING {}", - #table_schema_data, - #insert_columns, - #placeholders, - #primary_key - ); - - let rows = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, - values, - datasource_name - ).await?; - - match rows { - #[cfg(feature = "postgres")] - canyon_sql::crud::CanyonRows::Postgres(mut v) => { - self.#pk_ident = v - .get(0) - .ok_or("Failed getting the returned IDs for an insert")? - .get::<&str, #pk_type>(#primary_key); - Ok(()) - }, - #[cfg(feature = "mssql")] - canyon_sql::crud::CanyonRows::Tiberius(mut v) => { - self.#pk_ident = v - .get(0) - .ok_or("Failed getting the returned IDs for a multi insert")? - .get::<#pk_type, &str>(#primary_key) - .ok_or("SQL Server primary key type failed to be set as value")?; - Ok(()) - }, - #[cfg(feature = "mysql")] - canyon_sql::crud::CanyonRows::MySQL(mut v) => { - self.#pk_ident = v - .get(0) - .ok_or("Failed getting the returned IDs for a multi insert")? - .get::<#pk_type,usize>(0) - .ok_or("MYSQL primary key type failed to be set as value")?; - Ok(()) - }, - _ => panic!("Reached the panic match arm of insert for the DatabaseConnection type") // TODO remove when the generics will be refactored - } - } - } else { - quote! { - let stmt = format!( - "INSERT INTO {} ({}) VALUES ({})", - #table_schema_data, - #insert_columns, - #placeholders, - #primary_key - ); - - <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, - values, - datasource_name - ).await?; - - Ok(()) - } - }; - - quote! { - /// Inserts into a database entity the current data in `self`, generating a new - /// entry (row), returning the `PRIMARY KEY` = `self.` with the specified - /// datasource by it's `datasouce name`, defined in the configuration file. - /// - /// This `insert` operation needs a `&mut` reference. That's because typically, - /// an insert operation represents *new* data stored in the database, so, when - /// inserted, the database will generate a unique new value for the - /// `pk` field, having a unique identifier for every record, and it will - /// automatically assign that returned pk to `self.`. So, after the `insert` - /// operation, you instance will have the correct value that is the *PRIMARY KEY* - /// of the database row that represents. - /// - /// This operation returns a result type, indicating a possible failure querying the database. - /// - /// ## *Examples* - ///``` - /// let mut lec: League = League { - /// id: Default::default(), - /// ext_id: 1, - /// slug: "LEC".to_string(), - /// name: "League Europe Champions".to_string(), - /// region: "EU West".to_string(), - /// image_url: "https://lec.eu".to_string(), - /// }; - /// - /// println!("LEC before: {:?}", &lec); - /// - /// let ins_result = lec.insert_result().await; - /// - /// Now, we can handle the result returned, because it can contains a - /// critical error that may leads your program to panic - /// if let Ok(_) = ins_result { - /// println!("LEC after: {:?}", &lec); - /// } else { - /// eprintln!("{:?}", ins_result.err()) - /// } - /// ``` - /// - async fn insert<'a>(&mut self) - -> Result<(), Box> - { - let datasource_name = ""; - let mut values: Vec<&dyn canyon_sql::crud::bounds::QueryParameter<'_>> = vec![#(#insert_values),*]; - #insert_transaction - } - - /// Inserts into a database entity the current data in `self`, generating a new - /// entry (row), returning the `PRIMARY KEY` = `self.` with the specified - /// datasource by it's `datasouce name`, defined in the configuration file. - /// - /// This `insert` operation needs a `&mut` reference. That's because typically, - /// an insert operation represents *new* data stored in the database, so, when - /// inserted, the database will generate a unique new value for the - /// `pk` field, having a unique identifier for every record, and it will - /// automatically assign that returned pk to `self.`. So, after the `insert` - /// operation, you instance will have the correct value that is the *PRIMARY KEY* - /// of the database row that represents. - /// - /// This operation returns a result type, indicating a possible failure querying the database. - /// - /// ## *Examples* - ///``` - /// let mut lec: League = League { - /// id: Default::default(), - /// ext_id: 1, - /// slug: "LEC".to_string(), - /// name: "League Europe Champions".to_string(), - /// region: "EU West".to_string(), - /// image_url: "https://lec.eu".to_string(), - /// }; - /// - /// println!("LEC before: {:?}", &lec); - /// - /// let ins_result = lec.insert_result().await; - /// - /// Now, we can handle the result returned, because it can contains a - /// critical error that may leads your program to panic - /// if let Ok(_) = ins_result { - /// println!("LEC after: {:?}", &lec); - /// } else { - /// eprintln!("{:?}", ins_result.err()) - /// } - /// ``` - /// - async fn insert_datasource<'a>(&mut self, datasource_name: &'a str) - -> Result<(), Box> - { - let mut values: Vec<&dyn canyon_sql::crud::bounds::QueryParameter<'_>> = vec![#(#insert_values_cloned),*]; - #insert_transaction - } - - } -} - -/// Generates the TokenStream for the __insert() CRUD operation, but being available -/// as a [`QueryBuilder`] object, and instead of being a method over some [`T`] type, -/// as an associated function for [`T`] -/// -/// This, also lets the user to have the option to be able to insert multiple -/// [`T`] objects in only one query -pub fn generate_multiple_insert_tokens( - macro_data: &MacroTokens, - table_schema_data: &String, -) -> TokenStream { - let ty = macro_data.ty; - - // Retrieves the fields of the Struct as continuous String - let column_names = macro_data.get_struct_fields_as_strings(); - - // Retrieves the fields of the Struct - let fields = macro_data.get_struct_fields(); - - let macro_fields = fields.iter().map(|field| quote! { &instance.#field }); - let macro_fields_cloned = macro_fields.clone(); - - let pk = macro_data.get_primary_key_annotation().unwrap_or_default(); - - let pk_ident_type = macro_data - ._fields_with_types() - .into_iter() - .find(|(i, _t)| *i == pk); - - let multi_insert_transaction = if let Some(pk_data) = &pk_ident_type { - let pk_ident = &pk_data.0; - let pk_type = &pk_data.1; - - quote! { - mapped_fields = #column_names - .split(", ") - .map( |column_name| format!("\"{}\"", column_name)) - .collect::>() - .join(", "); - - let mut split = mapped_fields.split(", ") - .collect::>(); - - let pk_value_index = split.iter() - .position(|pk| *pk == format!("\"{}\"", #pk).as_str()) - .expect("Error. No primary key found when should be there"); - split.retain(|pk| *pk != format!("\"{}\"", #pk).as_str()); - mapped_fields = split.join(", ").to_string(); - - let mut fields_placeholders = String::new(); - - let mut elements_counter = 0; - let mut values_counter = 1; - let values_arr_len = final_values.len(); - - for vector in final_values.iter_mut() { - let mut inner_counter = 0; - fields_placeholders.push('('); - vector.remove(pk_value_index); - - for _value in vector.iter() { - if inner_counter < vector.len() - 1 { - fields_placeholders.push_str(&("$".to_owned() + &values_counter.to_string() + ",")); - } else { - fields_placeholders.push_str(&("$".to_owned() + &values_counter.to_string())); - } - - inner_counter += 1; - values_counter += 1; - } - - elements_counter += 1; - - if elements_counter < values_arr_len { - fields_placeholders.push_str("), "); - } else { - fields_placeholders.push(')'); - } - } - - let stmt = format!( - "INSERT INTO {} ({}) VALUES {} RETURNING {}", - #table_schema_data, - mapped_fields, - fields_placeholders, - #pk - ); - - let mut v_arr = Vec::new(); - for arr in final_values.iter() { - for value in arr { - v_arr.push(*value) - } - } - - let multi_insert_result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, - v_arr, - datasource_name - ).await?; - - match multi_insert_result { - #[cfg(feature="postgres")] - canyon_sql::crud::CanyonRows::Postgres(mut v) => { - for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = v - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") - .get::<&str, #pk_type>(#pk); - } - - Ok(()) - }, - #[cfg(feature="mssql")] - canyon_sql::crud::CanyonRows::Tiberius(mut v) => { - for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = v - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") - .get::<#pk_type, &str>(#pk) - .expect("SQL Server primary key type failed to be set as value"); - } - - Ok(()) - }, - #[cfg(feature="mysql")] - canyon_sql::crud::CanyonRows::MySQL(mut v) => { - for (idx, instance) in instances.iter_mut().enumerate() { - instance.#pk_ident = v - .get(idx) - .expect("Failed getting the returned IDs for a multi insert") - .get::<#pk_type,usize>(0) - .expect("MYSQL primary key type failed to be set as value"); - } - Ok(()) - }, - _ => panic!() // TODO remove when the generics will be refactored - } - } - } else { - quote! { - mapped_fields = #column_names - .split(", ") - .map( |column_name| format!("\"{}\"", column_name)) - .collect::>() - .join(", "); - - let mut split = mapped_fields.split(", ") - .collect::>(); - - let mut fields_placeholders = String::new(); - - let mut elements_counter = 0; - let mut values_counter = 1; - let values_arr_len = final_values.len(); - - for vector in final_values.iter_mut() { - let mut inner_counter = 0; - fields_placeholders.push('('); - - for _value in vector.iter() { - if inner_counter < vector.len() - 1 { - fields_placeholders.push_str(&("$".to_owned() + &values_counter.to_string() + ",")); - } else { - fields_placeholders.push_str(&("$".to_owned() + &values_counter.to_string())); - } - - inner_counter += 1; - values_counter += 1; - } - - elements_counter += 1; - - if elements_counter < values_arr_len { - fields_placeholders.push_str("), "); - } else { - fields_placeholders.push(')'); - } - } - - let stmt = format!( - "INSERT INTO {} ({}) VALUES {}", - #table_schema_data, - mapped_fields, - fields_placeholders - ); - - let mut v_arr = Vec::new(); - for arr in final_values.iter() { - for value in arr { - v_arr.push(*value) - } - } - - <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, - v_arr, - datasource_name - ).await?; - - Ok(()) - } - }; - - quote! { - /// Inserts multiple instances of some type `T` into its related table. - /// - /// ``` - /// let mut new_league = League { - /// id: Default::default(), - /// ext_id: 392489032, - /// slug: "League10".to_owned(), - /// name: "League10also".to_owned(), - /// region: "Turkey".to_owned(), - /// image_url: "https://www.sdklafjsd.com".to_owned() - /// }; - /// let mut new_league2 = League { - /// id: Default::default(), - /// ext_id: 392489032, - /// slug: "League11".to_owned(), - /// name: "League11also".to_owned(), - /// region: "LDASKJF".to_owned(), - /// image_url: "https://www.sdklafjsd.com".to_owned() - /// }; - /// let mut new_league3 = League { - /// id: Default::default(), - /// ext_id: 9687392489032, - /// slug: "League3".to_owned(), - /// name: "3League".to_owned(), - /// region: "EU".to_owned(), - /// image_url: "https://www.lag.com".to_owned() - /// }; - /// - /// League::insert_multiple( - /// &mut [&mut new_league, &mut new_league2, &mut new_league3] - /// ).await - /// .ok(); - /// ``` - async fn multi_insert<'a>(instances: &'a mut [&'a mut #ty]) -> ( - Result<(), Box> - ) { - use canyon_sql::crud::bounds::QueryParameter; - let datasource_name = ""; - - let mut final_values: Vec>> = Vec::new(); - for instance in instances.iter() { - let intermediate: &[&dyn QueryParameter<'_>] = &[#(#macro_fields),*]; - - let mut longer_lived: Vec<&dyn QueryParameter<'_>> = Vec::new(); - for value in intermediate.into_iter() { - longer_lived.push(*value) - } - - final_values.push(longer_lived) - } - - let mut mapped_fields: String = String::new(); - - #multi_insert_transaction - } - - /// Inserts multiple instances of some type `T` into its related table with the specified - /// datasource by it's `datasouce name`, defined in the configuration file. - /// - /// ``` - /// let mut new_league = League { - /// id: Default::default(), - /// ext_id: 392489032, - /// slug: "League10".to_owned(), - /// name: "League10also".to_owned(), - /// region: "Turkey".to_owned(), - /// image_url: "https://www.sdklafjsd.com".to_owned() - /// }; - /// let mut new_league2 = League { - /// id: Default::default(), - /// ext_id: 392489032, - /// slug: "League11".to_owned(), - /// name: "League11also".to_owned(), - /// region: "LDASKJF".to_owned(), - /// image_url: "https://www.sdklafjsd.com".to_owned() - /// }; - /// let mut new_league3 = League { - /// id: Default::default(), - /// ext_id: 9687392489032, - /// slug: "League3".to_owned(), - /// name: "3League".to_owned(), - /// region: "EU".to_owned(), - /// image_url: "https://www.lag.com".to_owned() - /// }; - /// - /// League::insert_multiple( - /// &mut [&mut new_league, &mut new_league2, &mut new_league3] - /// ).await - /// .ok(); - /// ``` - async fn multi_insert_datasource<'a>(instances: &'a mut [&'a mut #ty], datasource_name: &'a str) -> ( - Result<(), Box> - ) { - use canyon_sql::crud::bounds::QueryParameter; - - let mut final_values: Vec>> = Vec::new(); - for instance in instances.iter() { - let intermediate: &[&dyn QueryParameter<'_>] = &[#(#macro_fields_cloned),*]; - - let mut longer_lived: Vec<&dyn QueryParameter<'_>> = Vec::new(); - for value in intermediate.into_iter() { - longer_lived.push(*value) - } - - final_values.push(longer_lived) - } - - let mut mapped_fields: String = String::new(); - - #multi_insert_transaction - } - } -} diff --git a/canyon_macros/src/query_operations/insert/entity.rs b/canyon_macros/src/query_operations/insert/entity.rs new file mode 100644 index 00000000..083c2b57 --- /dev/null +++ b/canyon_macros/src/query_operations/insert/entity.rs @@ -0,0 +1,147 @@ +use proc_macro2::TokenStream; +use quote::quote; + +pub fn generate_insert_entity_function_tokens(table_schema_data: &str) -> syn::Result { + let insert_entity_signature = __detail::generate_insert_entity_signature(); + + let insert_entity_with_signature = __detail::generate_insert_entity_with_signature(); + + let insert_entity_body = __detail::generate_insert_entity_body(table_schema_data); + + let insert_entity_with_body = __detail::generate_insert_entity_with_body(table_schema_data); + + Ok(quote! { + #insert_entity_signature { + #insert_entity_body + } + + #insert_entity_with_signature { + #insert_entity_with_body + } + }) +} + +mod __detail { + use proc_macro2::TokenStream; + use quote::quote; + + use crate::query_operations::consts; + + pub(crate) fn generate_insert_entity_body(table_schema_data: &str) -> TokenStream { + let default_db_conn_and_type = consts::generate_default_db_conn_and_type_tokens(); + + let insert_execution = + generate_insert_execution(table_schema_data, quote! { default_db_conn }); + + quote! { + #default_db_conn_and_type + #insert_execution + + Ok(()) + } + } + + pub(crate) fn generate_insert_entity_with_body(table_schema_data: &str) -> TokenStream { + let insert_execution = generate_insert_execution(table_schema_data, quote! { input }); + + quote! { + let db_type = input.get_database_type()?; + + #insert_execution + + Ok(()) + } + } + + fn generate_insert_execution(table_schema_data: &str, connection: TokenStream) -> TokenStream { + let no_fields_to_insert_err = + crate::query_operations::insert::__shared::no_fields_to_insert_err(); + + quote! { + use canyon_sql::connection::DbConnection; + use canyon_sql::query::querybuilder::{ + InsertQueryBuilderOps, + QueryBuilderOps, + }; + + let columns = + + ::field_columns(); + + if columns.is_empty() { + return #no_fields_to_insert_err; + } + + let values = + + ::field_values(entity); + + let statement = + canyon_sql::query::querybuilder::InsertQueryBuilder::new( + #table_schema_data, + db_type, + ) + .with_known_columns(columns); + + if let Some(primary_key_column) = + + ::primary_key_column() + { + let statement = statement + .returning_columns( + ::core::iter::once(primary_key_column), + ) + .build()?; + + let primary_key = #connection + .query_one_for::< + + ::PrimaryKey + >( + statement.sql(), + &values, + ) + .await?; + + + ::set_primary_key(entity, primary_key)?; + } else { + let statement = statement.build()?; + + #connection + .execute(statement.sql(), &values) + .await?; + } + } + } + + pub(crate) fn generate_insert_entity_signature() -> TokenStream { + quote! { + async fn insert_entity<'canyon_lt, 'err_lt, Entity>( + entity: &'canyon_lt mut Entity, + ) -> Result<(), Box> + where + Entity: canyon_sql::core::RowMapper + + canyon_sql::query::bounds::EntityRuntimeInfo + + Sync + + 'canyon_lt + } + } + + pub(crate) fn generate_insert_entity_with_signature() -> TokenStream { + quote! { + async fn insert_entity_with<'canyon_lt, 'err_lt, Entity, Input>( + entity: &'canyon_lt mut Entity, + input: Input, + ) -> Result<(), Box> + where + Entity: canyon_sql::core::RowMapper + + canyon_sql::query::bounds::EntityRuntimeInfo + + Sync + + 'canyon_lt, + Input: canyon_sql::connection::DbConnection + + Send + + 'canyon_lt + } + } +} diff --git a/canyon_macros/src/query_operations/insert/method.rs b/canyon_macros/src/query_operations/insert/method.rs new file mode 100644 index 00000000..642d3653 --- /dev/null +++ b/canyon_macros/src/query_operations/insert/method.rs @@ -0,0 +1,149 @@ +use crate::utils::macro_tokens::MacroTokens; +use proc_macro2::TokenStream; +use quote::quote; + +// Generates the TokenStream for the _insert operation +pub(crate) fn generate_insert_method_tokens( + macro_data: &MacroTokens, + table_schema_data: &str, +) -> syn::Result { + let insert_signature = quote! { + async fn insert<'a>(&mut self) + -> Result<(), Box> + }; + let insert_with_signature = quote! { + async fn insert_with<'a, I>(&mut self, input: I) + -> Result<(), Box> + where + I: canyon_sql::connection::DbConnection + Send + 'a + }; + + let insert_body; + let insert_with_body; + let insert_values; + + if macro_data.retrieve_mapping_target_type().is_some() { + let raised_err = __details::generate_unsupported_operation_err(); + insert_body = raised_err.clone(); // TODO: Can't we do it better? + insert_with_body = raised_err; + insert_values = quote! {}; + } else { + insert_values = __details::generate_insert_fn_values_slice_expr(macro_data); + insert_body = + __details::generate_insert_fn_body_tokens(macro_data, table_schema_data, false); + insert_with_body = + __details::generate_insert_fn_body_tokens(macro_data, table_schema_data, true); + }; + + Ok(quote! { + #insert_signature { + #insert_values + #insert_body + } + + #insert_with_signature { + #insert_values + #insert_with_body + } + }) +} + +mod __details { + use super::*; + use crate::utils::helpers; + + pub(crate) fn generate_insert_fn_body_tokens( + macro_data: &MacroTokens, + table_schema_data: &str, + is_with_method: bool, + ) -> TokenStream { + let pk_ident_and_type = macro_data.get_primary_key_ident_and_type(); + let insert_columns = + helpers::get_struct_fields_as_column_ref_token_stream(macro_data, true); + + let connection_initializer = if is_with_method { + quote! { input } + } else { + quote! { + canyon_sql::core::Canyon::instance()? + .get_default_connection()? + } + }; + + let mut insert_body_tokens = TokenStream::new(); + insert_body_tokens.extend(quote! { + use canyon_sql::connection::DbConnection; + use canyon_sql::query::querybuilder::{InsertQueryBuilderOps, QueryBuilderOps}; + + let db_conn = #connection_initializer; + let insert_columns = #insert_columns; + let stmt = canyon_sql::query::querybuilder::InsertQueryBuilder::new( + #table_schema_data, + db_conn.get_database_type()?, + ) + .with_known_columns(insert_columns) + }); + + if let Some((pk_ident, pk_type)) = pk_ident_and_type.as_ref() { + let primary_key = macro_data + .get_primary_key_annotation() + .expect("Primary key annotation must exist when primary key ident and type exist"); + + let returning_columns = helpers::get_fields_as_iterable_of_column_refs(vec![( + pk_ident.to_string(), + primary_key, + )]); + + insert_body_tokens.extend(quote! { + .returning_columns(#returning_columns) + .build()?; + + self.#pk_ident = db_conn + .query_one_for::<#pk_type>(stmt.sql(), values) + .await?; + + Ok(()) + }); + } else { + insert_body_tokens.extend(quote! { + .build()?; + + let _ = db_conn.execute(stmt.sql(), values).await?; + + Ok(()) + }); + } + + insert_body_tokens + } + + pub(crate) fn generate_insert_fn_values_slice_expr(macro_data: &MacroTokens) -> TokenStream { + // Retrieves the fields of the Struct + let fields = macro_data.get_columns_skipping_pk(); + + let insert_values = fields.map(|field| { + let field = field + .ident + .as_ref() + .expect("Error converting a Field to its ident on the insert"); + quote! { &self.#field } + }); + + quote! { + let values: &[&dyn canyon_sql::query::QueryParameter] = &[#(#insert_values),*]; + } + } + + pub(crate) fn generate_unsupported_operation_err() -> TokenStream { + quote! { + Err( + std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Can't use the 'Insert' family transactions as a method (that receives self as first parameter) \ + if your T type in CrudOperations is NOT the same type that implements RowMapper. \ + Consider to use instead the provided insert_entity or insert_entity_with functions." + ).into_inner().unwrap() + ) + } + } +} diff --git a/canyon_macros/src/query_operations/insert/mod.rs b/canyon_macros/src/query_operations/insert/mod.rs new file mode 100644 index 00000000..f826bacb --- /dev/null +++ b/canyon_macros/src/query_operations/insert/mod.rs @@ -0,0 +1,39 @@ +mod entity; +mod method; + +use crate::{ + query_operations::insert::{ + entity::generate_insert_entity_function_tokens as insert_entity_function_tokens, + method::generate_insert_method_tokens as insert_method_tokens, + }, + utils::macro_tokens::MacroTokens, +}; +use proc_macro2::TokenStream; + +pub fn generate_insert_method_tokens( + macro_tokens: &MacroTokens, + table_schema_data: &str, +) -> syn::Result { + insert_method_tokens(macro_tokens, table_schema_data) +} + +pub fn generate_insert_entity_function_tokens(table_schema_data: &str) -> syn::Result { + insert_entity_function_tokens(table_schema_data) +} + +mod __shared { + use proc_macro2::TokenStream; + use quote::quote; + + pub(crate) fn no_fields_to_insert_err() -> TokenStream { + quote! { + Err( + std::io::Error::new( + std::io::ErrorKind::Unsupported, + "The type has either zero fields or exactly one that is annotated with #[primary_key].\ + That's makes it ineligibly to be used in the INSERT family of operations." + ).into_inner().unwrap() + ) + } + } +} diff --git a/canyon_macros/src/query_operations/mod.rs b/canyon_macros/src/query_operations/mod.rs index dbba723f..137fd4df 100644 --- a/canyon_macros/src/query_operations/mod.rs +++ b/canyon_macros/src/query_operations/mod.rs @@ -1,4 +1,158 @@ +use crate::{ + query_operations::{ + delete::{generate_delete_entity_tokens, generate_delete_method_tokens}, + insert::{generate_insert_entity_function_tokens, generate_insert_method_tokens}, + read::{foreign_key::generate_find_by_fk_ops, generate_read_operations_tokens}, + update::{generate_update_entity_tokens, generate_update_method_tokens}, + }, + utils::{ + helpers::compute_crud_ops_mapping_target_type_with_generics, macro_tokens::MacroTokens, + }, +}; +use proc_macro2::TokenStream; +use quote::quote; + pub mod delete; pub mod insert; -pub mod select; +pub mod read; pub mod update; + +mod consts; +mod doc_comments; + +/// Generates every static CRUD implementation. +/// +/// `CrudOperations` itself is provided by its blanket implementation once the +/// type implements `ReadOperations`, `InsertOperations`, `UpdateOperations` +/// and `DeleteOperations`. +pub fn impl_crud_operations_trait_for_struct( + macro_data: &MacroTokens<'_>, + table_schema_data: &str, +) -> syn::Result { + let read_operations = impl_read_operations_trait_for_struct(macro_data, table_schema_data)?; + let insert_operations = impl_insert_operations_trait_for_struct(macro_data, table_schema_data)?; + let update_operations = impl_update_operations_trait_for_struct(macro_data, table_schema_data)?; + let delete_operations = impl_delete_operations_trait_for_struct(macro_data, table_schema_data)?; + let transaction = impl_transaction_trait_for_struct(macro_data); + + Ok(quote! { + #read_operations + #insert_operations + #update_operations + #delete_operations + #transaction + }) +} + +/// Generates the static read implementation. +/// +/// The mapping target only determines the type returned by read operations. It +/// does not switch the operation to the runtime entity API. +pub fn impl_read_operations_trait_for_struct( + macro_data: &MacroTokens<'_>, + table_schema_data: &str, +) -> syn::Result { + let ty = macro_data.ty; + let (impl_generics, ty_generics, where_clause) = macro_data.generics.split_for_impl(); + let mapper_ty = compute_crud_ops_mapping_target_type_with_generics( + ty, + &ty_generics, + macro_data.retrieve_mapping_target_type().as_ref(), + ); + + let methods = generate_read_operations_tokens(macro_data, table_schema_data)?; + let foreign_key_operations = generate_find_by_fk_ops(macro_data, table_schema_data); + + Ok(quote! { + impl #impl_generics + canyon_sql::crud::ReadOperations<#mapper_ty> for #ty #ty_generics #where_clause { + #methods + } + + #foreign_key_operations + }) +} + +/// Generates the static insert implementation. +pub fn impl_insert_operations_trait_for_struct( + macro_data: &MacroTokens<'_>, + table_schema_data: &str, +) -> syn::Result { + let ty = macro_data.ty; + let (impl_generics, ty_generics, where_clause) = macro_data.generics.split_for_impl(); + + let methods = generate_insert_method_tokens(macro_data, table_schema_data)?; + + Ok(quote! { + impl #impl_generics canyon_sql::crud::InsertOperations for #ty #ty_generics #where_clause { + #methods + } + }) +} + +/// Generates the static update implementation. +pub fn impl_update_operations_trait_for_struct( + macro_data: &MacroTokens<'_>, + table_schema_data: &str, +) -> syn::Result { + let ty = macro_data.ty; + let (impl_generics, ty_generics, where_clause) = macro_data.generics.split_for_impl(); + + let methods = generate_update_method_tokens(macro_data, table_schema_data)?; + + Ok(quote! { + impl #impl_generics canyon_sql::crud::UpdateOperations for #ty #ty_generics #where_clause { + #methods + } + }) +} + +/// Generates the static delete implementation. +pub fn impl_delete_operations_trait_for_struct( + macro_data: &MacroTokens<'_>, + table_schema_data: &str, +) -> syn::Result { + let ty = macro_data.ty; + let (impl_generics, ty_generics, where_clause) = macro_data.generics.split_for_impl(); + + let methods = generate_delete_method_tokens(macro_data, table_schema_data)?; + + Ok(quote! { + impl #impl_generics canyon_sql::crud::DeleteOperations for #ty #ty_generics #where_clause { + #methods + } + }) +} + +/// Generates the runtime entity CRUD implementation. +/// +/// This contract is completely separate from `CrudOperations`: its methods +/// receive the entity to persist instead of operating on `self`. +pub fn impl_crud_entity_operations_trait_for_struct( + macro_data: &MacroTokens<'_>, + table_schema_data: &str, +) -> syn::Result { + let ty = macro_data.ty; + let (impl_generics, ty_generics, where_clause) = macro_data.generics.split_for_impl(); + let insert_operations = generate_insert_entity_function_tokens(table_schema_data)?; + let update_operations = generate_update_entity_tokens(table_schema_data)?; + let delete_operations = generate_delete_entity_tokens(table_schema_data)?; + + Ok(quote! { + impl #impl_generics canyon_sql::crud::EntityCrudOperations for #ty #ty_generics #where_clause { + #insert_operations + #update_operations + #delete_operations + } + }) +} + +fn impl_transaction_trait_for_struct(macro_data: &MacroTokens<'_>) -> TokenStream { + let ty = macro_data.ty; + + let (impl_generics, ty_generics, where_clause) = macro_data.generics.split_for_impl(); + + quote! { + impl #impl_generics canyon_sql::core::Transaction for #ty #ty_generics #where_clause {} + } +} diff --git a/canyon_macros/src/query_operations/read/count.rs b/canyon_macros/src/query_operations/read/count.rs new file mode 100644 index 00000000..68b8a29c --- /dev/null +++ b/canyon_macros/src/query_operations/read/count.rs @@ -0,0 +1,120 @@ +use crate::query_operations::consts; +use proc_macro2::TokenStream; +use quote::quote; +use std::borrow::Cow; + +pub fn generate_count_operations_tokens(table_schema_data: &str) -> TokenStream { + let table_metadata = + canyon_core::query::querybuilder::syntax::table_metadata::TableMetadata::from( + table_schema_data, + ); + let schema_name = table_metadata.schema; + let table_name = table_metadata.name; + let count = create_count_macro(schema_name.clone(), table_name.as_ref()); + let count_with = create_count_with_macro(schema_name, table_name.as_ref()); + + quote! { + #count + #count_with + } +} + +pub fn create_count_macro(schema_name: Option>, table_name: &str) -> TokenStream { + let mssql_arm = get_mssql_arm_tokens_if_enabled(false); + let schema_tokens = get_schema_tokens(schema_name); + let table_name = create_cow_borrowed_table_name(table_name); + let default_db_conn_and_type_tokens = consts::generate_default_db_conn_and_type_tokens(); + + quote! { + async fn count() -> Result> { + use canyon_sql::connection::DbConnection; + use canyon_sql::query::querybuilder::{QueryBuilderOps, SelectQueryBuilderOps}; + + #default_db_conn_and_type_tokens + + let query = canyon_sql::query::querybuilder::SelectQueryBuilder::new_from_parts( + #schema_tokens, + #table_name, + db_type + ).count() + .build()?; + + match db_type { + #mssql_arm + _ => { + default_db_conn.query_one_for::(query.sql(), query.params()).await + } + } + } + } +} + +pub fn create_count_with_macro(schema_name: Option>, table_name: &str) -> TokenStream { + let mssql_arm = get_mssql_arm_tokens_if_enabled(true); + let schema_tokens = get_schema_tokens(schema_name); + let table_name = create_cow_borrowed_table_name(table_name); + + quote! { + async fn count_with<'a, I>(input: I) + -> Result> + where + I: canyon_sql::connection::DbConnection + Send + 'a + { + use canyon_sql::connection::DbConnection; + use canyon_sql::query::querybuilder::{QueryBuilderOps, SelectQueryBuilderOps}; + + let db_type = input.get_database_type()?; + let query = canyon_sql::query::querybuilder::SelectQueryBuilder::new_from_parts( + #schema_tokens, + #table_name, + db_type) + .count() + .build()?; + + match db_type { + #mssql_arm + _ => { + input.query_one_for::(query.sql(), query.params()).await + } + } + } + } +} + +fn get_mssql_arm_tokens_if_enabled(is_with_input: bool) -> TokenStream { + if !cfg!(feature = "mssql") { + return quote! {}; + } + + let base_expr = quote! { + let count_i32: i32 = + }; + + let query_call = if is_with_input { + quote! { + input.query_one_for::(query.sql(), query.params()).await?; + } + } else { + quote! { + default_db_conn.query_one_for::(query.sql(), query.params()).await?; + } + }; + + quote! { + canyon_sql::connection::DatabaseType::SqlServer => { + #base_expr #query_call + Ok(count_i32 as i64) + } + } +} + +fn get_schema_tokens(schema_name: Option>) -> TokenStream { + match schema_name { + Some(schema_name) => quote! { Some(#schema_name) }, + None => quote! { None }, + } +} + +fn create_cow_borrowed_table_name(table_name: &str) -> TokenStream { + quote! { std::borrow::Cow::Borrowed(#table_name) } +} diff --git a/canyon_macros/src/query_operations/read/find_all.rs b/canyon_macros/src/query_operations/read/find_all.rs new file mode 100644 index 00000000..46edeacf --- /dev/null +++ b/canyon_macros/src/query_operations/read/find_all.rs @@ -0,0 +1,67 @@ +use crate::query_operations::consts; +use crate::utils::helpers; +use crate::utils::macro_tokens::MacroTokens; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +pub fn generate_find_all_operations_tokens( + mapper_ty: &Ident, + table_schema_data: &str, + macro_data: &MacroTokens, +) -> TokenStream { + let columns = helpers::get_struct_fields_as_column_ref_token_stream(macro_data, false); + let find_all = create_find_all_macro(mapper_ty, table_schema_data, &columns); + let find_all_with = create_find_all_with_macro(mapper_ty, table_schema_data, &columns); + + quote! { + #find_all + #find_all_with + } +} + +fn create_find_all_macro( + mapper_ty: &Ident, + table_schema_data: &str, + columns: &TokenStream, +) -> TokenStream { + let default_db_conn_and_type_tokens = consts::generate_default_db_conn_and_type_tokens(); + + quote! { + async fn find_all() + -> Result, Box<(dyn std::error::Error + Send + Sync)>> + { + use canyon_sql::connection::DbConnection; + use crate::canyon_sql::query::querybuilder::SelectQueryBuilderOps; + + #default_db_conn_and_type_tokens + let stmt = canyon_sql::query::querybuilder::SelectQueryBuilder::new(#table_schema_data, db_type) + .with_known_columns(#columns) + .build()?; + default_db_conn.query(stmt.sql(), &[]).await + } + } +} + +fn create_find_all_with_macro( + mapper_ty: &Ident, + table_schema_data: &str, + columns: &TokenStream, +) -> TokenStream { + quote! { + async fn find_all_with<'a, I>(input: I) + -> Result, Box<(dyn std::error::Error + Send + Sync)>> + where + I: canyon_sql::connection::DbConnection + Send + 'a + { + use canyon_sql::connection::DbConnection; + use canyon_sql::crud::ReadOperations; + use crate::canyon_sql::query::querybuilder::SelectQueryBuilderOps; + + let db_type = input.get_database_type()?; + let stmt = canyon_sql::query::querybuilder::SelectQueryBuilder::new(#table_schema_data, db_type) + .with_known_columns(#columns) + .build()?; + input.query::<&str, #mapper_ty>(stmt.sql(), &[]).await + } + } +} diff --git a/canyon_macros/src/query_operations/read/find_by_primary_key.rs b/canyon_macros/src/query_operations/read/find_by_primary_key.rs new file mode 100644 index 00000000..c762c1d5 --- /dev/null +++ b/canyon_macros/src/query_operations/read/find_by_primary_key.rs @@ -0,0 +1,204 @@ +use crate::{ + query_operations::consts, + utils::{helpers, macro_tokens::MacroTokens}, +}; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +pub fn generate_find_by_pk_operations_tokens( + macro_data: &MacroTokens<'_>, + table_schema_data: &str, +) -> syn::Result { + let ty = macro_data.ty; + + let mapping_target_ty = macro_data.retrieve_mapping_target_type().as_ref(); + + match mapping_target_ty { + Some(mapped_ty) => { + let query = generate_mapped_find_by_pk_query(mapped_ty, table_schema_data); + + Ok(generate_find_by_pk_methods(mapped_ty, &query)) + } + + None => { + let Some(primary_key) = macro_data.get_primary_key_annotation() else { + return Ok(generate_unsupported_find_by_pk_operations(ty)); + }; + + let columns = helpers::get_struct_fields_as_column_ref_token_stream(macro_data, false); + + let query = generate_static_find_by_pk_query(table_schema_data, &columns, &primary_key); + + Ok(generate_find_by_pk_methods(ty, &query)) + } + } +} + +fn generate_static_find_by_pk_query( + table_schema_data: &str, + columns: &TokenStream, + primary_key: &str, +) -> TokenStream { + quote! { + let stmt = + canyon_sql::query::querybuilder::SelectQueryBuilder::new( + #table_schema_data, + db_type, + ) + .with_known_columns(#columns) + .r#where( + #primary_key, + canyon_sql::query::operators::Operator::Eq, + ) + .build()?; + } +} + +fn generate_mapped_find_by_pk_query(mapped_ty: &Ident, table_schema_data: &str) -> TokenStream { + quote! { + let primary_key = + <#mapped_ty as canyon_sql::query::bounds::EntityRuntimeInfo> + ::primary_key_name() + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + concat!( + "Cannot find by primary key because mapped entity `", + stringify!(#mapped_ty), + "` has no primary key", + ), + ) + })?; + + let stmt = + canyon_sql::query::querybuilder::SelectQueryBuilder::new( + #table_schema_data, + db_type, + ) + .r#where( + primary_key, + canyon_sql::query::operators::Operator::Eq, + ) + .build()?; + } +} + +fn generate_find_by_pk_methods(result_ty: &Ident, query: &TokenStream) -> TokenStream { + let find_by_pk = generate_find_by_pk(result_ty, query); + + let find_by_pk_with = generate_find_by_pk_with(result_ty, query); + + quote! { + #find_by_pk + #find_by_pk_with + } +} + +fn generate_find_by_pk(result_ty: &Ident, query: &TokenStream) -> TokenStream { + let signature = __detail::generate_find_by_pk_signature(result_ty); + + let default_db_conn_call = consts::generate_default_db_conn_tokens(); + + let body = quote! { + use canyon_sql::connection::DbConnection; + use canyon_sql::query::querybuilder::{ + QueryBuilderOps, + SelectQueryBuilderOps, + }; + + let input = { + #default_db_conn_call + }; + + let db_type = + input.get_database_type()?; + + #query + + input + .query_one::<#result_ty>( + stmt.as_ref(), + &[value], + ) + .await + }; + + __detail::generate_method(signature, body) +} + +fn generate_find_by_pk_with(result_ty: &Ident, query: &TokenStream) -> TokenStream { + let signature = __detail::generate_find_by_pk_with_signature(result_ty); + + let body = quote! { + use canyon_sql::connection::DbConnection; + use canyon_sql::query::querybuilder::{ + QueryBuilderOps, + SelectQueryBuilderOps, + }; + + let db_type = + input.get_database_type()?; + + #query + + input + .query_one::<#result_ty>( + stmt.as_ref(), + &[value], + ) + .await + }; + + __detail::generate_method(signature, body) +} + +fn generate_unsupported_find_by_pk_operations(result_ty: &Ident) -> TokenStream { + let find_by_pk_signature = __detail::generate_find_by_pk_signature(result_ty); + + let find_by_pk_with_signature = __detail::generate_find_by_pk_with_signature(result_ty); + + let find_by_pk = + __detail::generate_method(find_by_pk_signature, consts::generate_no_pk_error()); + + let find_by_pk_with = + __detail::generate_method(find_by_pk_with_signature, consts::generate_no_pk_error()); + + quote! { + #find_by_pk + #find_by_pk_with + } +} + +mod __detail { + use proc_macro2::{Ident, TokenStream}; + use quote::quote; + + pub(super) fn generate_find_by_pk_signature(result_ty: &Ident) -> TokenStream { + quote! { + async fn find_by_pk<'canyon_lt, 'err_lt>( + value: &'canyon_lt dyn canyon_sql::query::QueryParameter, + ) -> Result, Box> + } + } + + pub(super) fn generate_find_by_pk_with_signature(result_ty: &Ident) -> TokenStream { + quote! { + async fn find_by_pk_with<'canyon_lt, 'err_lt, Input>( + value: &'canyon_lt dyn canyon_sql::query::QueryParameter, + input: Input, + ) -> Result, Box> + where + Input: canyon_sql::connection::DbConnection + + Send + + 'canyon_lt + } + } + + pub(super) fn generate_method(signature: TokenStream, body: TokenStream) -> TokenStream { + quote! { + #signature { + #body + } + } + } +} diff --git a/canyon_macros/src/query_operations/read/foreign_key.rs b/canyon_macros/src/query_operations/read/foreign_key.rs new file mode 100644 index 00000000..f22e7f53 --- /dev/null +++ b/canyon_macros/src/query_operations/read/foreign_key.rs @@ -0,0 +1,481 @@ +use crate::utils::macro_tokens::MacroTokens; +use canyon_entities::field_annotation::EntityFieldAnnotation; +use canyon_entities::helpers::database_table_name_to_struct_ident; +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; + +/// Generates all read operations derived from Canyon's `foreign_key` field annotation. +/// +/// A foreign key represents an outbound relationship from the current entity to another +/// entity. For example, if `Player.team_id` points to `Team.id`, then `Player` owns the +/// foreign-key column and `Team` is the referenced parent entity. +/// +/// From a single `foreign_key(table = "teams", column = "id")` annotation, Canyon generates +/// two families of operations: +/// +/// - Parent lookup operations: instance methods that start from the child entity and fetch +/// the referenced parent entity. Example: `player.search_teams().await`. +/// - Child lookup operations: associated functions that start from a parent entity and fetch +/// all child rows that reference it. Example: `Player::search_teams_childrens(&team).await`. +/// +/// The generated method names are kept for backwards compatibility. Internally, the code uses +/// the terms `parent_lookup` and `child_lookup` because they map directly to the direction of +/// the database relationship. +pub fn generate_find_by_fk_ops( + macro_data: &MacroTokens<'_>, + table_schema_data: &str, +) -> TokenStream { + __operations::generate_find_by_fk_ops(macro_data, table_schema_data) +} + +mod __operations { + use super::*; + use crate::utils::helpers::{CanyonMethodKind, ReturnTypeTokens}; + + pub(super) fn generate_find_by_fk_ops( + macro_data: &MacroTokens<'_>, + table_schema_data: &str, + ) -> TokenStream { + let ty = macro_data.ty; + let fk_trait_ident = fk_operations_trait_ident(ty); + + let parent_lookup_operations = generate_parent_lookup_tokens(macro_data); + let child_lookup_operations = generate_child_lookup_tokens(macro_data, table_schema_data); + + if parent_lookup_operations.is_empty() && child_lookup_operations.is_empty() { + return TokenStream::new(); + } + + let method_signatures = parent_lookup_operations + .iter() + .chain(child_lookup_operations.iter()) + .map(FkOperationTokens::signature); + + let method_implementations = parent_lookup_operations + .iter() + .chain(child_lookup_operations.iter()) + .map(FkOperationTokens::implementation); + + quote! { + /// Hidden trait that exposes the foreign-key read operations generated by Canyon. + /// + /// The concrete method names depend on each `foreign_key` annotation, so they cannot + /// be declared statically in `CrudOperations`. + pub trait #fk_trait_ident<#ty> { + #(#method_signatures)* + } + + impl #fk_trait_ident<#ty> for #ty + where + #ty: std::fmt::Debug + canyon_sql::core::RowMapper, + { + #(#method_implementations)* + } + } + } + + fn fk_operations_trait_ident(ty: &Ident) -> Ident { + format_ident!("{}FkOperations", ty) + } + + /// Generates parent lookup operations for every foreign-key field declared by the entity. + /// + /// A parent lookup follows the foreign-key reference from the current row to the row it points + /// to. In relational terms, this is the many-to-one side of the relationship. + fn generate_parent_lookup_tokens(macro_data: &MacroTokens<'_>) -> Vec { + macro_data + .get_fk_annotations() + .iter() + .filter_map(|(field_ident, annotation)| match annotation { + EntityFieldAnnotation::ForeignKey(table, column) => { + Some((field_ident, table, column)) + } + _ => None, + }) + .flat_map(|(field_ident, table, column)| { + let parent_ty = database_table_name_to_struct_ident(table); + // TODO: we must ensure that the generated method names are singular, so there's no confusion with the child lookup methods. + let method_name = format_ident!("search_{}", table); + let method_name_with = format_ident!("search_{}_with", table); + let query_source = __detail::LookupQuerySource::Parent { + table, + predicate_column: column, + }; + + let fk_operation = __impl::generate_fk_operations_tokens( + query_source, + field_ident, + &method_name, + &parent_ty, + ReturnTypeTokens::Option, + CanyonMethodKind::Default, + ); + + let fk_operation_with = __impl::generate_fk_operations_tokens( + query_source, + field_ident, + &method_name_with, + &parent_ty, + ReturnTypeTokens::Option, + CanyonMethodKind::WithInput, + ); + + [fk_operation, fk_operation_with] + }) + .collect() + } + + /// Generates child lookup operations for every foreign-key field declared by the entity. + /// + /// This is sometimes called a "reverse foreign-key search", but the database does not contain + /// a second or inverted foreign key. The same child table foreign-key column is reused in the + /// opposite navigation direction: starting from a parent row, Canyon fetches all child rows that + /// reference it. + fn generate_child_lookup_tokens( + macro_data: &MacroTokens<'_>, + table_schema_data: &str, + ) -> Vec { + let ty = macro_data.ty; + let mapper_ty = macro_data + .retrieve_mapping_target_type() + .as_ref() + .unwrap_or(ty); + + macro_data + .get_fk_annotations() + .iter() + .filter_map(|(field_ident, annotation)| match annotation { + EntityFieldAnnotation::ForeignKey(table, column) => { + Some((field_ident, table, column)) + } + _ => None, + }) + .flat_map(|(field_ident, table, column)| { + let method_name = format_ident!("search_{}_childrens", table); + let method_name_with = format_ident!("search_{}_childrens_with", table); + let field_name = field_ident.to_string(); + let lookup_value = lookup_value(column, table); + let query_source = __detail::LookupQuerySource::Child { + table: table_schema_data, + predicate_column: &field_name, + }; + + let child_operation = __impl::generate_child_fk_operations_tokens( + query_source, + &lookup_value, + &method_name, + mapper_ty, + CanyonMethodKind::Default, + ); + + let child_operation_with = __impl::generate_child_fk_operations_tokens( + query_source, + &lookup_value, + &method_name_with, + mapper_ty, + CanyonMethodKind::WithInput, + ); + + [child_operation, child_operation_with] + }) + .collect() + } + + /// Generates the expression that extracts the referenced parent column value from a + /// `ForeignKeyable` parent entity. + fn lookup_value(column: &str, table: &str) -> TokenStream { + let column = column.to_owned(); + let table = table.to_owned(); + + quote! { + value.foreign_key_value(#column) + .ok_or_else(|| format!( + "Column: {:?} not found in type: {:?}", + #column, + #table, + ))? + } + } +} + +mod __impl { + use super::*; + use crate::utils::helpers::{CanyonMethodKind, ReturnTypeTokens}; + + pub(crate) fn generate_fk_operations_tokens( + query_source: __detail::LookupQuerySource<'_>, + field_ident: &Ident, + method_name: &Ident, + parent_ty: &Ident, + return_type_tokens: ReturnTypeTokens, + method_kind: CanyonMethodKind, + ) -> FkOperationTokens { + __detail::generate_operation_tokens( + query_source, + __detail::LookupValueSource::SelfField(field_ident), + method_name, + parent_ty, + return_type_tokens, + __detail::FkLookupKind::Parent, + method_kind, + ) + } + + pub(crate) fn generate_child_fk_operations_tokens( + query_source: __detail::LookupQuerySource<'_>, + lookup_value: &TokenStream, + method_name: &Ident, + mapper_ty: &Ident, + method_kind: CanyonMethodKind, + ) -> FkOperationTokens { + __detail::generate_operation_tokens( + query_source, + __detail::LookupValueSource::ForeignKeyable(lookup_value), + method_name, + mapper_ty, + ReturnTypeTokens::Vec, + __detail::FkLookupKind::Child, + method_kind, + ) + } +} + +mod __detail { + use super::*; + use crate::utils::helpers::{CanyonMethodKind, ReturnTypeTokens}; + + #[derive(Clone, Copy)] + pub(super) enum FkLookupKind { + Parent, + Child, + } + + #[derive(Clone, Copy)] + pub(super) enum LookupQuerySource<'a> { + Parent { + table: &'a str, + predicate_column: &'a str, + }, + Child { + table: &'a str, + predicate_column: &'a str, + }, + } + + #[derive(Clone, Copy)] + pub(super) enum LookupValueSource<'a> { + SelfField(&'a Ident), + ForeignKeyable(&'a TokenStream), + } + + pub(super) fn generate_operation_tokens( + query_source: LookupQuerySource<'_>, + lookup_value_source: LookupValueSource<'_>, + method_name: &Ident, + return_ty: &Ident, + return_type_tokens: ReturnTypeTokens, + lookup_kind: FkLookupKind, + method_kind: CanyonMethodKind, + ) -> FkOperationTokens { + let signature = create_method_signature( + method_name, + return_ty, + return_type_tokens, + lookup_kind, + method_kind, + ); + + let implementation = generate_method_implementation_body( + query_source, + lookup_value_source, + return_ty, + lookup_kind, + method_kind, + ); + + FkOperationTokens::new(signature, implementation) + } + + fn create_method_signature( + method_name: &Ident, + return_ty: &Ident, + ret_ty: ReturnTypeTokens, + lookup_kind: FkLookupKind, + method_kind: CanyonMethodKind, + ) -> TokenStream { + let method_generics_and_args = method_generics_and_args(lookup_kind, method_kind); + let where_clause = get_where_clause(lookup_kind, method_kind); + + quote! { + async fn #method_name #method_generics_and_args + -> Result<#ret_ty<#return_ty>, Box> + #where_clause + } + } + + fn method_generics_and_args( + lookup_kind: FkLookupKind, + method_kind: CanyonMethodKind, + ) -> TokenStream { + match (lookup_kind, method_kind) { + (FkLookupKind::Parent, CanyonMethodKind::Default) => quote! { <'a>(&self) }, + (FkLookupKind::Parent, CanyonMethodKind::WithInput) => { + quote! { <'a, I>(&self, input: I) } + } + (FkLookupKind::Child, CanyonMethodKind::Default) => quote! { <'a, F>(value: &F) }, + (FkLookupKind::Child, CanyonMethodKind::WithInput) => { + quote! { <'a, F, I>(value: &F, input: I) } + } + } + } + + fn get_where_clause(lookup_kind: FkLookupKind, method_kind: CanyonMethodKind) -> TokenStream { + match (lookup_kind, method_kind) { + (FkLookupKind::Parent, CanyonMethodKind::Default) => quote! {}, + (FkLookupKind::Parent, CanyonMethodKind::WithInput) => quote! { + where I: canyon_sql::connection::DbConnection + Send + 'a + }, + (FkLookupKind::Child, CanyonMethodKind::Default) => quote! { + where F: canyon_sql::query::bounds::ForeignKeyable + Send + Sync + }, + (FkLookupKind::Child, CanyonMethodKind::WithInput) => quote! { + where + F: canyon_sql::query::bounds::ForeignKeyable + Send + Sync, + I: canyon_sql::connection::DbConnection + Send + 'a + }, + } + } + + fn connection_binding(method_kind: CanyonMethodKind) -> TokenStream { + match method_kind { + CanyonMethodKind::Default => quote! { + let db_conn = canyon_sql::core::Canyon::instance()? + .get_default_connection()?; + }, + CanyonMethodKind::WithInput => quote! { + let db_conn = input; + }, + } + } + + fn query_builder_stmt(query_source: LookupQuerySource) -> TokenStream { + let (table, predicate_column) = match query_source { + LookupQuerySource::Parent { + table, + predicate_column, + } => (table, predicate_column), + LookupQuerySource::Child { + table, + predicate_column, + } => (table, predicate_column), + }; + + quote! { + let db_type = db_conn.get_database_type()?; + let stmt = canyon_sql::query::querybuilder::SelectQueryBuilder::new(#table, db_type) + .r#where(#predicate_column, canyon_sql::query::operators::Operator::Eq) + .build()?; + } + } + + fn lookup_value_tokens(lookup_value_source: LookupValueSource<'_>) -> TokenStream { + match lookup_value_source { + LookupValueSource::SelfField(field_ident) => quote! { + &self.#field_ident as &dyn canyon_sql::query::QueryParameter + }, + LookupValueSource::ForeignKeyable(_) => quote! { + lookup_value + }, + } + } + + fn lookup_value_binding(lookup_value_source: LookupValueSource<'_>) -> TokenStream { + match lookup_value_source { + LookupValueSource::SelfField(_) => quote! {}, + LookupValueSource::ForeignKeyable(lookup_value) => quote! { + let lookup_value = #lookup_value; + }, + } + } + + fn query_execution_tokens( + lookup_value_source: LookupValueSource<'_>, + return_ty: &Ident, + lookup_kind: FkLookupKind, + ) -> TokenStream { + let lookup_value = lookup_value_tokens(lookup_value_source); + + match lookup_kind { + FkLookupKind::Parent => quote! { + db_conn + .query_one::<#return_ty>( + stmt.sql(), + &[#lookup_value], + ) + .await + }, + FkLookupKind::Child => quote! { + db_conn + .query::<&str, #return_ty>( + stmt.sql(), + &[#lookup_value], + ) + .await + }, + } + } + + fn generate_method_implementation_body( + query_source: LookupQuerySource<'_>, + lookup_value_source: LookupValueSource<'_>, + return_ty: &Ident, + lookup_kind: FkLookupKind, + method_kind: CanyonMethodKind, + ) -> TokenStream { + let connection_binding = connection_binding(method_kind); + let lookup_value_binding = lookup_value_binding(lookup_value_source); + let query_builder_stmt = query_builder_stmt(query_source); + let query_execution = query_execution_tokens(lookup_value_source, return_ty, lookup_kind); + + quote! { + { + use canyon_sql::connection::DbConnection; + use crate::canyon_sql::query::querybuilder::{QueryBuilderOps, SelectQueryBuilderOps}; + + #lookup_value_binding + #connection_binding + #query_builder_stmt + #query_execution + } + } + } +} + +#[derive(Debug)] +struct FkOperationTokens { + signature: TokenStream, + implementation: TokenStream, +} + +impl FkOperationTokens { + fn new(signature: TokenStream, implementation: TokenStream) -> Self { + let method_definition = quote! { + #signature; + }; + let method_implementation = quote! { + #signature #implementation + }; + Self { + signature: method_definition, + implementation: method_implementation, + } + } + + fn signature(&self) -> &TokenStream { + &self.signature + } + + fn implementation(&self) -> &TokenStream { + &self.implementation + } +} diff --git a/canyon_macros/src/query_operations/read/mod.rs b/canyon_macros/src/query_operations/read/mod.rs new file mode 100644 index 00000000..5c3bef8e --- /dev/null +++ b/canyon_macros/src/query_operations/read/mod.rs @@ -0,0 +1,39 @@ +use crate::query_operations::read::count::generate_count_operations_tokens; +use crate::query_operations::read::find_all::generate_find_all_operations_tokens; +use crate::query_operations::read::find_by_primary_key::generate_find_by_pk_operations_tokens; +use crate::query_operations::read::select_querybuilder::generate_select_querybuilder_tokens; +use crate::utils::macro_tokens::MacroTokens; +use proc_macro2::TokenStream; +use quote::quote; + +mod count; +mod find_all; +mod find_by_primary_key; +pub(crate) mod foreign_key; +mod select_querybuilder; + +/// Facade function that acts as the unique API for export to the real macro implementation +/// of all the generated macros for the READ operations +pub(crate) fn generate_read_operations_tokens( + macro_data: &MacroTokens<'_>, + table_schema_data: &str, +) -> syn::Result { + let ty = macro_data.ty; + let mapper_ty = macro_data + .retrieve_mapping_target_type() + .as_ref() + .unwrap_or(ty); + + let find_all_tokens = + generate_find_all_operations_tokens(mapper_ty, table_schema_data, macro_data); + let count_tokens = generate_count_operations_tokens(table_schema_data); + let find_by_pk_tokens = generate_find_by_pk_operations_tokens(macro_data, table_schema_data)?; + let read_querybuilder_ops = generate_select_querybuilder_tokens(table_schema_data); + + Ok(quote! { + #find_all_tokens + #read_querybuilder_ops + #count_tokens + #find_by_pk_tokens + }) +} diff --git a/canyon_macros/src/query_operations/read/select_querybuilder.rs b/canyon_macros/src/query_operations/read/select_querybuilder.rs new file mode 100644 index 00000000..c8ce0c81 --- /dev/null +++ b/canyon_macros/src/query_operations/read/select_querybuilder.rs @@ -0,0 +1,16 @@ +use proc_macro2::TokenStream; +use quote::quote; + +pub fn generate_select_querybuilder_tokens(table_schema_data: &str) -> TokenStream { + quote! { + fn select_query<'a>() -> Result, Box> { + let default_db_type = canyon_sql::core::Canyon::instance()?.get_default_db_type()?; + Ok(canyon_sql::query::querybuilder::SelectQueryBuilder::new(#table_schema_data, default_db_type)) + } + + fn select_query_with<'a>(database_type: canyon_sql::connection::DatabaseType) + -> Result, Box> { + Ok(canyon_sql::query::querybuilder::SelectQueryBuilder::new(#table_schema_data, database_type)) + } + } +} diff --git a/canyon_macros/src/query_operations/select.rs b/canyon_macros/src/query_operations/select.rs deleted file mode 100644 index 82a1a5b5..00000000 --- a/canyon_macros/src/query_operations/select.rs +++ /dev/null @@ -1,488 +0,0 @@ -use canyon_entities::field_annotation::EntityFieldAnnotation; - -use proc_macro2::TokenStream; -use quote::quote; - -use crate::utils::helpers::*; -use crate::utils::macro_tokens::MacroTokens; - -/// Generates the TokenStream for build the __find_all() CRUD -/// associated function -pub fn generate_find_all_unchecked_tokens( - macro_data: &MacroTokens<'_>, - table_schema_data: &String, -) -> TokenStream { - let ty = macro_data.ty; - let stmt = format!("SELECT * FROM {table_schema_data}"); - - quote! { - /// Performs a `SELECT * FROM table_name`, where `table_name` it's - /// the name of your entity but converted to the corresponding - /// database convention. P.ej. PostgreSQL prefers table names declared - /// with snake_case identifiers. - async fn find_all_unchecked<'a>() -> Vec<#ty> { - <#ty as canyon_sql::crud::Transaction<#ty>>::query( - #stmt, - &[], - "" - ).await - .unwrap() - .into_results::<#ty>() - } - - /// Performs a `SELECT * FROM table_name`, where `table_name` it's - /// the name of your entity but converted to the corresponding - /// database convention. P.ej. PostgreSQL prefers table names declared - /// with snake_case identifiers. - /// - /// The query it's made against the database with the configured datasource - /// described in the configuration file, and selected with the [`&str`] - /// passed as parameter. - async fn find_all_unchecked_datasource<'a>(datasource_name: &'a str) -> Vec<#ty> { - <#ty as canyon_sql::crud::Transaction<#ty>>::query( - #stmt, - &[], - datasource_name - ).await - .unwrap() - .into_results::<#ty>() - } - } -} - -/// Generates the TokenStream for build the __find_all_result() CRUD -/// associated function -pub fn generate_find_all_tokens( - macro_data: &MacroTokens<'_>, - table_schema_data: &String, -) -> TokenStream { - let ty = macro_data.ty; - let stmt = format!("SELECT * FROM {table_schema_data}"); - - quote! { - /// Performs a `SELECT * FROM table_name`, where `table_name` it's - /// the name of your entity but converted to the corresponding - /// database convention. P.ej. PostgreSQL prefers table names declared - /// with snake_case identifiers. - async fn find_all<'a>() -> - Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> - { - Ok( - <#ty as canyon_sql::crud::Transaction<#ty>>::query( - #stmt, - &[], - "" - ).await? - .into_results::<#ty>() - ) - } - - /// Performs a `SELECT * FROM table_name`, where `table_name` it's - /// the name of your entity but converted to the corresponding - /// database convention. P.ej. PostgreSQL prefers table names declared - /// with snake_case identifiers. - /// - /// The query it's made against the database with the configured datasource - /// described in the configuration file, and selected with the [`&str`] - /// passed as parameter. - /// - /// Also, returns a [`Vec, Error>`], wrapping a possible failure - /// querying the database, or, if no errors happens, a Vec containing - /// the data found. - async fn find_all_datasource<'a>(datasource_name: &'a str) -> - Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> - { - Ok( - <#ty as canyon_sql::crud::Transaction<#ty>>::query( - #stmt, - &[], - datasource_name - ).await? - .into_results::<#ty>() - ) - } - } -} - -/// Same as above, but with a [`canyon_sql::query::QueryBuilder`] -pub fn generate_find_all_query_tokens( - macro_data: &MacroTokens<'_>, - table_schema_data: &String, -) -> TokenStream { - let ty = macro_data.ty; - - quote! { - /// Generates a [`canyon_sql::query::SelectQueryBuilder`] - /// that allows you to customize the query by adding parameters and constrains dynamically. - /// - /// It performs a `SELECT * FROM table_name`, where `table_name` it's the name of your - /// entity but converted to the corresponding database convention, - /// unless concrete values are set on the available parameters of the - /// `canyon_macro(table_name = "table_name", schema = "schema")` - fn select_query<'a>() -> canyon_sql::query::SelectQueryBuilder<'a, #ty> { - canyon_sql::query::SelectQueryBuilder::new(#table_schema_data, "") - } - - /// Generates a [`canyon_sql::query::SelectQueryBuilder`] - /// that allows you to customize the query by adding parameters and constrains dynamically. - /// - /// It performs a `SELECT * FROM table_name`, where `table_name` it's the name of your - /// entity but converted to the corresponding database convention, - /// unless concrete values are set on the available parameters of the - /// `canyon_macro(table_name = "table_name", schema = "schema")` - /// - /// The query it's made against the database with the configured datasource - /// described in the configuration file, and selected with the [`&str`] - /// passed as parameter. - fn select_query_datasource<'a>(datasource_name: &'a str) -> canyon_sql::query::SelectQueryBuilder<'a, #ty> { - canyon_sql::query::SelectQueryBuilder::new(#table_schema_data, datasource_name) - } - } -} - -/// Performs a COUNT(*) query over some table, returning a [`Result`] wrapping -/// a possible success or error coming from the database -pub fn generate_count_tokens( - macro_data: &MacroTokens<'_>, - table_schema_data: &String, -) -> TokenStream { - let ty = macro_data.ty; - let ty_str = &ty.to_string(); - let stmt = format!("SELECT COUNT(*) FROM {table_schema_data}"); - - let result_handling = quote! { - #[cfg(feature="postgres")] - canyon_sql::crud::CanyonRows::Postgres(mut v) => Ok( - v.remove(0).get::<&str, i64>("count") - ), - #[cfg(feature="mssql")] - canyon_sql::crud::CanyonRows::Tiberius(mut v) => - v.remove(0) - .get::(0) - .map(|c| c as i64) - .ok_or(format!("Failure in the COUNT query for MSSQL for: {}", #ty_str).into()) - .into(), - #[cfg(feature="mysql")] - canyon_sql::crud::CanyonRows::MySQL(mut v) => v.remove(0) - .get::(0) - .ok_or(format!("Failure in the COUNT query for MYSQL for: {}", #ty_str).into()), - _ => panic!() // TODO remove when the generics will be refactored - }; - - quote! { - /// Performs a COUNT(*) query over some table, returning a [`Result`] rather than panicking, - /// wrapping a possible success or error coming from the database - async fn count() -> Result> { - let count = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - #stmt, - &[], - "" - ).await?; - - match count { - #result_handling - } - } - - /// Performs a COUNT(*) query over some table, returning a [`Result`] rather than panicking, - /// wrapping a possible success or error coming from the database with the specified datasource - async fn count_datasource<'a>(datasource_name: &'a str) -> Result> { - let count = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - #stmt, - &[], - datasource_name - ).await?; - - match count { - #result_handling - } - } - } -} - -/// Generates the TokenStream for build the __find_by_pk() CRUD operation -pub fn generate_find_by_pk_tokens( - macro_data: &MacroTokens<'_>, - table_schema_data: &String, -) -> TokenStream { - let ty = macro_data.ty; - let pk = macro_data.get_primary_key_annotation().unwrap_or_default(); - let stmt = format!("SELECT * FROM {table_schema_data} WHERE {pk} = $1"); - - // Disabled if there's no `primary_key` annotation - if pk.is_empty() { - return quote! { - async fn find_by_pk<'a>(value: &'a dyn canyon_sql::crud::bounds::QueryParameter<'a>) - -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> - { - Err( - std::io::Error::new( - std::io::ErrorKind::Unsupported, - "You can't use the 'find_by_pk' associated function on a \ - CanyonEntity that does not have a #[primary_key] annotation. \ - If you need to perform an specific search, use the Querybuilder instead." - ).into_inner().unwrap() - ) - } - - async fn find_by_pk_datasource<'a>( - value: &'a dyn canyon_sql::crud::bounds::QueryParameter<'a>, - datasource_name: &'a str - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> { - Err( - std::io::Error::new( - std::io::ErrorKind::Unsupported, - "You can't use the 'find_by_pk_datasource' associated function on a \ - CanyonEntity that does not have a #[primary_key] annotation. \ - If you need to perform an specific search, use the Querybuilder instead." - ).into_inner().unwrap() - ) - } - }; - } - - let result_handling = quote! { - match result { - n if n.len() == 0 => Ok(None), - _ => Ok( - Some(result.into_results::<#ty>().remove(0)) - ) - } - }; - - quote! { - /// Finds an element on the queried table that matches the - /// value of the field annotated with the `primary_key` attribute, - /// filtering by the column that it's declared as the primary - /// key on the database. - /// - /// This operation it's only available if the [`CanyonEntity`] contains - /// some field declared as primary key. - /// - /// Also, returns a [`Result, Error>`], wrapping a possible failure - /// querying the database, or, if no errors happens, a success containing - /// and Option with the data found wrapped in the Some(T) variant, - /// or None if the value isn't found on the table. - async fn find_by_pk<'a>(value: &'a dyn canyon_sql::crud::bounds::QueryParameter<'a>) -> - Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> - { - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - #stmt, - vec![value], - "" - ).await?; - - #result_handling - } - - /// Finds an element on the queried table that matches the - /// value of the field annotated with the `primary_key` attribute, - /// filtering by the column that it's declared as the primary - /// key on the database. - /// - /// The query it's made against the database with the configured datasource - /// described in the configuration file, and selected with the [`&str`] - /// passed as parameter. - /// - /// This operation it's only available if the [`CanyonEntity`] contains - /// some field declared as primary key. - /// - /// Also, returns a [`Result, Error>`], wrapping a possible failure - /// querying the database, or, if no errors happens, a success containing - /// and Option with the data found wrapped in the Some(T) variant, - /// or None if the value isn't found on the table. - async fn find_by_pk_datasource<'a>( - value: &'a dyn canyon_sql::crud::bounds::QueryParameter<'a>, - datasource_name: &'a str - ) -> Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> { - - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - #stmt, - vec![value], - datasource_name - ).await?; - - #result_handling - } - } -} - -/// Generates the TokenStream for build the search by foreign key feature, also as a method instance -/// of a T type of as an associated function of same T type, but wrapped as a Result, representing -/// a possible failure querying the database, a bad or missing FK annotation or a missed ForeignKeyable -/// derive macro on the parent side of the relation -pub fn generate_find_by_foreign_key_tokens( - macro_data: &MacroTokens<'_>, -) -> Vec<(TokenStream, TokenStream)> { - let mut fk_quotes: Vec<(TokenStream, TokenStream)> = Vec::new(); - - for (field_ident, fk_annot) in macro_data.get_fk_annotations().iter() { - if let EntityFieldAnnotation::ForeignKey(table, column) = fk_annot { - let method_name = "search_".to_owned() + table; - - // TODO this is not a good implementation. We must try to capture the - // related entity in some way, and compare it with something else - let fk_ty = database_table_name_to_struct_ident(table); - - // Generate and identifier for the method based on the convention of "search_related_types" - // where types is a placeholder for the plural name of the type referenced - let method_name_ident = - proc_macro2::Ident::new(&method_name, proc_macro2::Span::call_site()); - let method_name_ident_ds = proc_macro2::Ident::new( - &format!("{}_datasource", &method_name), - proc_macro2::Span::call_site(), - ); - let quoted_method_signature: TokenStream = quote! { - async fn #method_name_ident(&self) -> - Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> - }; - let quoted_datasource_method_signature: TokenStream = quote! { - async fn #method_name_ident_ds<'a>(&self, datasource_name: &'a str) -> - Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> - }; - - let stmt = format!( - "SELECT * FROM {} WHERE {} = $1", - table, - format!("\"{column}\"").as_str(), - ); - let result_handler = quote! { - match result { - n if n.len() == 0 => Ok(None), - _ => Ok(Some( - result.into_results::<#fk_ty>().remove(0) - )) - } - }; - - fk_quotes.push(( - quote! { #quoted_method_signature; }, - quote! { - /// Searches the parent entity (if exists) for this type - #quoted_method_signature { - let result = <#fk_ty as canyon_sql::crud::Transaction<#fk_ty>>::query( - #stmt, - &[&self.#field_ident as &dyn canyon_sql::crud::bounds::QueryParameter<'_>], - "" - ).await?; - - #result_handler - } - } - )); - - fk_quotes.push(( - quote! { #quoted_datasource_method_signature; }, - quote! { - /// Searches the parent entity (if exists) for this type with the specified datasource - #quoted_datasource_method_signature { - let result = <#fk_ty as canyon_sql::crud::Transaction<#fk_ty>>::query( - #stmt, - &[&self.#field_ident as &dyn canyon_sql::crud::bounds::QueryParameter<'_>], - datasource_name - ).await?; - - #result_handler - } - } - )); - } - } - - fk_quotes -} - -/// Generates the TokenStream for build the __search_by_foreign_key() CRUD -/// associated function, but wrapped as a Result, representing -/// a possible failure querying the database, a bad or missing FK annotation or a missed ForeignKeyable -/// derive macro on the parent side of the relation -pub fn generate_find_by_reverse_foreign_key_tokens( - macro_data: &MacroTokens<'_>, - table_schema_data: &String, -) -> Vec<(TokenStream, TokenStream)> { - let mut rev_fk_quotes: Vec<(TokenStream, TokenStream)> = Vec::new(); - let ty = macro_data.ty; - - for (field_ident, fk_annot) in macro_data.get_fk_annotations().iter() { - if let EntityFieldAnnotation::ForeignKey(table, column) = fk_annot { - let method_name = format!("search_{table}_childrens"); - - // Generate and identifier for the method based on the convention of "search_by__" (note the double underscore) - // plus the 'table_name' property of the ForeignKey annotation - let method_name_ident = - proc_macro2::Ident::new(&method_name, proc_macro2::Span::call_site()); - let method_name_ident_ds = proc_macro2::Ident::new( - &format!("{}_datasource", &method_name), - proc_macro2::Span::call_site(), - ); - let quoted_method_signature: TokenStream = quote! { - async fn #method_name_ident<'a, F: canyon_sql::crud::bounds::ForeignKeyable + Sync + Send>(value: &F) -> - Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> - }; - let quoted_datasource_method_signature: TokenStream = quote! { - async fn #method_name_ident_ds<'a, F: canyon_sql::crud::bounds::ForeignKeyable + Sync + Send> - (value: &F, datasource_name: &'a str) -> - Result, Box<(dyn std::error::Error + Send + Sync + 'static)>> - }; - - let f_ident = field_ident.to_string(); - - rev_fk_quotes.push(( - quote! { #quoted_method_signature; }, - quote! { - /// Given a parent entity T annotated with the derive proc macro `ForeignKeyable`, - /// performns a search to find the children that belong to that concrete parent. - #quoted_method_signature - { - let lookage_value = value.get_fk_column(#column) - .expect(format!( - "Column: {:?} not found in type: {:?}", #column, #table - ).as_str()); - - let stmt = format!( - "SELECT * FROM {} WHERE {} = $1", - #table_schema_data, - format!("\"{}\"", #f_ident).as_str() - ); - - Ok(<#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, - &[lookage_value], - "" - ).await?.into_results::<#ty>()) - } - }, - )); - - rev_fk_quotes.push(( - quote! { #quoted_datasource_method_signature; }, - quote! { - /// Given a parent entity T annotated with the derive proc macro `ForeignKeyable`, - /// performns a search to find the children that belong to that concrete parent - /// with the specified datasource. - #quoted_datasource_method_signature - { - let lookage_value = value.get_fk_column(#column) - .expect(format!( - "Column: {:?} not found in type: {:?}", #column, #table - ).as_str()); - - let stmt = format!( - "SELECT * FROM {} WHERE {} = $1", - #table_schema_data, - format!("\"{}\"", #f_ident).as_str() - ); - - Ok(<#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, - &[lookage_value], - datasource_name - ).await?.into_results::<#ty>()) - } - }, - )); - } - } - - rev_fk_quotes -} diff --git a/canyon_macros/src/query_operations/update.rs b/canyon_macros/src/query_operations/update.rs deleted file mode 100644 index 5837325a..00000000 --- a/canyon_macros/src/query_operations/update.rs +++ /dev/null @@ -1,142 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; - -use crate::utils::macro_tokens::MacroTokens; - -/// Generates the TokenStream for the __update() CRUD operation -pub fn generate_update_tokens(macro_data: &MacroTokens, table_schema_data: &String) -> TokenStream { - let ty = macro_data.ty; - - let update_columns = macro_data.get_column_names_pk_parsed(); - - // Retrieves the fields of the Struct - let fields = macro_data.get_struct_fields(); - - let mut vec_columns_values: Vec = Vec::new(); - for (i, column_name) in update_columns.iter().enumerate() { - let column_equal_value = format!("{} = ${}", column_name.to_owned(), i + 2); - vec_columns_values.push(column_equal_value) - } - - let str_columns_values = vec_columns_values.join(", "); - - let update_values = fields.iter().map(|ident| { - quote! { &self.#ident } - }); - let update_values_cloned = update_values.clone(); - - if let Some(primary_key) = macro_data.get_primary_key_annotation() { - let pk_index = macro_data - .get_pk_index() - .expect("Update method failed to retrieve the index of the primary key"); - - quote! { - /// Updates a database record that matches - /// the current instance of a T type, returning a result - /// indicating a possible failure querying the database. - async fn update(&self) -> Result<(), Box> { - let stmt = format!( - "UPDATE {} SET {} WHERE {} = ${:?}", - #table_schema_data, #str_columns_values, #primary_key, #pk_index + 1 - ); - let update_values: &[&dyn canyon_sql::crud::bounds::QueryParameter<'_>] = &[#(#update_values),*]; - - <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, update_values, "" - ).await?; - - Ok(()) - } - - - /// Updates a database record that matches - /// the current instance of a T type, returning a result - /// indicating a possible failure querying the database with the - /// specified datasource - async fn update_datasource<'a>(&self, datasource_name: &'a str) - -> Result<(), Box> - { - let stmt = format!( - "UPDATE {} SET {} WHERE {} = ${:?}", - #table_schema_data, #str_columns_values, #primary_key, #pk_index + 1 - ); - let update_values: &[&dyn canyon_sql::crud::bounds::QueryParameter<'_>] = &[#(#update_values_cloned),*]; - - <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, update_values, datasource_name - ).await?; - - Ok(()) - } - } - } else { - // If there's no primary key, update method over self won't be available. - // Use instead the update associated function of the querybuilder - - // TODO Returning an error should be a provisional way of doing this - quote! { - async fn update(&self) - -> Result<(), Box> - { - Err( - std::io::Error::new( - std::io::ErrorKind::Unsupported, - "You can't use the 'update' method on a \ - CanyonEntity that does not have a #[primary_key] annotation. \ - If you need to perform an specific search, use the Querybuilder instead." - ).into_inner().unwrap() - ) - } - - async fn update_datasource<'a>(&self, datasource_name: &'a str) - -> Result<(), Box> - { - Err( - std::io::Error::new( - std::io::ErrorKind::Unsupported, - "You can't use the 'update_datasource' method on a \ - CanyonEntity that does not have a #[primary_key] annotation. \ - If you need to perform an specific search, use the Querybuilder instead." - ).into_inner().unwrap() - ) - } - } - } -} - -/// Generates the TokenStream for the __update() CRUD operation -/// being the query generated with the [`QueryBuilder`] -pub fn generate_update_query_tokens( - macro_data: &MacroTokens, - table_schema_data: &String, -) -> TokenStream { - let ty = macro_data.ty; - - quote! { - /// Generates a [`canyon_sql::query::UpdateQueryBuilder`] - /// that allows you to customize the query by adding parameters and constrains dynamically. - /// - /// It performs an `UPDATE table_name`, where `table_name` it's the name of your - /// entity but converted to the corresponding database convention, - /// unless concrete values are set on the available parameters of the - /// `canyon_macro(table_name = "table_name", schema = "schema")` - fn update_query<'a>() -> canyon_sql::query::UpdateQueryBuilder<'a, #ty> { - canyon_sql::query::UpdateQueryBuilder::new(#table_schema_data, "") - } - - /// Generates a [`canyon_sql::query::UpdateQueryBuilder`] - /// that allows you to customize the query by adding parameters and constrains dynamically. - /// - /// It performs an `UPDATE table_name`, where `table_name` it's the name of your - /// entity but converted to the corresponding database convention, - /// unless concrete values are set on the available parameters of the - /// `canyon_macro(table_name = "table_name", schema = "schema")` - /// - /// The query it's made against the database with the configured datasource - /// described in the configuration file, and selected with the [`&str`] - /// passed as parameter. - fn update_query_datasource<'a>(datasource_name: &'a str) -> canyon_sql::query::UpdateQueryBuilder<'a, #ty> { - canyon_sql::query::UpdateQueryBuilder::new(#table_schema_data, datasource_name) - } - } -} diff --git a/canyon_macros/src/query_operations/update/entity.rs b/canyon_macros/src/query_operations/update/entity.rs new file mode 100644 index 00000000..3e816ed4 --- /dev/null +++ b/canyon_macros/src/query_operations/update/entity.rs @@ -0,0 +1,144 @@ +use proc_macro2::TokenStream; +use quote::quote; + +pub(crate) fn generate_update_entity_tokens(table_schema_data: &str) -> syn::Result { + let update_entity_signature = __detail::generate_update_entity_signature(); + + let update_entity_with_signature = __detail::generate_update_entity_with_signature(); + + let update_entity_body = __detail::generate_update_entity_body(table_schema_data); + + let update_entity_with_body = __detail::generate_update_entity_with_body(table_schema_data); + + Ok(quote! { + #update_entity_signature { + #update_entity_body + } + + #update_entity_with_signature { + #update_entity_with_body + } + }) +} + +mod __detail { + use proc_macro2::TokenStream; + use quote::quote; + + use crate::query_operations::consts; + + pub(crate) fn generate_update_entity_body(table_schema_data: &str) -> TokenStream { + let default_db_conn_and_type = consts::generate_default_db_conn_and_type_tokens(); + + let update_execution = + generate_update_execution(table_schema_data, quote! { default_db_conn }); + + quote! { + #default_db_conn_and_type + #update_execution + + Ok(()) + } + } + + pub(crate) fn generate_update_entity_with_body(table_schema_data: &str) -> TokenStream { + let update_execution = generate_update_execution(table_schema_data, quote! { input }); + + quote! { + let db_type = input.get_database_type()?; + + #update_execution + + Ok(()) + } + } + + fn generate_update_execution(table_schema_data: &str, connection: TokenStream) -> TokenStream { + quote! { + use canyon_sql::connection::DbConnection; + use canyon_sql::query::querybuilder::{ + QueryBuilderOps, + UpdateQueryBuilderOps, + }; + + let primary_key_name = + + ::primary_key_name() + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Cannot update an entity without a primary key", + ) + })?; + + let primary_key_value = + + ::primary_key_value(entity) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Cannot update an entity without a primary-key value", + ) + })?; + + let update_columns = + + ::field_columns(); + + let mut update_values = + + ::field_values(entity); + + update_values.push(primary_key_value); + + let query = + canyon_sql::query::querybuilder::UpdateQueryBuilder::new( + #table_schema_data, + db_type, + ) + .set(update_columns)? + .r#where( + primary_key_name, + canyon_sql::query::operators::Operator::Eq, + ) + .build()?; + + #connection + .execute( + query.as_ref(), + &update_values, + ) + .await?; + } + } + + pub(crate) fn generate_update_entity_signature() -> TokenStream { + quote! { + async fn update_entity<'canyon_lt, 'err_lt, Entity>( + entity: &'canyon_lt Entity, + ) -> Result<(), Box> + where + Entity: canyon_sql::core::RowMapper + + canyon_sql::query::bounds::EntityRuntimeInfo + + Sync + + 'canyon_lt + } + } + + pub(crate) fn generate_update_entity_with_signature() -> TokenStream { + quote! { + async fn update_entity_with<'canyon_lt, 'err_lt, Entity, Input>( + entity: &'canyon_lt Entity, + input: Input, + ) -> Result<(), Box> + where + Entity: canyon_sql::core::RowMapper + + canyon_sql::query::bounds::EntityRuntimeInfo + + Sync + + 'canyon_lt, + Input: canyon_sql::connection::DbConnection + + Send + + 'canyon_lt + } + } +} diff --git a/canyon_macros/src/query_operations/update/method.rs b/canyon_macros/src/query_operations/update/method.rs new file mode 100644 index 00000000..6b4bbabd --- /dev/null +++ b/canyon_macros/src/query_operations/update/method.rs @@ -0,0 +1,149 @@ +use crate::query_operations::update::__err; +use crate::utils::helpers; +use crate::utils::macro_tokens::MacroTokens; +use proc_macro2::TokenStream; +use quote::quote; + +pub(crate) fn generate_update_method_tokens( + macro_data: &MacroTokens, + table_schema_data: &str, +) -> syn::Result { + let mut update_ops_tokens = TokenStream::new(); + + if let Some(primary_key) = macro_data.get_primary_key_field_annotation() { + let update_columns = helpers::get_struct_fields_as_table_column_pairs_pk_parsed(macro_data); + let update_values = __details::generate_update_values(macro_data, primary_key.ident); + + let query = + __details::generate_update_stmt(table_schema_data, update_columns, &primary_key.name); + + let update_method_tokens = + __details::generate_update_method_tokens(macro_data, &query, &update_values); + let update_with_method_tokens = + __details::generate_update_with_method_tokens(&query, &update_values); + + update_ops_tokens.extend(quote! { + #update_method_tokens + #update_with_method_tokens + }); + } else { + // If there's no primary key, update method over self won't be available. + // Use instead the update associated function of the querybuilder + __details::handle_no_primary_key_case(&mut update_ops_tokens); + } + + Ok(update_ops_tokens) +} + +mod __details { + use super::*; + use crate::query_operations::consts; + use proc_macro2::Ident; + + pub(crate) fn generate_update_method_tokens( + macro_data: &MacroTokens, + query: &TokenStream, + update_values: &Vec, + ) -> TokenStream { + let ty = macro_data.ty; + let (_, ty_generics, _) = macro_data.generics.split_for_impl(); + + let update_signature = __signatures::get_update_signature(); + let default_db_conn_and_type_tokens = consts::generate_default_db_conn_and_type_tokens(); + + quote! { + #update_signature { + use canyon_sql::connection::DbConnection; + use canyon_sql::query::querybuilder::{QueryBuilderOps, UpdateQueryBuilderOps}; + + #default_db_conn_and_type_tokens + + let query = #query; + let update_values: &[&dyn canyon_sql::query::QueryParameter] = &[#(#update_values),*]; + <#ty #ty_generics as canyon_sql::core::Transaction>::execute(query.as_ref(), update_values, default_db_conn).await + } + } + } + + pub(crate) fn generate_update_with_method_tokens( + query: &TokenStream, + update_values: &Vec, + ) -> TokenStream { + let update_with_signature = __signatures::get_update_with_signature(); + + quote! { + #update_with_signature { + use canyon_sql::connection::DbConnection; + use canyon_sql::query::querybuilder::{QueryBuilderOps, UpdateQueryBuilderOps}; + let db_type = input.get_database_type()?; + let query = #query; + let update_values: &[&dyn canyon_sql::query::QueryParameter] = &[#(#update_values),*]; + input.execute(query.as_ref(), update_values).await + } + } + } + + pub(crate) fn generate_update_stmt( + table_schema_data: &str, + update_columns: Vec, + pk_name: &str, + ) -> TokenStream { + quote! { + canyon_sql::query::querybuilder::UpdateQueryBuilder::new( + #table_schema_data, // TODO: construct a const value + db_type, + ) + .set(vec![#(#update_columns),*])? + .r#where( + #pk_name, + canyon_sql::query::operators::Operator::Eq, + ) + .build()?; + } + } + + pub(crate) fn generate_update_values(macro_data: &MacroTokens, pk: &Ident) -> Vec { + macro_data + .get_fields_idents_skipping_pk() + .map(|ident| { + quote! { + &self.#ident as &dyn canyon_sql::query::QueryParameter + } + }) + .chain(std::iter::once(quote! { + &self.#pk as &dyn canyon_sql::query::QueryParameter + })) + .collect::>() + } + + pub(crate) fn handle_no_primary_key_case(update_ops_tokens: &mut TokenStream) { + let update_signature = __signatures::get_update_signature(); + let update_with_signature = __signatures::get_update_with_signature(); + + let no_pk_err = __err::generate_no_pk_err(); + + update_ops_tokens.extend(quote! { + #update_signature { #no_pk_err } + #update_with_signature{ #no_pk_err } + }); + } +} + +mod __signatures { + use proc_macro2::TokenStream; + use quote::quote; + + pub(crate) fn get_update_signature() -> TokenStream { + quote! { + async fn update(&self) -> Result> + } + } + + pub(crate) fn get_update_with_signature() -> TokenStream { + quote! { + async fn update_with<'a, I>(&self, input: I) + -> Result> + where I: canyon_sql::connection::DbConnection + Send + 'a + } + } +} diff --git a/canyon_macros/src/query_operations/update/mod.rs b/canyon_macros/src/query_operations/update/mod.rs new file mode 100644 index 00000000..7f86a4b2 --- /dev/null +++ b/canyon_macros/src/query_operations/update/mod.rs @@ -0,0 +1,48 @@ +mod entity; +mod method; +mod querybuilder; + +use crate::{ + query_operations::update::{ + entity::generate_update_entity_tokens as update_entity_tokens, + method::generate_update_method_tokens as update_method_tokens, + querybuilder::generate_update_querybuilder_tokens, + }, + utils::macro_tokens::MacroTokens, +}; +use proc_macro2::TokenStream; +use quote::quote; + +pub fn generate_update_method_tokens( + macro_tokens: &MacroTokens, + table_schema_data: &str, +) -> syn::Result { + let update_tokens = update_method_tokens(macro_tokens, table_schema_data)?; + let querybuilder_tokens = generate_update_querybuilder_tokens(table_schema_data); + + Ok(quote! { + #update_tokens + #querybuilder_tokens + }) +} + +pub fn generate_update_entity_tokens(table_schema_data: &str) -> syn::Result { + update_entity_tokens(table_schema_data) +} + +mod __err { + use proc_macro2::TokenStream; + use quote::quote; + + pub(crate) fn generate_no_pk_err() -> TokenStream { + quote! { + Err( + std::io::Error::new( + std::io::ErrorKind::Unsupported, + "The type has either zero fields or exactly one that is annotated with #[primary_key].\ + That's makes it ineligibly to be used in the update_entity family of operations." + ).into_inner().unwrap() + ) + } + } +} diff --git a/canyon_macros/src/query_operations/update/querybuilder.rs b/canyon_macros/src/query_operations/update/querybuilder.rs new file mode 100644 index 00000000..1cb7a470 --- /dev/null +++ b/canyon_macros/src/query_operations/update/querybuilder.rs @@ -0,0 +1,37 @@ +use proc_macro2::TokenStream; +use quote::quote; + +/// Generates the TokenStream for the __update() CRUD operation +/// being the query generated with the [`QueryBuilder`] +pub(crate) fn generate_update_querybuilder_tokens(table_schema_data: &str) -> TokenStream { + quote! { + /// Generates a [`canyon_sql::query::querybuilder::UpdateQueryBuilder`] + /// that allows you to customize the query by adding parameters and constrains dynamically. + /// + /// It performs an `UPDATE table_name`, where `table_name` it's the name of your + /// entity but converted to the corresponding database convention, + /// unless concrete values are set on the available parameters of the + /// `canyon_macro(table_name = "table_name", schema = "schema")` + fn update_query<'canyon, 'err>() -> Result, Box> + where 'canyon: 'err + { + let default_db_type = canyon_sql::core::Canyon::instance()?.get_default_db_type()?; + Ok(canyon_sql::query::querybuilder::UpdateQueryBuilder::new(#table_schema_data, default_db_type)) + } + + /// Generates a [`canyon_sql::query::querybuilder::UpdateQueryBuilder`] + /// that allows you to customize the query by adding parameters and constrains dynamically. + /// + /// It performs an `UPDATE table_name`, where `table_name` it's the name of your + /// entity but converted to the corresponding database convention, + /// unless concrete values are set on the available parameters of the + /// `canyon_macro(table_name = "table_name", schema = "schema")` + /// + /// The query it's made against the database with the configured datasource + /// described in the configuration file, and selected with the input parameter + fn update_query_with<'a>(database_type: canyon_sql::connection::DatabaseType) -> + canyon_sql::query::querybuilder::UpdateQueryBuilder<'a> { + canyon_sql::query::querybuilder::UpdateQueryBuilder::new(#table_schema_data, database_type) + } + } +} diff --git a/canyon_macros/src/utils/canyon_crud_attribute.rs b/canyon_macros/src/utils/canyon_crud_attribute.rs new file mode 100644 index 00000000..265affe4 --- /dev/null +++ b/canyon_macros/src/utils/canyon_crud_attribute.rs @@ -0,0 +1,33 @@ +use proc_macro2::Ident; +use syn::Token; +use syn::parse::{Parse, ParseStream}; + +/// Type that helps to parse the: `#[canyon_crud(maps_to = Ident)]` proc macro attribute +/// +/// The ident value of the `maps_to` argument brings a type that is the target type for which +/// `CrudOperations` will write the queries as the implementor of [`RowMapper`] +pub(crate) struct CanyonCrudAttribute { + pub maps_to: Option, +} + +impl Parse for CanyonCrudAttribute { + fn parse(input: ParseStream<'_>) -> syn::Result { + let arg_name: Ident = input.parse()?; + if arg_name != "maps_to" { + return Err(syn::Error::new_spanned( + arg_name, + "unsupported 'canyon_crud' attribute, expected `maps_to`", + )); + } + + // Parse (and discard the span of) the `=` token + let _: Token![=] = input.parse()?; + + // Parse the argument value + let name = input.parse()?; + + Ok(Self { + maps_to: Some(name), + }) + } +} diff --git a/canyon_macros/src/utils/function_parser.rs b/canyon_macros/src/utils/function_parser.rs index 841e534d..7f0a294b 100644 --- a/canyon_macros/src/utils/function_parser.rs +++ b/canyon_macros/src/utils/function_parser.rs @@ -1,11 +1,10 @@ use syn::{ - parse::{Parse, ParseBuffer}, Attribute, Block, ItemFn, Signature, Visibility, + parse::{Parse, ParseBuffer}, }; /// Implementation of syn::Parse for the `#[canyon]` proc-macro #[derive(Clone)] -#[allow(dead_code)] pub struct FunctionParser { pub attrs: Vec, pub vis: Visibility, @@ -15,21 +14,13 @@ pub struct FunctionParser { impl Parse for FunctionParser { fn parse(input: &ParseBuffer) -> syn::Result { - let func = input.parse::(); - - if func.is_err() { - return Err(syn::Error::new( - input.cursor().span(), - "Error on `fn main()`", - )); - } + let func = input.parse::()?; - let func_ok = func.ok().unwrap(); Ok(Self { - attrs: func_ok.attrs, - vis: func_ok.vis, - sig: func_ok.sig, - block: func_ok.block, + attrs: func.attrs, + vis: func.vis, + sig: func.sig, + block: func.block, }) } } diff --git a/canyon_macros/src/utils/helpers.rs b/canyon_macros/src/utils/helpers.rs index 2db52be5..3fac25cb 100644 --- a/canyon_macros/src/utils/helpers.rs +++ b/canyon_macros/src/utils/helpers.rs @@ -1,184 +1,316 @@ -use proc_macro2::{Ident, Span, TokenStream}; -use syn::{punctuated::Punctuated, MetaNameValue, Token}; - use super::macro_tokens::MacroTokens; +use canyon_core::query::querybuilder::syntax::table_metadata::TableMetadata; +pub(crate) use canyon_entities::helpers::default_database_table_name_from_entity_name; +use proc_macro2::{Ident, TokenStream}; +use quote::{ToTokens, quote}; +use std::borrow::Cow; +use syn::{Attribute, Field, Fields, TypeGenerics, Visibility}; + +#[derive(Copy, Clone)] +pub(crate) enum CanyonMethodKind { + Default, + WithInput, +} + +#[derive(Copy, Clone)] +pub(crate) enum ReturnTypeTokens { + Vec, + Option, +} + +impl ToTokens for ReturnTypeTokens { + fn to_tokens(&self, tokens: &mut TokenStream) { + let expanded = match self { + Self::Vec => quote! { Vec }, + Self::Option => quote! { Option }, + }; + tokens.extend(expanded); + } +} + +pub(crate) fn get_struct_fields_as_column_ref_token_stream( + macro_tokens: &MacroTokens, + skip_primary_key: bool, +) -> TokenStream { + let struct_fields = if skip_primary_key { + macro_tokens.get_struct_fields_as_table_column_pairs_skipping_pk() + } else { + macro_tokens.get_struct_fields_as_table_column_pairs() + }; + get_fields_as_iterable_of_column_refs(struct_fields) +} + +pub(crate) fn get_struct_fields_as_table_column_pairs_pk_parsed( + macro_tokens: &MacroTokens, +) -> Vec { + let struct_fields_without_pk = + macro_tokens.get_struct_fields_as_table_column_pairs_skipping_pk(); + get_fields_as_vec_of_column_refs(struct_fields_without_pk) +} + +pub(crate) fn get_fields_as_iterable_of_column_refs( + elements: Vec<(String, String)>, +) -> TokenStream { + let columns = elements.iter().map(|(table, column)| { + quote! { + canyon_sql::query::ColumnRef::new(#table, #column) + } + }); + quote! { + ::core::array::IntoIter::new([ + #(#columns),* + ]) + } +} + +pub(crate) fn get_fields_as_vec_of_column_refs( + struct_fields: Vec<(String, String)>, +) -> Vec { + struct_fields + .iter() + .map(|(table, column)| { + quote! { + canyon_sql::query::ColumnRef::new(#table, #column) + } + }) + .collect::>() +} + +/// Given the derived type of CrudOperations, and the possible mapping type if the `#[canyon_crud(maps_to=]` exists, +/// returns a [`TokenStream`] with the final `RowMapper` implementor. +pub fn compute_crud_ops_mapping_target_type_with_generics( + row_mapper_ty: &Ident, + row_mapper_ty_generics: &TypeGenerics, + crud_ops_ty: Option<&Ident>, +) -> TokenStream { + if let Some(crud_ops_ty) = crud_ops_ty { + quote! { #crud_ops_ty } + } else { + quote! { #row_mapper_ty #row_mapper_ty_generics } + } +} + +pub fn filter_fields(fields: &Fields) -> Vec<(Visibility, Ident)> { + fields + .iter() + .map(|field| (field.vis.clone(), field.ident.as_ref().unwrap().clone())) + .collect::>() +} + +pub fn field_has_target_attribute(field: &Field, target_attribute: &str) -> bool { + field.attrs.iter().any(|attr| { + attr.path() + .segments + .first() + .map(|segment| segment.ident == target_attribute) + .unwrap_or(false) + }) +} /// If the `canyon_entity` macro has valid attributes attached, and those attrs are the /// user's desired `table_name` and/or the `schema_name`, this method returns its /// correct form to be wired as the table name that the CRUD methods requires for generate /// the queries -pub fn table_schema_parser(macro_data: &MacroTokens<'_>) -> Result { - let mut table_name: Option = None; - let mut schema: Option = None; +pub fn table_schema_parser<'a>( + macro_data: &MacroTokens<'_>, +) -> Result, TokenStream> { + let mut table_name: Option> = None; + let mut schema: Option> = None; for attr in macro_data.attrs { - if attr - .path - .segments - .iter() - .any(|seg| seg.ident == "canyon_macros" || seg.ident == "canyon_entity") - { - let name_values_result: Result, syn::Error> = - attr.parse_args_with(Punctuated::parse_terminated); - - if let Ok(meta_name_values) = name_values_result { - for nv in meta_name_values { - let ident = nv.path.get_ident(); - if let Some(i) = ident { - let identifier = i; - match &nv.lit { - syn::Lit::Str(s) => { - if identifier == "table_name" { - table_name = Some(s.value()) - } else if identifier == "schema" { - schema = Some(s.value()) - } else { - return Err( - syn::Error::new_spanned( - Ident::new(&identifier.to_string(), i.span()), - "Only string literals are valid values for the attribute arguments" - ).into_compile_error() - ); - } - } - _ => return Err(syn::Error::new_spanned( - Ident::new(&identifier.to_string(), i.span()), - "Only string literals are valid values for the attribute arguments", - ) - .into_compile_error()), - } - } else { - return Err(syn::Error::new( - Span::call_site(), - "Only string literals are valid values for the attribute arguments", - ) - .into_compile_error()); - } - } - } - - let mut final_table_name = String::new(); - if schema.is_some() { - final_table_name.push_str(format!("{}.", schema.unwrap()).as_str()) - } + if __impl::is_canyon_entity_attr(attr) { + parse_canyon_entity_attr(attr, &mut schema, &mut table_name)?; + } + } - if let Some(t_name) = table_name { - final_table_name.push_str(t_name.as_str()) - } else { - let defaulted = - &default_database_table_name_from_entity_name(¯o_data.ty.to_string()); - final_table_name.push_str(defaulted) - } + let mut table_meta = TableMetadata::default(); + if let Some(schema_) = schema { + table_meta.schema(schema_); + } - return Ok(final_table_name); - } + if let Some(t_name) = table_name { + table_meta.table_name(t_name); + } else { + let target_type = if let Some(mapper_ty) = macro_data.retrieve_mapping_target_type() { + mapper_ty.to_string() + } else { + macro_data.ty.to_string() + }; + table_meta.table_name(default_database_table_name_from_entity_name(&target_type)); } - Ok(macro_data.ty.to_string()) + Ok(table_meta) } -/// Parses a syn::Identifier to get a snake case database name from the type identifier -pub fn _database_table_name_from_struct(ty: &Ident) -> String { - let struct_name: String = ty.to_string(); - let mut table_name: String = String::new(); +fn parse_canyon_entity_attr( + attr: &Attribute, + schema: &mut Option>, + table_name: &mut Option>, +) -> Result<(), TokenStream> { + for name_value in __impl::parse_canyon_entity_args(attr)? { + let key = __impl::name_value_key(&name_value)?; + let value = __impl::string_literal_value(&name_value)?; - let mut index = 0; - for char in struct_name.chars() { - if index < 1 { - table_name.push(char.to_ascii_lowercase()); - index += 1; + if key == "schema" { + *schema = Some(Cow::Owned(value)); + } else if key == "table_name" { + *table_name = Some(Cow::Owned(value)); } else { - match char { - n if n.is_ascii_uppercase() => { - table_name.push('_'); - table_name.push(n.to_ascii_lowercase()); - } - _ => table_name.push(char), - } + return Err(__impl::unknown_canyon_entity_arg(&name_value)); } } - table_name + Ok(()) } -/// Parses a syn::Identifier to create a defaulted snake case database table name -#[test] -#[cfg(not(target_env = "msvc"))] -fn test_entity_database_name_defaulter() { - assert_eq!( - default_database_table_name_from_entity_name("League"), - "league".to_owned() - ); - assert_eq!( - default_database_table_name_from_entity_name("MajorLeague"), - "major_league".to_owned() - ); - assert_eq!( - default_database_table_name_from_entity_name("MajorLeagueTournament"), - "major_league_tournament".to_owned() - ); - - assert_ne!( - default_database_table_name_from_entity_name("MajorLeague"), - "majorleague".to_owned() - ); - assert_ne!( - default_database_table_name_from_entity_name("MajorLeague"), - "MajorLeague".to_owned() - ); -} +mod __impl { + use proc_macro2::TokenStream; + use syn::{Attribute, Expr, Lit, Meta, MetaNameValue, Token, punctuated::Punctuated}; -/// Autogenerates a default table name for an entity given their struct name -pub fn default_database_table_name_from_entity_name(ty: &str) -> String { - let struct_name: String = ty.to_string(); - let mut table_name: String = String::new(); + pub(super) fn is_canyon_entity_attr(attr: &Attribute) -> bool { + attr.path() + .segments + .last() + .is_some_and(|segment| segment.ident == "canyon_entity") + } - let mut index = 0; - for char in struct_name.chars() { - if index < 1 { - table_name.push(char.to_ascii_lowercase()); - index += 1; - } else { - match char { - n if n.is_ascii_uppercase() => { - table_name.push('_'); - table_name.push(n.to_ascii_lowercase()); - } - _ => table_name.push(char), - } + pub(super) fn parse_canyon_entity_args( + attr: &Attribute, + ) -> Result, TokenStream> { + match &attr.meta { + Meta::Path(_) => Ok(Punctuated::new()), + Meta::List(_) => attr + .parse_args_with(Punctuated::parse_terminated) + .map_err(syn::Error::into_compile_error), + Meta::NameValue(_) => Err(syn::Error::new_spanned( + &attr.meta, + "`canyon_entity` attribute expects a list of arguments", + ) + .into_compile_error()), } } - table_name -} + pub(super) fn name_value_key(name_value: &MetaNameValue) -> Result<&syn::Ident, TokenStream> { + name_value.path.get_ident().ok_or_else(|| { + syn::Error::new_spanned( + &name_value.path, + "Only simple identifiers are valid keys for `canyon_entity` attribute arguments", + ) + .into_compile_error() + }) + } + + pub(super) fn string_literal_value(name_value: &MetaNameValue) -> Result { + match &name_value.value { + Expr::Lit(expr_lit) => match &expr_lit.lit { + Lit::Str(value) => Ok(value.value()), + _ => Err(syn::Error::new_spanned( + &name_value.value, + "Only string literals are valid values for `canyon_entity` attribute arguments", + ) + .into_compile_error()), + }, + _ => Err(syn::Error::new_spanned( + &name_value.value, + "Only literal expressions are valid values for `canyon_entity` attribute arguments", + ) + .into_compile_error()), + } + } -/// Parses the content of an &str to get the related identifier of a type -pub fn database_table_name_to_struct_ident(name: &str) -> Ident { - let mut struct_name: String = String::new(); + pub(super) fn unknown_canyon_entity_arg(name_value: &MetaNameValue) -> TokenStream { + syn::Error::new_spanned( + &name_value.path, + "Only `table_name` and `schema` are valid `canyon_entity` attribute arguments", + ) + .into_compile_error() + } +} - let mut first_iteration = true; - let mut previous_was_underscore = false; +#[cfg(test)] +mod tests_for_parse_struct_field_attributes { + use super::*; + use syn::{ItemStruct, parse_str}; - for char in name.chars() { - if first_iteration { - struct_name.push(char.to_ascii_uppercase()); - first_iteration = false; - } else { - match char { - '_' => { - previous_was_underscore = true; - } - char if char.is_ascii_lowercase() => { - if previous_was_underscore { - struct_name.push(char.to_ascii_lowercase()) - } else { - struct_name.push(char) - } - } - _ => panic!("Detected wrong format or broken convention for database table names"), + #[test] + fn detects_target_attribute_correctly() { + let input = r#" + struct Test { + #[my_attr] + field1: String, + field2: i32, } - } + "#; + + // Parse the struct + let item: ItemStruct = parse_str(input).expect("Failed to parse struct"); + let fields: Vec<_> = item.fields.iter().collect(); + + // Check the field with #[my_attr] + assert!(field_has_target_attribute(fields[0], "my_attr")); + // Check the field without the attribute + assert!(!field_has_target_attribute(fields[1], "my_attr")); } - Ident::new(&struct_name, proc_macro2::Span::call_site()) + #[test] + fn parses_canyon_entity_table_name_and_schema() { + let input: syn::DeriveInput = parse_str( + r#" + #[canyon_entity(table_name = "users", schema = "public")] + struct User; + "#, + ) + .expect("failed to parse derive input"); + + let mut schema = None; + let mut table_name = None; + + parse_canyon_entity_attr(&input.attrs[0], &mut schema, &mut table_name) + .expect("failed to parse canyon_entity attribute"); + + assert_eq!(table_name.as_deref(), Some("users")); + assert_eq!(schema.as_deref(), Some("public")); + } + + #[test] + fn rejects_unknown_canyon_entity_attribute_keys() { + let input: syn::DeriveInput = parse_str( + r#" + #[canyon_entity(foo = "bar")] + struct User; + "#, + ) + .expect("failed to parse derive input"); + + let mut schema = None; + let mut table_name = None; + + let err = parse_canyon_entity_attr(&input.attrs[0], &mut schema, &mut table_name) + .expect_err("unknown canyon_entity keys must fail"); + + assert!(err.to_string().contains("compile_error")); + assert_eq!(table_name, None); + assert_eq!(schema, None); + } + + #[test] + fn rejects_non_string_canyon_entity_attribute_values() { + let input: syn::DeriveInput = parse_str( + r#" + #[canyon_entity(table_name = 42)] + struct User; + "#, + ) + .expect("failed to parse derive input"); + + let mut schema = None; + let mut table_name = None; + + let err = parse_canyon_entity_attr(&input.attrs[0], &mut schema, &mut table_name) + .expect_err("non-string canyon_entity values must fail"); + + assert!(err.to_string().contains("compile_error")); + assert_eq!(table_name, None); + assert_eq!(schema, None); + } } diff --git a/canyon_macros/src/utils/macro_tokens.rs b/canyon_macros/src/utils/macro_tokens.rs index 415d9ccc..76d4a741 100644 --- a/canyon_macros/src/utils/macro_tokens.rs +++ b/canyon_macros/src/utils/macro_tokens.rs @@ -1,8 +1,12 @@ -use std::convert::TryFrom; - -use canyon_entities::field_annotation::EntityFieldAnnotation; +use crate::utils::{ + canyon_crud_attribute::CanyonCrudAttribute, primary_key_attribute::PrimaryKeyAttribute, +}; +use canyon_entities::{ + field_annotation::EntityFieldAnnotation, helpers::default_database_table_name_from_entity_name, +}; use proc_macro2::Ident; -use syn::{Attribute, DeriveInput, Fields, Generics, Type, Visibility}; +use std::convert::TryFrom; +use syn::{Attribute, DeriveInput, Field, Fields, Generics, Type, Visibility}; /// Provides a convenient way of store the data for the TokenStream /// received on a macro @@ -13,32 +17,57 @@ pub struct MacroTokens<'a> { pub generics: &'a Generics, pub attrs: &'a Vec, pub fields: &'a Fields, + // -------- the new fields that must help to avoid recalculations every time that the user compiles + pub(crate) canyon_crud_attribute: Option, // Type level + pub(crate) primary_key_attribute: Option>, // Field level, quick access without iterations } impl<'a> MacroTokens<'a> { - pub fn new(ast: &'a DeriveInput) -> Self { - Self { - vis: &ast.vis, - ty: &ast.ident, - generics: &ast.generics, - attrs: &ast.attrs, - fields: match &ast.data { - syn::Data::Struct(ref s) => &s.fields, - _ => panic!("This derive macro can only be automatically derived for structs"), - }, + pub fn new(ast: &'a DeriveInput) -> Result { + // TODO: impl syn::parse instead + if let syn::Data::Struct(ref s) = ast.data { + let attrs = &ast.attrs; + + let primary_key_attribute = __details::find_primary_key_field_annotation(&s.fields) + .map(PrimaryKeyAttribute::from); + + let mut canyon_crud_attribute = None; + for attr in attrs { + if attr.path().is_ident("canyon_crud") { + canyon_crud_attribute = Some(attr.parse_args::()?); + } + } + + Ok(Self { + vis: &ast.vis, + ty: &ast.ident, + generics: &ast.generics, + attrs: &ast.attrs, + fields: &s.fields, + canyon_crud_attribute, + primary_key_attribute, + }) + } else { + __details::raise_canyon_crud_only_for_structs_err() } } - /// Gives a Vec of tuples that contains the visibility, the name and - /// the type of every field on a Struct - pub fn _fields_with_visibility_and_types(&self) -> Vec<(Visibility, Ident, Type)> { + pub fn retrieve_mapping_target_type(&self) -> &Option { + if let Some(canyon_crud_attribute) = &self.canyon_crud_attribute { + &canyon_crud_attribute.maps_to + } else { + &None + } + } + + pub fn fields(&self) -> Vec<(Visibility, Ident, Type)> { self.fields .iter() .map(|field| { ( field.vis.clone(), - field.ident.as_ref().unwrap().clone(), - field.ty.clone(), + field.ident.clone().unwrap(), + field.clone().ty, ) }) .collect::>() @@ -46,102 +75,80 @@ impl<'a> MacroTokens<'a> { /// Gives a Vec of tuples that contains the name and /// the type of every field on a Struct - pub fn _fields_with_types(&self) -> Vec<(Ident, Type)> { + pub fn fields_with_types(&self) -> Vec<(&Ident, &Type)> { self.fields .iter() - .map(|field| (field.ident.as_ref().unwrap().clone(), field.ty.clone())) + .map(|field| (field.ident.as_ref().unwrap(), &field.ty)) .collect::>() } - /// Gives a Vec of Ident with the fields of a Struct - pub fn get_struct_fields(&self) -> Vec { + pub fn get_struct_fields_as_table_column_pairs(&self) -> Vec<(String, String)> { + let table_name = default_database_table_name_from_entity_name(&self.ty.to_string()); + self.fields .iter() - .map(|field| field.ident.as_ref().unwrap().clone()) - .collect::>() + .map(|field| { + let column_name = field.ident.as_ref().unwrap().to_string(); + (table_name.clone(), column_name) + }) + .collect() } - /// Gives a Vec populated with the name of the fields of the struct - pub fn _get_struct_fields_as_collection_strings(&self) -> Vec { - self.get_struct_fields() - .iter() - .map(|ident| ident.to_owned().to_string()) - .collect::>() - } + pub fn get_columns_skipping_pk(&self) -> impl Iterator { + let primary_key = self.primary_key_attribute.as_ref().map(|pk| &pk.ident); - /// Returns a Vec populated with the name of the fields of the struct - /// already quote scaped for avoid the upper case column name mangling. - /// - /// If the type contains a `#[primary_key]` annotation (and), returns the - /// name of the columns without the fields that maps against the column designed as - /// primary key (if its present and its autoincremental attribute is set to true) - /// (autoincremental = true) or its without the autoincremental attribute, which leads - /// to the same behaviour. - /// - /// Returns every field if there's no PK, or if it's present but autoincremental = false - pub fn get_column_names_pk_parsed(&self) -> Vec { - self.fields - .iter() - .filter(|field| { - if !field.attrs.is_empty() { - field.attrs.iter().any(|attr| { - let a = attr.path.segments[0].clone().ident; - let b = attr.tokens.to_string(); - !(a == "primary_key" || b.contains("false")) - }) - } else { - true - } - }) - .map(|c| format!("\"{}\"", c.ident.as_ref().unwrap())) - .collect::>() + self.fields.iter().filter(move |field| { + !matches!( + (primary_key, field.ident.as_ref()), + (Some(pk), Some(field_ident)) + if field_ident == *pk && __details::primary_key_is_autoincremental(field) + ) + }) } - /// Retrieves the fields of the Struct as continuous String, comma separated - pub fn get_struct_fields_as_strings(&self) -> String { - let column_names: String = self - .get_struct_fields() - .iter() - .map(|ident| ident.to_owned().to_string()) - .collect::>() - .iter() - .map(|column| column.to_owned() + ", ") - .collect::(); + pub fn get_struct_fields_as_table_column_pairs_skipping_pk(&self) -> Vec<(String, String)> { + let table_name = default_database_table_name_from_entity_name(&self.ty.to_string()); - let mut column_names_as_chars = column_names.chars(); - column_names_as_chars.next_back(); - column_names_as_chars.next_back(); + self.get_columns_skipping_pk() + .map(|field| { + let column_name = field + .ident + .as_ref() + .expect("Struct fields must be named") + .to_string(); - column_names_as_chars.as_str().to_owned() + (table_name.clone(), column_name) + }) + .collect() } - /// Retrieves the value of the index of an annotated field with #[primary_key] - pub fn get_pk_index(&self) -> Option { - let mut pk_index = None; - for (idx, field) in self.fields.iter().enumerate() { - for attr in &field.attrs { - if attr.path.segments[0].clone().ident == "primary_key" { - pk_index = Some(idx); - } - } - } - pk_index + /// Returns a collection with all the [`syn::Ident`] for all the type members, skipping (if present) + /// the field which is annotated with #[primary_key] + pub fn get_fields_idents_skipping_pk(&self) -> impl Iterator { + self.get_columns_skipping_pk() + .map(|field| field.ident.as_ref().unwrap()) + } + + pub fn get_primary_key_field_annotation(&self) -> Option<&PrimaryKeyAttribute<'a>> { + self.primary_key_attribute.as_ref() } /// Utility for find the primary key attribute (if exists) and the /// column name (field) which belongs pub fn get_primary_key_annotation(&self) -> Option { - let f = self.fields.iter().find(|field| { - field - .attrs - .iter() - .map(|attr| attr.path.segments[0].clone().ident) - .map(|ident| ident.to_string()) - .find(|a| a == "primary_key") - == Some("primary_key".to_string()) - }); + self.get_primary_key_field_annotation() + .map(|attr| attr.ident.clone().to_string()) + } - f.map(|v| v.ident.clone().unwrap().to_string()) + pub fn get_primary_key_ident_and_type(&self) -> Option<(&Ident, &Type)> { + let primary_key = self.get_primary_key_annotation(); + if let Some(primary_key) = primary_key { + self.fields_with_types() + .into_iter() + .find(|(i, _t)| i.to_string() == primary_key) + } else { + None + } } /// Utility for find the `foreign_key` attributes (if exists) @@ -152,7 +159,7 @@ impl<'a> MacroTokens<'a> { let attrs = field .attrs .iter() - .filter(|attr| attr.path.segments[0].clone().ident == "foreign_key"); + .filter(|attr| attr.path().segments[0].clone().ident == "foreign_key"); attrs.for_each(|attr| { let fk_parse = EntityFieldAnnotation::try_from(&attr); if let Ok(fk_annotation) = fk_parse { @@ -163,46 +170,39 @@ impl<'a> MacroTokens<'a> { foreign_key_annotations } +} - /// Boolean that returns true if the type contains a `#[primary_key]` - /// annotation. False otherwise. - pub fn type_has_primary_key(&self) -> bool { - self.fields.iter().any(|field| { - field - .attrs - .iter() - .map(|attr| attr.path.segments[0].clone().ident) - .map(|ident| ident.to_string()) - .find(|a| a == "primary_key") - == Some("primary_key".to_string()) +mod __details { + use crate::utils::{helpers, macro_tokens::MacroTokens}; + use canyon_entities::field_annotation::EntityFieldAnnotation; + use proc_macro2::Span; + use syn::{Field, Fields}; + + pub(super) fn find_primary_key_field_annotation(fields: &Fields) -> Option<&Field> { + fields.iter().enumerate().find_map(|index_and_field| { + let field = index_and_field.1; + if helpers::field_has_target_attribute(field, "primary_key") { + Some(field) + } else { + None + } }) } - /// Returns an String ready to be inserted on the VALUES Sql clause - /// representing generic query parameters ($x). - /// - /// Already returns the correct number of placeholders, skipping one - /// entry in the type contains a `#[primary_key]` - pub fn placeholders_generator(&self) -> String { - let mut placeholders = String::new(); - if self.type_has_primary_key() { - for num in 1..self.fields.len() { - if num < self.fields.len() - 1 { - placeholders.push_str(&("$".to_owned() + &(num).to_string() + ", ")); - } else { - placeholders.push_str(&("$".to_owned() + &(num).to_string())); - } - } - } else { - for num in 1..self.fields.len() + 1 { - if num < self.fields.len() { - placeholders.push_str(&("$".to_owned() + &(num).to_string() + ", ")); - } else { - placeholders.push_str(&("$".to_owned() + &(num).to_string())); - } - } - } + pub(super) fn primary_key_is_autoincremental(field: &Field) -> bool { + field + .attrs + .iter() + .find(|attr| attr.path().is_ident("primary_key")) + .and_then(|attr| EntityFieldAnnotation::try_from(&attr).ok()) + .is_none_or(|annotation| matches!(annotation, EntityFieldAnnotation::PrimaryKey(true))) + } - placeholders + pub(crate) fn raise_canyon_crud_only_for_structs_err<'a>() -> Result, syn::Error> + { + Err(syn::Error::new( + Span::call_site(), + "CanyonCrud may only be implemented for structs", + )) } } diff --git a/canyon_macros/src/utils/mod.rs b/canyon_macros/src/utils/mod.rs index be2269df..f8529a05 100644 --- a/canyon_macros/src/utils/mod.rs +++ b/canyon_macros/src/utils/mod.rs @@ -1,3 +1,5 @@ +mod canyon_crud_attribute; pub mod function_parser; pub mod helpers; pub mod macro_tokens; +pub(crate) mod primary_key_attribute; diff --git a/canyon_macros/src/utils/primary_key_attribute.rs b/canyon_macros/src/utils/primary_key_attribute.rs new file mode 100644 index 00000000..d108f1a8 --- /dev/null +++ b/canyon_macros/src/utils/primary_key_attribute.rs @@ -0,0 +1,34 @@ +use proc_macro2::Ident; +use quote::ToTokens; +use std::fmt::{Display, Formatter}; +use syn::{Field, Type}; + +pub(crate) struct PrimaryKeyAttribute<'a> { + pub ident: &'a Ident, + pub ty: &'a Type, + pub name: String, +} + +impl<'a> Display for &'a PrimaryKeyAttribute<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let _ = f.write_fmt(format_args!( + "ident:{},ty:{},name:{}", + self.ident, + self.ty.to_token_stream(), + self.name + )); + Ok(()) + } +} + +/// Ad-hoc creation for the process of parsing a primary key attribute along with its index position +/// on the struct +impl<'a> From<&'a Field> for PrimaryKeyAttribute<'a> { + fn from(field: &'a Field) -> Self { + Self { + ident: field.ident.as_ref().unwrap(), + ty: &field.ty, + name: field.ident.as_ref().unwrap().to_string(), + } + } +} diff --git a/canyon_migrations/Cargo.toml b/canyon_migrations/Cargo.toml index ec9a31db..b8ab15f3 100644 --- a/canyon_migrations/Cargo.toml +++ b/canyon_migrations/Cargo.toml @@ -10,28 +10,21 @@ license.workspace = true description.workspace = true [dependencies] +canyon_core = { workspace = true } canyon_crud = { workspace = true } -canyon_connection = { workspace = true } canyon_entities = { workspace = true } -tokio = { workspace = true } tokio-postgres = { workspace = true, optional = true } tiberius = { workspace = true, optional = true } mysql_async = { workspace = true, optional = true } mysql_common = { workspace = true, optional = true } - -async-trait = { workspace = true } - regex = { workspace = true } partialdebug = { workspace = true } walkdir = { workspace = true } -proc-macro2 = { workspace = true } -quote = { workspace = true } -syn = { version = "1.0.86", features = ["full", "parsing"] } # TODO Pending to refactor and upgrade [features] -postgres = ["tokio-postgres", "canyon_connection/postgres", "canyon_crud/postgres"] -mssql = ["tiberius", "canyon_connection/mssql", "canyon_crud/mssql"] -mysql = ["mysql_async","mysql_common", "canyon_connection/mysql", "canyon_crud/mysql"] +postgres = ["tokio-postgres", "canyon_core/postgres", "canyon_crud/postgres"] +mssql = ["tiberius", "canyon_core/mssql", "canyon_crud/mssql"] +mysql = ["mysql_async", "mysql_common", "canyon_core/mysql", "canyon_crud/mysql"] diff --git a/canyon_migrations/src/constants.rs b/canyon_migrations/src/constants.rs index 9f025762..7674efe5 100644 --- a/canyon_migrations/src/constants.rs +++ b/canyon_migrations/src/constants.rs @@ -168,98 +168,3 @@ pub mod sqlserver_type { pub const TIME: &str = "TIME"; pub const DATETIME: &str = "DATETIME2"; } - -pub mod mocked_data { - use crate::migrations::information_schema::{ColumnMetadata, TableMetadata}; - use canyon_connection::lazy_static::lazy_static; - - lazy_static! { - pub static ref TABLE_METADATA_LEAGUE_EX: TableMetadata = TableMetadata { - table_name: "league".to_string(), - columns: vec![ - ColumnMetadata { - column_name: "id".to_owned(), - datatype: "int".to_owned(), - character_maximum_length: None, - is_nullable: false, - column_default: None, - foreign_key_info: None, - foreign_key_name: None, - primary_key_info: Some("PK__league__3213E83FBDA92571".to_owned()), - primary_key_name: Some("PK__league__3213E83FBDA92571".to_owned()), - is_identity: false, - identity_generation: None - }, - ColumnMetadata { - column_name: "ext_id".to_owned(), - datatype: "bigint".to_owned(), - character_maximum_length: None, - is_nullable: false, - column_default: None, - foreign_key_info: None, - foreign_key_name: None, - primary_key_info: None, - primary_key_name: None, - is_identity: false, - identity_generation: None - }, - ColumnMetadata { - column_name: "slug".to_owned(), - datatype: "nvarchar".to_owned(), - character_maximum_length: None, - is_nullable: false, - column_default: None, - foreign_key_info: None, - foreign_key_name: None, - primary_key_info: None, - primary_key_name: None, - is_identity: false, - identity_generation: None - }, - ColumnMetadata { - column_name: "name".to_owned(), - datatype: "nvarchar".to_owned(), - character_maximum_length: None, - is_nullable: false, - column_default: None, - foreign_key_info: None, - foreign_key_name: None, - primary_key_info: None, - primary_key_name: None, - is_identity: false, - identity_generation: None - }, - ColumnMetadata { - column_name: "region".to_owned(), - datatype: "nvarchar".to_owned(), - character_maximum_length: None, - is_nullable: false, - column_default: None, - foreign_key_info: None, - foreign_key_name: None, - primary_key_info: None, - primary_key_name: None, - is_identity: false, - identity_generation: None - }, - ColumnMetadata { - column_name: "image_url".to_owned(), - datatype: "nvarchar".to_owned(), - character_maximum_length: None, - is_nullable: false, - column_default: None, - foreign_key_info: None, - foreign_key_name: None, - primary_key_info: None, - primary_key_name: None, - is_identity: false, - identity_generation: None - } - ] - }; - pub static ref NON_MATCHING_TABLE_METADATA: TableMetadata = TableMetadata { - table_name: "random_name_to_assert_false".to_string(), - columns: vec![] - }; - } -} diff --git a/canyon_migrations/src/lib.rs b/canyon_migrations/src/lib.rs index 5743cc8b..757597cc 100644 --- a/canyon_migrations/src/lib.rs +++ b/canyon_migrations/src/lib.rs @@ -11,35 +11,26 @@ /// in order to perform the migrations pub mod migrations; -extern crate canyon_connection; extern crate canyon_crud; extern crate canyon_entities; mod constants; -use canyon_connection::lazy_static::lazy_static; +use std::sync::OnceLock; use std::{collections::HashMap, sync::Mutex}; -lazy_static! { - pub static ref QUERIES_TO_EXECUTE: Mutex>> = - Mutex::new(HashMap::new()); - pub static ref CM_QUERIES_TO_EXECUTE: Mutex>> = - Mutex::new(HashMap::new()); -} +pub static QUERIES_TO_EXECUTE: OnceLock>>> = OnceLock::new(); +pub static CM_QUERIES_TO_EXECUTE: OnceLock>>> = OnceLock::new(); /// Stores a newly generated SQL statement from the migrations into the register pub fn save_migrations_query_to_execute(stmt: String, ds_name: &str) { - if QUERIES_TO_EXECUTE.lock().unwrap().contains_key(ds_name) { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .get_mut(ds_name) - .unwrap() - .push(stmt); + // Access the QUERIES_TO_EXECUTE hash map and lock it for safe access + let queries_to_execute = QUERIES_TO_EXECUTE.get_or_init(|| Mutex::new(HashMap::new())); + let mut queries = queries_to_execute.lock().unwrap(); + + if queries.contains_key(ds_name) { + queries.get_mut(ds_name).unwrap().push(stmt); } else { - QUERIES_TO_EXECUTE - .lock() - .unwrap() - .insert(ds_name.to_owned(), vec![stmt]); + queries.insert(ds_name.to_owned(), vec![stmt]); } } diff --git a/canyon_migrations/src/migrations/handler.rs b/canyon_migrations/src/migrations/handler.rs index 3d00da8b..416d1e89 100644 --- a/canyon_migrations/src/migrations/handler.rs +++ b/canyon_migrations/src/migrations/handler.rs @@ -1,43 +1,38 @@ -use canyon_connection::{datasources::Migrations as MigrationsStatus, DATASOURCES}; -use canyon_crud::rows::CanyonRows; -use canyon_entities::CANYON_REGISTER_ENTITIES; -use partialdebug::placeholder::PartialDebug; - use crate::{ - canyon_crud::{ - bounds::{Column, Row, RowOperations}, - crud::Transaction, - DatabaseType, - }, + canyon_crud::DatabaseType, constants, migrations::{ - information_schema::{ColumnMetadata, ColumnMetadataTypeValue, TableMetadata}, + information_schema::{ColumnMetadata, ColumnMetadataTypeValue, MacroTableMetadata}, memory::CanyonMemory, processor::MigrationsProcessor, }, }; +use canyon_core::canyon::Canyon; +use canyon_core::{ + column::Column, + connection::db_connector::DatabaseConnector, + row::{Row, RowOperations}, + rows::CanyonRows, + transaction::Transaction, +}; +use canyon_entities::CANYON_REGISTER_ENTITIES; +use partialdebug::placeholder::PartialDebug; #[derive(PartialDebug)] pub struct Migrations; // Makes this structure able to make queries to the database -impl Transaction for Migrations {} +impl Transaction for Migrations {} impl Migrations { /// Launches the mechanism to parse the Database schema, the Canyon register /// and the database table with the memory of Canyon to perform the /// migrations over the targeted database pub async fn migrate() { - for datasource in DATASOURCES.iter() { - if datasource - .properties - .migrations - .filter(|status| !status.eq(&MigrationsStatus::Disabled)) - .is_none() - { - println!( - "Skipped datasource: {:?} for being disabled (or not configured)", - datasource.name - ); + for datasource in Canyon::instance() + .expect("Failure getting datasources on migrations") + .datasources() + { + if !datasource.has_migrations_enabled() { continue; } println!( @@ -46,13 +41,22 @@ impl Migrations { ); let mut migrations_processor = MigrationsProcessor::default(); + let db_conn = Canyon::instance() + .unwrap_or_else(|_| panic!("Failure getting db connection: {}", datasource.name)) + .get_connection(&datasource.name) + .unwrap_or_else(|_| { + panic!( + "Unable to get a database connection on the migrations processor for: {:?}", + datasource.name + ) + }); let canyon_entities = CANYON_REGISTER_ENTITIES.lock().unwrap().to_vec(); let canyon_memory = CanyonMemory::remember(datasource, &canyon_entities).await; // Tracked entities that must be migrated whenever Canyon starts let schema_status = - Self::fetch_database(&datasource.name, datasource.get_db_type()).await; + Self::fetch_database(&datasource.name, db_conn, datasource.get_db_type()).await; let database_tables_schema_info = Self::map_rows(schema_status, datasource.get_db_type()); @@ -84,11 +88,12 @@ impl Migrations { } /// Fetches a concrete schema metadata by target the database - /// chosen by it's datasource name property + /// chosen by its datasource name property async fn fetch_database( - datasource_name: &str, + ds_name: &str, + db_conn: &DatabaseConnector, db_type: DatabaseType, - ) -> CanyonRows { + ) -> CanyonRows { let query = match db_type { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => constants::postgresql_queries::FETCH_PUBLIC_SCHEMA, @@ -98,39 +103,39 @@ impl Migrations { DatabaseType::MySQL => todo!("Not implemented fetch database in mysql"), }; - Self::query(query, [], datasource_name) + Self::query_rows(query, [], db_conn) .await .unwrap_or_else(|_| { - panic!( - "Error querying the schema information for the datasource: {datasource_name}" - ) + panic!("Error querying the schema information for the datasource: {ds_name}") }) } /// Handler for parse the result of query the information of some database schema, /// and extract the content of the returned rows into custom structures with /// the data well organized for every entity present on that schema - fn map_rows(db_results: CanyonRows, db_type: DatabaseType) -> Vec { + #[allow(unreachable_patterns)] + fn map_rows(db_results: CanyonRows, db_type: DatabaseType) -> Vec { match db_results { #[cfg(feature = "postgres")] CanyonRows::Postgres(v) => Self::process_tp_rows(v, db_type), #[cfg(feature = "mssql")] CanyonRows::Tiberius(v) => Self::process_tib_rows(v, db_type), - _ => panic!(), + #[cfg(feature = "mysql")] + CanyonRows::MySQL(_) => panic!("Not implemented fetch database in mysql"), } } /// Parses all the [`Row`] after query the information of the targeted schema, - /// grouping them in [`TableMetadata`] structs, by relating every [`Row`] that has + /// grouping them in [`MacroTableMetadata`] structs, by relating every [`Row`] that has /// the same "table_name" (asked with column.name()) being one field of the new - /// [`TableMetadata`], and parsing the other columns that belongs to that entity + /// [`MacroTableMetadata`], and parsing the other columns that belongs to that entity /// and appending as a new [`ColumnMetadata`] element to the columns field. - fn get_columns_metadata(res_row: &dyn Row, table: &mut TableMetadata) { + fn get_columns_metadata(res_row: &dyn Row, table: &mut MacroTableMetadata) { let mut entity_column = ColumnMetadata::default(); for column in res_row.columns().iter() { if column.name() != "table_name" { Self::set_column_metadata(res_row, column, &mut entity_column); - } // Discards the column "table_name", 'cause is already a field of [`TableMetadata`] + } // Discards the column "table_name", 'cause is already a field of [`TableMetadata<'a>`] } table.columns.push(entity_column); } @@ -197,10 +202,10 @@ impl Migrations { "YES" ) } - } else if column_identifier == "identity_generation" { - if let ColumnMetadataTypeValue::StringValue(value) = &column_value { - dest.identity_generation = value.to_owned() - } + } else if column_identifier == "identity_generation" + && let ColumnMetadataTypeValue::StringValue(value) = &column_value + { + dest.identity_generation = value.to_owned() }; } @@ -208,8 +213,8 @@ impl Migrations { fn process_tp_rows( db_results: Vec, db_type: DatabaseType, - ) -> Vec { - let mut schema_info: Vec = Vec::new(); + ) -> Vec { + let mut schema_info: Vec = Vec::new(); for res_row in db_results.iter() { let unique_table = schema_info .iter_mut() @@ -224,7 +229,7 @@ impl Migrations { /* If there's no table for a given "table_name" property on the collection yet, we must create a new instance and attach it the founded columns data in this iteration */ - let mut new_table = TableMetadata { + let mut new_table = MacroTableMetadata { table_name: get_table_name_from_tp_row(res_row), columns: Vec::new(), }; @@ -241,8 +246,8 @@ impl Migrations { fn process_tib_rows( db_results: Vec, db_type: DatabaseType, - ) -> Vec { - let mut schema_info: Vec = Vec::new(); + ) -> Vec { + let mut schema_info: Vec = Vec::new(); for res_row in db_results.iter() { let unique_table = schema_info .iter_mut() @@ -257,7 +262,7 @@ impl Migrations { /* If there's no table for a given "table_name" property on the collection yet, we must create a new instance and attach it the founded columns data in this iteration */ - let mut new_table = TableMetadata { + let mut new_table = MacroTableMetadata { table_name: get_table_name_from_tib_row(res_row), columns: Vec::new(), }; @@ -284,7 +289,7 @@ fn get_table_name_from_tib_row(res_row: &tiberius::Row) -> String { } fn check_for_table_name( - table: &&mut TableMetadata, + table: &&mut MacroTableMetadata, db_type: DatabaseType, res_row: &dyn Row, ) -> bool { @@ -294,6 +299,6 @@ fn check_for_table_name( #[cfg(feature = "mssql")] DatabaseType::SqlServer => table.table_name == res_row.get_mssql::<&str>("table_name"), #[cfg(feature = "mysql")] - DatabaseType::MySQL => todo!(), + DatabaseType::MySQL => todo!("Not implemented fetch database in mysql"), } } diff --git a/canyon_migrations/src/migrations/information_schema.rs b/canyon_migrations/src/migrations/information_schema.rs index 9e165eee..77cb47a3 100644 --- a/canyon_migrations/src/migrations/information_schema.rs +++ b/canyon_migrations/src/migrations/information_schema.rs @@ -1,16 +1,19 @@ #[cfg(feature = "mssql")] -use canyon_connection::tiberius::ColumnType as TIB_TY; +use canyon_core::connection::tiberius::ColumnType as TIB_TY; #[cfg(feature = "postgres")] -use canyon_connection::tokio_postgres::types::Type as TP_TYP; -use canyon_crud::bounds::{Column, ColumnType, Row, RowOperations}; +use canyon_core::connection::tokio_postgres::types::Type as TP_TYP; +use canyon_core::{ + column::{Column, ColumnType}, + row::{Row, RowOperations}, +}; /// Model that represents the database entities that belongs to the current schema. /// /// Basically, it's an agrupation of rows of results when Canyon queries the `information schema` -/// table, grouping by table name (one [`TableMetadata`] is the rows that contains the information +/// table, grouping by table name (one [`MacroTableMetadata`] is the rows that contains the information /// of a table) #[derive(Debug)] -pub struct TableMetadata { +pub struct MacroTableMetadata { pub table_name: String, pub columns: Vec, } diff --git a/canyon_migrations/src/migrations/memory.rs b/canyon_migrations/src/migrations/memory.rs index 1ad6263a..a0346a56 100644 --- a/canyon_migrations/src/migrations/memory.rs +++ b/canyon_migrations/src/migrations/memory.rs @@ -1,8 +1,13 @@ use crate::constants; -use canyon_crud::{crud::Transaction, DatabaseType, DatasourceConfig}; +use canyon_core::canyon::Canyon; +use canyon_core::connection::contracts::DbConnection; +use canyon_core::connection::db_connector::DatabaseConnector; +use canyon_core::transaction::Transaction; +use canyon_crud::{DatabaseType, DatasourceConfig}; use regex::Regex; use std::collections::HashMap; use std::fs; +use std::sync::Mutex; use walkdir::WalkDir; use canyon_entities::register_types::CanyonRegisterEntity; @@ -52,7 +57,7 @@ pub struct CanyonMemory { } // Makes this structure able to make queries to the database -impl Transaction for CanyonMemory {} +impl Transaction for CanyonMemory {} impl CanyonMemory { /// Queries the database to retrieve internal data about the structures @@ -62,11 +67,27 @@ impl CanyonMemory { datasource: &DatasourceConfig, canyon_entities: &[CanyonRegisterEntity<'_>], ) -> Self { + let db_conn = Canyon::instance() + .unwrap_or_else(|_| { + panic!( + "Failure getting db connection: {} on Canyon Memory", + datasource.name + ) + }) + .get_connection(&datasource.name) + .unwrap_or_else(|_| { + panic!( + "Unable to get a database connection on Canyon Memory: {:?}", + datasource.name + ) + }); + // Creates the memory table if not exists - Self::create_memory(&datasource.name, &datasource.get_db_type()).await; + Self::create_memory(&datasource.name, db_conn, &datasource.get_db_type()).await; // Retrieve the last status data from the `canyon_memory` table - let res = Self::query("SELECT * FROM canyon_memory", [], &datasource.name) + let res = db_conn + .query_rows("SELECT * FROM canyon_memory", &[]) .await .expect("Error querying Canyon Memory"); @@ -126,28 +147,27 @@ impl CanyonMemory { || el.declared_table_name == _struct.declared_table_name }); - if let Some(old) = already_in_db { - if !(old.filepath == _struct.filepath + if let Some(old) = already_in_db + && !(old.filepath == _struct.filepath && old.struct_name == _struct.struct_name && old.declared_table_name == _struct.declared_table_name) - { - updates.push(&old.struct_name); - let stmt = format!( - "UPDATE canyon_memory SET filepath = '{}', struct_name = '{}', declared_table_name = '{}' \ + { + updates.push(&old.struct_name); + let stmt = format!( + "UPDATE canyon_memory SET filepath = '{}', struct_name = '{}', declared_table_name = '{}' \ WHERE id = {}", - _struct.filepath, _struct.struct_name, _struct.declared_table_name, old.id - ); - save_canyon_memory_query(stmt, &datasource.name); + _struct.filepath, _struct.struct_name, _struct.declared_table_name, old.id + ); + save_canyon_memory_query(stmt, &datasource.name); - // if the updated element is the struct name, we add it to the table_rename Hashmap - let rename_table = old.declared_table_name != _struct.declared_table_name; + // if the updated element is the struct name, we add it to the table_rename Hashmap + let rename_table = old.declared_table_name != _struct.declared_table_name; - if rename_table { - mem.renamed_entities.insert( - _struct.declared_table_name.to_string(), // The new one - old.declared_table_name.to_string(), // The old one - ); - } + if rename_table { + mem.renamed_entities.insert( + _struct.declared_table_name.to_string(), // The new one + old.declared_table_name.to_string(), // The old one + ); } } @@ -187,6 +207,7 @@ impl CanyonMemory { &mut self, canyon_entities: &[CanyonRegisterEntity<'_>], ) { + let re = Regex::new(r#"\bstruct\s+(\w+)"#).unwrap(); for file in WalkDir::new("./src") .into_iter() .filter_map(|file| file.ok()) @@ -208,7 +229,6 @@ impl CanyonMemory { canyon_entity_macro_counter += 1; } - let re = Regex::new(r#"\bstruct\s+(\w+)"#).unwrap(); if let Some(captures) = re.captures(line) { struct_name.push_str(captures.get(1).unwrap().as_str()); } @@ -240,7 +260,11 @@ impl CanyonMemory { } /// Generates, if not exists the `canyon_memory` table - async fn create_memory(datasource_name: &str, database_type: &DatabaseType) { + async fn create_memory( + datasource_name: &str, + db_conn: &DatabaseConnector, + database_type: &DatabaseType, + ) { let query = match database_type { #[cfg(feature = "postgres")] DatabaseType::PostgreSql => constants::postgresql_queries::CANYON_MEMORY_TABLE, @@ -250,28 +274,19 @@ impl CanyonMemory { DatabaseType::MySQL => todo!("Memory table in mysql not implemented"), }; - Self::query(query, [], datasource_name) + Self::query_rows(query, [], db_conn) .await - .expect("Error creating the 'canyon_memory' table"); + .unwrap_or_else(|_| panic!("Error creating the 'canyon_memory' table while processing the datasource: {datasource_name}")); } } fn save_canyon_memory_query(stmt: String, ds_name: &str) { use crate::CM_QUERIES_TO_EXECUTE; - if CM_QUERIES_TO_EXECUTE.lock().unwrap().contains_key(ds_name) { - CM_QUERIES_TO_EXECUTE - .lock() - .unwrap() - .get_mut(ds_name) - .unwrap() - .push(stmt); - } else { - CM_QUERIES_TO_EXECUTE - .lock() - .unwrap() - .insert(ds_name.to_owned(), vec![stmt]); - } + let mutex = CM_QUERIES_TO_EXECUTE.get_or_init(|| Mutex::new(HashMap::new())); + let mut queries = mutex.lock().expect("Mutex poisoned"); + + queries.entry(ds_name.to_owned()).or_default().push(stmt); } /// Represents a single row from the `canyon_memory` table diff --git a/canyon_migrations/src/migrations/processor.rs b/canyon_migrations/src/migrations/processor.rs index 9296689f..98905eed 100644 --- a/canyon_migrations/src/migrations/processor.rs +++ b/canyon_migrations/src/migrations/processor.rs @@ -1,17 +1,19 @@ //! File that contains all the datatypes and logic to perform the migrations //! over a target database -use async_trait::async_trait; +use crate::canyon_crud::DatasourceConfig; +use crate::constants::regex_patterns; +use crate::save_migrations_query_to_execute; +use canyon_core::canyon::Canyon; +use canyon_core::connection::contracts::DbConnection; +use canyon_core::transaction::Transaction; use canyon_crud::DatabaseType; use regex::Regex; use std::collections::HashMap; use std::fmt::Debug; +use std::future::Future; use std::ops::Not; -use crate::canyon_crud::{crud::Transaction, DatasourceConfig}; -use crate::constants::regex_patterns; -use crate::save_migrations_query_to_execute; - -use super::information_schema::{ColumnMetadata, TableMetadata}; +use super::information_schema::{ColumnMetadata, MacroTableMetadata}; use super::memory::CanyonMemory; #[cfg(feature = "postgres")] use crate::migrations::transforms::{to_postgres_alter_syntax, to_postgres_syntax}; @@ -23,19 +25,23 @@ use canyon_entities::register_types::{CanyonRegisterEntity, CanyonRegisterEntity /// Rust source code managed by Canyon, for successfully make the migrations #[derive(Debug, Default)] pub struct MigrationsProcessor { - operations: Vec>, - set_primary_key_operations: Vec>, - drop_primary_key_operations: Vec>, - constraints_operations: Vec>, + table_operations: Vec, + column_operations: Vec, + set_primary_key_operations: Vec, + drop_primary_key_operations: Vec, + constraints_table_operations: Vec, + constraints_column_operations: Vec, + #[cfg(feature = "postgres")] + constraints_sequence_operations: Vec, } -impl Transaction for MigrationsProcessor {} +impl Transaction for MigrationsProcessor {} impl MigrationsProcessor { pub async fn process<'a>( &'a mut self, canyon_memory: CanyonMemory, canyon_entities: Vec>, - database_tables: Vec<&'a TableMetadata>, + database_tables: Vec<&'a MacroTableMetadata>, datasource: &'_ DatasourceConfig, ) { // The database type formally represented in Canyon @@ -66,7 +72,7 @@ impl MigrationsProcessor { db_type, ); - // For each field (column) on the this canyon register entity + // For each field (column) on the canyon register entity for canyon_register_field in canyon_register_entity.entity_fields { let current_column_metadata = MigrationsHelper::get_current_column_metadata( canyon_register_field.field_name.clone(), @@ -106,7 +112,10 @@ impl MigrationsProcessor { } } - for operation in &self.operations { + for operation in &self.table_operations { + operation.generate_sql(datasource).await; // This should be moved again to runtime + } + for operation in &self.column_operations { operation.generate_sql(datasource).await; // This should be moved again to runtime } for operation in &self.drop_primary_key_operations { @@ -115,9 +124,19 @@ impl MigrationsProcessor { for operation in &self.set_primary_key_operations { operation.generate_sql(datasource).await; // This should be moved again to runtime } - for operation in &self.constraints_operations { + for operation in &self.constraints_table_operations { operation.generate_sql(datasource).await; // This should be moved again to runtime } + for operation in &self.constraints_column_operations { + operation.generate_sql(datasource).await; // This should be moved again to runtime + } + + #[cfg(feature = "postgres")] + { + for operation in &self.constraints_sequence_operations { + operation.generate_sql(datasource).await; // This should be moved again to runtime + } + } // TODO Still pending to decouple de executions of cargo check to skip the process if this // code is not processed by cargo build or cargo run // Self::from_query_register(datasource_name).await; @@ -129,7 +148,7 @@ impl MigrationsProcessor { canyon_memory: &'_ CanyonMemory, entity_name: &'a str, entity_fields: Vec, - database_tables: &'a [&'a TableMetadata], + database_tables: &'a [&'a MacroTableMetadata], ) { // 1st operation -> Check if the current entity is already on the target database. if !MigrationsHelper::entity_already_on_database(entity_name, database_tables) { @@ -153,19 +172,16 @@ impl MigrationsProcessor { /// Generates a database agnostic query to change the name of a table fn create_table(&mut self, table_name: String, entity_fields: Vec) { - self.operations.push(Box::new(TableOperation::CreateTable( - table_name, - entity_fields, - ))); + self.table_operations + .push(TableOperation::CreateTable(table_name, entity_fields)); } /// Generates a database agnostic query to change the name of a table fn table_rename(&mut self, old_table_name: String, new_table_name: String) { - self.operations - .push(Box::new(TableOperation::AlterTableName( - old_table_name, - new_table_name, - ))); + self.table_operations.push(TableOperation::AlterTableName( + old_table_name, + new_table_name, + )); } // Creates or modify (currently only datatype) a column for a given canyon register entity field @@ -173,7 +189,7 @@ impl MigrationsProcessor { &mut self, entity_name: &'a str, entity_fields: Vec, - current_table_metadata: Option<&'a TableMetadata>, + current_table_metadata: Option<&'a MacroTableMetadata>, _db_type: DatabaseType, ) { if current_table_metadata.is_none() { @@ -215,40 +231,42 @@ impl MigrationsProcessor { canyon_register_entity_field: CanyonRegisterEntityField, current_column_metadata: Option<&ColumnMetadata>, ) { - // If we do not retrieve data for this database column, it does not exist yet - // and therefore it has to be created - if current_column_metadata.is_none() { + if let Some(current_col_met) = current_column_metadata { + if !MigrationsHelper::is_same_datatype( + db_type, + &canyon_register_entity_field, + current_col_met, + ) { + self.change_column_datatype( + entity_name.to_string(), + canyon_register_entity_field.clone(), + ) + } + } else { + // If we do not retrieve data for this database column, it does not exist yet, + // and therefore it has to be created self.create_column( entity_name.to_string(), canyon_register_entity_field.clone(), ) - } else if !MigrationsHelper::is_same_datatype( - db_type, - &canyon_register_entity_field, - current_column_metadata.unwrap(), - ) { - self.change_column_datatype( - entity_name.to_string(), - canyon_register_entity_field.clone(), - ) } - if let Some(column_metadata) = current_column_metadata { - if canyon_register_entity_field.is_nullable() != column_metadata.is_nullable { - if column_metadata.is_nullable { - self.set_not_null(entity_name.to_string(), canyon_register_entity_field) - } else { - self.drop_not_null(entity_name.to_string(), canyon_register_entity_field) - } + if let Some(column_metadata) = current_column_metadata + && canyon_register_entity_field.is_nullable() != column_metadata.is_nullable + { + if column_metadata.is_nullable { + self.set_not_null(entity_name.to_string(), canyon_register_entity_field) + } else { + self.drop_not_null(entity_name.to_string(), canyon_register_entity_field) } } } fn delete_column(&mut self, table_name: &str, column_name: String) { - self.operations.push(Box::new(ColumnOperation::DeleteColumn( + self.column_operations.push(ColumnOperation::DeleteColumn( table_name.to_string(), column_name, - ))); + )); } #[cfg(feature = "mssql")] @@ -258,38 +276,32 @@ impl MigrationsProcessor { column_name: String, column_datatype: String, ) { - self.operations - .push(Box::new(ColumnOperation::DropNotNullBeforeDropColumn( + self.column_operations + .push(ColumnOperation::DropNotNullBeforeDropColumn( table_name.to_string(), column_name, column_datatype, - ))); + )); } fn create_column(&mut self, table_name: String, field: CanyonRegisterEntityField) { - self.operations - .push(Box::new(ColumnOperation::CreateColumn(table_name, field))); + self.column_operations + .push(ColumnOperation::CreateColumn(table_name, field)); } fn change_column_datatype(&mut self, table_name: String, field: CanyonRegisterEntityField) { - self.operations - .push(Box::new(ColumnOperation::AlterColumnType( - table_name, field, - ))); + self.column_operations + .push(ColumnOperation::AlterColumnType(table_name, field)); } fn set_not_null(&mut self, table_name: String, field: CanyonRegisterEntityField) { - self.operations - .push(Box::new(ColumnOperation::AlterColumnSetNotNull( - table_name, field, - ))); + self.column_operations + .push(ColumnOperation::AlterColumnSetNotNull(table_name, field)); } fn drop_not_null(&mut self, table_name: String, field: CanyonRegisterEntityField) { - self.operations - .push(Box::new(ColumnOperation::AlterColumnDropNotNull( - table_name, field, - ))); + self.column_operations + .push(ColumnOperation::AlterColumnDropNotNull(table_name, field)); } fn add_constraints( @@ -308,7 +320,7 @@ impl MigrationsProcessor { let foreign_key_name = format!( "{entity_name}_{}_fkey", - &canyon_register_entity_field.field_name + canyon_register_entity_field.field_name ); Self::add_foreign_key( @@ -341,14 +353,14 @@ impl MigrationsProcessor { column_to_reference: String, canyon_register_entity_field: &CanyonRegisterEntityField, ) { - self.constraints_operations - .push(Box::new(TableOperation::AddTableForeignKey( + self.constraints_table_operations + .push(TableOperation::AddTableForeignKey( entity_name.to_string(), foreign_key_name, canyon_register_entity_field.field_name.clone(), table_to_reference, column_to_reference, - ))); + )); } fn add_primary_key( @@ -357,25 +369,25 @@ impl MigrationsProcessor { canyon_register_entity_field: CanyonRegisterEntityField, ) { self.set_primary_key_operations - .push(Box::new(TableOperation::AddTablePrimaryKey( + .push(TableOperation::AddTablePrimaryKey( entity_name.to_string(), canyon_register_entity_field, - ))); + )); } #[cfg(feature = "postgres")] fn add_identity(&mut self, entity_name: &str, field: CanyonRegisterEntityField) { - self.constraints_operations - .push(Box::new(ColumnOperation::AlterColumnAddIdentity( + self.constraints_column_operations + .push(ColumnOperation::AlterColumnAddIdentity( entity_name.to_string(), field.clone(), - ))); + )); - self.constraints_operations - .push(Box::new(SequenceOperation::ModifySequence( + self.constraints_sequence_operations + .push(SequenceOperation::ModifySequence( entity_name.to_string(), field, - ))); + )); } fn add_modify_or_remove_constraints( @@ -419,7 +431,7 @@ impl MigrationsProcessor { } } } - // Case when field doesn't contains a primary key annotation, but there is one in the database column + // Case when field doesn't contain a primary key annotation, but there is one in the database column else if !field_is_primary_key && current_column_metadata.primary_key_info.is_some() { Self::drop_primary_key( self, @@ -449,7 +461,7 @@ impl MigrationsProcessor { let foreign_key_name = format!( "{entity_name}_{}_fkey", - &canyon_register_entity_field.field_name + canyon_register_entity_field.field_name ); Self::add_foreign_key( @@ -471,7 +483,7 @@ impl MigrationsProcessor { let foreign_key_name = format!( "{entity_name}_{}_fkey", - &canyon_register_entity_field.field_name + canyon_register_entity_field.field_name ); // Example of information in foreign_key_info: FOREIGN KEY (league) REFERENCES leagues(id) @@ -526,26 +538,20 @@ impl MigrationsProcessor { &canyon_register_entity_field, ) } - } else if !field_is_foreign_key && current_column_metadata.foreign_key_name.is_some() { - // Case when field don't contains a foreign key annotation, but there is already one in the database column - Self::delete_foreign_key( - self, - entity_name, - current_column_metadata - .foreign_key_name - .as_ref() - .expect("ForeignKey constrain name not found") - .to_string(), - ); + } else if !field_is_foreign_key + && let Some(foreign_key_name) = current_column_metadata.foreign_key_name.as_ref() + { + // Case when field don't contain a foreign key annotation, but there is already one in the database column + Self::delete_foreign_key(self, entity_name, foreign_key_name.to_owned()); } } fn drop_primary_key(&mut self, entity_name: &str, primary_key_name: String) { self.drop_primary_key_operations - .push(Box::new(TableOperation::DeleteTablePrimaryKey( + .push(TableOperation::DeleteTablePrimaryKey( entity_name.to_string(), primary_key_name, - ))); + )); } #[cfg(feature = "postgres")] @@ -554,37 +560,46 @@ impl MigrationsProcessor { entity_name: &str, canyon_register_entity_field: CanyonRegisterEntityField, ) { - self.constraints_operations - .push(Box::new(ColumnOperation::AlterColumnDropIdentity( + self.constraints_column_operations + .push(ColumnOperation::AlterColumnDropIdentity( entity_name.to_string(), canyon_register_entity_field, - ))); + )); } fn delete_foreign_key(&mut self, entity_name: &str, constrain_name: String) { - self.constraints_operations - .push(Box::new(TableOperation::DeleteTableForeignKey( + self.constraints_table_operations + .push(TableOperation::DeleteTableForeignKey( // table_with_foreign_key,constrain_name entity_name.to_string(), constrain_name, - ))); + )); } /// Make the detected migrations for the next Canyon-SQL run - #[allow(clippy::await_holding_lock)] pub async fn from_query_register(queries_to_execute: &HashMap<&str, Vec<&str>>) { for datasource in queries_to_execute.iter() { - for query_to_execute in datasource.1 { - let res = Self::query(query_to_execute, [], datasource.0).await; + let datasource_name = datasource.0; + let db_conn = Canyon::instance() + .expect("Error getting db connection on `from_query_register`") + .get_connection(datasource_name) + .unwrap_or_else(|_| { + panic!( + "Unable to get a database connection on Canyon Memory: {:?}", + datasource_name + ) + }); + for query_to_execute in datasource.1 { + let res = db_conn.query_rows(query_to_execute, &[]).await; match res { Ok(_) => println!( "\t[OK] - {:?} - Query: {:?}", - datasource.0, &query_to_execute + datasource.0, query_to_execute ), Err(e) => println!( "\t[ERR] - {:?} - Query: {:?}\nCause: {:?}", - datasource.0, &query_to_execute, e + datasource.0, query_to_execute, e ), } // TODO Ask for user input? @@ -600,7 +615,7 @@ impl MigrationsHelper { /// Checks if a tracked Canyon entity is already present in the database fn entity_already_on_database<'a>( entity_name: &'a str, - database_tables: &'a [&'_ TableMetadata], + database_tables: &'a [&'_ MacroTableMetadata], ) -> bool { database_tables .iter() @@ -610,8 +625,8 @@ impl MigrationsHelper { fn get_current_table_metadata<'a>( canyon_memory: &'_ CanyonMemory, entity_name: &'a str, - database_tables: &'a [&'_ TableMetadata], - ) -> Option<&'a TableMetadata> { + database_tables: &'a [&'_ MacroTableMetadata], + ) -> Option<&'a MacroTableMetadata> { let correct_entity_name = canyon_memory .renamed_entities .get(&entity_name.to_lowercase()) @@ -629,7 +644,7 @@ impl MigrationsHelper { /// Get the column metadata for a given column name fn get_current_column_metadata( column_name: String, - current_table_metadata: Option<&TableMetadata>, + current_table_metadata: Option<&MacroTableMetadata>, ) -> Option<&ColumnMetadata> { if let Some(metadata_table) = current_table_metadata { metadata_table @@ -716,40 +731,8 @@ impl MigrationsHelper { } } -#[cfg(test)] -mod migrations_helper_tests { - use super::*; - use crate::constants; - - const MOCKED_ENTITY_NAME: &str = "league"; - - #[test] - fn test_entity_already_on_database() { - let parse_result_empty_db_tables = - MigrationsHelper::entity_already_on_database(MOCKED_ENTITY_NAME, &[]); - // Always should be false - assert!(!parse_result_empty_db_tables); - - // Rust has a League entity. Database has a `league` entity. Case should be normalized - // and a match must raise - let mocked_league_entity_on_database = MigrationsHelper::entity_already_on_database( - MOCKED_ENTITY_NAME, - &[&constants::mocked_data::TABLE_METADATA_LEAGUE_EX], - ); - assert!(mocked_league_entity_on_database); - - let mocked_league_entity_on_database = MigrationsHelper::entity_already_on_database( - MOCKED_ENTITY_NAME, - &[&constants::mocked_data::NON_MATCHING_TABLE_METADATA], - ); - assert!(!mocked_league_entity_on_database) - } -} - -/// Trait that enables implementors to generate the migration queries -#[async_trait] trait DatabaseOperation: Debug { - async fn generate_sql(&self, datasource: &DatasourceConfig); + fn generate_sql(&self, datasource: &DatasourceConfig) -> impl Future; } /// Helper to relate the operations that Canyon should do when it's managing a schema @@ -769,73 +752,75 @@ enum TableOperation { DeleteTablePrimaryKey(String, String), } -impl Transaction for TableOperation {} +impl Transaction for TableOperation {} -#[async_trait] impl DatabaseOperation for TableOperation { async fn generate_sql(&self, datasource: &DatasourceConfig) { let db_type = datasource.get_db_type(); let stmt = match self { - TableOperation::CreateTable(table_name, table_fields) => { - match db_type { - #[cfg(feature = "postgres")] DatabaseType::PostgreSql => { - format!( - "CREATE TABLE \"{table_name}\" ({});", - table_fields - .iter() - .map(|entity_field| format!( - "\"{}\" {}", - entity_field.field_name, - to_postgres_syntax(entity_field) - )) - .collect::>() - .join(", ") - ) - } - #[cfg(feature = "mssql")] DatabaseType::SqlServer => { - format!( - "CREATE TABLE {:?} ({:?});", - table_name, - table_fields - .iter() - .map(|entity_field| format!( - "{} {}", - entity_field.field_name, - to_sqlserver_syntax(entity_field) - )) - .collect::>() - .join(", ") - ) - .replace('"', "") - }, - #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() - + TableOperation::CreateTable(table_name, table_fields) => match db_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => { + format!( + "CREATE TABLE \"{table_name}\" ({});", + table_fields + .iter() + .map(|entity_field| format!( + "\"{}\" {}", + entity_field.field_name, + to_postgres_syntax(entity_field) + )) + .collect::>() + .join(", ") + ) } - } + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => format!( + "CREATE TABLE {:?} ({:?});", + table_name, + table_fields + .iter() + .map(|entity_field| format!( + "{} {}", + entity_field.field_name, + to_sqlserver_syntax(entity_field) + )) + .collect::>() + .join(", ") + ) + .replace('"', ""), + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!(), + }, TableOperation::AlterTableName(old_table_name, new_table_name) => { match db_type { - #[cfg(feature = "postgres")] DatabaseType::PostgreSql => - format!("ALTER TABLE {old_table_name} RENAME TO {new_table_name};"), - #[cfg(feature = "mssql")] DatabaseType::SqlServer => - /* - Notes: Brackets around `old_table_name`, p.e. - exec sp_rename ['league'], 'leagues' // NOT VALID! - is only allowed for compound names split by a dot. - exec sp_rename ['random.league'], 'leagues' // OK - - CARE! This doesn't mean that we are including the schema. - exec sp_rename ['dbo.random.league'], 'leagues' // OK - exec sp_rename 'dbo.league', 'leagues' // OK - Schema doesn't need brackets - - Due to the automatic mapped name from Rust to DB and vice-versa, this won't - be an allowed behaviour for now, only with the table_name parameter on the - CanyonEntity annotation. - */ - format!("exec sp_rename '{old_table_name}', '{new_table_name}';"), - #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() - + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => { + format!("ALTER TABLE {old_table_name} RENAME TO {new_table_name};") + } + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => + /* + Notes: Brackets around `old_table_name`, p.e. + exec sp_rename ['league'], 'leagues' // NOT VALID! + is only allowed for compound names split by a dot. + exec sp_rename ['random.league'], 'leagues' // OK + + CARE! This doesn't mean that we are including the schema. + exec sp_rename ['dbo.random.league'], 'leagues' // OK + exec sp_rename 'dbo.league', 'leagues' // OK - Schema doesn't need brackets + + Due to the automatic mapped name from Rust to DB and vice versa, this won't + be an allowed behaviour for now, only with the table_name parameter on the + CanyonEntity annotation. + */ + { + format!("exec sp_rename '{old_table_name}', '{new_table_name}';") + } + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!(), } } @@ -845,57 +830,61 @@ impl DatabaseOperation for TableOperation { _column_foreign_key, _table_to_reference, _column_to_reference, - ) => { - match db_type { - #[cfg(feature = "postgres")] DatabaseType::PostgreSql => - format!( - "ALTER TABLE {_table_name} ADD CONSTRAINT {_foreign_key_name} \ + ) => match db_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => format!( + "ALTER TABLE {_table_name} ADD CONSTRAINT {_foreign_key_name} \ FOREIGN KEY ({_column_foreign_key}) REFERENCES {_table_to_reference} ({_column_to_reference});" - ), - #[cfg(feature = "mssql")] DatabaseType::SqlServer => - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]"), - #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() - + ), + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => { + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } - } + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!(), + }, TableOperation::DeleteTableForeignKey(_table_with_foreign_key, _constraint_name) => { match db_type { - #[cfg(feature = "postgres")] DatabaseType::PostgreSql => - format!( - "ALTER TABLE {_table_with_foreign_key} DROP CONSTRAINT {_constraint_name};", - ), - #[cfg(feature = "mssql")] DatabaseType::SqlServer => - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]"), - #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() - + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => format!( + "ALTER TABLE {_table_with_foreign_key} DROP CONSTRAINT {_constraint_name};", + ), + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => todo!( + "[MS-SQL -> Operation still won't supported by Canyon for Sql Server]" + ), + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!(), } } - TableOperation::AddTablePrimaryKey(_table_name, _entity_field) => { - match db_type { - #[cfg(feature = "postgres")] DatabaseType::PostgreSql => - format!( - "ALTER TABLE \"{_table_name}\" ADD PRIMARY KEY (\"{}\");", - _entity_field.field_name - ), - #[cfg(feature = "mssql")] DatabaseType::SqlServer => - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]"), - #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() - + TableOperation::AddTablePrimaryKey(_table_name, _entity_field) => match db_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => format!( + "ALTER TABLE \"{_table_name}\" ADD PRIMARY KEY (\"{}\");", + _entity_field.field_name + ), + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => { + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") } - } - - TableOperation::DeleteTablePrimaryKey(table_name, primary_key_name) => { - match db_type { - #[cfg(feature = "postgres")] DatabaseType::PostgreSql => - format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;"), - #[cfg(feature = "mssql")] DatabaseType::SqlServer => - format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;"), - #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!(), + }, + TableOperation::DeleteTablePrimaryKey(table_name, primary_key_name) => match db_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => { + format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;") } - } + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => { + format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;") + } + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!(), + }, }; save_migrations_query_to_execute(stmt, &datasource.name); @@ -922,9 +911,8 @@ enum ColumnOperation { AlterColumnDropIdentity(String, CanyonRegisterEntityField), } -impl Transaction for ColumnOperation {} +impl Transaction for ColumnOperation {} -#[async_trait] impl DatabaseOperation for ColumnOperation { async fn generate_sql(&self, datasource: &DatasourceConfig) { let db_type = datasource.get_db_type(); @@ -947,7 +935,6 @@ impl DatabaseOperation for ColumnOperation { to_sqlserver_syntax(entity_field) ), #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() - } ColumnOperation::DeleteColumn(table_name, column_name) => { // TODO Check if operation for SQL server is different @@ -964,7 +951,6 @@ impl DatabaseOperation for ColumnOperation { todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]"), #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() - } ColumnOperation::AlterColumnDropNotNull(table_name, entity_field) => match db_type { @@ -976,7 +962,6 @@ impl DatabaseOperation for ColumnOperation { entity_field.field_name, to_sqlserver_alter_syntax(entity_field) ), #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() - } #[cfg(feature = "mssql")] ColumnOperation::DropNotNullBeforeDropColumn(table_name, column_name, column_datatype) => format!( @@ -1004,7 +989,6 @@ impl DatabaseOperation for ColumnOperation { to_sqlserver_alter_syntax(entity_field) ), #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() - } } @@ -1028,10 +1012,9 @@ enum SequenceOperation { ModifySequence(String, CanyonRegisterEntityField), } #[cfg(feature = "postgres")] -impl Transaction for SequenceOperation {} +impl Transaction for SequenceOperation {} #[cfg(feature = "postgres")] -#[async_trait] impl DatabaseOperation for SequenceOperation { async fn generate_sql(&self, datasource: &DatasourceConfig) { let stmt = match self { @@ -1045,3 +1028,132 @@ impl DatabaseOperation for SequenceOperation { save_migrations_query_to_execute(stmt, &datasource.name); } } + +#[cfg(test)] +mod migrations_helper_tests { + use super::*; + const MOCKED_ENTITY_NAME: &str = "league"; + + #[test] + fn test_entity_already_on_database() { + mocked_data::init_mocked_data(); + + let parse_result_empty_db_tables = + MigrationsHelper::entity_already_on_database(MOCKED_ENTITY_NAME, &[]); + // Always should be false + assert!(!parse_result_empty_db_tables); + + // Rust has a League entity. Database has a `league` entity. Case should be normalized + // and a match must raise + let mocked_league_entity_on_database = MigrationsHelper::entity_already_on_database( + MOCKED_ENTITY_NAME, + &[mocked_data::TABLE_METADATA_LEAGUE_EX.get().unwrap()], + ); + assert!(mocked_league_entity_on_database); + + let mocked_league_entity_on_database = MigrationsHelper::entity_already_on_database( + MOCKED_ENTITY_NAME, + &[mocked_data::NON_MATCHING_TABLE_METADATA.get().unwrap()], + ); + assert!(!mocked_league_entity_on_database) + } + + pub mod mocked_data { + use crate::migrations::information_schema::{ColumnMetadata, MacroTableMetadata}; + use std::sync::OnceLock; + + pub static TABLE_METADATA_LEAGUE_EX: OnceLock = OnceLock::new(); + pub static NON_MATCHING_TABLE_METADATA: OnceLock = OnceLock::new(); + + pub fn init_mocked_data() { + TABLE_METADATA_LEAGUE_EX.get_or_init(|| MacroTableMetadata { + table_name: "league".to_string(), + columns: vec![ + ColumnMetadata { + column_name: "id".to_owned(), + datatype: "int".to_owned(), + character_maximum_length: None, + is_nullable: false, + column_default: None, + foreign_key_info: None, + foreign_key_name: None, + primary_key_info: Some("PK__league__3213E83FBDA92571".to_owned()), + primary_key_name: Some("PK__league__3213E83FBDA92571".to_owned()), + is_identity: false, + identity_generation: None, + }, + ColumnMetadata { + column_name: "ext_id".to_owned(), + datatype: "bigint".to_owned(), + character_maximum_length: None, + is_nullable: false, + column_default: None, + foreign_key_info: None, + foreign_key_name: None, + primary_key_info: None, + primary_key_name: None, + is_identity: false, + identity_generation: None, + }, + ColumnMetadata { + column_name: "slug".to_owned(), + datatype: "nvarchar".to_owned(), + character_maximum_length: None, + is_nullable: false, + column_default: None, + foreign_key_info: None, + foreign_key_name: None, + primary_key_info: None, + primary_key_name: None, + is_identity: false, + identity_generation: None, + }, + ColumnMetadata { + column_name: "name".to_owned(), + datatype: "nvarchar".to_owned(), + character_maximum_length: None, + is_nullable: false, + column_default: None, + foreign_key_info: None, + foreign_key_name: None, + primary_key_info: None, + primary_key_name: None, + is_identity: false, + identity_generation: None, + }, + ColumnMetadata { + column_name: "region".to_owned(), + datatype: "nvarchar".to_owned(), + character_maximum_length: None, + is_nullable: false, + column_default: None, + foreign_key_info: None, + foreign_key_name: None, + primary_key_info: None, + primary_key_name: None, + is_identity: false, + identity_generation: None, + }, + ColumnMetadata { + column_name: "image_url".to_owned(), + datatype: "nvarchar".to_owned(), + character_maximum_length: None, + is_nullable: false, + column_default: None, + foreign_key_info: None, + foreign_key_name: None, + primary_key_info: None, + primary_key_name: None, + is_identity: false, + identity_generation: None, + }, + ], + }); + + NON_MATCHING_TABLE_METADATA.get_or_init(|| MacroTableMetadata { + table_name: "random_name_to_assert_false".to_string(), + columns: vec![], + }); + } + } +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d24de91c..258d798e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,6 +1,6 @@ services: postgres: - image: postgres:14 + image: postgres:latest restart: always hostname: postgres environment: @@ -13,7 +13,7 @@ services: ports: - '5438:5432' volumes: - - ./postgres-data:/var/lib/postgresql/data + - ./postgres-data:/var/lib/postgresql # copy the sql script to create tables - ./sql/10-create_tables.sql:/docker-entrypoint-initdb.d/create_tables.sql # copy the sql script to fill tables @@ -21,7 +21,8 @@ services: sql-server: container_name: sql-server image: mcr.microsoft.com/mssql/server:2022-latest - restart: always + platform: linux/amd64 + restart: unless-stopped ports: - "1434:1433" environment: @@ -37,4 +38,4 @@ services: volumes: - ./mysql-data:/var/lib/mysql - ./mysql/create_tables.sql:/docker-entrypoint-initdb.d/create_tables.sql - - ./mysql/fill_tables.sql:/docker-entrypoint-initdb.d/fill_tables.sql \ No newline at end of file + - ./mysql/fill_tables.sql:/docker-entrypoint-initdb.d/fill_tables.sql diff --git a/octocat.png b/octocat.png deleted file mode 100644 index f9050b935792512349e6f1ba0e6e55d99ecbf508..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2468 zcmdT_`8U)H8~>UyLoN;~FS1vbQdF{J8?J4NA)_*3EJd=#Ysq#qBaJ2d9!+*iOqN76 z(^xZO--od$2`LQ5i0|#b_pf+A=RBX~x96PabIucIjWQG9li&jYKmciOZ1Ygs5xr>DERy2_uF=WzDH z?*pQu$A67A2j97g#bUvYVfOAWxI||&X=%zQq$DMKdU`Ze6t`L1At50a78Yt3)UFz6 zLZMLb*EGl)1KE=xIDatIC?g}&*VotE+q>0TGCMo#=H})X6fiL{5$0xh-_CN4Quehz zAM0~(lvF`-vTbQ;;q=!Vo0$#_3`mKIZftB!PftfjM|*pFYiMe8baV_24S_!fIfD)0 zOq;K-?-9NudwYB10Y25?2~4K)K0N77Rnt?f zE1Yh}DvRH`ICrvj1Dmv1-U8)%QtNaV2D$%{knUKIrHY0|4MMNMl3W5NzqV zo86ST7|%~?E1`q<&UOw*uYY(ebJZ~|NOS7@W#sQnpDbYS_9KDp0cDm`BYpX)e{f9Fux9 zIBaHbgExwuw+dM73(IHL3{U;s7JF|gqtiLoF}NcIZQBUEcIl~CQ0y5axVBmuAZ{-A zAgTBPwfENu1Zj~_BHMQ|d!`7x1yBX$!(Zy8KA4gXmMyx3R4y zI5*C@cmz?}SVBEd-r68f(l4QqmpfUJ#~W+S%_&QRiZYcUj&;wwfTN+ZMi=}`nzhbL z7fP1)zQ(zz8R^NPv9KAGk*V;^T^}k#O58HIZxGIJV+N~4HH<*YqGgNuVn|1Fwgtmu zkS3PO%D0l8ROr*A!^7$1vke{((O2)#H*yfuqaM=*@4_hKWZxe73ONNt+%=_>|9|D zwpH@HH9r)ZtCRK#&k2I)Jv?!id=>bQs3+Gu5#X4NBW)2h^7_a;+`?oezT!HhMfcvD z><8mG&fOmsaf0FB-K)ibM?9q#hhM=L6KnPUP>D_{$bf-HSm<9(unyyeb zX#kQ;fX=PIrv7-0Xy=O7BH9+OeG@Vhr+$WUb1T`Zx~Qf{Cgu3%b&AMuNgMbYM2f1+ z!+|WN7BOin8eVH!U_3&#`);i*AZZTGV`OE1$wYX|XMHd#nQV8etCcSJ%Iu~lB zc5Aogv-&v;o;1t0C;N^S8w^NxnO zsS1<;NeAgBTIUI-iuy_43v|f9#yU|y$PCwz8e6-$S=;_a*wvpsh%SHXr?)?(u35XW zt{ECi;2jf*SI)jX85o>dSJe?Roo&v3V25WL z{Bh$!e*(toq=fQw(->1;4oh3IiSuTR(t^@#mKnUd!bkIES!ty zOiRQ~%m+a=f3uVn_ShKd;K@FhP?G@9yCs~obQSr?BL0?ezFa&v{q^5JSA_umycY#oGsD2MCI;jqMAX6hlh@>GIb7qHfpaMoXj zlbzvw+ApCJ{x4S0FtKXLb7>hIodmf&Qty1^yhGGI8>o9ks+Gg(IA#EZEn%LHKQYjK z8-ChZgt9}|(q2RA1_|dR@~A*BQ$EFu6y(pH3)Co3g*}bgmSe+|VvwRsl$lU>q0222 z`4z&d?l%N`sCtcZP7B}o43AcwD;#Ve9c)Fyu2|&%s0g4&e)1F1XV%ak|Nc1u(gbBp IGIEak8#) -> Result>, Err> + // Remember that `find_by_primary_key(&dyn QueryParameter) -> Result>, Err> assert_eq!( League::find_by_pk(&new_league.id) .await @@ -67,7 +68,7 @@ fn test_crud_delete_method_operation() { /// Same as the delete test, but performing the operations with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_delete_datasource_mssql_method_operation() { +fn test_crud_delete_with_mssql_method_operation() { // For test the delete, we will insert a new instance of the database, and then, // after inspect it, we will proceed to delete it let mut new_league: League = League { @@ -81,12 +82,12 @@ fn test_crud_delete_datasource_mssql_method_operation() { // We insert the instance on the database, on the `League` entity new_league - .insert_datasource(SQL_SERVER_DS) + .insert_with(SQL_SERVER_DS) .await .expect("Failed insert operation"); assert_eq!( new_league.id, - League::find_by_pk_datasource(&new_league.id, SQL_SERVER_DS) + League::find_by_pk_with(&new_league.id, SQL_SERVER_DS) .await .expect("Request error") .expect("None value") @@ -96,15 +97,15 @@ fn test_crud_delete_datasource_mssql_method_operation() { // Now that we have an instance mapped to some entity by a primary key, we can now // remove that entry from the database with the delete operation new_league - .delete_datasource(SQL_SERVER_DS) + .delete_with(SQL_SERVER_DS) .await .expect("Failed to delete the operation"); // To check the success, we can query by the primary key value and check if, after unwrap() // the result of the operation, the find by primary key contains Some(v) or None - // Remember that `find_by_primary_key(&dyn QueryParameter<'a>) -> Result>, Err> + // Remember that `find_by_primary_key(&dyn QueryParameter) -> Result>, Err> assert_eq!( - League::find_by_pk_datasource(&new_league.id, SQL_SERVER_DS) + League::find_by_pk_with(&new_league.id, SQL_SERVER_DS) .await .expect("Unwrapping the result, letting the Option"), None @@ -114,7 +115,7 @@ fn test_crud_delete_datasource_mssql_method_operation() { /// Same as the delete test, but performing the operations with the specified datasource #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_delete_datasource_mysql_method_operation() { +fn test_crud_delete_with_mysql_method_operation() { // For test the delete, we will insert a new instance of the database, and then, // after inspect it, we will proceed to delete it let mut new_league: League = League { @@ -128,12 +129,12 @@ fn test_crud_delete_datasource_mysql_method_operation() { // We insert the instance on the database, on the `League` entity new_league - .insert_datasource(MYSQL_DS) + .insert_with(MYSQL_DS) .await .expect("Failed insert operation"); assert_eq!( new_league.id, - League::find_by_pk_datasource(&new_league.id, MYSQL_DS) + League::find_by_pk_with(&new_league.id, MYSQL_DS) .await .expect("Request error") .expect("None value") @@ -143,15 +144,15 @@ fn test_crud_delete_datasource_mysql_method_operation() { // Now that we have an instance mapped to some entity by a primary key, we can now // remove that entry from the database with the delete operation new_league - .delete_datasource(MYSQL_DS) + .delete_with(MYSQL_DS) .await .expect("Failed to delete the operation"); // To check the success, we can query by the primary key value and check if, after unwrap() // the result of the operation, the find by primary key contains Some(v) or None - // Remember that `find_by_primary_key(&dyn QueryParameter<'a>) -> Result>, Err> + // Remember that `find_by_primary_key(&dyn QueryParameter) -> Result>, Err> assert_eq!( - League::find_by_pk_datasource(&new_league.id, MYSQL_DS) + League::find_by_pk_with(&new_league.id, MYSQL_DS) .await .expect("Unwrapping the result, letting the Option"), None diff --git a/tests/crud/foreign_key_operations.rs b/tests/crud/foreign_key_operations.rs index 87630ad1..f980459a 100644 --- a/tests/crud/foreign_key_operations.rs +++ b/tests/crud/foreign_key_operations.rs @@ -1,5 +1,5 @@ /// Integration tests for the CRUD operations available in `Canyon` that -/// generates and executes *SELECT* statements based on a entity +/// generates and executes *SELECT* statements based on an entity /// annotated with the `#[foreign_key(... args)]` annotation looking /// for the related data with some entity `U` that acts as is parent, where `U` /// impls `ForeignKeyable` (isn't required, but it won't unlock the @@ -7,10 +7,7 @@ /// /// Names of the foreign key methods are autogenerated for the direct and /// reverse side of the implementations. -/// For more info: TODO -> Link to the docs of the foreign key chapter -use canyon_sql::crud::CrudOperations; - -#[cfg(feature = "mssql")] +#[cfg(feature = "mysql")] use crate::constants::MYSQL_DS; #[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; @@ -18,6 +15,8 @@ use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; use crate::tests_models::tournament::*; +use canyon_sql::crud::ReadOperations; + /// Given an entity `T` which has some field declaring a foreign key relation /// with some another entity `U`, for example, performs a search to find /// what is the parent type `U` of `T` @@ -45,15 +44,15 @@ fn test_crud_search_by_foreign_key() { /// Same as the search by foreign key, but with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_search_by_foreign_key_datasource_mssql() { - let some_tournament: Tournament = Tournament::find_by_pk_datasource(&10, SQL_SERVER_DS) +fn test_crud_search_by_foreign_key_with_mssql() { + let some_tournament: Tournament = Tournament::find_by_pk_with(&10, SQL_SERVER_DS) .await .expect("Result variant of the query is err") .expect("No result found for the given parameter"); // We can get the parent entity for the retrieved child instance let parent_entity: Option = some_tournament - .search_league_datasource(SQL_SERVER_DS) + .search_league_with(SQL_SERVER_DS) .await .expect("Result variant of the query is err"); @@ -71,15 +70,15 @@ fn test_crud_search_by_foreign_key_datasource_mssql() { /// Same as the search by foreign key, but with the specified datasource #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_search_by_foreign_key_datasource_mysql() { - let some_tournament: Tournament = Tournament::find_by_pk_datasource(&10, MYSQL_DS) +fn test_crud_search_by_foreign_key_with_mysql() { + let some_tournament: Tournament = Tournament::find_by_pk_with(&10, MYSQL_DS) .await .expect("Result variant of the query is err") .expect("No result found for the given parameter"); // We can get the parent entity for the retrieved child instance let parent_entity: Option = some_tournament - .search_league_datasource(MYSQL_DS) + .search_league_with(MYSQL_DS) .await .expect("Result variant of the query is err"); @@ -108,7 +107,7 @@ fn test_crud_search_reverse_side_foreign_key() { .expect("No result found for the given parameter"); // Computes how many tournaments are pointing to the retrieved league - let child_tournaments: Vec = Tournament::search_league_childrens(&some_league) + let child_tournaments = Tournament::search_league_childrens(&some_league) .await .expect("Result variant of the query is err"); @@ -122,15 +121,15 @@ fn test_crud_search_reverse_side_foreign_key() { /// but with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_search_reverse_side_foreign_key_datasource_mssql() { - let some_league: League = League::find_by_pk_datasource(&1, SQL_SERVER_DS) +fn test_crud_search_reverse_side_foreign_key_with_mssql() { + let some_league: League = League::find_by_pk_with(&1, SQL_SERVER_DS) .await .expect("Result variant of the query is err") .expect("No result found for the given parameter"); // Computes how many tournaments are pointing to the retrieved league let child_tournaments: Vec = - Tournament::search_league_childrens_datasource(&some_league, SQL_SERVER_DS) + Tournament::search_league_childrens_with(&some_league, SQL_SERVER_DS) .await .expect("Result variant of the query is err"); @@ -144,15 +143,15 @@ fn test_crud_search_reverse_side_foreign_key_datasource_mssql() { /// but with the specified datasource #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_search_reverse_side_foreign_key_datasource_mysql() { - let some_league: League = League::find_by_pk_datasource(&1, MYSQL_DS) +fn test_crud_search_reverse_side_foreign_key_with_mysql() { + let some_league: League = League::find_by_pk_with(&1, MYSQL_DS) .await .expect("Result variant of the query is err") .expect("No result found for the given parameter"); // Computes how many tournaments are pointing to the retrieved league let child_tournaments: Vec = - Tournament::search_league_childrens_datasource(&some_league, MYSQL_DS) + Tournament::search_league_childrens_with(&some_league, MYSQL_DS) .await .expect("Result variant of the query is err"); diff --git a/tests/crud/hex_arch_example.rs b/tests/crud/hex_arch_example.rs new file mode 100644 index 00000000..247b60e9 --- /dev/null +++ b/tests/crud/hex_arch_example.rs @@ -0,0 +1,236 @@ +#![cfg(feature = "postgres")] + +use std::error::Error; + +use canyon_sql::{ + connection::DatabaseConnector, + connection::DbConnection, + core::Canyon, + crud::EntityCrudOperations, + crud::ReadOperations, + macros::{CanyonEntityCrud, CanyonMapper, CanyonRead, canyon_entity}, + query::{QueryParameter, querybuilder::SelectQueryBuilder}, +}; + +#[cfg(feature = "postgres")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_hex_arch_ops() { + let default_db_conn = Canyon::instance() + .unwrap() + .get_default_connection() + .unwrap(); + let league_service = LeagueHexServiceAdapter { + league_repository: LeagueHexRepositoryAdapter { + db_conn: default_db_conn, + }, + }; + + let find_all_result = league_service.find_all().await; + + // Connection doesn't return an error + assert!(find_all_result.is_ok()); + let find_all_result = find_all_result.unwrap(); + assert!(!find_all_result.is_empty()); + // If we try to do a call using the adapter, count will use the default datasource, which is locked at this point, + // since we passed the same connection that it will be using here to the repository! + assert_eq!( + LeagueHexRepositoryAdapter::::count() + .await + .unwrap() as usize, + find_all_result.len() + ); +} + +#[cfg(feature = "postgres")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_hex_arch_insert_entity_ops() { + let default_db_conn = Canyon::instance() + .unwrap() + .get_default_connection() + .unwrap(); + let league_service = LeagueHexServiceAdapter { + league_repository: LeagueHexRepositoryAdapter { + db_conn: default_db_conn, + }, + }; + + let mut other_league: LeagueHex = LeagueHex { + id: Default::default(), + ext_id: Default::default(), + slug: "leaguehex-slug".to_string(), + name: "Test LeagueHex on layered".to_string(), + region: "LeagueHex Region".to_string(), + image_url: "http://example.com/image.png".to_string(), + }; + league_service.create(&mut other_league).await.unwrap(); + + let find_new_league = league_service.get(&other_league.id).await.unwrap(); + assert!(find_new_league.is_some()); + assert_eq!( + find_new_league.as_ref().unwrap().name, + String::from("Test LeagueHex on layered") + ); +} + +#[cfg(feature = "postgres")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_hex_arch_update_entity_ops() { + let default_db_conn = Canyon::instance() + .unwrap() + .get_default_connection() + .unwrap(); + let league_service = LeagueHexServiceAdapter { + league_repository: LeagueHexRepositoryAdapter { + db_conn: default_db_conn, + }, + }; + + let mut other_league: LeagueHex = LeagueHex { + id: Default::default(), + ext_id: Default::default(), + slug: "leaguehex-slug".to_string(), + name: "Test LeagueHex on layered".to_string(), + region: "LeagueHex Region".to_string(), + image_url: "http://example.com/image.png".to_string(), + }; + league_service.create(&mut other_league).await.unwrap(); + + let find_new_league = league_service.get(&other_league.id).await.unwrap(); + assert!(find_new_league.is_some()); + assert_eq!( + find_new_league.as_ref().unwrap().name, + String::from("Test LeagueHex on layered") + ); + + let mut updt = find_new_league.unwrap(); + updt.ext_id = 5; + let r = LeagueHexRepositoryAdapter::::update_entity(&updt).await; + assert!(r.is_ok()); + + let updated = league_service.get(&other_league.id).await.unwrap(); + assert_eq!(updated.unwrap().ext_id, 5); +} + +#[cfg(feature = "postgres")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_hex_arch_delete_entity_ops() { + let mut league = LeagueHex { + id: Default::default(), + ext_id: Default::default(), + slug: "leaguehex-delete".to_string(), + name: "LeagueHex to delete".to_string(), + region: "LeagueHex Region".to_string(), + image_url: "http://example.com/image.png".to_string(), + }; + + LeagueHexRepositoryAdapter::::insert_entity(&mut league) + .await + .unwrap(); + + let inserted = LeagueHexRepositoryAdapter::::find_by_pk(&league.id) + .await + .unwrap(); + + assert!(inserted.is_some()); + + LeagueHexRepositoryAdapter::::delete_entity(&league) + .await + .unwrap(); + + let deleted = LeagueHexRepositoryAdapter::::find_by_pk(&league.id) + .await + .unwrap(); + + assert!(deleted.is_none()); +} + +#[derive(CanyonMapper, Debug)] +#[canyon_entity] +pub struct LeagueHex { + // The core model of the 'LeagueHex' domain + #[primary_key] + pub id: i32, + pub ext_id: i64, + pub slug: String, + pub name: String, + pub region: String, + pub image_url: String, +} + +pub trait LeagueHexService { + async fn find_all(&self) -> Result, Box>; + async fn create<'a>( + &self, + league: &'a mut LeagueHex, + ) -> Result<(), Box>; + + async fn get<'a, Pk: QueryParameter>( + &self, + id: &'a Pk, + ) -> Result, Box>; +} // As a domain boundary for the application side of the hexagon + +pub struct LeagueHexServiceAdapter { + league_repository: T, +} +impl LeagueHexService for LeagueHexServiceAdapter { + async fn find_all(&self) -> Result, Box> { + self.league_repository.find_all().await + } + + async fn create<'a>( + &self, + league: &'a mut LeagueHex, + ) -> Result<(), Box> { + self.league_repository.create(league).await + } + + async fn get<'a, Pk: QueryParameter>( + &self, + id: &'a Pk, + ) -> Result, Box> { + self.league_repository.get(id).await + } +} + +pub trait LeagueHexRepository { + async fn find_all(&self) -> Result, Box>; + async fn create<'a>( + &self, + league: &'a mut LeagueHex, + ) -> Result<(), Box>; + + async fn get<'a, Pk: QueryParameter>( + &self, + id: &'a Pk, + ) -> Result, Box>; +} // As a domain boundary for the infrastructure side of the hexagon + +#[derive(CanyonRead, CanyonEntityCrud)] +#[canyon_crud(maps_to=LeagueHex)] +#[canyon_entity(table_name = "league")] +pub struct LeagueHexRepositoryAdapter { + db_conn: T, +} +impl LeagueHexRepository for LeagueHexRepositoryAdapter { + async fn find_all(&self) -> Result, Box> { + let db_conn = &self.db_conn; + let select_query = + SelectQueryBuilder::new("league", db_conn.get_database_type()?).build()?; + db_conn.query(select_query, &[]).await + } + + async fn create<'a>( + &self, + league: &'a mut LeagueHex, + ) -> Result<(), Box> { + Self::insert_entity(league).await + } + + async fn get<'a, Pk: QueryParameter>( + &self, + id: &'a Pk, + ) -> Result, Box> { + Self::find_by_pk(id).await + } +} diff --git a/tests/crud/init_mssql.rs b/tests/crud/init_mssql.rs index 19b08549..9bc16ce1 100644 --- a/tests/crud/init_mssql.rs +++ b/tests/crud/init_mssql.rs @@ -3,33 +3,37 @@ use crate::constants::SQL_SERVER_DS; use crate::constants::SQL_SERVER_FILL_TABLE_VALUES; use crate::tests_models::league::League; -use canyon_sql::crud::CrudOperations; -use canyon_sql::db_clients::tiberius::{Client, Config}; +use canyon_sql::crud::ReadOperations; +use canyon_sql::db_clients::tiberius::{Client, Config, EncryptionLevel}; use canyon_sql::runtime::tokio::net::TcpStream; use canyon_sql::runtime::tokio_util::compat::TokioAsyncWriteCompatExt; -/// In order to initialize data on `SqlServer`. we must manually insert it -/// when the docker starts. SqlServer official docker from Microsoft does -/// not allow you to run `.sql` files against the database (not at least, without) -/// using a workaround. So, we are going to query the `SqlServer` to check if already -/// has some data (other processes, persistence or multi-threading envs), af if not, -/// we are going to retrieve the inserted data on the `postgreSQL` at start-up and -/// inserting into the `SqlServer` instance. -/// -/// This will be marked as `#[ignore]`, so we can force to run first the marked as -/// ignored, check the data available, perform the necessary init operations and -/// then *cargo test * the real integration tests +// /// In order to initialize data on `SqlServer`. we must manually insert it +// /// when the docker starts. SqlServer official docker from Microsoft does +// /// not allow you to run `.sql` files against the database (not at least, without) +// /// using a workaround. So, we are going to query the `SqlServer` to check if already +// /// has some data (other processes, persistence or multi-threading envs), af if not, +// /// we are going to retrieve the inserted data on the `postgreSQL` at start-up and +// /// inserting into the `SqlServer` instance. +// /// +// /// This will be marked as `#[ignore]`, so we can force to run first the marked as +// /// ignored, check the data available, perform the necessary init operations and +// /// then *cargo test * the real integration tests #[canyon_sql::macros::canyon_tokio_test] #[ignore] fn initialize_sql_server_docker_instance() { - static CONN_STR: &str = - "server=tcp:localhost,1434;User Id=SA;Password=SqlServer-10;TrustServerCertificate=true"; + static CONN_STR: &str = "server=tcp:localhost,1434;User Id=SA;Password=SqlServer-10;TrustServerCertificate=true;Encrypt=true"; canyon_sql::runtime::futures::executor::block_on(async { - let config = Config::from_ado_string(CONN_STR).unwrap(); + let mut config = Config::from_ado_string(CONN_STR).expect("could not parse ado string"); - let tcp = TcpStream::connect(config.get_addr()).await.unwrap(); - let tcp2 = TcpStream::connect(config.get_addr()).await.unwrap(); + config.encryption(EncryptionLevel::NotSupported); + let tcp = TcpStream::connect(config.get_addr()) + .await + .expect("could not connect to stream 1"); + let tcp2 = TcpStream::connect(config.get_addr()) + .await + .expect("could not connect to stream 2"); tcp.set_nodelay(true).ok(); let mut client = Client::connect(config.clone(), tcp.compat_write()) @@ -40,14 +44,14 @@ fn initialize_sql_server_docker_instance() { let query_result = client.query(SQL_SERVER_CREATE_TABLES, &[]).await; assert!(query_result.is_ok()); - let leagues_sql = League::find_all_datasource(SQL_SERVER_DS).await; - println!("LSQL ERR: {leagues_sql:?}"); + let leagues_sql = League::find_all_with(SQL_SERVER_DS).await; + println!("LSqlServer: {leagues_sql:?}"); assert!(leagues_sql.is_ok()); match leagues_sql { Ok(ref leagues) => { let leagues_len = leagues.len(); - println!("Leagues already inserted on SQLSERVER: {:?}", &leagues_len); + println!("Leagues already inserted on SQLSERVER: {:?}", leagues_len); if leagues.len() < 10 { let mut client2 = Client::connect(config, tcp2.compat_write()) .await diff --git a/tests/crud/insert_operations.rs b/tests/crud/insert_operations.rs index 13e2747e..0ac6ebef 100644 --- a/tests/crud/insert_operations.rs +++ b/tests/crud/insert_operations.rs @@ -1,6 +1,5 @@ //! Integration tests for the CRUD operations available in `Canyon` that //! generates and executes *INSERT* statements -use canyon_sql::crud::CrudOperations; #[cfg(feature = "mysql")] use crate::constants::MYSQL_DS; @@ -8,6 +7,7 @@ use crate::constants::MYSQL_DS; use crate::constants::SQL_SERVER_DS; use crate::tests_models::league::*; +use canyon_sql::crud::{InsertOperations, ReadOperations}; /// Inserts a new record on the database, given an entity that is /// annotated with `#[canyon_entity]` macro over a *T* type. @@ -61,7 +61,7 @@ fn test_crud_insert_operation() { /// the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_insert_datasource_mssql_operation() { +fn test_crud_insert_with_mssql_operation() { let mut new_league: League = League { id: Default::default(), ext_id: 7892635306594_i64, @@ -73,7 +73,7 @@ fn test_crud_insert_datasource_mssql_operation() { // We insert the instance on the database, on the `League` entity new_league - .insert_datasource(SQL_SERVER_DS) + .insert_with(SQL_SERVER_DS) .await .expect("Failed insert datasource operation"); @@ -81,7 +81,7 @@ fn test_crud_insert_datasource_mssql_operation() { // value for the primary key field, which is id. So, we can query the // database again with the find by primary key operation to check if // the value was really inserted - let inserted_league = League::find_by_pk_datasource(&new_league.id, SQL_SERVER_DS) + let inserted_league = League::find_by_pk_with(&new_league.id, SQL_SERVER_DS) .await .expect("Failed the query to the database") .expect("No entity found for the primary key value passed in"); @@ -93,7 +93,7 @@ fn test_crud_insert_datasource_mssql_operation() { /// the specified datasource #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_insert_datasource_mysql_operation() { +fn test_crud_insert_with_mysql_operation() { let mut new_league: League = League { id: Default::default(), ext_id: 7892635306594_i64, @@ -105,7 +105,7 @@ fn test_crud_insert_datasource_mysql_operation() { // We insert the instance on the database, on the `League` entity new_league - .insert_datasource(MYSQL_DS) + .insert_with(MYSQL_DS) .await .expect("Failed insert datasource operation"); @@ -113,205 +113,205 @@ fn test_crud_insert_datasource_mysql_operation() { // value for the primary key field, which is id. So, we can query the // database again with the find by primary key operation to check if // the value was really inserted - let inserted_league = League::find_by_pk_datasource(&new_league.id, MYSQL_DS) + let inserted_league = League::find_by_pk_with(&new_league.id, MYSQL_DS) .await .expect("Failed the query to the database") .expect("No entity found for the primary key value passed in"); assert_eq!(new_league.id, inserted_league.id); } - -/// The multi insert operation is a shorthand for insert multiple instances of *T* -/// in the database at once. -/// -/// It works pretty much the same that the insert operation, with the same behaviour -/// of the `#[primary_key]` annotation over some field. It will auto set the primary -/// key field with the autogenerated value on the database on the insert operation, but -/// for every entity passed in as an array of mutable instances of `T`. -/// -/// The instances without `#[primary_key]` inserts all the values on the instaqce fields -/// on the database. -#[cfg(feature = "postgres")] -#[canyon_sql::macros::canyon_tokio_test] -fn test_crud_multi_insert_operation() { - let mut new_league_mi: League = League { - id: Default::default(), - ext_id: 54376478_i64, - slug: "some-new-random-league".to_string(), - name: "Some New Random League".to_string(), - region: "Unknown".to_string(), - image_url: "https://what-a-league.io".to_string(), - }; - let mut new_league_mi_2: League = League { - id: Default::default(), - ext_id: 3475689769678906_i64, - slug: "new-league-2".to_string(), - name: "New League 2".to_string(), - region: "Really unknown".to_string(), - image_url: "https://what-an-unknown-league.io".to_string(), - }; - let mut new_league_mi_3: League = League { - id: Default::default(), - ext_id: 46756867_i64, - slug: "a-new-multinsert".to_string(), - name: "New League 3".to_string(), - region: "The dark side of the moon".to_string(), - image_url: "https://interplanetary-league.io".to_string(), - }; - - // Insert the instance as database entities - new_league_mi - .insert() - .await - .expect("Failed insert datasource operation"); - new_league_mi_2 - .insert() - .await - .expect("Failed insert datasource operation"); - new_league_mi_3 - .insert() - .await - .expect("Failed insert datasource operation"); - - // Recover the inserted data by primary key - let inserted_league = League::find_by_pk(&new_league_mi.id) - .await - .expect("[1] - Failed the query to the database") - .expect("[1] - No entity found for the primary key value passed in"); - let inserted_league_2 = League::find_by_pk(&new_league_mi_2.id) - .await - .expect("[2] - Failed the query to the database") - .expect("[2] - No entity found for the primary key value passed in"); - let inserted_league_3 = League::find_by_pk(&new_league_mi_3.id) - .await - .expect("[3] - Failed the query to the database") - .expect("[3] - No entity found for the primary key value passed in"); - - assert_eq!(new_league_mi.id, inserted_league.id); - assert_eq!(new_league_mi_2.id, inserted_league_2.id); - assert_eq!(new_league_mi_3.id, inserted_league_3.id); -} - -/// Same as the multi insert above, but with the specified datasource -#[cfg(feature = "mssql")] -#[canyon_sql::macros::canyon_tokio_test] -fn test_crud_multi_insert_datasource_mssql_operation() { - let mut new_league_mi: League = League { - id: Default::default(), - ext_id: 54376478_i64, - slug: "some-new-random-league".to_string(), - name: "Some New Random League".to_string(), - region: "Unknown".to_string(), - image_url: "https://what-a-league.io".to_string(), - }; - let mut new_league_mi_2: League = League { - id: Default::default(), - ext_id: 3475689769678906_i64, - slug: "new-league-2".to_string(), - name: "New League 2".to_string(), - region: "Really unknown".to_string(), - image_url: "https://what-an-unknown-league.io".to_string(), - }; - let mut new_league_mi_3: League = League { - id: Default::default(), - ext_id: 46756867_i64, - slug: "a-new-multinsert".to_string(), - name: "New League 3".to_string(), - region: "The dark side of the moon".to_string(), - image_url: "https://interplanetary-league.io".to_string(), - }; - - // Insert the instance as database entities - new_league_mi - .insert_datasource(SQL_SERVER_DS) - .await - .expect("Failed insert datasource operation"); - new_league_mi_2 - .insert_datasource(SQL_SERVER_DS) - .await - .expect("Failed insert datasource operation"); - new_league_mi_3 - .insert_datasource(SQL_SERVER_DS) - .await - .expect("Failed insert datasource operation"); - - // Recover the inserted data by primary key - let inserted_league = League::find_by_pk_datasource(&new_league_mi.id, SQL_SERVER_DS) - .await - .expect("[1] - Failed the query to the database") - .expect("[1] - No entity found for the primary key value passed in"); - let inserted_league_2 = League::find_by_pk_datasource(&new_league_mi_2.id, SQL_SERVER_DS) - .await - .expect("[2] - Failed the query to the database") - .expect("[2] - No entity found for the primary key value passed in"); - let inserted_league_3 = League::find_by_pk_datasource(&new_league_mi_3.id, SQL_SERVER_DS) - .await - .expect("[3] - Failed the query to the database") - .expect("[3] - No entity found for the primary key value passed in"); - - assert_eq!(new_league_mi.id, inserted_league.id); - assert_eq!(new_league_mi_2.id, inserted_league_2.id); - assert_eq!(new_league_mi_3.id, inserted_league_3.id); -} - -/// Same as the multi insert above, but with the specified datasource -#[cfg(feature = "mysql")] -#[canyon_sql::macros::canyon_tokio_test] -fn test_crud_multi_insert_datasource_mysql_operation() { - let mut new_league_mi: League = League { - id: Default::default(), - ext_id: 54376478_i64, - slug: "some-new-random-league".to_string(), - name: "Some New Random League".to_string(), - region: "Unknown".to_string(), - image_url: "https://what-a-league.io".to_string(), - }; - let mut new_league_mi_2: League = League { - id: Default::default(), - ext_id: 3475689769678906_i64, - slug: "new-league-2".to_string(), - name: "New League 2".to_string(), - region: "Really unknown".to_string(), - image_url: "https://what-an-unknown-league.io".to_string(), - }; - let mut new_league_mi_3: League = League { - id: Default::default(), - ext_id: 46756867_i64, - slug: "a-new-multinsert".to_string(), - name: "New League 3".to_string(), - region: "The dark side of the moon".to_string(), - image_url: "https://interplanetary-league.io".to_string(), - }; - - // Insert the instance as database entities - new_league_mi - .insert_datasource(MYSQL_DS) - .await - .expect("Failed insert datasource operation"); - new_league_mi_2 - .insert_datasource(MYSQL_DS) - .await - .expect("Failed insert datasource operation"); - new_league_mi_3 - .insert_datasource(MYSQL_DS) - .await - .expect("Failed insert datasource operation"); - - // Recover the inserted data by primary key - let inserted_league = League::find_by_pk_datasource(&new_league_mi.id, MYSQL_DS) - .await - .expect("[1] - Failed the query to the database") - .expect("[1] - No entity found for the primary key value passed in"); - let inserted_league_2 = League::find_by_pk_datasource(&new_league_mi_2.id, MYSQL_DS) - .await - .expect("[2] - Failed the query to the database") - .expect("[2] - No entity found for the primary key value passed in"); - let inserted_league_3 = League::find_by_pk_datasource(&new_league_mi_3.id, MYSQL_DS) - .await - .expect("[3] - Failed the query to the database") - .expect("[3] - No entity found for the primary key value passed in"); - - assert_eq!(new_league_mi.id, inserted_league.id); - assert_eq!(new_league_mi_2.id, inserted_league_2.id); - assert_eq!(new_league_mi_3.id, inserted_league_3.id); -} +// +// /// The multi insert operation is a shorthand for insert multiple instances of *T* +// /// in the database at once. +// /// +// /// It works pretty much the same that the insert operation, with the same behaviour +// /// of the `#[primary_key]` annotation over some field. It will auto set the primary +// /// key field with the autogenerated value on the database on the insert operation, but +// /// for every entity passed in as an array of mutable instances of `T`. +// /// +// /// The instances without `#[primary_key]` inserts all the values on the instaqce fields +// /// on the database. +// #[cfg(feature = "postgres")] +// #[canyon_sql::macros::canyon_tokio_test] +// fn test_crud_multi_insert_operation() { +// let mut new_league_mi: League = League { +// id: Default::default(), +// ext_id: 54376478_i64, +// slug: "some-new-random-league".to_string(), +// name: "Some New Random League".to_string(), +// region: "Unknown".to_string(), +// image_url: "https://what-a-league.io".to_string(), +// }; +// let mut new_league_mi_2: League = League { +// id: Default::default(), +// ext_id: 3475689769678906_i64, +// slug: "new-league-2".to_string(), +// name: "New League 2".to_string(), +// region: "Really unknown".to_string(), +// image_url: "https://what-an-unknown-league.io".to_string(), +// }; +// let mut new_league_mi_3: League = League { +// id: Default::default(), +// ext_id: 46756867_i64, +// slug: "a-new-multinsert".to_string(), +// name: "New League 3".to_string(), +// region: "The dark side of the moon".to_string(), +// image_url: "https://interplanetary-league.io".to_string(), +// }; +// +// // Insert the instance as database entities +// new_league_mi +// .insert() +// .await +// .expect("Failed insert datasource operation"); +// new_league_mi_2 +// .insert() +// .await +// .expect("Failed insert datasource operation"); +// new_league_mi_3 +// .insert() +// .await +// .expect("Failed insert datasource operation"); +// +// // Recover the inserted data by primary key +// let inserted_league = League::find_by_pk(&new_league_mi.id) +// .await +// .expect("[1] - Failed the query to the database") +// .expect("[1] - No entity found for the primary key value passed in"); +// let inserted_league_2 = League::find_by_pk(&new_league_mi_2.id) +// .await +// .expect("[2] - Failed the query to the database") +// .expect("[2] - No entity found for the primary key value passed in"); +// let inserted_league_3 = League::find_by_pk(&new_league_mi_3.id) +// .await +// .expect("[3] - Failed the query to the database") +// .expect("[3] - No entity found for the primary key value passed in"); +// +// assert_eq!(new_league_mi.id, inserted_league.id); +// assert_eq!(new_league_mi_2.id, inserted_league_2.id); +// assert_eq!(new_league_mi_3.id, inserted_league_3.id); +// } +// +// /// Same as the multi insert above, but with the specified datasource +// #[cfg(feature = "mssql")] +// #[canyon_sql::macros::canyon_tokio_test] +// fn test_crud_multi_insert_with_mssql_operation() { +// let mut new_league_mi: League = League { +// id: Default::default(), +// ext_id: 54376478_i64, +// slug: "some-new-random-league".to_string(), +// name: "Some New Random League".to_string(), +// region: "Unknown".to_string(), +// image_url: "https://what-a-league.io".to_string(), +// }; +// let mut new_league_mi_2: League = League { +// id: Default::default(), +// ext_id: 3475689769678906_i64, +// slug: "new-league-2".to_string(), +// name: "New League 2".to_string(), +// region: "Really unknown".to_string(), +// image_url: "https://what-an-unknown-league.io".to_string(), +// }; +// let mut new_league_mi_3: League = League { +// id: Default::default(), +// ext_id: 46756867_i64, +// slug: "a-new-multinsert".to_string(), +// name: "New League 3".to_string(), +// region: "The dark side of the moon".to_string(), +// image_url: "https://interplanetary-league.io".to_string(), +// }; +// +// // Insert the instance as database entities +// new_league_mi +// .insert_with(SQL_SERVER_DS) +// .await +// .expect("Failed insert datasource operation"); +// new_league_mi_2 +// .insert_with(SQL_SERVER_DS) +// .await +// .expect("Failed insert datasource operation"); +// new_league_mi_3 +// .insert_with(SQL_SERVER_DS) +// .await +// .expect("Failed insert datasource operation"); +// +// // Recover the inserted data by primary key +// let inserted_league = League::find_by_pk_with(&new_league_mi.id, SQL_SERVER_DS) +// .await +// .expect("[1] - Failed the query to the database") +// .expect("[1] - No entity found for the primary key value passed in"); +// let inserted_league_2 = League::find_by_pk_with(&new_league_mi_2.id, SQL_SERVER_DS) +// .await +// .expect("[2] - Failed the query to the database") +// .expect("[2] - No entity found for the primary key value passed in"); +// let inserted_league_3 = League::find_by_pk_with(&new_league_mi_3.id, SQL_SERVER_DS) +// .await +// .expect("[3] - Failed the query to the database") +// .expect("[3] - No entity found for the primary key value passed in"); +// +// assert_eq!(new_league_mi.id, inserted_league.id); +// assert_eq!(new_league_mi_2.id, inserted_league_2.id); +// assert_eq!(new_league_mi_3.id, inserted_league_3.id); +// } +// +// /// Same as the multi insert above, but with the specified datasource +// #[cfg(feature = "mysql")] +// #[canyon_sql::macros::canyon_tokio_test] +// fn test_crud_multi_insert_with_mysql_operation() { +// let mut new_league_mi: League = League { +// id: Default::default(), +// ext_id: 54376478_i64, +// slug: "some-new-random-league".to_string(), +// name: "Some New Random League".to_string(), +// region: "Unknown".to_string(), +// image_url: "https://what-a-league.io".to_string(), +// }; +// let mut new_league_mi_2: League = League { +// id: Default::default(), +// ext_id: 3475689769678906_i64, +// slug: "new-league-2".to_string(), +// name: "New League 2".to_string(), +// region: "Really unknown".to_string(), +// image_url: "https://what-an-unknown-league.io".to_string(), +// }; +// let mut new_league_mi_3: League = League { +// id: Default::default(), +// ext_id: 46756867_i64, +// slug: "a-new-multinsert".to_string(), +// name: "New League 3".to_string(), +// region: "The dark side of the moon".to_string(), +// image_url: "https://interplanetary-league.io".to_string(), +// }; +// +// // Insert the instance as database entities +// new_league_mi +// .insert_with(MYSQL_DS) +// .await +// .expect("Failed insert datasource operation"); +// new_league_mi_2 +// .insert_with(MYSQL_DS) +// .await +// .expect("Failed insert datasource operation"); +// new_league_mi_3 +// .insert_with(MYSQL_DS) +// .await +// .expect("Failed insert datasource operation"); +// +// // Recover the inserted data by primary key +// let inserted_league = League::find_by_pk_with(&new_league_mi.id, MYSQL_DS) +// .await +// .expect("[1] - Failed the query to the database") +// .expect("[1] - No entity found for the primary key value passed in"); +// let inserted_league_2 = League::find_by_pk_with(&new_league_mi_2.id, MYSQL_DS) +// .await +// .expect("[2] - Failed the query to the database") +// .expect("[2] - No entity found for the primary key value passed in"); +// let inserted_league_3 = League::find_by_pk_with(&new_league_mi_3.id, MYSQL_DS) +// .await +// .expect("[3] - Failed the query to the database") +// .expect("[3] - No entity found for the primary key value passed in"); +// +// assert_eq!(new_league_mi.id, inserted_league.id); +// assert_eq!(new_league_mi_2.id, inserted_league_2.id); +// assert_eq!(new_league_mi_3.id, inserted_league_3.id); +// } diff --git a/tests/crud/mod.rs b/tests/crud/mod.rs index 407e727c..f333a6de 100644 --- a/tests/crud/mod.rs +++ b/tests/crud/mod.rs @@ -1,10 +1,9 @@ -#![allow(unused_imports)] - pub mod delete_operations; pub mod foreign_key_operations; +pub mod hex_arch_example; #[cfg(feature = "mssql")] pub mod init_mssql; pub mod insert_operations; pub mod querybuilder_operations; -pub mod select_operations; +pub mod read_operations; pub mod update_operations; diff --git a/tests/crud/querybuilder_operations.rs b/tests/crud/querybuilder_operations.rs index f2dc8b57..037b67bf 100644 --- a/tests/crud/querybuilder_operations.rs +++ b/tests/crud/querybuilder_operations.rs @@ -2,6 +2,19 @@ use crate::constants::MYSQL_DS; #[cfg(feature = "mssql")] use crate::constants::SQL_SERVER_DS; +use canyon_sql::connection::DatabaseType; + +/// Tests for the QueryBuilder available operations within Canyon. +/// +/// QueryBuilder are the way of obtain more flexibility that with +/// the default generated queries, essentially for build the queries +/// with the SQL filters +/// +use canyon_sql::query::operators::{ + LikeKind::{Full, Left, Right}, + Operator, + Operator::*, +}; /// Tests for the QueryBuilder available operations within Canyon. /// @@ -10,47 +23,49 @@ use crate::constants::SQL_SERVER_DS; /// with the SQL filters /// use canyon_sql::{ - crud::CrudOperations, - query::{operators::Comp, operators::Like, ops::QueryBuilder}, + crud::{DeleteOperations, ReadOperations, UpdateOperations}, + query::querybuilder::{QueryBuilderOps, SelectQueryBuilderOps, UpdateQueryBuilderOps}, }; use crate::tests_models::league::*; use crate::tests_models::player::*; + +#[cfg(feature = "postgres")] use crate::tests_models::tournament::*; -/// Builds a new SQL statement for retrieves entities of the `T` type, filtered -/// with the parameters that modifies the base SQL to SELECT * FROM #[canyon_sql::macros::canyon_tokio_test] +#[cfg(feature = "postgres")] fn test_generated_sql_by_the_select_querybuilder() { - let mut select_with_joins = League::select_query(); - select_with_joins - .inner_join("tournament", "league.id", "tournament.league_id") - .left_join("team", "tournament.id", "player.tournament_id") - .r#where(LeagueFieldValue::id(&7), Comp::Gt) - .and(LeagueFieldValue::name(&"KOREA"), Comp::Eq) + let fv = LeagueFieldValue::name("KOREA".to_string()); + let select_with_joins = League::select_query()? + .inner_join( + TournamentTable::DbName, + LeagueField::id, + TournamentField::league, + ) + .left_join(PlayerTable::DbName, TournamentField::id, PlayerField::id) + .where_value(&LeagueFieldValue::id(7), Operator::Gt) + .and(&fv, Operator::Eq) .and_values_in(LeagueField::name, &["LCK", "STRANGER THINGS"]); - // .query() - // .await; - // NOTE: We don't have in the docker the generated relationships - // with the joins, so for now, we are just going to check that the - // generated SQL by the SelectQueryBuilder is the spected + assert_eq!( - select_with_joins.read_sql(), - "SELECT * FROM league INNER JOIN tournament ON league.id = tournament.league_id LEFT JOIN team ON tournament.id = player.tournament_id WHERE id > $1 AND name = $2 AND name IN ($2, $3)" + select_with_joins?.build().unwrap().sql(), // TODO: That .unwrap instead of '?' because the lt issues associated with the &'a Z on .and + "SELECT * FROM \"league\" INNER JOIN \"tournament\" ON \"league\".\"id\" = \"tournament\".\"league\" LEFT JOIN \"player\" ON \"tournament\".\"id\" = \"player\".\"id\" WHERE \"league\".\"id\" > $1 AND \"league\".\"name\" = $2 AND \"name\" IN ($3, $4);" ) } -/// Builds a new SQL statement for retrieves entities of the `T` type, filtered -/// with the parameters that modifies the base SQL to SELECT * FROM #[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_with_querybuilder() { // Find all the leagues with ID less or equals that 7 // and where it's region column value is equals to 'Korea' - let filtered_leagues_result: Result, _> = League::select_query() - .r#where(LeagueFieldValue::id(&50), Comp::LtEq) - .and(LeagueFieldValue::region(&"KOREA"), Comp::Eq) - .query() + let fv = LeagueFieldValue::region("KOREA".to_string()); + let filtered_leagues_result: Result, _> = League::select_query()? + .where_value(&LeagueFieldValue::id(50), Operator::LtEq) + .and(&fv, Operator::Eq) + .build() + .unwrap() + .launch_default() .await; let filtered_leagues: Vec = filtered_leagues_result.unwrap(); @@ -67,12 +82,12 @@ fn test_crud_find_with_querybuilder() { #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_with_querybuilder_and_fulllike() { // Find all the leagues with "LC" in their name - let mut filtered_leagues_result = League::select_query(); - filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Full); + let binding = LeagueFieldValue::name("LEC".to_string()); + let filtered_leagues_result = League::select_query()?.where_value(&binding, Like(Full)); assert_eq!( - filtered_leagues_result.read_sql(), - "SELECT * FROM league WHERE name LIKE CONCAT('%', CAST($1 AS VARCHAR) ,'%')" + filtered_leagues_result.build().unwrap().sql(), + "SELECT * FROM \"league\" WHERE \"league\".\"name\" LIKE CONCAT ('%', CAST ($1 AS VARCHAR), '%');" ) } @@ -80,14 +95,15 @@ fn test_crud_find_with_querybuilder_and_fulllike() { /// with the parameters that modifies the base SQL to SELECT * FROM #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_with_querybuilder_and_fulllike_datasource_mssql() { +fn test_crud_find_with_querybuilder_and_fulllike_with_mssql() { // Find all the leagues with "LC" in their name - let mut filtered_leagues_result = League::select_query_datasource(SQL_SERVER_DS); - filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Full); + let fv = LeagueFieldValue::name("LEC".to_string()); + let filtered_leagues_result = + League::select_query_with(DatabaseType::SqlServer)?.where_value(&fv, Like(Full)); assert_eq!( - filtered_leagues_result.read_sql(), - "SELECT * FROM league WHERE name LIKE CONCAT('%', CAST($1 AS VARCHAR) ,'%')" + filtered_leagues_result.build().unwrap().sql(), + "SELECT * FROM [league] WHERE [league].[name] LIKE CONCAT ('%', CAST (@P1 AS VARCHAR), '%');" ) } @@ -95,14 +111,15 @@ fn test_crud_find_with_querybuilder_and_fulllike_datasource_mssql() { /// with the parameters that modifies the base SQL to SELECT * FROM #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_with_querybuilder_and_fulllike_datasource_mysql() { +fn test_crud_find_with_querybuilder_and_fulllike_with_mysql() { // Find all the leagues with "LC" in their name - let mut filtered_leagues_result = League::select_query_datasource(MYSQL_DS); - filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Full); + let fv = LeagueFieldValue::name("LEC".to_string()); + let filtered_leagues_result = + League::select_query_with(DatabaseType::MySQL)?.where_value(&fv, Like(Full)); assert_eq!( - filtered_leagues_result.read_sql(), - "SELECT * FROM league WHERE name LIKE CONCAT('%', CAST($1 AS CHAR) ,'%')" + filtered_leagues_result.build().unwrap().sql(), + "SELECT * FROM `league` WHERE `league`.`name` LIKE CONCAT ('%', CAST (? AS CHAR), '%');" ) } @@ -112,12 +129,12 @@ fn test_crud_find_with_querybuilder_and_fulllike_datasource_mysql() { #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_with_querybuilder_and_leftlike() { // Find all the leagues whose name ends with "CK" - let mut filtered_leagues_result = League::select_query(); - filtered_leagues_result.r#where(LeagueFieldValue::name(&"CK"), Like::Left); + let fv = LeagueFieldValue::name("CK".to_string()); + let filtered_leagues_result = League::select_query()?.where_value(&fv, Like(Left)); assert_eq!( - filtered_leagues_result.read_sql(), - "SELECT * FROM league WHERE name LIKE CONCAT('%', CAST($1 AS VARCHAR))" + filtered_leagues_result.build().unwrap().sql(), + "SELECT * FROM \"league\" WHERE \"league\".\"name\" LIKE CONCAT ('%', CAST ($1 AS VARCHAR));" ) } @@ -125,14 +142,15 @@ fn test_crud_find_with_querybuilder_and_leftlike() { /// with the parameters that modifies the base SQL to SELECT * FROM #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_with_querybuilder_and_leftlike_datasource_mssql() { +fn test_crud_find_with_querybuilder_and_leftlike_with_mssql() { // Find all the leagues whose name ends with "CK" - let mut filtered_leagues_result = League::select_query(); - filtered_leagues_result.r#where(LeagueFieldValue::name(&"CK"), Like::Left); + let fv = LeagueFieldValue::name("CK".to_string()); + let filtered_leagues_result = + League::select_query_with(DatabaseType::SqlServer)?.where_value(&fv, Like(Left)); assert_eq!( - filtered_leagues_result.read_sql(), - "SELECT * FROM league WHERE name LIKE CONCAT('%', CAST($1 AS VARCHAR))" + filtered_leagues_result.build().unwrap().sql(), + "SELECT * FROM [league] WHERE [league].[name] LIKE CONCAT ('%', CAST (@P1 AS VARCHAR));" ) } @@ -140,14 +158,15 @@ fn test_crud_find_with_querybuilder_and_leftlike_datasource_mssql() { /// with the parameters that modifies the base SQL to SELECT * FROM #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_with_querybuilder_and_leftlike_datasource_mysql() { +fn test_crud_find_with_querybuilder_and_leftlike_with_mysql() { // Find all the leagues whose name ends with "CK" - let mut filtered_leagues_result = League::select_query_datasource(MYSQL_DS); - filtered_leagues_result.r#where(LeagueFieldValue::name(&"CK"), Like::Left); + let fv = LeagueFieldValue::name("CK".to_string()); + let filtered_leagues_result = + League::select_query_with(DatabaseType::MySQL)?.where_value(&fv, Like(Left)); assert_eq!( - filtered_leagues_result.read_sql(), - "SELECT * FROM league WHERE name LIKE CONCAT('%', CAST($1 AS CHAR))" + filtered_leagues_result.build().unwrap().sql(), + "SELECT * FROM `league` WHERE `league`.`name` LIKE CONCAT ('%', CAST (? AS CHAR));" ) } @@ -157,12 +176,12 @@ fn test_crud_find_with_querybuilder_and_leftlike_datasource_mysql() { #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_with_querybuilder_and_rightlike() { // Find all the leagues whose name starts with "LC" - let mut filtered_leagues_result = League::select_query(); - filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Right); + let fv = LeagueFieldValue::name("LEC".to_string()); + let filtered_leagues_result = League::select_query()?.where_value(&fv, Like(Right)); assert_eq!( - filtered_leagues_result.read_sql(), - "SELECT * FROM league WHERE name LIKE CONCAT(CAST($1 AS VARCHAR) ,'%')" + filtered_leagues_result.build().unwrap().sql(), + "SELECT * FROM \"league\" WHERE \"league\".\"name\" LIKE CONCAT (CAST ($1 AS VARCHAR), '%');" ) } @@ -170,39 +189,44 @@ fn test_crud_find_with_querybuilder_and_rightlike() { /// with the parameters that modifies the base SQL to SELECT * FROM #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_with_querybuilder_and_rightlike_datasource_mssql() { +fn test_crud_find_with_querybuilder_and_rightlike_with_mssql() { // Find all the leagues whose name starts with "LC" - let mut filtered_leagues_result = League::select_query_datasource(SQL_SERVER_DS); - filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Right); + let fv = LeagueFieldValue::name("LEC".to_string()); + let filtered_leagues_result = + League::select_query_with(DatabaseType::SqlServer)?.where_value(&fv, Like(Right)); assert_eq!( - filtered_leagues_result.read_sql(), - "SELECT * FROM league WHERE name LIKE CONCAT(CAST($1 AS VARCHAR) ,'%')" + filtered_leagues_result.build().unwrap().sql(), + "SELECT * FROM [league] WHERE [league].[name] LIKE CONCAT (CAST (@P1 AS VARCHAR), '%');" ) } + /// Builds a new SQL statement for retrieves entities of the `T` type, filtered /// with the parameters that modifies the base SQL to SELECT * FROM #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_with_querybuilder_and_rightlike_datasource_mysql() { +fn test_crud_find_with_querybuilder_and_rightlike_with_mysql() { // Find all the leagues whose name starts with "LC" - let mut filtered_leagues_result = League::select_query_datasource(MYSQL_DS); - filtered_leagues_result.r#where(LeagueFieldValue::name(&"LC"), Like::Right); + let wh = LeagueFieldValue::name("LEC".to_string()); + let filtered_leagues_result = + League::select_query_with(DatabaseType::MySQL)?.where_value(&wh, Like(Right)); assert_eq!( - filtered_leagues_result.read_sql(), - "SELECT * FROM league WHERE name LIKE CONCAT(CAST($1 AS CHAR) ,'%')" + filtered_leagues_result.build().unwrap().sql(), + "SELECT * FROM `league` WHERE `league`.`name` LIKE CONCAT (CAST (? AS CHAR), '%');" ) } /// Same than the above but with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_with_querybuilder_datasource_mssql() { - // Find all the players where its ID column value is greater that 50 - let filtered_find_players = Player::select_query_datasource(SQL_SERVER_DS) - .r#where(PlayerFieldValue::id(&50), Comp::Gt) - .query() +fn test_crud_find_with_querybuilder_with_mssql() { + // Find all the players where its ID column value is greater than 50 + let filtered_find_players = Player::select_query_with(DatabaseType::SqlServer)? + .where_value(&PlayerFieldValue::id(50), Operator::Gt) + .build() + .unwrap() + .launch_with::<&str, Player>(SQL_SERVER_DS) .await; assert!(!filtered_find_players.unwrap().is_empty()); @@ -211,14 +235,23 @@ fn test_crud_find_with_querybuilder_datasource_mssql() { /// Same than the above but with the specified datasource #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_with_querybuilder_datasource_mysql() { - // Find all the players where its ID column value is greater that 50 - let filtered_find_players = Player::select_query_datasource(MYSQL_DS) - .r#where(PlayerFieldValue::id(&50), Comp::Gt) - .query() +fn test_crud_find_with_querybuilder_with_mysql() { + // Find all the players where its ID column value is greater than 50 + let filtered_find_players = Player::select_query_with(DatabaseType::MySQL)? + .where_value(&PlayerFieldValue::id(50), Operator::Gt) + .build() + .unwrap(); + + assert_eq!( + filtered_find_players.sql(), + "SELECT * FROM `player` WHERE `player`.`id` > ?;" + ); + + let result = filtered_find_players + .launch_with::<&str, Player>(MYSQL_DS) .await; - assert!(!filtered_find_players.unwrap().is_empty()); + assert!(!result.unwrap().is_empty()); } /// Updates the values of the range on entries defined by the constraint parameters @@ -228,28 +261,26 @@ fn test_crud_find_with_querybuilder_datasource_mysql() { fn test_crud_update_with_querybuilder() { // Find all the leagues with ID less or equals that 7 // and where it's region column value is equals to 'Korea' - let mut q = League::update_query(); - q.set(&[ - (LeagueField::slug, "Updated with the QueryBuilder"), - (LeagueField::name, "Random"), - ]) - .r#where(LeagueFieldValue::id(&1), Comp::Gt) - .and(LeagueFieldValue::id(&8), Comp::Lt); - - /* NOTE: Family of QueryBuilders are clone, useful in case of need to read the generated SQL - let qpr = q.clone(); - println!("PSQL: {:?}", qpr.read_sql()); - */ - - // We can now back to the original an throw the query - q.query() + League::update_query()? + .set_values(&[ + (LeagueField::slug, "Updated with the QueryBuilder"), + (LeagueField::name, "Random"), + ]) + .unwrap() + .where_value(&LeagueFieldValue::id(1), Operator::Gt) + .and(&LeagueFieldValue::id(8), Operator::Lt) + .build() + .expect("Failed to update records with the querybuilder") + .launch_default::() .await - .expect("Failed to update records with the querybuilder"); + .unwrap(); - let found_updated_values = League::select_query() - .r#where(LeagueFieldValue::id(&1), Comp::Gt) - .and(LeagueFieldValue::id(&7), Comp::Lt) - .query() + let found_updated_values = League::select_query()? + .where_value(&LeagueFieldValue::id(1), Operator::Gt) + .and(&LeagueFieldValue::id(8), Operator::Lt) + .build() + .unwrap() + .launch_default::() .await .expect("Failed to retrieve database League entries with the querybuilder"); @@ -261,24 +292,29 @@ fn test_crud_update_with_querybuilder() { /// Same as above, but with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_update_with_querybuilder_datasource_mssql() { +fn test_crud_update_with_querybuilder_with_mssql() { // Find all the leagues with ID less or equals that 7 // and where it's region column value is equals to 'Korea' - let mut q = Player::update_query_datasource(SQL_SERVER_DS); - q.set(&[ + let q = Player::update_query_with(DatabaseType::SqlServer); + q.set_values(&[ (PlayerField::summoner_name, "Random updated player name"), (PlayerField::first_name, "I am an updated first name"), ]) - .r#where(PlayerFieldValue::id(&1), Comp::Gt) - .and(PlayerFieldValue::id(&8), Comp::Lt) - .query() + .unwrap() + .where_value(&PlayerFieldValue::id(1), Operator::Gt) + .and(&PlayerFieldValue::id(8), Operator::Lt) + .build() + .unwrap() + .launch_with::<&str, Player>(SQL_SERVER_DS) .await .expect("Failed to update records with the querybuilder"); - let found_updated_values = Player::select_query_datasource(SQL_SERVER_DS) - .r#where(PlayerFieldValue::id(&1), Comp::Gt) - .and(PlayerFieldValue::id(&7), Comp::LtEq) - .query() + let found_updated_values = Player::select_query_with(DatabaseType::SqlServer)? + .where_value(&PlayerFieldValue::id(1), Operator::Gt) + .and(&PlayerFieldValue::id(7), Operator::LtEq) + .build() + .unwrap() + .launch_with::<&str, Player>(SQL_SERVER_DS) .await .expect("Failed to retrieve database League entries with the querybuilder"); @@ -291,25 +327,35 @@ fn test_crud_update_with_querybuilder_datasource_mssql() { /// Same as above, but with the specified datasource #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_update_with_querybuilder_datasource_mysql() { +fn test_crud_update_with_querybuilder_with_mysql() { // Find all the leagues with ID less or equals that 7 // and where it's region column value is equals to 'Korea' - let mut q = Player::update_query_datasource(MYSQL_DS); - q.set(&[ - (PlayerField::summoner_name, "Random updated player name"), - (PlayerField::first_name, "I am an updated first name"), - ]) - .r#where(PlayerFieldValue::id(&1), Comp::Gt) - .and(PlayerFieldValue::id(&8), Comp::Lt) - .query() - .await - .expect("Failed to update records with the querybuilder"); + let q = Player::update_query_with(DatabaseType::MySQL); + let update_query = q + .set_values(&[ + (PlayerField::summoner_name, "Random updated player name"), + (PlayerField::first_name, "I am an updated first name"), + ])? + .where_value(&PlayerFieldValue::id(1), Operator::Gt) + .and(&PlayerFieldValue::id(8), Operator::Lt) + .build()?; - let found_updated_values = Player::select_query_datasource(MYSQL_DS) - .r#where(PlayerFieldValue::id(&1), Comp::Gt) - .and(PlayerFieldValue::id(&7), Comp::LtEq) - .query() + assert_eq!( + update_query.sql(), + "UPDATE `player` SET `summoner_name` = ?, `first_name` = ? WHERE `player`.`id` > ? AND `player`.`id` < ?;" + ); + + update_query + .launch_with::<&str, Player>(MYSQL_DS) + .await + .expect("Failed to update records with the querybuilder"); + + let found_updated_values = Player::select_query_with(DatabaseType::MySQL)? + .where_value(&PlayerFieldValue::id(1), Operator::Gt) + .and(&PlayerFieldValue::id(7), Operator::LtEq) + .build()? + .launch_with::<&str, Player>(MYSQL_DS) .await .expect("Failed to retrieve database League entries with the querybuilder"); @@ -324,134 +370,279 @@ fn test_crud_update_with_querybuilder_datasource_mysql() { /// /// Note if the database is persisted (not created and destroyed on every docker or /// GitHub Action wake up), it won't delete things that already have been deleted, -/// but this isn't an error. They just don't exists. +/// but this isn't an error. They just don't exist. #[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_delete_with_querybuilder() { - Tournament::delete_query() - .r#where(TournamentFieldValue::id(&14), Comp::Gt) - .and(TournamentFieldValue::id(&16), Comp::Lt) - .query() + Tournament::delete_query()? + .where_value(&TournamentFieldValue::id(14), Operator::Gt) + .and(&TournamentFieldValue::id(16), Operator::Lt) + .build()? + .launch_default::() .await .expect("Error connecting with the database on the delete operation"); assert_eq!(Tournament::find_by_pk(&15).await.unwrap(), None); } +#[cfg(feature = "postgres")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_delete_with_querybuilder_lt_creation() { + let q = Tournament::delete_query()?.where_value(&TournamentFieldValue::id(10), Operator::Gt); + assert_eq!( + q.build()?.sql(), + "DELETE FROM \"tournament\" WHERE \"tournament\".\"id\" > $1;" + ); +} + /// Same as the above delete, but with the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_delete_with_querybuilder_datasource_mssql() { - Player::delete_query_datasource(SQL_SERVER_DS) - .r#where(PlayerFieldValue::id(&120), Comp::Gt) - .and(PlayerFieldValue::id(&130), Comp::Lt) - .query() +fn test_crud_delete_with_querybuilder_with_mssql() { + Player::delete_query_with(DatabaseType::SqlServer) + .where_value(&PlayerFieldValue::id(120), Operator::Gt) + .and(&PlayerFieldValue::id(130), Operator::Lt) + .build()? + .launch_with::<&str, Player>(SQL_SERVER_DS) .await .expect("Error connecting with the database when we are going to delete data! :)"); - assert!(Player::select_query_datasource(SQL_SERVER_DS) - .r#where(PlayerFieldValue::id(&122), Comp::Eq) - .query() - .await - .unwrap() - .is_empty()); + assert!( + Player::select_query_with(DatabaseType::SqlServer)? + .where_value(&PlayerFieldValue::id(122), Operator::Eq) + .build() + .unwrap() + .launch_with::<&str, Player>(SQL_SERVER_DS) + .await + .unwrap() + .is_empty() + ); } /// Same as the above delete, but with the specified datasource #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_delete_with_querybuilder_datasource_mysql() { - Player::delete_query_datasource(MYSQL_DS) - .r#where(PlayerFieldValue::id(&120), Comp::Gt) - .and(PlayerFieldValue::id(&130), Comp::Lt) - .query() +fn test_crud_delete_with_querybuilder_with_mysql() { + Player::delete_query_with(DatabaseType::MySQL) + .where_value(&PlayerFieldValue::id(120), Operator::Gt) + .and(&PlayerFieldValue::id(130), Operator::Lt) + .build() + .unwrap() + .launch_with::<&str, Player>(MYSQL_DS) .await .expect("Error connecting with the database when we are going to delete data! :)"); - assert!(Player::select_query_datasource(MYSQL_DS) - .r#where(PlayerFieldValue::id(&122), Comp::Eq) - .query() - .await - .unwrap() - .is_empty()); + assert!( + Player::select_query_with(DatabaseType::MySQL)? + .where_value(&PlayerFieldValue::id(122), Operator::Eq) + .build() + .unwrap() + .launch_with::<&str, Player>(MYSQL_DS) + .await + .unwrap() + .is_empty() + ); } -/// Tests for the generated SQL query after use the -/// WHERE clause +/// Returns every database backend enabled for this compilation. +fn enabled_database_types() -> Vec { + vec![ + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql, + #[cfg(feature = "mssql")] + DatabaseType::SqlServer, + #[cfg(feature = "mysql")] + DatabaseType::MySQL, + ] +} + +/// Tests for the generated SQL query after using the WHERE clause. #[canyon_sql::macros::canyon_tokio_test] fn test_where_clause() { - let mut l = League::select_query(); - l.r#where(LeagueFieldValue::name(&"LEC"), Comp::Eq); + for database_type in enabled_database_types() { + let wh = LeagueFieldValue::name("LEC".to_string()); + + let query = League::select_query_with(database_type)? + .where_value(&wh, Operator::Eq) + .build() + .unwrap(); + + let expected = match database_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => "SELECT * FROM \"league\" WHERE \"league\".\"name\" = $1;", - assert_eq!(l.read_sql(), "SELECT * FROM league WHERE name = $1") + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => "SELECT * FROM [league] WHERE [league].[name] = @P1;", + + #[cfg(feature = "mysql")] + DatabaseType::MySQL => "SELECT * FROM `league` WHERE `league`.`name` = ?;", + }; + + assert_eq!(query.sql(), expected); + } } -/// Tests for the generated SQL query after use the -/// AND clause +/// Tests for the generated SQL query after using the AND clause. #[canyon_sql::macros::canyon_tokio_test] fn test_and_clause() { - let mut l = League::select_query(); - l.r#where(LeagueFieldValue::name(&"LEC"), Comp::Eq) - .and(LeagueFieldValue::id(&10), Comp::LtEq); - - assert_eq!( - l.read_sql().trim(), - "SELECT * FROM league WHERE name = $1 AND id <= $2" - ) + for database_type in enabled_database_types() { + let wh = LeagueFieldValue::name("LEC".to_string()); + + let query = League::select_query_with(database_type)? + .where_value(&wh, Operator::Eq) + .and(&LeagueFieldValue::id(10), Operator::LtEq) + .build() + .unwrap(); + + let expected = match database_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => { + "SELECT * FROM \"league\" WHERE \"league\".\"name\" = $1 AND \"league\".\"id\" <= $2;" + } + + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => { + "SELECT * FROM [league] WHERE [league].[name] = @P1 AND [league].[id] <= @P2;" + } + + #[cfg(feature = "mysql")] + DatabaseType::MySQL => { + "SELECT * FROM `league` WHERE `league`.`name` = ? AND `league`.`id` <= ?;" + } + }; + + assert_eq!(query.sql(), expected); + } } -/// Tests for the generated SQL query after use the -/// AND clause +/// Tests for the generated SQL query after using AND with an IN constraint. #[canyon_sql::macros::canyon_tokio_test] fn test_and_clause_with_in_constraint() { - let mut l = League::select_query(); - l.r#where(LeagueFieldValue::name(&"LEC"), Comp::Eq) - .and_values_in(LeagueField::id, &[1, 7, 10]); - - assert_eq!( - l.read_sql().trim(), - "SELECT * FROM league WHERE name = $1 AND id IN ($1, $2, $3)" - ) + for database_type in enabled_database_types() { + let wh = LeagueFieldValue::name("LEC".to_string()); + + let query = League::select_query_with(database_type)? + .where_value(&wh, Operator::Eq) + .and_values_in(LeagueField::id, &[1, 7, 10])? + .build() + .unwrap(); + + let expected = match database_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => { + "SELECT * FROM \"league\" WHERE \"league\".\"name\" = $1 AND \"id\" IN ($2, $3, $4);" + } + + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => { + "SELECT * FROM [league] WHERE [league].[name] = @P1 AND [id] IN (@P2, @P3, @P4);" + } + + #[cfg(feature = "mysql")] + DatabaseType::MySQL => { + "SELECT * FROM `league` WHERE `league`.`name` = ? AND `id` IN (?, ?, ?);" + } + }; + + assert_eq!(query.sql(), expected); + } } -/// Tests for the generated SQL query after use the -/// AND clause +/// Tests for the generated SQL query after using the OR clause. #[canyon_sql::macros::canyon_tokio_test] fn test_or_clause() { - let mut l = League::select_query(); - l.r#where(LeagueFieldValue::name(&"LEC"), Comp::Eq) - .or(LeagueFieldValue::id(&10), Comp::LtEq); - - assert_eq!( - l.read_sql().trim(), - "SELECT * FROM league WHERE name = $1 OR id <= $2" - ) + for database_type in enabled_database_types() { + let wh = LeagueFieldValue::name("LEC".to_string()); + + let query = League::select_query_with(database_type)? + .where_value(&wh, Operator::Eq) + .or(&LeagueFieldValue::id(10), Operator::LtEq) + .build() + .unwrap(); + + let expected = match database_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => { + "SELECT * FROM \"league\" WHERE \"league\".\"name\" = $1 OR \"league\".\"id\" <= $2;" + } + + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => { + "SELECT * FROM [league] WHERE [league].[name] = @P1 OR [league].[id] <= @P2;" + } + + #[cfg(feature = "mysql")] + DatabaseType::MySQL => { + "SELECT * FROM `league` WHERE `league`.`name` = ? OR `league`.`id` <= ?;" + } + }; + + assert_eq!(query.sql(), expected); + } } -/// Tests for the generated SQL query after use the -/// AND clause +/// Tests for the generated SQL query after using OR with an IN constraint. #[canyon_sql::macros::canyon_tokio_test] fn test_or_clause_with_in_constraint() { - let mut l = League::select_query(); - l.r#where(LeagueFieldValue::name(&"LEC"), Comp::Eq) - .or_values_in(LeagueField::id, &[1, 7, 10]); - - assert_eq!( - l.read_sql(), - "SELECT * FROM league WHERE name = $1 OR id IN ($1, $2, $3)" - ) + for database_type in enabled_database_types() { + let wh = LeagueFieldValue::name("LEC".to_string()); + + let query = League::select_query_with(database_type)? + .where_value(&wh, Operator::Eq) + .or_values_in(LeagueField::id, &[1, 7, 10])? + .build() + .unwrap(); + + let expected = match database_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => { + "SELECT * FROM \"league\" WHERE \"league\".\"name\" = $1 OR \"id\" IN ($2, $3, $4);" + } + + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => { + "SELECT * FROM [league] WHERE [league].[name] = @P1 OR [id] IN (@P2, @P3, @P4);" + } + + #[cfg(feature = "mysql")] + DatabaseType::MySQL => { + "SELECT * FROM `league` WHERE `league`.`name` = ? OR `id` IN (?, ?, ?);" + } + }; + + assert_eq!(query.sql(), expected); + } } -/// Tests for the generated SQL query after use the -/// AND clause +/// Tests for the generated SQL query after using the ORDER BY clause. #[canyon_sql::macros::canyon_tokio_test] fn test_order_by_clause() { - let mut l = League::select_query(); - l.r#where(LeagueFieldValue::name(&"LEC"), Comp::Eq) - .order_by(LeagueField::id, false); - - assert_eq!( - l.read_sql(), - "SELECT * FROM league WHERE name = $1 ORDER BY id" - ) + for database_type in enabled_database_types() { + let fv = LeagueFieldValue::name("LEC".to_string()); + + let query = League::select_query_with(database_type)? + .where_value(&fv, Operator::Eq) + .order_by(LeagueField::id, false) + .build() + .unwrap(); + + let expected = match database_type { + #[cfg(feature = "postgres")] + DatabaseType::PostgreSql => { + "SELECT * FROM \"league\" WHERE \"league\".\"name\" = $1 ORDER BY \"league\".\"id\";" + } + + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => { + "SELECT * FROM [league] WHERE [league].[name] = @P1 ORDER BY [league].[id];" + } + + #[cfg(feature = "mysql")] + DatabaseType::MySQL => { + "SELECT * FROM `league` WHERE `league`.`name` = ? ORDER BY `league`.`id`;" + } + }; + + assert_eq!(query.sql(), expected); + } } diff --git a/tests/crud/select_operations.rs b/tests/crud/read_operations.rs similarity index 76% rename from tests/crud/select_operations.rs rename to tests/crud/read_operations.rs index f3342c02..8691d9fc 100644 --- a/tests/crud/select_operations.rs +++ b/tests/crud/read_operations.rs @@ -8,11 +8,13 @@ use crate::constants::SQL_SERVER_DS; // Integration tests for the CRUD operations available in `Canyon` that /// generates and executes *SELECT* statements use crate::Error; -use canyon_sql::crud::CrudOperations; - use crate::tests_models::league::*; + +#[cfg(feature = "postgres")] use crate::tests_models::player::*; +use canyon_sql::crud::ReadOperations; + /// Tests the behaviour of a SELECT * FROM {table_name} within Canyon, through the /// `::find_all()` associated function derived with the `CanyonCrud` derive proc-macro /// and using the *default datasource* @@ -31,47 +33,30 @@ fn test_crud_find_all() { assert!(!find_all_players.unwrap().is_empty()); } -/// Same as the `find_all()`, but with the unchecked variant, which directly returns `Vec` not -/// `Result` wrapped -#[cfg(feature = "postgres")] -#[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_all_unchecked() { - let find_all_result: Vec = League::find_all_unchecked().await; - assert!(!find_all_result.is_empty()); -} - /// Tests the behaviour of a SELECT * FROM {table_name} within Canyon, through the /// `::find_all()` associated function derived with the `CanyonCrud` derive proc-macro /// and using the specified datasource #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_all_datasource_mssql() { +fn test_crud_find_all_with_mssql() { let find_all_result: Result, Box> = - League::find_all_datasource(SQL_SERVER_DS).await; + League::find_all_with(SQL_SERVER_DS).await; // Connection doesn't return an error - assert!(!find_all_result.is_err()); + assert!(!find_all_result.is_err(), "{:?}", find_all_result); assert!(!find_all_result.unwrap().is_empty()); } #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_all_datasource_mysql() { +fn test_crud_find_all_with_mysql() { let find_all_result: Result, Box> = - League::find_all_datasource(MYSQL_DS).await; + League::find_all_with(MYSQL_DS).await; + // Connection doesn't return an error assert!(!find_all_result.is_err()); assert!(!find_all_result.unwrap().is_empty()); } -/// Same as the `find_all_datasource()`, but with the unchecked variant and the specified dataosource, -/// returning directly `Vec` and not `Result, Err>` -#[cfg(feature = "mssql")] -#[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_all_unchecked_datasource() { - let find_all_result: Vec = League::find_all_unchecked_datasource(SQL_SERVER_DS).await; - assert!(!find_all_result.is_empty()); -} - /// Tests the behaviour of a SELECT * FROM {table_name} WHERE = , where the pk is /// defined with the #[primary_key] attribute over some field of the type. /// @@ -101,9 +86,9 @@ fn test_crud_find_by_pk() { /// Uses the *specified datasource mssql* in the second parameter of the function call. #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_by_pk_datasource_mssql() { +fn test_crud_find_by_pk_with_mssql() { let find_by_pk_result: Result, Box> = - League::find_by_pk_datasource(&27, SQL_SERVER_DS).await; + League::find_by_pk_with(&27, SQL_SERVER_DS).await; assert!(find_by_pk_result.as_ref().unwrap().is_some()); let some_league = find_by_pk_result.unwrap().unwrap(); @@ -124,9 +109,9 @@ fn test_crud_find_by_pk_datasource_mssql() { /// Uses the *specified datasource mysql* in the second parameter of the function call. #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_find_by_pk_datasource_mysql() { +fn test_crud_find_by_pk_with_mysql() { let find_by_pk_result: Result, Box> = - League::find_by_pk_datasource(&27, MYSQL_DS).await; + League::find_by_pk_with(&27, MYSQL_DS).await; assert!(find_by_pk_result.as_ref().unwrap().is_some()); let some_league = find_by_pk_result.unwrap().unwrap(); @@ -155,13 +140,10 @@ fn test_crud_count_operation() { /// the specified datasource mssql #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_count_datasource_operation_mssql() { +fn test_crud_count_with_operation_mssql() { assert_eq!( - League::find_all_datasource(SQL_SERVER_DS) - .await - .unwrap() - .len() as i64, - League::count_datasource(SQL_SERVER_DS).await.unwrap() + League::find_all_with(SQL_SERVER_DS).await.unwrap().len() as i64, + League::count_with(SQL_SERVER_DS).await.unwrap() ); } @@ -169,9 +151,9 @@ fn test_crud_count_datasource_operation_mssql() { /// the specified datasource mysql #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_count_datasource_operation_mysql() { +fn test_crud_count_with_operation_mysql() { assert_eq!( - League::find_all_datasource(MYSQL_DS).await.unwrap().len() as i64, - League::count_datasource(MYSQL_DS).await.unwrap() + League::find_all_with(MYSQL_DS).await.unwrap().len() as i64, + League::count_with(MYSQL_DS).await.unwrap() ); } diff --git a/tests/crud/update_operations.rs b/tests/crud/update_operations.rs index dfc4af15..cdc3416c 100644 --- a/tests/crud/update_operations.rs +++ b/tests/crud/update_operations.rs @@ -1,7 +1,7 @@ use crate::tests_models::league::*; // Integration tests for the CRUD operations available in `Canyon` that /// generates and executes *UPDATE* statements -use canyon_sql::crud::CrudOperations; +use canyon_sql::crud::{ReadOperations, UpdateOperations}; #[cfg(feature = "mysql")] use crate::constants::MYSQL_DS; @@ -12,7 +12,7 @@ use crate::constants::SQL_SERVER_DS; /// some change to a Rust's entity instance, and persisting them into the database. /// /// The `t.update(&self)` operation is only enabled for types that -/// has, at least, one of it's fields annotated with a `#[primary_key]` +/// has, at least, one of its fields annotated with a `#[primary_key]` /// operation, because we use that concrete field to construct the clause that targets /// that entity. /// @@ -30,7 +30,7 @@ fn test_crud_update_method_operation() { // The ext_id field value is extracted from the sql scripts under the // docker/sql folder. We are retrieving the first entity inserted at the - // wake up time of the database, and now checking some of its properties. + // wake-up time of the database, and now checking some of its properties. assert_eq!(updt_candidate.ext_id, 100695891328981122_i64); // Modify the value, and perform the update @@ -49,52 +49,52 @@ fn test_crud_update_method_operation() { assert_eq!(updt_entity.ext_id, updt_value); - // We rollback the changes to the initial value to don't broke other tests + // We roll back the changes to the initial value to don't broke other tests // the next time that will run updt_candidate.ext_id = 100695891328981122_i64; updt_candidate .update() .await - .expect("Failed the restablish initial value update operation"); + .expect("Failed to restore the initial value in the psql update operation"); } /// Same as the above test, but with the specified datasource. #[cfg(feature = "mssql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_update_datasource_mssql_method_operation() { +fn test_crud_update_with_mssql_method_operation() { // We first retrieve some entity from the database. Note that we must make // the retrieved instance mutable of clone it to a new mutable resource - let mut updt_candidate: League = League::find_by_pk_datasource(&1, SQL_SERVER_DS) + let mut updt_candidate: League = League::find_by_pk_with(&1, SQL_SERVER_DS) .await .expect("[1] - Failed the query to the database") .expect("[1] - No entity found for the primary key value passed in"); // The ext_id field value is extracted from the sql scripts under the // docker/sql folder. We are retrieving the first entity inserted at the - // wake up time of the database, and now checking some of its properties. + // wake-up time of the database, and now checking some of its properties. assert_eq!(updt_candidate.ext_id, 100695891328981122_i64); // Modify the value, and perform the update let updt_value: i64 = 59306442534_i64; updt_candidate.ext_id = updt_value; updt_candidate - .update_datasource(SQL_SERVER_DS) + .update_with(SQL_SERVER_DS) .await .expect("Failed the update operation"); // Retrieve it again, and check if the value was really updated - let updt_entity: League = League::find_by_pk_datasource(&1, SQL_SERVER_DS) + let updt_entity: League = League::find_by_pk_with(&1, SQL_SERVER_DS) .await .expect("[2] - Failed the query to the database") .expect("[2] - No entity found for the primary key value passed in"); assert_eq!(updt_entity.ext_id, updt_value); - // We rollback the changes to the initial value to don't broke other tests + // We roll back the changes to the initial value to don't broke other tests // the next time that will run updt_candidate.ext_id = 100695891328981122_i64; updt_candidate - .update_datasource(SQL_SERVER_DS) + .update_with(SQL_SERVER_DS) .await .expect("Failed to restablish the initial value update operation"); } @@ -102,41 +102,41 @@ fn test_crud_update_datasource_mssql_method_operation() { /// Same as the above test, but with the specified datasource. #[cfg(feature = "mysql")] #[canyon_sql::macros::canyon_tokio_test] -fn test_crud_update_datasource_mysql_method_operation() { +fn test_crud_update_with_mysql_method_operation() { // We first retrieve some entity from the database. Note that we must make // the retrieved instance mutable of clone it to a new mutable resource - let mut updt_candidate: League = League::find_by_pk_datasource(&1, MYSQL_DS) + let mut updt_candidate: League = League::find_by_pk_with(&1, MYSQL_DS) .await .expect("[1] - Failed the query to the database") .expect("[1] - No entity found for the primary key value passed in"); // The ext_id field value is extracted from the sql scripts under the // docker/sql folder. We are retrieving the first entity inserted at the - // wake up time of the database, and now checking some of its properties. + // wake-up time of the database, and now checking some of its properties. assert_eq!(updt_candidate.ext_id, 100695891328981122_i64); // Modify the value, and perform the update let updt_value: i64 = 59306442534_i64; updt_candidate.ext_id = updt_value; updt_candidate - .update_datasource(MYSQL_DS) + .update_with(MYSQL_DS) .await .expect("Failed the update operation"); // Retrieve it again, and check if the value was really updated - let updt_entity: League = League::find_by_pk_datasource(&1, MYSQL_DS) + let updt_entity: League = League::find_by_pk_with(&1, MYSQL_DS) .await .expect("[2] - Failed the query to the database") .expect("[2] - No entity found for the primary key value passed in"); assert_eq!(updt_entity.ext_id, updt_value); - // We rollback the changes to the initial value to don't broke other tests + // We roll back the changes to the initial value to don't broke other tests // the next time that will run updt_candidate.ext_id = 100695891328981122_i64; updt_candidate - .update_datasource(MYSQL_DS) + .update_with(MYSQL_DS) .await .expect("Failed to restablish the initial value update operation"); } diff --git a/tests/migrations/mod.rs b/tests/migrations/mod.rs index b0fbed96..957c0a5d 100644 --- a/tests/migrations/mod.rs +++ b/tests/migrations/mod.rs @@ -1,15 +1,34 @@ #![allow(unused_imports)] + use crate::constants; +use canyon_sql::connection::DbConnection; +use canyon_sql::core::Canyon; /// Integration tests for the migrations feature of `Canyon-SQL` -use canyon_sql::crud::Transaction; -#[cfg(feature = "migrations")] +use canyon_sql::core::Transaction; use canyon_sql::migrations::handler::Migrations; +use std::ops::DerefMut; /// Brings the information of the `PostgreSQL` requested schema #[cfg(all(feature = "postgres", feature = "migrations"))] #[canyon_sql::macros::canyon_tokio_test] fn test_migrations_postgresql_status_query() { - let results = Migrations::query(constants::FETCH_PUBLIC_SCHEMA, [], constants::PSQL_DS).await; + let canyon = Canyon::instance().unwrap(); + + let ds = canyon.find_datasource_by_name_or_default(constants::PSQL_DS); + assert!(ds.is_ok()); + let ds = ds.unwrap(); + let ds_name = &ds.name; + + let db_conn = canyon.get_connection(ds_name).unwrap_or_else(|_| { + panic!( + "Unable to get a database connection on Canyon Memory: {:?}", + ds_name + ) + }); + + let results = db_conn + .query_rows(constants::FETCH_PUBLIC_SCHEMA, &[]) + .await; assert!(results.is_ok()); let res = results.unwrap(); diff --git a/tests/simple_canyon.toml b/tests/simple_canyon.toml new file mode 100644 index 00000000..a5536b6e --- /dev/null +++ b/tests/simple_canyon.toml @@ -0,0 +1,12 @@ +[canyon_sql] + +[[canyon_sql.datasources]] +name = 'postgres_docker' + +[canyon_sql.datasources.auth] +postgresql = { basic = { username = 'postgres', password = 'postgres'}} + +[canyon_sql.datasources.properties] +host = 'localhost' +port = 5438 +db_name = 'postgres' \ No newline at end of file diff --git a/tests/tests_models/league.rs b/tests/tests_models/league.rs index 3f3037e7..b1503117 100644 --- a/tests/tests_models/league.rs +++ b/tests/tests_models/league.rs @@ -1,8 +1,7 @@ use canyon_sql::macros::*; #[derive(Debug, Fields, CanyonCrud, CanyonMapper, ForeignKeyable, Eq, PartialEq)] -// #[canyon_entity(table_name = "league", schema = "public")] -#[canyon_entity(table_name = "league")] +#[canyon_entity(table_name = "league", /* schema = "public"*/)] pub struct League { #[primary_key] id: i32, diff --git a/tests/tests_models/player.rs b/tests/tests_models/player.rs index 59c03daa..3bdc251e 100644 --- a/tests/tests_models/player.rs +++ b/tests/tests_models/player.rs @@ -9,11 +9,11 @@ use canyon_sql::macros::*; /// Note that this entity has a primary key declared in the database, but we will /// omit this in Canyon, so for us, is like if the primary key wasn't set up. /// -/// Remember that the entities that does not declares at least a field as `#[primary_key]` +/// Remember that the entities that does not declare at least a field as `#[primary_key]` /// does not have all the CRUD operations available, only the ones that doesn't -/// requires of a primary key. +/// require of a primary key. pub struct Player { - // #[primary_key] We will omit this to use it as a mock of entities that doesn't declares primary key + // #[primary_key] // We will omit this to use it as a mock of entities that doesn't declare primary key id: i32, ext_id: i64, first_name: String, From 9884024173111aa1283cf60fc396ffa5be300aff Mon Sep 17 00:00:00 2001 From: Evgeniy Nekrasov Shetinin Date: Fri, 14 Aug 2026 12:28:16 +0200 Subject: [PATCH 82/82] chore: remove .DS_Store file --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 303e71a49951a0f7f04457f60495b57cac8cfbf2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMzi-qq6#je^dZIc}wn{K$WkKr18mdDSi3N2B?nhKAdd;cV8(1KJ02>o49atD( zVTFnA44u0$F@X>>-?I&8CrvM15Ebu9_DkaT#ozm!i(?-Evo;;B0xJO8EaI1!SRG^1 zE~R2Ee?`ECcwmAYyW5+q8-u(Otrt`QRX`O`1yli5;9pRHbGBH!X5M$LTB`!8z<;TL zydM%4@u~NP^Y+!j$}R!u7kJqkkGT%8n8f?k`@(q(SdKJ~tE z-oarU9LD!-{0ha`-ib?WI85ri)~bLiP*p(I?u)pN0jAgu*YBfo5L}8F<=t+7JMS~1 zebBl4a(MONX7xU!eApmr{77~SqTZIsriUFo;cmhX9`KC23dr2Pf{$!X-?7J<$dKQw z&z%ojl{9zS1CuDtnb^El!D;WN_=5%F(*6#kbh{I{@4 z{yjS)@=V2JV#Z&Zp~8IjuAP6s@A7HTUzpeFkWqfd=63-@j>Q0vk^SCnbH9q^d(WRT zlc+u9e2B$w&kpSvlwxL2PF+mM