diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 0f019f69..43fce450 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]+' @@ -56,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/code-quality.yml b/.github/workflows/code-quality.yml index ff194b49..9e55587c 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -2,9 +2,9 @@ name: Code quality and sanity on: push: - branches: '*' + branches: [main, dev, develop] pull_request: - branches: '*' + branches: [main, dev, develop] jobs: clippy: @@ -22,7 +22,8 @@ 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 --all-features + rustfmt: name: Verify code formatting runs-on: ubuntu-latest @@ -45,7 +46,7 @@ jobs: strategy: fail-fast: false matrix: - crate: [canyon_connection, canyon_crud, canyon_macros, canyon_observer, canyon_sql] + crate: [canyon_core, canyon_crud, canyon_macros, canyon_entities, canyon_migrations] steps: - uses: actions/checkout@v3 @@ -57,4 +58,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 83c1861b..c4e2fcaa 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 @@ -18,20 +18,16 @@ 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 } steps: - - name: Make the USER own the working directory - if: ${{ matrix.os == 'ubuntu-latest' }} - run: sudo chown -R $USER:$USER ${{ github.workspace }} - - uses: actions/checkout@v3 - name: docker-compose if: ${{ matrix.os == 'ubuntu-latest' }} - run: docker-compose -f ./docker/docker-compose.yml up -d + run: docker compose -f ./docker/docker-compose.yml up -d - name: Caching cargo dependencies id: project-cache @@ -43,11 +39,20 @@ 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 UNIT tests with no external connections for the rest of the defined targets - run: cargo test --verbose --workspace --exclude tests --all-features --no-fail-fast -- --show-output + - name: Run only UNIT tests for Windows + if: ${{ matrix.os == 'windows-latest' }} + 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 --lib --all-features --no-fail-fast -- --show-output + cargo test --verbose --workspace --doc --all-features --no-fail-fast -- --show-output diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml new file mode 100644 index 00000000..49a7b0bf --- /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: "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!" 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..e4da6ed0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Generate Canyon-SQL release on: push: - tags: + tags: - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+rc[0-9]+' @@ -21,30 +21,11 @@ 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 + args: --all-features release-publisher: needs: 'publish' @@ -65,8 +46,8 @@ 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: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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/.gitignore b/.gitignore index a38bca38..41f9d9a6 100755 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ Cargo.lock /tester_canyon_sql/ canyon_tester/ macro_utils.rs -.vscode/ -postgres-data/ \ No newline at end of file +postgres-data/ +mysql-data/ +.DS_Store \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..1a510cc9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "rust-analyzer.cargo.features": ["postgres, mssql, mysql, migrations"], + "rust-analyzer.check.workspace": true, + "rust-analyzer.cargo.buildScripts.enable": true, + "rust-analyzer.procMacro.enable": true, + "rust-analyzer.diagnostics.disabled": ["unresolved-proc-macro"], + "rust-analyzer.linkedProjects": [ + "./Cargo.toml" + ] + } + diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e1d3570..555be516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,76 @@ 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 + +Fixed a bug related to migrations that prevented compiling if features were not specified. + +## [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 + +- 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 + +- 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 + +### Update + +- Adding more types to the supported ones for Tiberius in the row mapper + ## [0.1.0] - 2022 - 12 - 25 ### Added diff --git a/Cargo.toml b/Cargo.toml index 800ad578..f707dd30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,103 @@ -# 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 +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true +license.workspace = true +description.workspace = true [workspace] members = [ - "canyon_sql", - "canyon_observer", - "canyon_macros", + "canyon_core", "canyon_crud", - "canyon_connection", + "canyon_entities", + "canyon_migrations", + "canyon_macros", + "tests", +] + +[dependencies] +# Project crates +canyon_core = { 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 } +tiberius = { workspace = true, optional = true } +mysql_async = { workspace = true, optional = true } +mysql_common = { workspace = true, optional = true } + + +[workspace.dependencies] +canyon_core = { version = "0.5.1", path = "canyon_core" } +canyon_crud = { version = "0.5.1", path = "canyon_crud" } +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"] } +tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"] } +tiberius = { version = "0.12.3", features = ["tds73", "chrono"] } +mysql_async = { version = "0.36.1" } +mysql_common = { version = "0.35.4", features = [ "chrono" ]} + +chrono = { version = "0.4", features = ["serde"] } # Just from TP better? +serde = { version = "1.0.138", features = ["derive"] } + +futures = "0.3.25" +async-std = "1.12.0" +toml = "0.7.3" +walkdir = "2.3.3" +regex = "1.9.3" +partialdebug = "0.2.0" + +quote = "1.0.47" +proc-macro2 = "1.0.107" + +[workspace.package] +version = "0.5.1" +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" +license = "MIT" +description = "A Rust ORM and QueryBuilder" + +[features] +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", +] - "tests" +migrations = [ + "dep:canyon_migrations", + "canyon_macros/migrations", ] diff --git a/README.md b/README.md index c62a762d..4eded051 100755 --- a/README.md +++ b/README.md @@ -1,35 +1,42 @@ -# CANYON-SQL - -**A full written in `Rust` ORM for multiple databases.** - -- ![crates.io](https://img.shields.io/crates/v/canyon_sql.svg) -- [![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) - -`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/) +If you want to contribute in some section of the documentation [canyon-book repository](https://github.com/zerodaycode/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` 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`! +- 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 @@ -37,10 +44,11 @@ Also, in the future, we have plans to allow you to manipulate the whole server, - 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. -There are plans for include more databases engines. +There are plans to include more databases engines. ## Better by example @@ -57,7 +65,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; @@ -76,20 +84,20 @@ 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. -### 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(); 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 @@ -100,34 +108,66 @@ 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)). +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. -## 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` -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. +- 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. + -- 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!) diff --git a/bash_aliases.sh b/bash_aliases.sh old mode 100644 new mode 100755 index 0466aac8..3c3aeed9 --- a/bash_aliases.sh +++ b/bash_aliases.sh @@ -4,16 +4,20 @@ # 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 +# 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' # 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_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_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/Cargo.toml b/canyon_connection/Cargo.toml deleted file mode 100644 index d62b3fc3..00000000 --- a/canyon_connection/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "canyon_connection" -version = "0.1.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" - - -[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.11.3", features = ["tds73", "chrono"] } -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 diff --git a/canyon_connection/src/canyon_database_connector.rs b/canyon_connection/src/canyon_database_connector.rs deleted file mode 100644 index 7da2c2ed..00000000 --- a/canyon_connection/src/canyon_database_connector.rs +++ /dev/null @@ -1,148 +0,0 @@ -use async_std::net::TcpStream; - -use serde::Deserialize; -use tiberius::{AuthMethod, Config}; -use tokio_postgres::{Client, NoTls}; - -use crate::datasources::DatasourceProperties; - -/// Represents the current supported databases by Canyon -#[derive(Deserialize, Debug, Eq, PartialEq, Clone, Copy, Default)] -pub enum DatabaseType { - #[default] - #[serde(alias = "postgres", alias = "postgresql")] - PostgreSql, - #[serde(alias = "sqlserver", alias = "mssql")] - SqlServer, -} - -/// A connection with a `PostgreSQL` database -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 -pub struct SqlServerConnection { - pub client: &'static mut tiberius::Client, -} - -/// 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 struct DatabaseConnection { - pub postgres_connection: Option, - pub sqlserver_connection: Option, - pub database_type: DatabaseType, -} - -unsafe impl Send for DatabaseConnection {} -unsafe impl Sync for DatabaseConnection {} - -impl DatabaseConnection { - pub async fn new( - datasource: &DatasourceProperties<'_>, - ) -> 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 - )[..], - 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(Self { - postgres_connection: Some(PostgreSqlConnection { - client: new_client, - // connection: new_connection, - }), - sqlserver_connection: None, - database_type: DatabaseType::PostgreSql, - }) - } - DatabaseType::SqlServer => { - let mut config = Config::new(); - - config.host(datasource.host); - config.port(datasource.port.unwrap_or_default()); - config.database(datasource.db_name); - - // Using SQL Server authentication. - config.authentication(AuthMethod::sql_server( - datasource.username, - datasource.password, - )); - - // 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(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, - }) - } - } - } -} - -#[cfg(test)] -mod database_connection_handler { - use super::*; - use crate::CanyonSqlConfig; - - 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'} - ] - "#; - - /// 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"); - - 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); - } -} diff --git a/canyon_connection/src/datasources.rs b/canyon_connection/src/datasources.rs deleted file mode 100644 index 7c87583d..00000000 --- a/canyon_connection/src/datasources.rs +++ /dev/null @@ -1,77 +0,0 @@ -use serde::Deserialize; - -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'} - ] - "#; - - 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]; - - 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.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.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); -} -/// -#[derive(Deserialize, Debug, Clone)] -pub struct CanyonSqlConfig<'a> { - #[serde(borrow)] - pub canyon_sql: Datasources<'a>, -} -#[derive(Deserialize, Debug, Clone)] -pub struct Datasources<'a> { - #[serde(borrow)] - pub datasources: Vec>, -} - -#[derive(Deserialize, Debug, Clone, Copy)] -pub struct DatasourceConfig<'a> { - #[serde(borrow)] - pub name: &'a str, - 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, - pub port: Option, - pub db_name: &'a str, - 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 9a4ebe90..00000000 --- a/canyon_connection/src/lib.rs +++ /dev/null @@ -1,65 +0,0 @@ -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; - -pub mod canyon_database_connector; -pub mod datasources; - -use std::fs; - -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"; - -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) - .expect("Error opening or reading the Canyon configuration file"); - static ref CONFIG_FILE: CanyonSqlConfig<'static> = 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()); -} - -/// 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 -/// derivated 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 -/// 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 - ) - }), - )), - ); - } -} diff --git a/canyon_core/Cargo.toml b/canyon_core/Cargo.toml new file mode 100644 index 00000000..2385c703 --- /dev/null +++ b/canyon_core/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "canyon_core" +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-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 } +toml = { workspace = true } +serde = { workspace = 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"] 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 406da6fc..05eca2d3 100644 --- a/canyon_crud/Cargo.toml +++ b/canyon_crud/Cargo.toml @@ -1,15 +1,25 @@ [package] name = "canyon_crud" -version = "0.1.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" } +canyon_core = { workspace = true } -canyon_connection = { version = "0.1.0", path = "../canyon_connection" } +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 } + +[features] +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 9a00b12c..00000000 --- a/canyon_crud/src/bounds.rs +++ /dev/null @@ -1,576 +0,0 @@ -#![allow(clippy::extra_unused_lifetimes)] - -use crate::{ - crud::{CrudOperations, Transaction}, - mapper::RowMapper, -}; -use canyon_connection::{ - tiberius::{self, ColumnData, IntoSql}, - tokio_postgres::{self, types::ToSql}, -}; -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 -/// 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<'_>>; -} - -/// 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; -} -impl Row for tokio_postgres::Row { - fn as_any(&self) -> &dyn Any { - self - } -} - -impl Row for tiberius::Row { - fn as_any(&self) -> &dyn Any { - self - } -} - -pub struct Column<'a> { - name: &'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_ { - ColumnType::Postgres(v) => v as &'a dyn Type, - ColumnType::SqlServer(v) => v as &'a dyn Type, - } - } -} - -pub trait Type { - fn as_any(&self) -> &dyn Any; -} -impl Type for tokio_postgres::types::Type { - fn as_any(&self) -> &dyn Any { - self - } -} -impl Type for tiberius::ColumnType { - fn as_any(&self) -> &dyn Any { - self - } -} - -pub enum ColumnType { - Postgres(tokio_postgres::types::Type), - 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 - where - Output: tokio_postgres::types::FromSql<'a> + tiberius::FromSql<'a>; - - fn get_opt<'a, Output>(&'a self, col_name: &str) -> Option - where - Output: tokio_postgres::types::FromSql<'a> + tiberius::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>, - { - if let Some(row) = self.as_any().downcast_ref::() { - return row.get::<&str, Output>(col_name); - }; - 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!() - } - - 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 - 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. -pub trait QueryParameter<'a>: std::fmt::Debug + Sync + Send { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync); - fn as_sqlserver_param(&self) -> ColumnData<'_>; -} - -/// 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 -impl<'a> IntoSql<'a> for &'a dyn QueryParameter<'a> { - fn into_sql(self) -> ColumnData<'a> { - self.as_sqlserver_param() - } -} - -impl<'a> QueryParameter<'a> for i16 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I16(Some(*self)) - } -} -impl<'a> QueryParameter<'a> for &i16 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I16(Some(**self)) - } -} -impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I16(*self) - } -} -impl<'a> QueryParameter<'a> for Option<&i16> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - 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) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I32(Some(*self)) - } -} -impl<'a> QueryParameter<'a> for &i32 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I32(Some(**self)) - } -} -impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I32(*self) - } -} -impl<'a> QueryParameter<'a> for Option<&i32> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - 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) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F32(Some(*self)) - } -} -impl<'a> QueryParameter<'a> for &f32 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F32(Some(**self)) - } -} -impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F32(*self) - } -} -impl<'a> QueryParameter<'a> for Option<&f32> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - 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) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F64(Some(*self)) - } -} -impl<'a> QueryParameter<'a> for &f64 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F64(Some(**self)) - } -} -impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::F64(*self) - } -} -impl<'a> QueryParameter<'a> for Option<&f64> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - 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) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I64(Some(*self)) - } -} -impl<'a> QueryParameter<'a> for &i64 { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I64(Some(**self)) - } -} -impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - ColumnData::I64(*self) - } -} -impl<'a> QueryParameter<'a> for Option<&i64> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - 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) { - self - } - - 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) { - self - } - - 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) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - match self { - Some(string) => ColumnData::String(Some(std::borrow::Cow::Owned(string.to_owned()))), - None => ColumnData::String(None), - } - } -} -impl<'a> QueryParameter<'a> for Option<&String> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - match self { - Some(string) => ColumnData::String(Some(std::borrow::Cow::Borrowed(string))), - None => ColumnData::String(None), - } - } -} -impl<'a> QueryParameter<'_> for &'_ str { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - 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) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - match *self { - Some(str) => ColumnData::String(Some(std::borrow::Cow::Borrowed(str))), - None => ColumnData::String(None), - } - } -} -impl<'a> QueryParameter<'_> for NaiveDate { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } -} -impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } -} -impl<'a> QueryParameter<'_> for NaiveTime { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } -} -impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } -} -impl<'a> QueryParameter<'_> for NaiveDateTime { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } -} -impl<'a> QueryParameter<'a> for Option { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } -} -impl<'a> QueryParameter<'_> for DateTime { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } -} -impl<'a> QueryParameter<'a> for Option> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } -} -impl<'a> QueryParameter<'_> for DateTime { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } -} -impl<'a> QueryParameter<'_> for Option> { - fn as_postgres_param(&self) -> &(dyn ToSql + Sync) { - self - } - - fn as_sqlserver_param(&self) -> ColumnData<'_> { - self.into_sql() - } -} diff --git a/canyon_crud/src/crud.rs b/canyon_crud/src/crud.rs index aed59307..b6b8a1ea 100644 --- a/canyon_crud/src/crud.rs +++ b/canyon_crud/src/crud.rs @@ -1,255 +1,113 @@ -use std::fmt::Display; - -use async_trait::async_trait; -use canyon_connection::canyon_database_connector::DatabaseType; -use canyon_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::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. -/// -/// 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. -#[async_trait] -#[allow(clippy::question_mark)] -pub trait Transaction { - /// Performs a query against the targeted database by the selected datasource. - /// - /// No datasource means take the entry zero - 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 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") - } else { - guarded_cache.get(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 => { - postgres_query_launcher::launch::( - database_conn, - stmt.to_string(), - params.as_ref(), - ) - .await - } - DatabaseType::SqlServer => { - sqlserver_query_launcher::launch::( - database_conn, - &mut stmt.to_string(), - params, - ) - .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::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)>>; + input: I, + ) -> impl Future>> + Send + where + I: DbConnection + Send + 'connection; +} - async fn update(&self) -> Result<(), Box>; +pub trait UpdateOperations: Send { + fn update(&self) -> impl Future>> + Send; - async fn update_datasource<'a>( + fn update_with<'connection, I>( &self, - datasource_name: &'a str, - ) -> Result<(), Box>; + input: I, + ) -> impl Future>> + Send + where + I: DbConnection + Send + 'connection; - fn update_query<'a>() -> UpdateQueryBuilder<'a, T>; + fn update_query<'canyon, 'err>() + -> Result, Box> + where + 'canyon: 'err; - fn update_query_datasource(datasource_name: &str) -> UpdateQueryBuilder<'_, T>; + fn update_query_with<'a>(database_type: DatabaseType) -> UpdateQueryBuilder<'a>; +} - async fn delete(&self) -> Result<(), Box>; +pub trait DeleteOperations: Send { + fn delete(&self) -> impl Future>> + Send; - async fn delete_datasource<'a>( + fn delete_with<'connection, 'error, I>( &self, - datasource_name: &'a str, - ) -> Result<(), Box>; + input: I, + ) -> impl Future>> + Send + where + I: DbConnection + Send + 'connection; - fn delete_query<'a>() -> DeleteQueryBuilder<'a, T>; + fn delete_query<'canyon, 'err>() + -> Result, Box> + where + 'canyon: 'err; - fn delete_query_datasource(datasource_name: &str) -> DeleteQueryBuilder<'_, T>; + fn delete_query_with<'a>(database_type: DatabaseType) -> DeleteQueryBuilder<'a>; } -mod postgres_query_launcher { - use crate::bounds::QueryParameter; - use crate::result::DatabaseResult; - use canyon_connection::canyon_database_connector::DatabaseConnection; - - 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)>> { - let mut m_params = Vec::new(); - for param in params { - 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"), - )) - } - } +pub trait CrudOperations: + ReadOperations + InsertOperations + UpdateOperations + DeleteOperations +where + R: RowMapper, + Vec: FromIterator, +{ } -mod sqlserver_query_launcher { - use std::mem::transmute; - - use canyon_connection::tiberius::Row; - - 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)>> - 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") - .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]", - ); - - *stmt = format!( - "{} OUTPUT inserted.{} VALUES {}", - temp2.0.trim(), - temp.1.trim(), - temp2.1.trim() - ); - } - - let mut mssql_query = Query::new(stmt.to_owned().replace('$', "@P")); - params - .as_ref() - .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? - .into_results() - .await? - .into_iter() - .flatten() - .collect::>(); - - Ok(DatabaseResult::new_sqlserver(_results)) - } +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 8a20b48e..401734a7 100644 --- a/canyon_crud/src/lib.rs +++ b/canyon_crud/src/lib.rs @@ -1,12 +1,7 @@ -extern crate canyon_connection; - -pub mod bounds; pub mod crud; -pub mod mapper; -pub mod query_elements; -pub mod result; +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 71303785..00000000 --- a/canyon_crud/src/mapper.rs +++ /dev/null @@ -1,12 +0,0 @@ -use canyon_connection::{tiberius, 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 { - fn deserialize_postgresql(row: &tokio_postgres::Row) -> T; - - fn deserialize_sqlserver(row: &tiberius::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 7a91e7ef..00000000 --- a/canyon_crud/src/query_elements/operators.rs +++ /dev/null @@ -1,32 +0,0 @@ -pub trait Operator { - fn as_str(&self) -> &'static str; -} - -/// 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) -> &'static str { - match *self { - Self::Eq => " = ", - Self::Neq => " <> ", - Self::Gt => " > ", - Self::GtEq => " >= ", - Self::Lt => " < ", - Self::LtEq => " <= ", - } - } -} 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 3676d93c..00000000 --- a/canyon_crud/src/query_elements/query_builder.rs +++ /dev/null @@ -1,697 +0,0 @@ -use std::fmt::Debug; - -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 sentece 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: Debug + 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, -} - -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, - } - } - - /// 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( - 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::()) - } - } - - 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(); - - 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).to_string() - + " "; - - 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).to_string() - + " "; - - 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_str(") "); - } - - 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_str(") "); - } - - #[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`] holded 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`] holded 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`] holded 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`] holded 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>(&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 - Z: FieldIdentifier, - Q: QueryParameter<'a>, - { - self._inner.or_values_in(and, values); - 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 cap = columns.len() * 50; // Reserving an enough initial capacity per set clause - let mut set_clause = String::with_capacity(cap); - 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>(&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 - Z: FieldIdentifier, - Q: QueryParameter<'a>, - { - self._inner.or_values_in(or, values); - 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>(&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 - Z: FieldIdentifier, - Q: QueryParameter<'a>, - { - self._inner.or_values_in(or, values); - 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/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_entities/Cargo.toml b/canyon_entities/Cargo.toml new file mode 100644 index 00000000..8f2ce210 --- /dev/null +++ b/canyon_entities/Cargo.toml @@ -0,0 +1,16 @@ +[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] +partialdebug = { workspace = true } +quote = { workspace = true } +proc-macro2 = { workspace = true } +syn = { version = "2.0.117", 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 72% rename from canyon_observer/src/manager/entity.rs rename to canyon_entities/src/entity.rs index 78e2f157..b387ebf8 100644 --- a/canyon_observer/src/manager/entity.rs +++ b/canyon_entities/src/entity.rs @@ -1,16 +1,16 @@ 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; /// 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, @@ -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::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::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_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_entities/src/field_annotation.rs b/canyon_entities/src/field_annotation.rs new file mode 100644 index 00000000..1ada2840 --- /dev/null +++ b/canyon_entities/src/field_annotation.rs @@ -0,0 +1,399 @@ +use proc_macro2::Ident; +use std::convert::TryFrom; +use syn::{Attribute, Expr, Lit, MetaNameValue, Token, punctuated::Punctuated}; + +/// The available annotations for a field that belongs to any struct +/// annotated with `#[canyon_entity]`. +#[derive(Debug, Clone)] +pub enum EntityFieldAnnotation { + PrimaryKey(bool), + ForeignKey(String, String), +} + +impl EntityFieldAnnotation { + /// 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) => { + format!("Annotation: PrimaryKey, Autoincremental: {autoincremental}") + } + Self::ForeignKey(table, column) => { + format!("Annotation: ForeignKey, Table: {table}, Column: {column}") + } + } + } + + fn parse_primary_key( + ident: &Ident, + args: syn::Result>, + ) -> syn::Result { + let Ok(args) = args else { + return Ok(Self::PrimaryKey(true)); + }; + + let mut autoincremental = None; + + for arg in &args { + match arg_key(arg)?.as_str() { + "autoincremental" => { + autoincremental = Some(parse_bool_value(arg)?); + } + unknown => return Err(unknown_argument(arg, unknown)), + } + } + + autoincremental.map(Self::PrimaryKey).ok_or_else(|| { + syn::Error::new_spanned( + ident, + "Missing `autoincremental` argument on the Primary Key annotation", + ) + }) + } + + fn parse_foreign_key( + ident: &Ident, + args: syn::Result>, + ) -> syn::Result { + let args = args.map_err(|error| { + syn::Error::new_spanned(ident, format!("Error generating the Foreign Key: {error}")) + })?; + + 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)), + } + } + + 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", + ) + })?, + )) + } +} + +impl TryFrom<&&Attribute> for EntityFieldAnnotation { + type Error = syn::Error; + + fn try_from(attribute: &&Attribute) -> Result { + 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 new file mode 100644 index 00000000..9aebeab0 --- /dev/null +++ b/canyon_entities/src/lib.rs @@ -0,0 +1,12 @@ +use crate::register_types::CanyonRegisterEntity; +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; + +pub static CANYON_REGISTER_ENTITIES: Mutex>> = + Mutex::new(Vec::new()); diff --git a/canyon_entities/src/manager_builder.rs b/canyon_entities/src/manager_builder.rs new file mode 100644 index 00000000..60999297 --- /dev/null +++ b/canyon_entities/src/manager_builder.rs @@ -0,0 +1,268 @@ +use super::entity::CanyonEntity; +use crate::helpers; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::{Attribute, Generics, Visibility}; + +/// 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(); + + let struct_name: &Ident = &canyon_entity.struct_name; + let struct_visibility: &Visibility = &canyon_entity.vis; + let struct_generics: &Generics = &canyon_entity.generics; + let struct_attrs: &Vec = &canyon_entity.attrs; + + quote! { + #(#struct_attrs)* + #struct_visibility struct #struct_name #struct_generics { + #(#fields),* + } + } +} + +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, 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 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! { + #[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 + /// + /// The idea it's to have a representation of the field name as an enum + /// variant, avoiding the user to have to pass 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. + /// + /// That's particularly useful in Canyon when working with queries being constructed + /// through the [`QueryBuilder`], when one of the methods requires to get + /// a column name (which is the name of some field of the type) as a parameter + /// + /// ``` + /// pub struct League { + /// id: i32, + /// name: String + /// } + /// + /// #[derive(Debug)] + /// #[allow(non_camel_case_types)] + /// pub enum LeagueField { + /// id(i32), + /// name(String) + /// } + /// ``` + #visibility enum #enum_name #generics { + #(#fields_names),* + } + + 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),* + } + } + } + } +} + +/// Autogenerated Rust Enum type that contains as many variants +/// with inner value as fields has the structure to which it relates +/// +/// 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 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 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! { + #[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. + /// + /// ``` + /// pub struct League { + /// id: i32, + /// name: String, + /// opt: Option + /// } + /// + /// #[derive(Debug)] + /// #[allow(non_camel_case_types)] + /// pub enum LeagueFieldValue { + /// id(i32), + /// name(String), + /// opt(Option) + /// } + /// ``` + #visibility enum #enum_name { + #(#fields_names),* + } + + 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 { + #(#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_entities/src/register_types.rs b/canyon_entities/src/register_types.rs new file mode 100644 index 00000000..2702e61f --- /dev/null +++ b/canyon_entities/src/register_types.rs @@ -0,0 +1,44 @@ +/// 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 37b322c5..58c31ebe 100755 --- a/canyon_macros/Cargo.toml +++ b/canyon_macros/Cargo.toml @@ -1,23 +1,48 @@ [package] name = "canyon_macros" -version = "0.1.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.86", 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.0", path = "../canyon_observer" } -canyon_crud = { version = "0.1.0", path = "../canyon_crud" } -canyon_connection = { version = "0.1.0", path = "../canyon_connection" } +syn = { version = "2.0.117", features = ["full", "parsing"] } # TODO Pending to upgrade and refactor +quote = { workspace = true } +proc-macro2 = { workspace = true } +regex = { workspace = true } + +canyon_core = { workspace = true } +canyon_crud = { workspace = true } +canyon_entities = { workspace = true } +canyon_migrations = { workspace = true, optional = true } + +[features] +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", +] + diff --git a/canyon_macros/src/canyon_entity_macro.rs b/canyon_macros/src/canyon_entity_macro.rs new file mode 100644 index 00000000..a5a0afe5 --- /dev/null +++ b/canyon_macros/src/canyon_entity_macro.rs @@ -0,0 +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 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()); + } + } + + 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 ebc02629..b005e10a 100644 --- a/canyon_macros/src/canyon_macro.rs +++ b/canyon_macros/src/canyon_macro.rs @@ -1,125 +1,71 @@ //! Provides helpers to build the `#[canyon_macros::canyon]` procedural like attribute macro +#![cfg(feature = "migrations")] -use proc_macro::TokenStream as TokenStream1; -use proc_macro2::{Ident, TokenStream}; - +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; -use canyon_observer::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(); +pub fn main_with_queries() -> TokenStream { + // 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; + }); + // 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) { - let data = QUERIES_TO_EXECUTE.lock().unwrap(); - let data_to_wire = data.iter().map(|(key, value)| { - quote! { hm.insert(#key, vec![#(#value),*]); } - }); +fn wire_queries_to_execute(canyon_manager_tokens: &mut Vec) { + 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 = 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; 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; }; - 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 9257fd38..28bd4e8e 100755 --- a/canyon_macros/src/lib.rs +++ b/canyon_macros/src/lib.rs @@ -1,675 +1,226 @@ extern crate proc_macro; +extern crate regex; +#[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_connection::CANYON_TOKIO_RUNTIME; -use proc_macro::{Span, TokenStream as CompilerTokenStream}; -use proc_macro2::{Ident, TokenStream}; -use quote::{quote, ToTokens}; -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 proc_macro::TokenStream as CompilerTokenStream; +use proc_macro2::TokenStream; +use quote::quote; +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 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, + generated_enum_type_for_struct_data, }, - migrations::handler::Migrations, }; -use canyon_observer::{ - migrations::register_types::{CanyonRegisterEntity, CanyonRegisterEntityField}, - CANYON_REGISTER_ENTITIES, -}; +type MacroResult = syn::Result; -/// 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 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 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(); + 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(); } - // 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(); + 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(); } - // 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; - - 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); - - // 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 + quote! { + #(#attributes)* + #visibility #signature { + canyon_sql::runtime::get_canyon_tokio_runtime() .handle() - .block_on( async { - canyon_sql::runtime::init_connections_cache().await; - #(#body)* - } - ) - } + .block_on(async { + canyon_sql::core::Canyon::init() + .await + .expect( + "error initializing Canyon's connection pools", + ); + + #migrations_tokens + #(#body)* + }) } - .into() } + .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 querybuilder 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 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" { - table_name = Some(Box::leak(attr_arg_ident.into_boxed_str())); - match nv.lit { - syn::Lit::Str(ref l) => { - 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 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.user_table_name = table_name; - 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 = proc_macro2::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 struct's 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 struct's 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() - } - }); - - // 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()) - } - }); - - 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::<&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()) - } - } - }); - - // 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),* - } - } - - fn deserialize_sqlserver(row: &canyon_sql::db_clients::tiberius::Row) -> #ty { - Self { - #(#init_field_values_sqlserver),* - } - } - } - }; - - 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)?; -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 4d5f3fce..00000000 --- a/canyon_macros/src/query_operations/delete.rs +++ /dev/null @@ -1,125 +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)>> { - let stmt = format!("DELETE FROM {} WHERE {:?} = $1", #table_schema_data, #primary_key); - - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, - &[#pk_field_value], - "" - ).await; - - if let Err(error) = result { - Err(error) - } else { 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)>> - { - let stmt = format!("DELETE FROM {} WHERE {:?} = $1", #table_schema_data, #primary_key); - - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, - &[#pk_field_value], - datasource_name - ).await; - - if let Err(error) = result { - Err(error) - } else { 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 e5b8fc12..00000000 --- a/canyon_macros/src/query_operations/insert.rs +++ /dev/null @@ -1,515 +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 result = <#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) - } - } - } else { - quote! { - let stmt = format!( - "INSERT INTO {} ({}) VALUES ({})", - #table_schema_data, - #insert_columns, - #placeholders, - #primary_key - ); - - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, - values, - datasource_name - ).await; - - if let Err(error) = result { - Err(error) - } else { - 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 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) - } - } - } 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) - } - } - - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, - v_arr, - datasource_name - ).await; - - match result { - Ok(res) => Ok(()), - Err(e) => Err(e) - } - } - }; - - 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 c54a2a09..00000000 --- a/canyon_macros/src/query_operations/select.rs +++ /dev/null @@ -1,526 +0,0 @@ -use canyon_observer::manager::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! { - /// Performns 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 - .ok() - .unwrap() - .get_entities::<#ty>() - } - - /// Performns 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 - .ok() - .unwrap() - .get_entities::<#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! { - /// Performns 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)>> - { - 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>()) - } - } - - /// Performns 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)>> - { - 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>()) - } - } - } -} - -/// 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! { - 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() - ) - } - } - } - }; - - 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; - - #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; - - #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! { - 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) - ) - ) - } - } - }; - - 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! { - 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) - )) - } - } - }; - - 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 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(( - 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() - ); - - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, - &[lookage_value], - "" - ).await; - - #result_handler - } - }, - )); - - 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() - ); - - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, - &[lookage_value], - datasource_name - ).await; - - #result_handler - } - }, - )); - } - } - - 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 94a9abf3..00000000 --- a/canyon_macros/src/query_operations/update.rs +++ /dev/null @@ -1,146 +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),*]; - - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, update_values, "" - ).await; - - if let Err(e) = result { - Err(e) - } else { 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),*]; - - let result = <#ty as canyon_sql::crud::Transaction<#ty>>::query( - stmt, update_values, datasource_name - ).await; - - if let Err(e) = result { - Err(e) - } else { 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 4ab62025..7f0a294b 100644 --- a/canyon_macros/src/utils/function_parser.rs +++ b/canyon_macros/src/utils/function_parser.rs @@ -1,6 +1,6 @@ use syn::{ - parse::{Parse, ParseBuffer}, Attribute, Block, ItemFn, Signature, Visibility, + parse::{Parse, ParseBuffer}, }; /// Implementation of syn::Parse for the `#[canyon]` proc-macro @@ -14,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 9ad14792..3fac25cb 100644 --- a/canyon_macros/src/utils/helpers.rs +++ b/canyon_macros/src/utils/helpers.rs @@ -1,162 +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); - - 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() - ); - } - }, - _ => - return Err( - syn::Error::new_spanned( - Ident::new(&identifier, 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()); - } - } - } - Err(_) => return Ok(macro_data.ty.to_string()), - } - - 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 { - final_table_name.push_str(macro_data.ty.to_string().as_str()) - } + 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 -/// TODO: #[macro(table_name = 'user_defined_db_table_name)]' -pub fn _database_table_name_from_struct(ty: &Ident) -> String { - let struct_name: String = ty.to_string(); - let mut table_name: String = String::new(); - - let mut index = 0; - for char in struct_name.chars() { - if index < 1 { - table_name.push(char.to_ascii_lowercase()); - index += 1; +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)?; + + 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 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 { - let struct_name: String = ty.to_string(); - let mut table_name: String = String::new(); - - 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), - } +mod __impl { + use proc_macro2::TokenStream; + use syn::{Attribute, Expr, Lit, Meta, MetaNameValue, Token, punctuated::Punctuated}; + + pub(super) fn is_canyon_entity_attr(attr: &Attribute) -> bool { + attr.path() + .segments + .last() + .is_some_and(|segment| segment.ident == "canyon_entity") + } + + 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() + }) + } -/// 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 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()), + } + } - let mut first_iteration = true; - let mut previous_was_underscore = false; + 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() + } +} - for char in name.chars() { - if first_iteration { - struct_name.push(char.to_ascii_uppercase()); - first_iteration = false; - } else { - match char { - n if n == '_' => { - 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"), +#[cfg(test)] +mod tests_for_parse_struct_field_attributes { + use super::*; + use syn::{ItemStruct, parse_str}; + + #[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 370fbeea..76d4a741 100644 --- a/canyon_macros/src/utils/macro_tokens.rs +++ b/canyon_macros/src/utils/macro_tokens.rs @@ -1,43 +1,73 @@ -use std::convert::TryFrom; - -use canyon_observer::manager::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 +#[allow(dead_code)] pub struct MacroTokens<'a> { pub vis: &'a Visibility, pub ty: &'a Ident, 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::>() @@ -45,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() } - /// - 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) @@ -151,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 { @@ -162,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 new file mode 100644 index 00000000..b8ab15f3 --- /dev/null +++ b/canyon_migrations/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "canyon_migrations" +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] +canyon_core = { workspace = true } +canyon_crud = { workspace = true } +canyon_entities = { 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 } + +regex = { workspace = true } +partialdebug = { workspace = true } +walkdir = { workspace = true } + +[features] +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_observer/src/constants.rs b/canyon_migrations/src/constants.rs similarity index 59% rename from canyon_observer/src/constants.rs rename to canyon_migrations/src/constants.rs index 5383a0f2..7674efe5 100644 --- a/canyon_observer/src/constants.rs +++ b/canyon_migrations/src/constants.rs @@ -1,10 +1,10 @@ -pub mod queries {} - +#[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, 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 = @@ -34,13 +34,15 @@ 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 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"; @@ -140,7 +142,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"; @@ -153,6 +155,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"; @@ -165,107 +168,3 @@ pub mod sqlserver_type { pub const TIME: &str = "TIME"; 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! { - 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 new file mode 100644 index 00000000..757597cc --- /dev/null +++ b/canyon_migrations/src/lib.rs @@ -0,0 +1,36 @@ +/// Holds the data needed by Canyon when the user +/// application it's running. +/// +/// Takes care about provide a namespace where retrieve the +/// database credentials in only one place +/// +/// Takes care about track what data structures Canyon +/// should be managing +/// +/// Takes care about the queries that Canyon has to execute +/// in order to perform the migrations +pub mod migrations; + +extern crate canyon_crud; +extern crate canyon_entities; + +mod constants; + +use std::sync::OnceLock; +use std::{collections::HashMap, sync::Mutex}; + +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) { + // 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.insert(ds_name.to_owned(), vec![stmt]); + } +} diff --git a/canyon_observer/src/migrations/handler.rs b/canyon_migrations/src/migrations/handler.rs similarity index 53% rename from canyon_observer/src/migrations/handler.rs rename to canyon_migrations/src/migrations/handler.rs index dfa84ef4..416d1e89 100644 --- a/canyon_observer/src/migrations/handler.rs +++ b/canyon_migrations/src/migrations/handler.rs @@ -1,43 +1,38 @@ -use canyon_connection::{datasources::Migrations as MigrationsStatus, DATASOURCES}; -use partialdebug::placeholder::PartialDebug; - use crate::{ - canyon_crud::{ - bounds::{Column, Row, RowOperations}, - crud::Transaction, - result::DatabaseResult, - DatabaseType, - }, + canyon_crud::DatabaseType, constants, migrations::{ - information_schema::{ColumnMetadata, ColumnMetadataTypeValue, TableMetadata}, + information_schema::{ColumnMetadata, ColumnMetadataTypeValue, MacroTableMetadata}, memory::CanyonMemory, processor::MigrationsProcessor, }, - CANYON_REGISTER_ENTITIES, }; +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,26 +41,36 @@ 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_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; // 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); + 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()); // 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) + .iter() + .any(|f| f.declared_table_name.eq(&parsed_table.table_name)) || canyon_memory .renamed_entities .values() - .any(|f| *f == parsed_table.table_name.to_lowercase()) + .any(|f| *f == parsed_table.table_name) { user_database_tables.append(&mut vec![parsed_table]); } @@ -74,7 +79,7 @@ impl Migrations { migrations_processor .process( canyon_memory, - canyon_tables, + canyon_entities, user_database_tables, datasource, ) @@ -83,69 +88,54 @@ 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, - ) -> 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, + #[cfg(feature = "mysql")] + 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: 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); - } - }; + #[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), + #[cfg(feature = "mysql")] + CanyonRows::MySQL(_) => panic!("Not implemented fetch database in mysql"), } - - schema_info } /// 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); } @@ -212,10 +202,103 @@ 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() }; } + + #[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 = MacroTableMetadata { + 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 = MacroTableMetadata { + 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 MacroTableMetadata, + 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"), + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!("Not implemented fetch database in mysql"), + } } diff --git a/canyon_observer/src/migrations/information_schema.rs b/canyon_migrations/src/migrations/information_schema.rs similarity index 64% rename from canyon_observer/src/migrations/information_schema.rs rename to canyon_migrations/src/migrations/information_schema.rs index bdf9f48e..77cb47a3 100644 --- a/canyon_observer/src/migrations/information_schema.rs +++ b/canyon_migrations/src/migrations/information_schema.rs @@ -1,13 +1,19 @@ -use canyon_connection::{tiberius::ColumnType as TIB_TY, tokio_postgres::types::Type as TP_TYP}; -use canyon_crud::bounds::{Column, ColumnType, Row, RowOperations}; +#[cfg(feature = "mssql")] +use canyon_core::connection::tiberius::ColumnType as TIB_TY; +#[cfg(feature = "postgres")] +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, } @@ -40,24 +46,32 @@ 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, }, + #[cfg(feature = "mysql")] + ColumnType::MySQL(_) => todo!(), } } } diff --git a/canyon_migrations/src/migrations/memory.rs b/canyon_migrations/src/migrations/memory.rs new file mode 100644 index 00000000..a0346a56 --- /dev/null +++ b/canyon_migrations/src/migrations/memory.rs @@ -0,0 +1,307 @@ +use crate::constants; +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; + +/// Convenient struct that contains the necessary data and operations to implement +/// the `Canyon Memory`. +/// +/// Canyon Memory it's just a convenient way of relate the data of a Rust source +/// code file and the `CanyonEntity` (if so), helping Canyon to know what source +/// file contains a `#[canyon_entity]` annotation and restricting it to just one +/// annotated struct per file. +/// +/// This limitation it's imposed by design. Canyon, when manages all the entities in +/// the user's source code, needs to know for future migrations the old data about a structure +/// and the new modified one. +/// +/// For example, let's say that you have a: +/// ``` +/// pub struct Person { +/// /* some fields */ +/// } +/// ``` +/// +/// and you decided to modify it's Ident and change it to `Human`. +/// +/// Canyon will take care about modifying the Database, and `ALTER TABLE` to edit the actual data for you, +/// but, if it's not able to get the data to know that the old one is `Person` and the new one it's `Human`. +/// it will simply drop the table (losing all your data) and creating a new table `Human`. +/// +/// So, we decised to follow the next approach: +/// Every entity annotated with a `#[canyon_entity]` annotation will be related to only unique Rust source +/// code file. If we find more, Canyon will raise and error saying that it does not allows to having more than +/// one managed entity per source file. +/// +/// Then, we will store the entities data in a special table only for Canyon, where we will create the relation +/// between the source file, the entity and it's fields and data. +/// +/// So, if the user wants or needs to modify the data of it's entity, Canyon can secure that will perform the +/// correct operations because we can't "remember" how that entity was, and how it should be now, avoiding +/// potentially dangerous operations due to lack of knowing what entity relates with new data. +/// +/// 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: Vec, + pub renamed_entities: HashMap, +} + +// Makes this structure able to make queries to the database +impl Transaction for CanyonMemory {} + +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, + 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, db_conn, &datasource.get_db_type()).await; + + // Retrieve the last status data from the `canyon_memory` table + let res = db_conn + .query_rows("SELECT * FROM canyon_memory", &[]) + .await + .expect("Error querying Canyon Memory"); + + // Manually maps the results + let mut db_rows = Vec::new(); + #[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); + } + } + + 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(), + }; + 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 + }); + + 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 = '{}' \ + WHERE id = {}", + _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 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 already_in_db.is_none() { + 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 from canyon_memory, because they stopped to be tracked by Canyon + 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)) + { + 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]` + async fn find_canyon_entity_annotated_structs( + &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()) + { + if file.metadata().unwrap().is_file() + && file.path().display().to_string().ends_with(".rs") + { + // Opening the source code file + let contents = + fs::read_to_string(file.path()).expect("Something went wrong reading the file"); + + let mut canyon_entity_macro_counter = 0; + let mut struct_name = String::new(); + for line in contents.split('\n') { + if line.contains("#[") // separated checks for possible different paths + && line.contains("canyon_entity") + && !line.starts_with("//") + { + canyon_entity_macro_counter += 1; + } + + if let Some(captures) = re.captures(line) { + struct_name.push_str(captures.get(1).unwrap().as_str()); + } + } + + // This limitation will be removed in future versions, when the memory + // will be able to track every aspect of an entity + match canyon_entity_macro_counter { + 0 => (), + 1 => { + 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-SQL does not support having multiple structs annotated + with `#[canyon::entity]` on the same file when the migrations are enabled" + ), + } + } + } + } + + /// Generates, if not exists the `canyon_memory` table + 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, + #[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_rows(query, [], db_conn) + .await + .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; + + 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 +#[derive(Debug)] +struct CanyonMemoryRow { + id: i32, + filepath: String, + struct_name: String, + declared_table_name: String, +} + +/// 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/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 53% rename from canyon_observer/src/migrations/processor.rs rename to canyon_migrations/src/migrations/processor.rs index fb991717..98905eed 100644 --- a/canyon_observer/src/migrations/processor.rs +++ b/canyon_migrations/src/migrations/processor.rs @@ -1,68 +1,78 @@ -///! File that contains all the datatypes and logic to perform the migrations -///! over a target database -use async_trait::async_trait; +//! File that contains all the datatypes and logic to perform the migrations +//! over a target database +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::QUERIES_TO_EXECUTE; - -use super::information_schema::{ColumnMetadata, TableMetadata}; +use super::information_schema::{ColumnMetadata, MacroTableMetadata}; 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 #[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>, - datasource: &'_ DatasourceConfig<'static>, + database_tables: Vec<&'a MacroTableMetadata>, + 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 { - // 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, ); - // 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(), @@ -74,7 +84,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 +97,14 @@ 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 + #[allow(clippy::unnecessary_unwrap)] 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(), ) @@ -101,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 { @@ -110,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; @@ -124,10 +148,9 @@ 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 isn't present (this if case), we 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, @@ -149,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 @@ -169,8 +189,8 @@ impl MigrationsProcessor { &mut self, entity_name: &'a str, entity_fields: Vec, - current_table_metadata: Option<&'a TableMetadata>, - db_type: DatabaseType, + current_table_metadata: Option<&'a MacroTableMetadata>, + _db_type: DatabaseType, ) { if current_table_metadata.is_none() { return; @@ -189,18 +209,21 @@ 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()); } } - // 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, @@ -208,50 +231,77 @@ 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() { - self.create_column(entity_name.to_string(), canyon_register_entity_field) - } 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) + 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(), + ) + } + + 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")] fn drop_column_not_null( &mut self, table_name: &str, 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.column_operations + .push(ColumnOperation::AlterColumnSetNotNull(table_name, field)); + } + + fn drop_not_null(&mut self, table_name: String, field: CanyonRegisterEntityField) { + self.column_operations + .push(ColumnOperation::AlterColumnDropNotNull(table_name, field)); } fn add_constraints( @@ -270,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( @@ -285,8 +335,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()); + } } } } @@ -300,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( @@ -316,24 +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( @@ -357,22 +411,27 @@ 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 + // 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, @@ -384,8 +443,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()); + } } } @@ -399,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( @@ -421,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) @@ -476,64 +538,68 @@ 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")] fn drop_identity( &mut self, 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? @@ -549,18 +615,18 @@ 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() - .any(|v| v.table_name.to_lowercase() == entity_name.to_lowercase()) + .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. + /// 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, - 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()) @@ -575,10 +641,10 @@ 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>, + current_table_metadata: Option<&MacroTableMetadata>, ) -> Option<&ColumnMetadata> { if let Some(metadata_table) = current_table_metadata { metadata_table @@ -590,9 +656,10 @@ 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 { @@ -611,20 +678,23 @@ 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 to_postgres_alter_syntax(canyon_register_entity_field).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 to_sqlserver_alter_syntax(canyon_register_entity_field).to_lowercase() + == current_column_metadata.datatype; + } + } + + false } fn extract_foreign_key_annotation(field_annotations: &[String]) -> (String, String) { @@ -646,7 +716,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 @@ -661,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<'static>); + fn generate_sql(&self, datasource: &DatasourceConfig) -> impl Future; } /// Helper to relate the operations that Canyon should do when it's managing a schema @@ -714,54 +752,56 @@ 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<'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) => { - 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(", ") - ) - .replace('"', "") - } else if db_type == DatabaseType::SqlServer { + TableOperation::CreateTable(table_name, table_fields) => match db_type { + #[cfg(feature = "postgres")] + 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_sqlserver_syntax() + to_postgres_syntax(entity_field) )) .collect::>() .join(", ") ) - .replace('"', "") - } else { - todo!() } - } + #[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) => { - if db_type == DatabaseType::PostgreSql { - format!("ALTER TABLE {old_table_name} RENAME TO {new_table_name};") - } else if db_type == DatabaseType::SqlServer { + 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! @@ -772,86 +812,82 @@ impl DatabaseOperation for TableOperation { 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 + 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!() + { + format!("exec sp_rename '{old_table_name}', '{new_table_name}';") + } + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!(), } } TableOperation::AddTableForeignKey( - 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 { + _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});" + ), + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => { todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } else { - todo!() } - } + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!(), + }, - 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]" + ), + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!(), } } - 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 { + 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]") - } else { - todo!() } - } + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!(), + }, - TableOperation::DeleteTablePrimaryKey(table_name, primary_key_name) => { - if db_type == DatabaseType::PostgreSql || db_type == DatabaseType::SqlServer { + 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;") - } else { - todo!() } - } + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => { + format!("ALTER TABLE {table_name} DROP CONSTRAINT {primary_key_name} CASCADE;") + } + #[cfg(feature = "mysql")] + DatabaseType::MySQL => todo!(), + }, }; - 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); } } @@ -864,71 +900,70 @@ 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), } -impl Transaction for ColumnOperation {} +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) => - 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, + to_postgres_syntax(entity_field) + ), + #[cfg(feature = "mssql")] DatabaseType::SqlServer => + format!( + "ALTER TABLE {} ADD \"{}\" {};", + 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 - 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()) - } else if db_type == DatabaseType::SqlServer { - todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]") - } else { - todo!() - } - , - 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 - ) - } else if db_type == DatabaseType::SqlServer { - format!( - "ALTER TABLE {} ALTER COLUMN {} {} NULL", - table_name, entity_field.field_name, entity_field.to_sqlserver_alter_syntax() - ) - } 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, to_postgres_alter_syntax(_entity_field) + ), + #[cfg(feature = "mssql")] DatabaseType::SqlServer => + todo!("[MS-SQL -> Operation still won't supported by Canyon for Sql Server]"), + #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() - ColumnOperation::DropNotNullBeforeDropColumn(table_name, column_name, column_datatype) => + } + 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 = "mssql")] DatabaseType::SqlServer => + 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!( "ALTER TABLE {table_name} ALTER COLUMN {column_name} {column_datatype} NULL; DECLARE @tableName VARCHAR(MAX) = '{table_name}' DECLARE @columnName VARCHAR(MAX) = '{column_name}' @@ -943,84 +978,182 @@ 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, + to_sqlserver_alter_syntax(entity_field) + ), + #[cfg(feature = "mysql")] DatabaseType::MySQL => todo!() + } + } - ColumnOperation::AlterColumnAddIdentity(table_name, entity_field) => format!( - "ALTER TABLE {table_name} ALTER COLUMN \"{}\" ADD GENERATED ALWAYS AS IDENTITY;", entity_field.field_name + #[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!( - "ALTER TABLE {table_name} ALTER COLUMN \"{}\" DROP IDENTITY;", entity_field.field_name + #[cfg(feature = "postgres")] ColumnOperation::AlterColumnDropIdentity(table_name, entity_field) => format!( + "ALTER TABLE \"{table_name}\" ALTER COLUMN \"{}\" DROP IDENTITY;", entity_field.field_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]); - } + save_migrations_query_to_execute(stmt, &datasource.name); } } /// 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 {} -impl Transaction for SequenceOperation {} - -#[async_trait] +#[cfg(feature = "postgres")] 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 stmt = match self { 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 + 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); + } +} - 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]); +#[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/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/Cargo.toml b/canyon_observer/Cargo.toml deleted file mode 100644 index c3bfbdf7..00000000 --- a/canyon_observer/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "canyon_observer" -version = "0.1.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" - -[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" - -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.1.0", path = "../canyon_crud" } -canyon_connection = { version = "0.1.0", path = "../canyon_connection" } diff --git a/canyon_observer/src/lib.rs b/canyon_observer/src/lib.rs deleted file mode 100644 index 2af5a36f..00000000 --- a/canyon_observer/src/lib.rs +++ /dev/null @@ -1,29 +0,0 @@ -/// Holds the data needed by Canyon when the user -/// application it's running. -/// -/// Takes care about provide a namespace where retrieve the -/// database credentials in only one place -/// -/// Takes care about track what data structures Canyon -/// should be managing -/// -/// Takes care about the queries that Canyon has to execute -/// in order to perform the migrations -pub mod migrations; - -extern crate canyon_crud; - -// The migrator tool -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/manager/field_annotation.rs b/canyon_observer/src/manager/field_annotation.rs deleted file mode 100644 index 8c01615d..00000000 --- a/canyon_observer/src/manager/field_annotation.rs +++ /dev/null @@ -1,150 +0,0 @@ -use proc_macro2::Ident; -use std::{collections::HashMap, convert::TryFrom}; -use syn::{punctuated::Punctuated, Attribute, MetaNameValue, Token}; - -/// The available annotations for a field that belongs to any struct -/// annotaded with `#[canyon_entity]` -#[derive(Debug, Clone)] -pub enum EntityFieldAnnotation { - PrimaryKey(bool), - ForeignKey(String, String), -} - -impl EntityFieldAnnotation { - /// Returns the data of the [`EntityFieldAnnotation`] in a understandable format for - /// operations that requires character matching - pub fn get_as_string(&self) -> String { - match self { - Self::PrimaryKey(autoincremental) => { - format!("Annotation: PrimaryKey, Autoincremental: {autoincremental}") - } - Self::ForeignKey(table, column) => { - format!("Annotation: ForeignKey, Table: {table}, Column: {column}") - } - } - } - - /// Retrieves the user defined data in the #[primary_key] attribute - fn primary_key_parser( - ident: &Ident, - attr_args: &Result, syn::Error>, - ) -> 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); - } - - 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(), - )); - } - }, - )) - } - Err(_) => Ok(EntityFieldAnnotation::PrimaryKey(true)), - } - } - - fn foreign_key_parser( - ident: &Ident, - attr_args: &Result, syn::Error>, - ) -> 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); - } - - 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(), - )) - } - }, - )) - } - Err(_) => Err(syn::Error::new_spanned( - ident, - "Error generating the Foreign Key".to_string(), - )), - } - } -} - -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), - )) - } - }) - } -} diff --git a/canyon_observer/src/manager/manager_builder.rs b/canyon_observer/src/manager/manager_builder.rs deleted file mode 100644 index d717909f..00000000 --- a/canyon_observer/src/manager/manager_builder.rs +++ /dev/null @@ -1,140 +0,0 @@ -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(); - - let struct_name: &Ident = &canyon_entity.struct_name; - let struct_visibility: &Visibility = &canyon_entity.vis; - let struct_generics: &Generics = &canyon_entity.generics; - let struct_attrs: &Vec = &canyon_entity.attrs; - - quote! { - #(#struct_attrs)* - #struct_visibility struct #struct_name #struct_generics { - #(#fields),* - } - } -} - -/// 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, -/// 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 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 visibility = &canyon_entity.vis; - let generics = &canyon_entity.generics; - - quote! { - #[derive(Clone, Debug)] - #[allow(non_camel_case_types)] - #[allow(unused_variables)] - #[allow(dead_code)] - /// 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 the user to have to pass 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. - /// - /// That's particularly useful in Canyon when working with queries being constructed - /// through the [`QueryBuilder`], when one of the methods requires to get - /// a column name (which is the name of some field of the type) as a parameter - /// - /// ``` - /// pub struct League { - /// id: i32, - /// name: String - /// } - /// - /// #[derive(Debug)] - /// #[allow(non_camel_case_types)] - /// pub enum LeagueField { - /// id(i32), - /// name(String) - /// } - /// ``` - #visibility enum #enum_name #generics { - #(#fields_names),* - } - - impl #generics canyon_sql::crud::bounds::FieldIdentifier<#ty> for #generics #enum_name #generics { - fn as_str(&self) -> &'static str { - match *self { - #(#match_arms_str),* - } - } - } - } -} - -/// Autogenerated Rust Enum type that contains as many variants -/// with inner value as fields has the structure to which it relates -/// -/// 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 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; - - quote! { - #[derive(Debug)] - #[allow(non_camel_case_types)] - #[allow(unused_variables)] - #[allow(dead_code)] - /// 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. - /// - /// ``` - /// pub struct League { - /// id: i32, - /// name: String, - /// opt: Option - /// } - /// - /// #[derive(Debug)] - /// #[allow(non_camel_case_types)] - /// pub enum LeagueFieldValue { - /// id(i32), - /// name(String) - /// opt(Option) - /// } - /// ``` - #visibility enum #enum_name<'a> { - #(#fields_names),* - } - - impl<'a> canyon_sql::crud::bounds::FieldValueIdentifier<'a, #ty> for #enum_name<'a> { - fn value(self) -> (&'static str, &'a dyn QueryParameter<'a>) { - match self { - #(#match_arms),* - } - } - } - } -} 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/memory.rs b/canyon_observer/src/migrations/memory.rs deleted file mode 100644 index f5047baa..00000000 --- a/canyon_observer/src/migrations/memory.rs +++ /dev/null @@ -1,285 +0,0 @@ -use canyon_crud::{bounds::RowOperations, crud::Transaction, DatabaseType, DatasourceConfig}; -use std::collections::HashMap; -use std::fs; -use walkdir::WalkDir; - -use crate::{constants, QUERIES_TO_EXECUTE}; - -/// Convenient struct that contains the necessary data and operations to implement -/// the `Canyon Memory`. -/// -/// Canyon Memory it's just a convenient way of relate the data of a Rust source -/// code file and the `CanyonEntity` (if so), helping Canyon to know what source -/// file contains a `#[canyon_entity]` annotation and restricting it to just one -/// annotated struct per file. -/// -/// This limitation it's imposed by design. Canyon, when manages all the entities in -/// the user's source code, needs to know for future migrations the old data about a structure -/// and the new modified one. -/// -/// For example, let's say that you have a: -/// ``` -/// pub struct Person { -/// /* some fields */ -/// } -/// ``` -/// -/// and you decided to modify it's Ident and change it to `Human`. -/// -/// Canyon will take care about modifying the Database, and `ALTER TABLE` to edit the actual data for you, -/// but, if it's not able to get the data to know that the old one is `Person` and the new one it's `Human`. -/// it will simply drop the table (losing all your data) and creating a new table `Human`. -/// -/// So, we decised to follow the next approach: -/// Every entity annotated with a `#[canyon_entity]` annotation will be related to only unique Rust source -/// code file. If we find more, Canyon will raise and error saying that it does not allows to having more than -/// one managed entity per source file. -/// -/// Then, we will store the entities data in a special table only for Canyon, where we will create the relation -/// between the source file, the entity and it's fields and data. -/// -/// So, if the user wants or needs to modify the data of it's entity, Canyon can secure that will perform the -/// correct operations because we can't "remember" how that entity was, and how it should be now, avoiding -/// potentially dangerous operations due to lack of knowing what entity relates with new data. -/// -/// 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 renamed_entities: HashMap, -} - -// Makes this structure able to make queries to the database -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 { - // Creates the memory table if not exists - 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"); - 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"), - }; - db_rows.push(db_row); - } - - // Parses the source code files looking for the #[canyon_entity] annotated classes - let mut mem = Self { - memory: HashMap::new(), - renamed_entities: HashMap::new(), - }; - Self::find_canyon_entity_annotated_structs(&mut mem).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 { - // 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)) - }); - if !already_in_db { - values_to_insert.push_str(format!("('{filepath}', '{struct_name}'),").as_str()); - } - // 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) - }); - - // 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]); - } - - // if the updated element is the struct name, we add it to the table_rename Hashmap - let rename_table = old.struct_name != struct_name; - - if rename_table { - mem.renamed_entities.insert( - struct_name.to_lowercase(), // The new one - old.struct_name.to_lowercase(), // 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()) - && !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]); - } - } - }); - - 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) { - for file in WalkDir::new("./src") - .into_iter() - .filter_map(|file| file.ok()) - { - if file.metadata().unwrap().is_file() - && file.path().display().to_string().ends_with(".rs") - { - // Opening the source code file - let contents = - fs::read_to_string(file.path()).expect("Something went wrong reading the file"); - - 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; - } - } - - // This limitation will be removed in future versions, when the memory - // will be able to track every aspect of an entity - match canyon_entity_macro_counter { - 0 => (), - 1 => { - self.memory.insert( - file.path().display().to_string().replace('\\', "/"), - struct_name, - ); - } - _ => 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" - ), - } - } - } - } - - /// Generates, if not exists the `canyon_memory` table - 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 - }; - - Self::query(query, [], datasource_name) - .await - .expect("Error creating the 'canyon_memory' table"); - } -} - -/// Represents a single row from the `canyon_memory` table -#[derive(Debug)] -struct CanyonMemoryRow<'a> { - id: i32, - filepath: &'a str, - struct_name: &'a str, -} diff --git a/canyon_observer/src/migrations/register_types.rs b/canyon_observer/src/migrations/register_types.rs deleted file mode 100644 index b101cb77..00000000 --- a/canyon_observer/src/migrations/register_types.rs +++ /dev/null @@ -1,265 +0,0 @@ -use regex::Regex; - -use crate::constants::{postgresql_type, regex_patterns, rust_type, sqlserver_type}; - -/// 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 user_table_name: Option<&'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 - 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 - 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"), - } - } - - 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"), - } - } - - 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 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 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 { - 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 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 { - 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 - .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 numeric = vec!["i16", "i32", "i64"]; - - numeric.contains(&self.field_type.as_str()) && pk_is_autoincremental - } -} diff --git a/canyon_sql/Cargo.toml b/canyon_sql/Cargo.toml deleted file mode 100755 index b0376f61..00000000 --- a/canyon_sql/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "canyon_sql" -version = "0.1.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.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" } diff --git a/canyon_sql/src/lib.rs b/canyon_sql/src/lib.rs deleted file mode 100755 index 330b8ed4..00000000 --- a/canyon_sql/src/lib.rs +++ /dev/null @@ -1,58 +0,0 @@ -///! 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 -/// through this crate, building the *public API* of the library - -/// Reexported elements to the root of the public API -pub mod migrations { - pub use canyon_observer::migrations::{handler, processor}; -} - -/// The top level reexport. Here we define the path to some really important -/// things in `Canyon-SQL`, like the `main` macro, the IT macro. -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_macros::*; -} - -/// 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::DatabaseType; -} - -/// Re-exports the query elements from the `crud`crate -pub mod query { - pub use canyon_crud::query_elements::operators; - pub use canyon_crud::query_elements::{query::*, query_builder::*}; -} - -/// Reexport the available database clients within Canyon -pub mod db_clients { - pub use canyon_connection::tiberius; - pub use canyon_connection::tokio_postgres; -} - -/// Reexport the needed runtime dependencies -pub mod runtime { - pub use canyon_connection::futures; - pub use canyon_connection::init_connections_cache; - pub use canyon_connection::tokio; - pub use canyon_connection::tokio_util; - pub use canyon_connection::CANYON_TOKIO_RUNTIME; -} - -/// Module for reexport the `chrono` crate with the allowed public and available types in Canyon -pub mod date_time { - pub use canyon_crud::chrono::{ - DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc, - }; -} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 04c21b89..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,9 +21,21 @@ 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: 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 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 new file mode 100644 index 00000000..79501bc5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,83 @@ +//! 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 +/// through this crate, building the *public API* of the library +extern crate canyon_core; +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}; +} + +/// The top level reexport. Here we define the path to some really important +/// things in `Canyon-SQL`, like the `main` macro, the IT macro. +pub use canyon_macros::main; + +/// Public API for the `Canyon-SQL` proc-macros, and for the external ones +pub mod macros { + 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_core::connection::contracts::DbConnection; + pub use canyon_core::connection::database_type::DatabaseType; + pub use canyon_core::connection::db_connector::DatabaseConnector; +} + +pub mod core { + pub use canyon_core::canyon::Canyon; + pub use canyon_core::mapper::*; + pub use canyon_core::rows::CanyonRows; + pub use canyon_core::transaction::Transaction; +} + +/// 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::crud::*; + pub use canyon_crud::entity::EntityCrudOperations; +} + +/// Re-exports the query elements from the `crud`crate +pub mod query { + pub use canyon_core::query::bounds; + pub use canyon_core::query::operators; + pub use canyon_core::query::parameters::QueryParameter; + pub use canyon_core::query::*; + + pub use canyon_core::query::ColumnRef; +} + +/// Reexport the available database clients within Canyon +pub mod db_clients { + #[cfg(feature = "mysql")] + pub use canyon_core::connection::mysql_async; + #[cfg(feature = "mssql")] + pub use canyon_core::connection::tiberius; + #[cfg(feature = "postgres")] + pub use canyon_core::connection::tokio_postgres; +} + +/// Reexport the needed runtime dependencies +pub mod runtime { + pub use canyon_core::connection::futures; + pub use canyon_core::connection::get_canyon_tokio_runtime; + pub use canyon_core::connection::tokio; + pub use canyon_core::connection::tokio_util; +} + +/// Module for reexport the `chrono` crate with the allowed public and available types in Canyon +pub mod date_time { + pub use canyon_crud::chrono::{ + DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc, + }; +} diff --git a/tests/Cargo.toml b/tests/Cargo.toml index a6aacb83..16f4462e 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,12 +1,18 @@ [package] name = "tests" -version = "0.1.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"] +mysql = ["canyon_sql/mysql"] +migrations = ["canyon_sql/migrations"] diff --git a/tests/canyon.toml b/tests/canyon.toml index 7bb56442..73c0b023 100644 --- a/tests/canyon.toml +++ b/tests/canyon.toml @@ -1,5 +1,36 @@ [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' + + +[[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/canyon_integration_tests.rs b/tests/canyon_integration_tests.rs index 8120ee8f..fa51d7f0 100644 --- a/tests/canyon_integration_tests.rs +++ b/tests/canyon_integration_tests.rs @@ -1,14 +1,17 @@ -use std::error::Error; - -///! 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. +extern crate canyon_sql; + +use std::error::Error; + mod crud; +#[cfg(feature = "migrations")] mod migrations; mod constants; diff --git a/tests/constants.rs b/tests/constants.rs index f7804e43..ad4d6ad4 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -1,7 +1,13 @@ -///! 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"; +#[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 = "SELECT gi.table_name, @@ -33,6 +39,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 +94,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..ee7178dc 100644 --- a/tests/crud/delete_operations.rs +++ b/tests/crud/delete_operations.rs @@ -1,10 +1,17 @@ -///! Integration tests for the CRUD operations available in `Canyon` that -///! generates and executes *INSERT* statements -use canyon_sql::crud::CrudOperations; +//! Integration tests for the CRUD operations available in `Canyon` that +//! generates and executes *INSERT* statements + +#[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::constants::{PSQL_DS, SQL_SERVER_DS}; use crate::tests_models::league::*; +use canyon_sql::crud::{DeleteOperations, InsertOperations, ReadOperations}; + /// Deletes a row from the database that is mapped into some instance of a `T` entity. /// /// The `t.delete(&self)` operation is only enabled for types that @@ -14,9 +21,10 @@ 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, + // For test the delete operation, 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(), @@ -32,7 +40,7 @@ fn test_crud_delete_method_operation() { assert_eq!( new_league.id, - League::find_by_pk_datasource(&new_league.id, PSQL_DS) + League::find_by_pk_with(&new_league.id, PSQL_DS) .await .expect("Request error") .expect("None value") @@ -48,7 +56,7 @@ fn test_crud_delete_method_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(&new_league.id) .await @@ -58,8 +66,56 @@ 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_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 { + 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_with(SQL_SERVER_DS) + .await + .expect("Failed insert operation"); + assert_eq!( + new_league.id, + League::find_by_pk_with(&new_league.id, SQL_SERVER_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_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) -> Result>, Err> + assert_eq!( + League::find_by_pk_with(&new_league.id, SQL_SERVER_DS) + .await + .expect("Unwrapping the result, letting the Option"), + 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_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 { @@ -73,12 +129,12 @@ fn test_crud_delete_datasource_method_operation() { // We insert the instance on the database, on the `League` entity new_league - .insert_datasource(SQL_SERVER_DS) + .insert_with(MYSQL_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, MYSQL_DS) .await .expect("Request error") .expect("None value") @@ -88,15 +144,15 @@ fn test_crud_delete_datasource_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(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, SQL_SERVER_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 b58df802..f980459a 100644 --- a/tests/crud/foreign_key_operations.rs +++ b/tests/crud/foreign_key_operations.rs @@ -1,22 +1,26 @@ -///! 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 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 +/// 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 -use canyon_sql::crud::CrudOperations; - +/// Names of the foreign key methods are autogenerated for the direct and +/// reverse side of the implementations. +#[cfg(feature = "mysql")] +use crate::constants::MYSQL_DS; +#[cfg(feature = "mssql")] 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, 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,16 +42,43 @@ 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) +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"); + + // 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) + } +} + +/// 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_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_with(MYSQL_DS) .await .expect("Result variant of the query is err"); @@ -67,6 +98,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) @@ -75,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"); @@ -87,16 +119,39 @@ 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_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_with(&some_league, SQL_SERVER_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)); +} + +/// 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() { - let some_league: League = League::find_by_pk_datasource(&1, SQL_SERVER_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, SQL_SERVER_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 new file mode 100644 index 00000000..9bc16ce1 --- /dev/null +++ b/tests/crud/init_mssql.rs @@ -0,0 +1,66 @@ +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::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 +#[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;Encrypt=true"; + + canyon_sql::runtime::futures::executor::block_on(async { + let mut config = Config::from_ado_string(CONN_STR).expect("could not parse ado string"); + + 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()) + .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_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); + 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..0ac6ebef 100644 --- a/tests/crud/insert_operations.rs +++ b/tests/crud/insert_operations.rs @@ -1,9 +1,13 @@ -///! Integration tests for the CRUD operations available in `Canyon` that -///! generates and executes *INSERT* statements -use canyon_sql::crud::CrudOperations; +//! Integration tests for the CRUD operations available in `Canyon` that +//! generates and executes *INSERT* statements +#[cfg(feature = "mysql")] +use crate::constants::MYSQL_DS; +#[cfg(feature = "mssql")] 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. @@ -25,7 +29,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,8 +59,9 @@ 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() { +fn test_crud_insert_with_mssql_operation() { let mut new_league: League = League { id: Default::default(), ext_id: 7892635306594_i64, @@ -67,7 +73,7 @@ fn test_crud_insert_datasource_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"); @@ -75,7 +81,7 @@ fn test_crud_insert_datasource_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"); @@ -83,133 +89,229 @@ fn test_crud_insert_datasource_operation() { 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. -#[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 +/// 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_multi_insert_datasource_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 { +fn test_crud_insert_with_mysql_operation() { + let mut new_league: 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(), + 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(), }; - // 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) + // We insert the instance on the database, on the `League` entity + new_league + .insert_with(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, 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) + // 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_with(&new_league.id, MYSQL_DS) .await - .expect("[3] - Failed the query to the database") - .expect("[3] - No entity found for the primary key value passed in"); + .expect("Failed the query to the database") + .expect("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); + 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_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 7526c8f6..f333a6de 100644 --- a/tests/crud/mod.rs +++ b/tests/crud/mod.rs @@ -1,69 +1,9 @@ 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; - -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..037b67bf 100644 --- a/tests/crud/querybuilder_operations.rs +++ b/tests/crud/querybuilder_operations.rs @@ -1,101 +1,286 @@ -///! Tests for the QueryBuilder available operations within Canyon. +#[cfg(feature = "mysql")] +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. /// -///! 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, - query::{operators::Comp, ops::QueryBuilder}, + crud::{DeleteOperations, ReadOperations, UpdateOperations}, + query::querybuilder::{QueryBuilderOps, SelectQueryBuilderOps, UpdateQueryBuilderOps}, }; -use crate::constants::SQL_SERVER_DS; 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(); 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"); } +/// 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 binding = LeagueFieldValue::name("LEC".to_string()); + let filtered_leagues_result = League::select_query()?.where_value(&binding, Like(Full)); + + assert_eq!( + filtered_leagues_result.build().unwrap().sql(), + "SELECT * FROM \"league\" WHERE \"league\".\"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_with_mssql() { + // Find all the leagues with "LC" in their name + 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.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_fulllike_with_mysql() { + // Find all the leagues with "LC" in their name + 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.build().unwrap().sql(), + "SELECT * FROM `league` WHERE `league`.`name` LIKE CONCAT ('%', CAST (? 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")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_with_querybuilder_and_leftlike() { + // Find all the leagues whose name ends with "CK" + let fv = LeagueFieldValue::name("CK".to_string()); + let filtered_leagues_result = League::select_query()?.where_value(&fv, Like(Left)); + + assert_eq!( + filtered_leagues_result.build().unwrap().sql(), + "SELECT * FROM \"league\" WHERE \"league\".\"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_with_mssql() { + // Find all the leagues whose name ends with "CK" + 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.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_leftlike_with_mysql() { + // Find all the leagues whose name ends with "CK" + 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.build().unwrap().sql(), + "SELECT * FROM `league` WHERE `league`.`name` LIKE CONCAT ('%', CAST (? 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")] +#[canyon_sql::macros::canyon_tokio_test] +fn test_crud_find_with_querybuilder_and_rightlike() { + // Find all the leagues whose name starts with "LC" + let fv = LeagueFieldValue::name("LEC".to_string()); + let filtered_leagues_result = League::select_query()?.where_value(&fv, Like(Right)); + + assert_eq!( + filtered_leagues_result.build().unwrap().sql(), + "SELECT * FROM \"league\" WHERE \"league\".\"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_with_mssql() { + // Find all the leagues whose name starts with "LC" + 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.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_with_mysql() { + // Find all the leagues whose name starts with "LC" + 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.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() { - // 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()); } +/// Same than the above but with the specified datasource +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +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!(!result.unwrap().is_empty()); +} + /// 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 // 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); - - /* 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"); @@ -105,25 +290,72 @@ 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_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"); + + 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"); + }); +} + +/// Same as above, but with the specified datasource +#[cfg(feature = "mysql")] +#[canyon_sql::macros::canyon_tokio_test] +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 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()?; + + 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"); @@ -138,113 +370,279 @@ 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. +/// 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() { - 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 + 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_with_mysql() { + Player::delete_query_with(DatabaseType::MySQL) + .where_value(&PlayerFieldValue::id(120), Operator::Gt) + .and(&PlayerFieldValue::id(130), Operator::Lt) + .build() .unwrap() - .is_empty()); + .launch_with::<&str, Player>(MYSQL_DS) + .await + .expect("Error connecting with the database when we are going to delete data! :)"); + + 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;", + + #[cfg(feature = "mssql")] + DatabaseType::SqlServer => "SELECT * FROM [league] WHERE [league].[name] = @P1;", - assert_eq!(l.read_sql(), "SELECT * FROM league WHERE name = $1") + #[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); + for database_type in enabled_database_types() { + let wh = LeagueFieldValue::name("LEC".to_string()); - assert_eq!( - l.read_sql().trim(), - "SELECT * FROM league WHERE name = $1 AND id <= $2" - ) + 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]); + for database_type in enabled_database_types() { + let wh = LeagueFieldValue::name("LEC".to_string()); - assert_eq!( - l.read_sql().trim(), - "SELECT * FROM league WHERE name = $1 AND id IN ($1, $2, $3)" - ) + 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); + for database_type in enabled_database_types() { + let wh = LeagueFieldValue::name("LEC".to_string()); - assert_eq!( - l.read_sql().trim(), - "SELECT * FROM league WHERE name = $1 OR id <= $2" - ) + 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]); + for database_type in enabled_database_types() { + let wh = LeagueFieldValue::name("LEC".to_string()); - assert_eq!( - l.read_sql(), - "SELECT * FROM league WHERE name = $1 OR id IN ($1, $2, $3) " - ) + 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); + for database_type in enabled_database_types() { + let fv = LeagueFieldValue::name("LEC".to_string()); - assert_eq!( - l.read_sql(), - "SELECT * FROM league WHERE name = $1 ORDER BY id" - ) + 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 57% rename from tests/crud/select_operations.rs rename to tests/crud/read_operations.rs index 26e0e5f2..8691d9fc 100644 --- a/tests/crud/select_operations.rs +++ b/tests/crud/read_operations.rs @@ -1,17 +1,24 @@ #![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 -///! 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; - 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* +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_all() { let find_all_result: Result, Box> = @@ -26,38 +33,35 @@ 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 -#[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() { +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()); } -/// Same as the `find_all_datasource()`, but with the unchecked variant and the specified dataosource, -/// returning directly `Vec` and not `Result, Err>` +#[cfg(feature = "mysql")] #[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()); +fn test_crud_find_all_with_mysql() { + let find_all_result: Result, Box> = + 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()); } /// 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 *default datasource*. +#[cfg(feature = "postgres")] #[canyon_sql::macros::canyon_tokio_test] fn test_crud_find_by_pk() { let find_by_pk_result: Result, Box> = @@ -79,11 +83,35 @@ 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. +/// 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_with_mssql() { + let find_by_pk_result: Result, Box> = + 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(); + 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" + ); +} + +/// 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() { +fn test_crud_find_by_pk_with_mysql() { let find_by_pk_result: Result, Box> = - League::find_by_pk_datasource(&27, SQL_SERVER_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(); @@ -99,6 +127,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!( @@ -108,14 +137,23 @@ 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_with_operation_mssql() { + assert_eq!( + League::find_all_with(SQL_SERVER_DS).await.unwrap().len() as i64, + League::count_with(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() { +fn test_crud_count_with_operation_mysql() { 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(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 fc7ae733..cdc3416c 100644 --- a/tests/crud/update_operations.rs +++ b/tests/crud/update_operations.rs @@ -1,20 +1,24 @@ -///! Integration tests for the CRUD operations available in `Canyon` that -///! generates and executes *UPDATE* statements -use canyon_sql::crud::CrudOperations; +use crate::tests_models::league::*; +// Integration tests for the CRUD operations available in `Canyon` that +/// generates and executes *UPDATE* statements +use canyon_sql::crud::{ReadOperations, UpdateOperations}; +#[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. /// /// 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. /// /// 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 @@ -26,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 @@ -45,51 +49,94 @@ 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_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_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. + 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_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_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 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_with(SQL_SERVER_DS) + .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_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, SQL_SERVER_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(SQL_SERVER_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, SQL_SERVER_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(SQL_SERVER_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 17b19c35..957c0a5d 100644 --- a/tests/migrations/mod.rs +++ b/tests/migrations/mod.rs @@ -1,26 +1,48 @@ -///! Integration tests for the migrations feature of `Canyon-SQL` -use canyon_sql::{crud::Transaction, migrations::handler::Migrations}; +#![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::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; - assert!(results.is_ok()); + 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 public_schema_info = results.ok().unwrap().postgres; + 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 first_result = public_schema_info.get(0).unwrap(); + let res = results.unwrap(); + let public_schema_info = res.get_postgres_rows(); + 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" ); } 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,