From 57b3a04181ebe3200fa5a12e3c5a126e990de26b Mon Sep 17 00:00:00 2001 From: honzuki <22032642+honzuki@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:40:22 +0900 Subject: [PATCH] push new version --- .cargo_vcs_info.json | 6 - .github/FUNDING.yml | 1 - .github/ISSUE_TEMPLATE/bug-report.md | 49 - .github/ISSUE_TEMPLATE/config.yml | 8 - .github/ISSUE_TEMPLATE/feature-request.md | 33 - .github/workflows/diesel.yml | 162 -- .github/workflows/release-bot.yml | 23 - .github/workflows/rust.yml | 268 --- .gitignore | 3 +- CHANGELOG.md | 283 ++- Cargo.toml | 60 +- Cargo.toml.orig | 17 +- README.md | 13 +- build-tools/{cargo-publish.sh => publish.sh} | 0 build-tools/{cargo-readme.sh => readme.sh} | 0 src/backend/index_builder.rs | 6 + src/backend/mysql/query.rs | 26 + src/backend/mysql/table.rs | 39 +- src/backend/postgres/index.rs | 5 + src/backend/postgres/query.rs | 8 + src/backend/postgres/table.rs | 18 +- src/backend/postgres/types.rs | 19 +- src/backend/query_builder.rs | 71 +- src/backend/sqlite/index.rs | 5 + src/backend/sqlite/query.rs | 8 + src/backend/sqlite/table.rs | 74 +- src/expr.rs | 1707 ++++++++++++++++-- src/extension/mysql/column.rs | 19 + src/extension/mysql/mod.rs | 2 + src/extension/postgres/expr.rs | 30 +- src/extension/postgres/func.rs | 57 + src/extension/postgres/mod.rs | 7 +- src/extension/postgres/types.rs | 72 +- src/extension/sqlite/expr.rs | 20 +- src/func.rs | 150 +- src/index/create.rs | 75 +- src/lib.rs | 19 +- src/prepare.rs | 35 +- src/query/insert.rs | 38 + src/query/on_conflict.rs | 114 +- src/query/select.rs | 69 + src/query/update.rs | 18 + src/query/with.rs | 2 +- src/table/alter.rs | 17 +- src/table/column.rs | 140 +- src/table/create.rs | 14 +- src/token.rs | 10 +- src/types.rs | 18 +- src/value.rs | 393 +++- tests/common.rs | 1 + tests/mysql/mod.rs | 2 +- tests/mysql/query.rs | 207 ++- tests/mysql/table.rs | 39 +- tests/postgres/index.rs | 15 + tests/postgres/query.rs | 71 + tests/postgres/table.rs | 14 +- tests/postgres/types.rs | 32 + tests/sqlite/index.rs | 15 + tests/sqlite/query.rs | 98 +- tests/sqlite/table.rs | 36 +- 60 files changed, 3631 insertions(+), 1130 deletions(-) delete mode 100644 .cargo_vcs_info.json delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.md delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature-request.md delete mode 100644 .github/workflows/diesel.yml delete mode 100644 .github/workflows/release-bot.yml delete mode 100644 .github/workflows/rust.yml rename build-tools/{cargo-publish.sh => publish.sh} (100%) rename build-tools/{cargo-readme.sh => readme.sh} (100%) create mode 100644 src/extension/mysql/column.rs diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json deleted file mode 100644 index a1edb72..0000000 --- a/.cargo_vcs_info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "git": { - "sha1": "30e0989ba55c416ec547d85b568db3b2a707dabf" - }, - "path_in_vcs": "" -} \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 74dbfd8..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: SeaQL \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index f7929bd..0000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -name: Bug Report -about: Report a bug or feature flaw -title: '' -labels: '' -assignees: '' - ---- - - - -## Description - - - -## Steps to Reproduce - -1. -2. -3. - -### Expected Behavior - - - -### Actual Behavior - - - -### Reproduces How Often - - - -## Versions - - - -## Additional Information - - diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 5685890..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -blank_issues_enabled: true -contact_links: - - name: Q & A - url: https://github.com/SeaQL/sea-query/discussions/new?category=q-a - about: Ask a question or look for help. Try to provide sufficient context, snippets to reproduce and error messages. - - name: SeaQL Discord Server - url: https://discord.com/invite/uCPdDXzbdv - about: Join our Discord server to chat with others in the SeaQL community! diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md deleted file mode 100644 index a233f7b..0000000 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: Feature Request -about: Suggest a new feature for this project -title: '' -labels: '' -assignees: '' - ---- - - - -## Motivation - - - -## Proposed Solutions - - - -## Additional Information - - diff --git a/.github/workflows/diesel.yml b/.github/workflows/diesel.yml deleted file mode 100644 index c153baa..0000000 --- a/.github/workflows/diesel.yml +++ /dev/null @@ -1,162 +0,0 @@ -name: diesel-tests - -on: - pull_request: - paths: - - '.github/workflows/diesel.yml' - - 'examples/diesel_*/**' - - 'sea-query-diesel/**' - push: - branches: - - master - - 0.*.x - - pr/**/ci - - ci-* - paths: - - '.github/workflows/diesel.yml' - - 'examples/diesel_*/**' - - 'sea-query-diesel/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref || github.run_id }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - -jobs: - rustfmt: - name: Rustfmt - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly - components: rustfmt - - run: cargo fmt --manifest-path examples/diesel_sqlite/Cargo.toml --all -- --check - - run: cargo fmt --manifest-path examples/diesel_postgres/Cargo.toml --all -- --check - - run: cargo fmt --manifest-path examples/diesel_mysql/Cargo.toml --all -- --check - - diesel-build: - name: Build `sea-query-diesel` - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo update --manifest-path sea-query-diesel/Cargo.toml --workspace -p bigdecimal:0.4.2 --precise 0.3.1 - - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-chrono,with-json,with-rust_decimal,with-bigdecimal,with-uuid,with-time,with-ipnetwork,with-mac_address,postgres-array - - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-chrono - - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-json - - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-rust_decimal - - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres --features=with-rust_decimal-postgres - - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features mysql --features=with-rust_decimal-mysql - - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-bigdecimal - - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-uuid - - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-time - - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-ipnetwork - - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-mac_address - - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=postgres-array - - sqlite: - name: SQLite - runs-on: ubuntu-latest - needs: diesel-build - strategy: - matrix: - example: [diesel_sqlite] - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --manifest-path examples/${{ matrix.example }}/Cargo.toml - - run: cargo run --manifest-path examples/${{ matrix.example }}/Cargo.toml - - mysql: - name: MySQL - runs-on: ubuntu-latest - needs: diesel-build - strategy: - matrix: - version: [8.0, 5.7] - example: [diesel_mysql] - services: - mysql: - image: mysql:${{ matrix.version }} - env: - MYSQL_HOST: 127.0.0.1 - MYSQL_DATABASE: query - MYSQL_USER: sea - MYSQL_PASSWORD: sea - MYSQL_ROOT_PASSWORD: sea - ports: - - "3306:3306" - options: >- - --health-cmd="mysqladmin ping" - --health-interval=10s - --health-timeout=5s - --health-retries=3 - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --manifest-path examples/${{ matrix.example }}/Cargo.toml - - run: cargo run --manifest-path examples/${{ matrix.example }}/Cargo.toml - - mariadb: - name: MariaDB - runs-on: ubuntu-latest - needs: diesel-build - strategy: - matrix: - version: [10.6] - example: [diesel_mysql] - services: - mariadb: - image: mariadb:${{ matrix.version }} - env: - MYSQL_HOST: 127.0.0.1 - MYSQL_DATABASE: query - MYSQL_USER: sea - MYSQL_PASSWORD: sea - MYSQL_ROOT_PASSWORD: sea - ports: - - "3306:3306" - options: >- - --health-cmd="mysqladmin ping" - --health-interval=10s - --health-timeout=5s - --health-retries=3 - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --manifest-path examples/${{ matrix.example }}/Cargo.toml - - run: cargo run --manifest-path examples/${{ matrix.example }}/Cargo.toml - - postgres: - name: PostgreSQL - runs-on: ubuntu-latest - needs: diesel-build - strategy: - matrix: - version: [14.4, 13.7, 12.11] - example: [diesel_postgres] - services: - postgres: - image: postgres:${{ matrix.version }} - env: - POSTGRES_HOST: 127.0.0.1 - POSTGRES_DB: query - POSTGRES_USER: sea - POSTGRES_PASSWORD: sea - ports: - - "5432:5432" - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo update --manifest-path examples/${{ matrix.example }}/Cargo.toml -p bigdecimal:0.4.2 --precise 0.3.1 - - run: cargo build --manifest-path examples/${{ matrix.example }}/Cargo.toml - - run: cargo run --manifest-path examples/${{ matrix.example }}/Cargo.toml diff --git a/.github/workflows/release-bot.yml b/.github/workflows/release-bot.yml deleted file mode 100644 index 0265c4b..0000000 --- a/.github/workflows/release-bot.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Release Bot - -on: - release: - types: [published] - -jobs: - comment: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: billy1624/release-comment-on-pr@master - with: - token: ${{ github.token }} - message: | - ### :tada: Released In [${releaseTag}](${releaseUrl}) :tada: - - Thank you everyone for the contribution! - This feature is now available in the latest release. Now is a good time to upgrade! - Your participation is what makes us unique; your adoption is what drives us forward. - You can support SeaQL 🌊 by starring our repos, sharing our libraries and becoming a sponsor ⭐. diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index b2c7633..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,268 +0,0 @@ -name: tests - -on: - pull_request: - paths-ignore: - - '**.md' - - '.github/ISSUE_TEMPLATE/**' - - .gitignore - push: - branches: - - master - - 0.*.x - - pr/**/ci - - ci-* - paths-ignore: - - '**.md' - - '.github/ISSUE_TEMPLATE/**' - - .gitignore - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref || github.run_id }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - -jobs: - rustfmt: - name: Rustfmt - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly - components: rustfmt - - run: cargo fmt --manifest-path Cargo.toml --all -- --check - - run: cargo fmt --manifest-path sea-query-attr/Cargo.toml --all -- --check - - run: cargo fmt --manifest-path sea-query-binder/Cargo.toml --all -- --check - - run: cargo fmt --manifest-path sea-query-derive/Cargo.toml --all -- --check - - run: cargo fmt --manifest-path sea-query-postgres/Cargo.toml --all -- --check - - run: cargo fmt --manifest-path sea-query-rusqlite/Cargo.toml --all -- --check - - run: cargo fmt --manifest-path examples/sqlx_sqlite/Cargo.toml --all -- --check - - run: cargo fmt --manifest-path examples/sqlx_any/Cargo.toml --all -- --check - - run: cargo fmt --manifest-path examples/rusqlite/Cargo.toml --all -- --check - - run: cargo fmt --manifest-path examples/sqlx_postgres/Cargo.toml --all -- --check - - run: cargo fmt --manifest-path examples/postgres/Cargo.toml --all -- --check - - run: cargo fmt --manifest-path examples/sqlx_mysql/Cargo.toml --all -- --check - - clippy: - name: Clippy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - components: clippy - - run: cargo clippy --features=all-features --workspace -- -D warnings - - run: cargo clippy --manifest-path sea-query-binder/Cargo.toml --workspace --features runtime-async-std-rustls --features=with-chrono,with-json,with-rust_decimal,with-bigdecimal,with-uuid,with-time,with-ipnetwork,with-mac_address,postgres-array -- -D warnings - - run: cargo clippy --manifest-path sea-query-rusqlite/Cargo.toml --all-features --workspace -- -D warnings - - run: cargo clippy --manifest-path sea-query-postgres/Cargo.toml --all-features --workspace -- -D warnings - - build: - name: Build `sea-query` - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --workspace --no-default-features - - run: cargo build --workspace --all-features - - run: cargo build --workspace --features=with-chrono - - run: cargo build --workspace --features=with-json - - run: cargo build --workspace --features=with-rust_decimal - - run: cargo build --workspace --features=with-bigdecimal - - run: cargo build --workspace --features=with-uuid - - run: cargo build --workspace --features=with-time - - run: cargo build --workspace --features=with-ipnetwork - - run: cargo build --workspace --features=with-mac_address - - run: cargo build --workspace --features=postgres-array - - run: cargo build --workspace --features=thread-safe - - binder-build: - name: Build `sea-query-binder` - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - runtime: [async-std] - tls: [rustls] - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --manifest-path sea-query-binder/Cargo.toml --workspace --features sqlx-postgres,sqlx-sqlite,sqlx-any,sqlx-mysql --features=runtime-${{ matrix.runtime }}-${{ matrix.tls }} --features=with-chrono,with-json,with-rust_decimal,with-bigdecimal,with-uuid,with-time,with-ipnetwork,with-mac_address,postgres-array - - run: cargo build --manifest-path sea-query-binder/Cargo.toml --workspace --features sqlx-postgres,sqlx-sqlite,sqlx-any,sqlx-mysql --features=runtime-${{ matrix.runtime }}-${{ matrix.tls }} --features=with-chrono - - run: cargo build --manifest-path sea-query-binder/Cargo.toml --workspace --features sqlx-postgres,sqlx-sqlite,sqlx-any,sqlx-mysql --features=runtime-${{ matrix.runtime }}-${{ matrix.tls }} --features=with-json - - run: cargo build --manifest-path sea-query-binder/Cargo.toml --workspace --features sqlx-postgres,sqlx-sqlite,sqlx-any,sqlx-mysql --features=runtime-${{ matrix.runtime }}-${{ matrix.tls }} --features=with-rust_decimal - - run: cargo build --manifest-path sea-query-binder/Cargo.toml --workspace --features sqlx-postgres,sqlx-sqlite,sqlx-any,sqlx-mysql --features=runtime-${{ matrix.runtime }}-${{ matrix.tls }} --features=with-bigdecimal - - run: cargo build --manifest-path sea-query-binder/Cargo.toml --workspace --features sqlx-postgres,sqlx-sqlite,sqlx-any,sqlx-mysql --features=runtime-${{ matrix.runtime }}-${{ matrix.tls }} --features=with-uuid - - run: cargo build --manifest-path sea-query-binder/Cargo.toml --workspace --features sqlx-postgres,sqlx-sqlite,sqlx-any,sqlx-mysql --features=runtime-${{ matrix.runtime }}-${{ matrix.tls }} --features=with-time - - run: cargo build --manifest-path sea-query-binder/Cargo.toml --workspace --features sqlx-postgres,sqlx-sqlite,sqlx-any,sqlx-mysql --features=runtime-${{ matrix.runtime }}-${{ matrix.tls }} --features=with-ipnetwork - - run: cargo build --manifest-path sea-query-binder/Cargo.toml --workspace --features sqlx-postgres,sqlx-sqlite,sqlx-any,sqlx-mysql --features=runtime-${{ matrix.runtime }}-${{ matrix.tls }} --features=with-mac_address - - run: cargo build --manifest-path sea-query-binder/Cargo.toml --workspace --features sqlx-postgres,sqlx-sqlite,sqlx-any,sqlx-mysql --features=runtime-${{ matrix.runtime }}-${{ matrix.tls }} --features=postgres-array - - rusqlite-build: - name: Build `sea-query-rusqlite` - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --manifest-path sea-query-rusqlite/Cargo.toml --workspace --features=with-chrono,with-json,with-rust_decimal,with-bigdecimal,with-uuid,with-time,with-ipnetwork,with-mac_address,postgres-array - - run: cargo build --manifest-path sea-query-rusqlite/Cargo.toml --workspace --features=with-chrono - - run: cargo build --manifest-path sea-query-rusqlite/Cargo.toml --workspace --features=with-json - - run: cargo build --manifest-path sea-query-rusqlite/Cargo.toml --workspace --features=with-rust_decimal - - run: cargo build --manifest-path sea-query-rusqlite/Cargo.toml --workspace --features=with-bigdecimal - - run: cargo build --manifest-path sea-query-rusqlite/Cargo.toml --workspace --features=with-uuid - - run: cargo build --manifest-path sea-query-rusqlite/Cargo.toml --workspace --features=with-time - - run: cargo build --manifest-path sea-query-rusqlite/Cargo.toml --workspace --features=with-ipnetwork - - run: cargo build --manifest-path sea-query-rusqlite/Cargo.toml --workspace --features=with-mac_address - - run: cargo build --manifest-path sea-query-rusqlite/Cargo.toml --workspace --features=postgres-array - - postgres-build: - name: Build `sea-query-postgres` - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --manifest-path sea-query-postgres/Cargo.toml --workspace --features=with-chrono,with-json,with-rust_decimal,with-bigdecimal,with-uuid,with-time,with-ipnetwork,with-mac_address,postgres-array - - run: cargo build --manifest-path sea-query-postgres/Cargo.toml --workspace --features=with-chrono - - run: cargo build --manifest-path sea-query-postgres/Cargo.toml --workspace --features=with-json - - run: cargo build --manifest-path sea-query-postgres/Cargo.toml --workspace --features=with-rust_decimal - - run: cargo build --manifest-path sea-query-postgres/Cargo.toml --workspace --features=with-bigdecimal - - run: cargo build --manifest-path sea-query-postgres/Cargo.toml --workspace --features=with-uuid - - run: cargo build --manifest-path sea-query-postgres/Cargo.toml --workspace --features=with-time - - run: cargo build --manifest-path sea-query-postgres/Cargo.toml --workspace --features=with-ipnetwork - - run: cargo build --manifest-path sea-query-postgres/Cargo.toml --workspace --features=with-mac_address - - run: cargo build --manifest-path sea-query-postgres/Cargo.toml --workspace --features=postgres-array - - test: - name: Unit Test - runs-on: ubuntu-latest - needs: ["build"] - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo test --features=all-features - - run: cargo test --test option-more-parentheses --features=tests-cfg,option-more-parentheses - - derive-test: - name: Derive Tests - runs-on: ubuntu-latest - needs: ["build"] - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --package sea-query-derive - - run: cargo test --package sea-query-derive - - sqlite: - name: SQLite - runs-on: ubuntu-latest - needs: ["test", "derive-test", "rusqlite-build", "binder-build"] - strategy: - matrix: - example: [rusqlite, sqlx_sqlite] - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --manifest-path examples/${{ matrix.example }}/Cargo.toml - - run: cargo run --manifest-path examples/${{ matrix.example }}/Cargo.toml - - mysql: - name: MySQL - runs-on: ubuntu-latest - needs: ["test", "derive-test", "binder-build"] - strategy: - matrix: - version: [8.0, 5.7] - example: [sqlx_mysql] - services: - mysql: - image: mysql:${{ matrix.version }} - env: - MYSQL_HOST: 127.0.0.1 - MYSQL_DATABASE: query - MYSQL_USER: sea - MYSQL_PASSWORD: sea - MYSQL_ROOT_PASSWORD: sea - ports: - - "3306:3306" - options: >- - --health-cmd="mysqladmin ping" - --health-interval=10s - --health-timeout=5s - --health-retries=3 - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --manifest-path examples/${{ matrix.example }}/Cargo.toml - - run: cargo run --manifest-path examples/${{ matrix.example }}/Cargo.toml - - mariadb: - name: MariaDB - runs-on: ubuntu-latest - needs: ["test", "derive-test", "binder-build"] - strategy: - matrix: - version: [10.6] - example: [sqlx_mysql] - services: - mariadb: - image: mariadb:${{ matrix.version }} - env: - MYSQL_HOST: 127.0.0.1 - MYSQL_DATABASE: query - MYSQL_USER: sea - MYSQL_PASSWORD: sea - MYSQL_ROOT_PASSWORD: sea - ports: - - "3306:3306" - options: >- - --health-cmd="mysqladmin ping" - --health-interval=10s - --health-timeout=5s - --health-retries=3 - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --manifest-path examples/${{ matrix.example }}/Cargo.toml - - run: cargo run --manifest-path examples/${{ matrix.example }}/Cargo.toml - - postgres: - name: PostgreSQL - runs-on: ubuntu-latest - needs: ["test", "derive-test", "binder-build", "postgres-build"] - strategy: - matrix: - version: [14.4, 13.7, 12.11] - example: [postgres, sqlx_postgres] - services: - postgres: - image: postgres:${{ matrix.version }} - env: - POSTGRES_HOST: 127.0.0.1 - POSTGRES_DB: query - POSTGRES_USER: sea - POSTGRES_PASSWORD: sea - ports: - - "5432:5432" - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --manifest-path examples/${{ matrix.example }}/Cargo.toml - - run: cargo run --manifest-path examples/${{ matrix.example }}/Cargo.toml - - any: - name: Any - runs-on: ubuntu-latest - needs: ["test", "derive-test", "binder-build"] - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - run: cargo build --manifest-path examples/sqlx_any/Cargo.toml diff --git a/.gitignore b/.gitignore index d3da335..ff13a57 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ Cargo.lock *.sublime* .vscode wip/ -.idea/* \ No newline at end of file +.idea/* +.DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee9a23..7fe8170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,291 @@ # Changelog All notable changes to this project will be documented in this file. - + The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## 0.32.1 - 2024-12-01 + +### New Features + +* Added `Value::as_null` +```rust +let v = Value::Int(Some(2)); +let n = v.as_null(); + +assert_eq!(n, Value::Int(None)); +``` +* Added bitwise and/or operators (`bit_and`, `bit_or`) https://github.com/SeaQL/sea-query/pull/841 +```rust +let query = Query::select() + .expr(1.bit_and(2).eq(3)) + .to_owned(); + +assert_eq!( + query.to_string(PostgresQueryBuilder), + r#"SELECT (1 & 2) = 3"# +); +``` + +### Enhancements + +* Added `GREATEST` & `LEAST` function https://github.com/SeaQL/sea-query/pull/844 +* Added `ValueType::enum_type_name()` https://github.com/SeaQL/sea-query/pull/836 +* Removed "one common table" restriction on recursive CTE https://github.com/SeaQL/sea-query/pull/835 + +### House keeping + +* Remove unnecessary string hashes https://github.com/SeaQL/sea-query/pull/815 + +## 0.32.0 - 2024-10-17 + +### Releases + +#### 2024-08-09 + ++ `sea-query`/`0.32.0-rc.1` ++ `sea-query-binder`/`0.7.0-rc.1` ++ `sea-query-binder`/`0.7.0-rc.2` ++ `sea-query-rusqlite`/`0.7.0-rc.1` ++ `sea-query-postgres`/`0.5.0-rc.1` + +#### 2024-10-05 + ++ `sea-query`/`0.32.0-rc.2` ++ `sea-query-attr`/`0.1.3` ++ `sea-query-derive`/`0.4.2` ++ `sea-query-rusqlite`/`0.7.0-rc.2` + +### New Features + +* Construct Postgres query with vector extension https://github.com/SeaQL/sea-query/pull/774 + * Added `postgres-vector` feature flag + * Added `Value::Vector`, `ColumnType::Vector`, `ColumnDef::vector()`, `PgBinOper::EuclideanDistance`, `PgBinOper::NegativeInnerProduct` and `PgBinOper::CosineDistance` + ```rust + assert_eq!( + Query::select() + .columns([Char::Character]) + .from(Char::Table) + .and_where( + Expr::col(Char::Character).eq(Expr::val(pgvector::Vector::from(vec![1.0, 2.0]))) + ) + .to_string(PostgresQueryBuilder), + r#"SELECT "character" FROM "character" WHERE "character" = '[1,2]'"# + ); + ``` +* Added `ExprTrait` to unify `Expr` and `SimpleExpr` methods https://github.com/SeaQL/sea-query/pull/791 +* Support partial index `CREATE INDEX .. WHERE ..` https://github.com/SeaQL/sea-query/pull/478 + +### Enhancements + +* Replace `Educe` with manual implementations https://github.com/SeaQL/sea-query/pull/817 + +#### `sea-query-derive` + +* Merged `#[enum_def]` into `sea-query-derive` +* `#[enum_def]` now impl additional `IdenStatic` and `AsRef` https://github.com/SeaQL/sea-query/pull/769 + +#### `sea-query-attr` + +* Updated `syn`, `heck` and `darling` +* `sea-query-attr` is now deprecated + +### Upgrades + +* Upgrade `sqlx` to `0.8` https://github.com/SeaQL/sea-query/pull/798 +* Upgrade `bigdecimal` to `0.4` https://github.com/SeaQL/sea-query/pull/798 +* Upgrade `rusqlite` to `0.32` https://github.com/SeaQL/sea-query/pull/802 + +## 0.31.1 - 2024-10-05 + +### Enhancements + +* Derive `Eq`, `Ord`, `Hash` for `Alias` https://github.com/SeaQL/sea-query/pull/818 +* Added `Func::md5` function https://github.com/SeaQL/sea-query/pull/786 +* Added Postgres Json functions: `JSON_BUILD_OBJECT` and `JSON_AGG` https://github.com/SeaQL/sea-query/pull/787 +* Added `cast_as_quoted` https://github.com/SeaQL/sea-query/pull/789 +* Added `IF NOT EXISTS` to `ALTER TYPE ADD VALUE` https://github.com/SeaQL/sea-query/pull/803 + +## 0.31.0 - 2024-08-02 + +### Versions + ++ `sea-query`/`0.31.0-rc.1`: 2024-01-31 ++ `sea-query`/`0.31.0-rc.4`: 2024-02-02 ++ `sea-query`/`0.31.0-rc.5`: 2024-04-14 ++ `sea-query`/`0.31.0-rc.6`: 2024-05-03 ++ `sea-query`/`0.31.0-rc.7`: 2024-06-02 ++ `sea-query`/`0.31.0-rc.8`: 2024-06-19 ++ `sea-query-binder`/`0.6.0-rc.1`: 2024-01-31 ++ `sea-query-binder`/`0.6.0-rc.2`: 2024-04-14 ++ `sea-query-binder`/`0.6.0-rc.3`: 2024-06-19 ++ `sea-query-binder`/`0.6.0-rc.4`: 2024-06-25 ++ `sea-query-binder`/`0.6.0`: 2024-08-02 ++ `sea-query-rusqlite`/`0.6.0-rc.1`: 2024-02-19 ++ `sea-query-rusqlite`/`0.6.0`: 2024-08-02 ++ `sea-query-attr`/`0.1.2`: 2024-04-14 ++ `sea-query-diesel`/`0.2.0`: 2024-08-02 + +### New Features + +* Added `table_name` attribute to `enum_def` macro https://github.com/SeaQL/sea-query/pull/759 +* Added `ColumnType::Blob` https://github.com/SeaQL/sea-query/pull/777 + +### Breaking Changes + +* Rework SQLite type mapping https://github.com/SeaQL/sea-query/pull/735 +```rust +assert_eq!( + Table::create() + .table(Alias::new("strange")) + .col(ColumnDef::new(Alias::new("id")).integer().not_null().auto_increment().primary_key()) + .col(ColumnDef::new(Alias::new("int1")).integer()) + .col(ColumnDef::new(Alias::new("int2")).tiny_integer()) + .col(ColumnDef::new(Alias::new("int3")).small_integer()) + .col(ColumnDef::new(Alias::new("int4")).big_integer()) + .col(ColumnDef::new(Alias::new("string1")).string()) + .col(ColumnDef::new(Alias::new("string2")).string_len(24)) + .col(ColumnDef::new(Alias::new("char1")).char()) + .col(ColumnDef::new(Alias::new("char2")).char_len(24)) + .col(ColumnDef::new(Alias::new("text_col")).text()) + .col(ColumnDef::new(Alias::new("json_col")).json()) + .col(ColumnDef::new(Alias::new("uuid_col")).uuid()) + .col(ColumnDef::new(Alias::new("decimal1")).decimal()) + .col(ColumnDef::new(Alias::new("decimal2")).decimal_len(12, 4)) + .col(ColumnDef::new(Alias::new("money1")).money()) + .col(ColumnDef::new(Alias::new("money2")).money_len(12, 4)) + .col(ColumnDef::new(Alias::new("float_col")).float()) + .col(ColumnDef::new(Alias::new("double_col")).double()) + .col(ColumnDef::new(Alias::new("date_col")).date()) + .col(ColumnDef::new(Alias::new("time_col")).time()) + .col(ColumnDef::new(Alias::new("datetime_col")).date_time()) + .col(ColumnDef::new(Alias::new("boolean_col")).boolean()) + .col(ColumnDef::new(Alias::new("binary2")).binary_len(1024)) + .col(ColumnDef::new(Alias::new("binary3")).var_binary(1024)) + .col(ColumnDef::new(Alias::new("binary4")).blob()) + .to_string(SqliteQueryBuilder), + [ + r#"CREATE TABLE "strange" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,"#, + r#""int1" integer,"#, + r#""int2" tinyint,"#, + r#""int3" smallint,"#, + r#""int4" bigint,"#, + r#""string1" varchar,"#, + r#""string2" varchar(24),"#, + r#""char1" char,"#, + r#""char2" char(24),"#, + r#""text_col" text,"#, + r#""json_col" json_text,"#, + r#""uuid_col" uuid_text,"#, + r#""decimal1" real,"#, + r#""decimal2" real(12, 4),"#, + r#""money1" real_money,"#, + r#""money2" real_money(12, 4),"#, + r#""float_col" float,"#, + r#""double_col" double,"#, + r#""date_col" date_text,"#, + r#""time_col" time_text,"#, + r#""datetime_col" datetime_text,"#, + r#""boolean_col" boolean,"#, + r#""binary2" blob(1024),"#, + r#""binary3" varbinary_blob(1024),"#, + r#""binary4" blob"#, + r#")"#, + ] + .join(" ") +); +``` +* MySQL money type maps to decimal +* MySQL blob types moved to `sea_query::extension::mysql::MySqlType`; `ColumnDef::blob()` now takes no parameters +```rust +assert_eq!( + Table::create() + .table(BinaryType::Table) + .col(ColumnDef::new(BinaryType::BinaryLen).binary_len(32)) + .col(ColumnDef::new(BinaryType::Binary).binary()) + .col(ColumnDef::new(BinaryType::Blob).blob()) + .col(ColumnDef::new(BinaryType::TinyBlob).custom(MySqlType::TinyBlob)) + .col(ColumnDef::new(BinaryType::MediumBlob).custom(MySqlType::MediumBlob)) + .col(ColumnDef::new(BinaryType::LongBlob).custom(MySqlType::LongBlob)) + .to_string(MysqlQueryBuilder), + [ + "CREATE TABLE `binary_type` (", + "`binlen` binary(32),", + "`bin` binary(1),", + "`b` blob,", + "`tb` tinyblob,", + "`mb` mediumblob,", + "`lb` longblob", + ")", + ] + .join(" ") +); +``` +* `ColumnDef::binary()` set column type as binary with default length of 1 +* Removed `BlobSize` enum +* Added `StringLen` to represent length of var-char/binary +```rust +/// Length for var-char/binary; default to 255 +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum StringLen { + /// String size + N(u32), + Max, + #[default] + None, +} +``` +* `ValueType::columntype()` of `Vec` maps to `VarBinary(StringLen::None)` +* `ValueType::columntype()` of `String` maps to `String(StringLen::None)` +* `ColumnType::Bit` maps to `bit` for Postgres +* `ColumnType::Binary` and `ColumnType::VarBinary` map to `bytea` for Postgres +* `Value::Decimal` and `Value::BigDecimal` bind as `real` for SQLite +* `ColumnType::Year(Option)` changed to `ColumnType::Year` + +### Enhancements + +* Added `IntoColumnDef` trait, allowing `&mut ColumnDef` / `ColumnDef` as argument +* Added `ColumnType::string()` and `ColumnType::var_binary()` as shim for old API +* Added `ON DUPLICATE KEY DO NOTHING` polyfill for MySQL https://github.com/SeaQL/sea-query/pull/765 +* Added non-TLS runtime https://github.com/SeaQL/sea-query/pull/783 + +### House keeping + +* Added `ColumnType` mapping documentation +* Replace `derivative` with `educe` https://github.com/SeaQL/sea-query/pull/763 + +### Upgrades + +* Upgrade `rusqlite` to `0.31` https://github.com/SeaQL/sea-query/pull/755 +* Upgrade `time` to `0.3.36` https://github.com/SeaQL/sea-query/pull/788 + +## 0.30.8 - Pending + +### Enhancements + +* Added `InsertStatement::values_from_panic` https://github.com/SeaQL/sea-query/pull/739 + +## 0.30.7 - 2024-01-12 + +### Enhancements + +* Added `SelectStatement::apply` https://github.com/SeaQL/sea-query/pull/730 + +### House keeping + +* Slight refactors and documentation update + +## 0.30.6 - 2024-01-01 + +### House keeping + +* Fix clippy warnings on Rust 1.75 https://github.com/SeaQL/sea-query/pull/729 + +## `sea-query-rusqlite` 0.5.0 - 2023-12-29 + +* Upgrade `rusqlite` to `0.30` https://github.com/SeaQL/sea-query/pull/728 + ## 0.30.5 - 2023-12-14 ### New Features diff --git a/Cargo.toml b/Cargo.toml index 2ef03f4..dc936d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,12 +13,17 @@ edition = "2021" rust-version = "1.62" name = "sea-query" -version = "0.30.5" +version = "0.32.1" authors = [ "Chris Tsang ", "Billy Chan ", "Ivan Krivosheev ", ] +build = false +autobins = false +autoexamples = false +autotests = false +autobenches = false description = "🔱 A dynamic query builder for MySQL, Postgres and SQLite" documentation = "https://docs.rs/sea-query" readme = "README.md" @@ -44,6 +49,19 @@ rustdoc-args = [ name = "sea_query" path = "src/lib.rs" +[[test]] +name = "common" +path = "tests/common.rs" + +[[test]] +name = "option-more-parentheses" +path = "tests/more-parentheses.rs" +required-features = [ + "tests-cfg", + "option-more-parentheses", + "backend-mysql", +] + [[test]] name = "test-derive" path = "tests/derive/mod.rs" @@ -78,21 +96,13 @@ required-features = [ "backend-sqlite", ] -[[test]] -name = "option-more-parentheses" -path = "tests/more-parentheses.rs" -required-features = [ - "tests-cfg", - "option-more-parentheses", - "backend-mysql", -] - [[bench]] name = "basic" +path = "benches/basic.rs" harness = false [dependencies.bigdecimal] -version = "0.3" +version = "0.4" optional = true default-features = false @@ -102,11 +112,6 @@ features = ["clock"] optional = true default-features = false -[dependencies.derivative] -version = "2.2" -optional = true -default-features = false - [dependencies.inherent] version = "1.0" @@ -125,6 +130,11 @@ version = "3.4" optional = true default-features = false +[dependencies.pgvector] +version = "~0.4" +optional = true +default-features = false + [dependencies.postgres-types] version = "0" optional = true @@ -135,13 +145,8 @@ version = "1" optional = true default-features = false -[dependencies.sea-query-attr] -version = "0.1.1" -optional = true -default-features = false - [dependencies.sea-query-derive] -version = "0.4.0" +version = "0.4.2" optional = true default-features = false @@ -152,7 +157,7 @@ optional = true default-features = false [dependencies.time] -version = "0.3" +version = "0.3.36" features = [ "macros", "formatting", @@ -185,6 +190,7 @@ all-features = [ all-types = [ "postgres-array", "postgres-interval", + "postgres-vector", "with-chrono", "with-json", "with-rust_decimal", @@ -194,7 +200,7 @@ all-types = [ "with-ipnetwork", "with-mac_address", ] -attr = ["sea-query-attr"] +attr = ["sea-query-derive"] backend-mysql = [] backend-postgres = [] backend-sqlite = [] @@ -205,14 +211,12 @@ default = [ "backend-sqlite", ] derive = ["sea-query-derive"] -hashable-value = [ - "derivative", - "ordered-float", -] +hashable-value = ["ordered-float"] option-more-parentheses = [] option-sqlite-exact-column-type = [] postgres-array = [] postgres-interval = [] +postgres-vector = ["pgvector"] tests-cfg = [] thread-safe = [] with-bigdecimal = ["bigdecimal"] diff --git a/Cargo.toml.orig b/Cargo.toml.orig index 5c7b8e9..b5f076c 100644 --- a/Cargo.toml.orig +++ b/Cargo.toml.orig @@ -3,7 +3,7 @@ members = [".", "sea-query-derive"] [package] name = "sea-query" -version = "0.30.5" +version = "0.32.1" authors = [ "Chris Tsang ", "Billy Chan ", @@ -29,16 +29,15 @@ path = "src/lib.rs" [dependencies] inherent = "1.0" -sea-query-attr = { version = "0.1.1", path = "sea-query-attr", default-features = false, optional = true } -sea-query-derive = { version = "0.4.0", path = "sea-query-derive", default-features = false, optional = true } +sea-query-derive = { version = "0.4.2", path = "sea-query-derive", default-features = false, optional = true } serde_json = { version = "1", default-features = false, optional = true, features = ["std"] } -derivative = { version = "2.2", default-features = false, optional = true } chrono = { version = "0.4.27", default-features = false, optional = true, features = ["clock"] } postgres-types = { version = "0", default-features = false, optional = true } +pgvector = { version = "~0.4", default-features = false, optional = true } rust_decimal = { version = "1", default-features = false, optional = true } -bigdecimal = { version = "0.3", default-features = false, optional = true } +bigdecimal = { version = "0.4", default-features = false, optional = true } uuid = { version = "1", default-features = false, optional = true } -time = { version = "0.3", default-features = false, optional = true, features = ["macros", "formatting"] } +time = { version = "0.3.36", default-features = false, optional = true, features = ["macros", "formatting"] } ipnetwork = { version = "0.20", default-features = false, optional = true } mac_address = { version = "1.1", default-features = false, optional = true } ordered-float = { version = "3.4", default-features = false, optional = true } @@ -55,9 +54,10 @@ backend-postgres = [] backend-sqlite = [] default = ["derive", "backend-mysql", "backend-postgres", "backend-sqlite"] derive = ["sea-query-derive"] -attr = ["sea-query-attr"] -hashable-value = ["derivative", "ordered-float"] +attr = ["sea-query-derive"] +hashable-value = ["ordered-float"] postgres-array = [] +postgres-vector = ["pgvector"] postgres-interval = [] thread-safe = [] with-chrono = ["chrono"] @@ -82,6 +82,7 @@ all-features = [ all-types = [ "postgres-array", "postgres-interval", + "postgres-vector", "with-chrono", "with-json", "with-rust_decimal", diff --git a/README.md b/README.md index 3a37a68..8aea535 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ SeaQuery is very lightweight, all dependencies are optional (except `inherent`). ### Feature flags -Macro: `derive` `attr` +Macro: `derive` Async support: `thread-safe` (use `Arc` inplace of `Rc`) @@ -175,10 +175,9 @@ impl Iden for Character { ``` If you're okay with running another procedural macro, you can activate -the `derive` or `attr` feature on the crate to save you some boilerplate. +the `derive` feature on the crate to save you some boilerplate. For more usage information, look at -[the derive examples](https://github.com/SeaQL/sea-query/tree/master/sea-query-derive/tests/pass) -or [the attribute examples](https://github.com/SeaQL/sea-query/tree/master/sea-query-attr/tests/pass). +[the derive examples](https://github.com/SeaQL/sea-query/tree/master/sea-query-derive/tests/pass). ```rust #[cfg(feature = "derive")] @@ -198,7 +197,7 @@ assert_eq!(Glyph.to_string(), "glyph"); ``` ```rust -#[cfg(feature = "attr")] +#[cfg(feature = "derive")] use sea_query::{enum_def, Iden}; #[enum_def] @@ -248,7 +247,7 @@ assert_eq!( r#"SELECT "character" FROM "character""#, r#"WHERE ("size_w" + 1) * 2 = ("size_h" / 2) - 1"#, r#"AND "size_w" IN (SELECT ln(2.4 ^ 1.2))"#, - r#"AND (("character" LIKE 'D') AND ("character" LIKE 'E'))"#, + r#"AND ("character" LIKE 'D' AND "character" LIKE 'E')"#, ] .join(" ") ); @@ -558,7 +557,7 @@ assert_eq!( r#"CREATE TABLE IF NOT EXISTS "character" ("#, r#""id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,"#, r#""font_size" integer NOT NULL,"#, - r#""character" text NOT NULL,"#, + r#""character" varchar NOT NULL,"#, r#""size_w" integer NOT NULL,"#, r#""size_h" integer NOT NULL,"#, r#""font_id" integer DEFAULT NULL,"#, diff --git a/build-tools/cargo-publish.sh b/build-tools/publish.sh similarity index 100% rename from build-tools/cargo-publish.sh rename to build-tools/publish.sh diff --git a/build-tools/cargo-readme.sh b/build-tools/readme.sh similarity index 100% rename from build-tools/cargo-readme.sh rename to build-tools/readme.sh diff --git a/src/backend/index_builder.rs b/src/backend/index_builder.rs index 922978b..2738913 100644 --- a/src/backend/index_builder.rs +++ b/src/backend/index_builder.rs @@ -23,6 +23,8 @@ pub trait IndexBuilder: QuotedBuilder + TableRefBuilder { self.prepare_index_prefix(create, sql); self.prepare_index_columns(&create.index.columns, sql); + + self.prepare_filter(&create.r#where, sql); } /// Translate [`IndexCreateStatement`] into SQL statement. @@ -74,4 +76,8 @@ pub trait IndexBuilder: QuotedBuilder + TableRefBuilder { }); write!(sql, ")").unwrap(); } + + #[doc(hidden)] + // Write WHERE clause for partial index. This function is not available in MySQL. + fn prepare_filter(&self, _condition: &ConditionHolder, _sql: &mut dyn SqlWriter) {} } diff --git a/src/backend/mysql/query.rs b/src/backend/mysql/query.rs index 4d0700b..5812601 100644 --- a/src/backend/mysql/query.rs +++ b/src/backend/mysql/query.rs @@ -96,6 +96,32 @@ impl QueryBuilder for MysqlQueryBuilder { // MySQL doesn't support declaring ON CONFLICT target. } + fn prepare_on_conflict_action( + &self, + on_conflict_action: &Option, + sql: &mut dyn SqlWriter, + ) { + match on_conflict_action { + Some(OnConflictAction::DoNothing(pk_cols)) => { + if !pk_cols.is_empty() { + self.prepare_on_conflict_do_update_keywords(sql); + pk_cols.iter().fold(true, |first, pk_col| { + if !first { + write!(sql, ", ").unwrap() + } + pk_col.prepare(sql.as_writer(), self.quote()); + write!(sql, " = ").unwrap(); + pk_col.prepare(sql.as_writer(), self.quote()); + false + }); + } else { + write!(sql, " IGNORE").unwrap(); + } + } + _ => self.prepare_on_conflict_action_common(on_conflict_action, sql), + } + } + fn prepare_on_conflict_keywords(&self, sql: &mut dyn SqlWriter) { write!(sql, " ON DUPLICATE KEY").unwrap(); } diff --git a/src/backend/mysql/table.rs b/src/backend/mysql/table.rs index 881922f..0da5ef3 100644 --- a/src/backend/mysql/table.rs +++ b/src/backend/mysql/table.rs @@ -34,8 +34,9 @@ impl TableBuilder for MysqlQueryBuilder { None => "char".into(), }, ColumnType::String(length) => match length { - Some(length) => format!("varchar({length})"), - None => "varchar(255)".into(), + StringLen::N(length) => format!("varchar({length})"), + StringLen::None => "varchar(255)".into(), + StringLen::Max => "varchar(65535)".into(), }, ColumnType::Text => "text".into(), ColumnType::TinyInteger | ColumnType::TinyUnsigned => "tinyint".into(), @@ -53,28 +54,15 @@ impl TableBuilder for MysqlQueryBuilder { ColumnType::TimestampWithTimeZone => "timestamp".into(), ColumnType::Time => "time".into(), ColumnType::Date => "date".into(), - ColumnType::Year(length) => { - match length { - Some(length) => match length { - MySqlYear::Two => "year(2)".into(), - MySqlYear::Four => "year(4)".into(), - }, - None => "year".into(), - } - } + ColumnType::Year => "year".into(), ColumnType::Interval(_, _) => "unsupported".into(), - ColumnType::Binary(blob_size) => match blob_size { - BlobSize::Tiny => "tinyblob".into(), - BlobSize::Blob(length) => { - match length { - Some(length) => format!("binary({length})"), - None => "blob".into(), - } - } - BlobSize::Medium => "mediumblob".into(), - BlobSize::Long => "longblob".into(), + ColumnType::Binary(length) => format!("binary({length})"), + ColumnType::VarBinary(length) => match length { + StringLen::N(length) => format!("varbinary({length})"), + StringLen::None => "varbinary(255)".into(), + StringLen::Max => "varbinary(65535)".into(), }, - ColumnType::VarBinary(length) => format!("varbinary({length})"), + ColumnType::Blob => "blob".into(), ColumnType::Bit(length) => { match length { Some(length) => format!("bit({length})"), @@ -86,8 +74,8 @@ impl TableBuilder for MysqlQueryBuilder { } ColumnType::Boolean => "bool".into(), ColumnType::Money(precision) => match precision { - Some((precision, scale)) => format!("money({precision}, {scale})"), - None => "money".into(), + Some((precision, scale)) => format!("decimal({precision}, {scale})"), + None => "decimal".into(), }, ColumnType::Json => "json".into(), ColumnType::JsonBinary => "json".into(), @@ -102,6 +90,7 @@ impl TableBuilder for MysqlQueryBuilder { .join("', '") ), ColumnType::Array(_) => unimplemented!("Array is not available in MySQL."), + ColumnType::Vector(_) => unimplemented!("Vector is not available in MySQL."), ColumnType::Cidr => unimplemented!("Cidr is not available in MySQL."), ColumnType::Inet => unimplemented!("Inet is not available in MySQL."), ColumnType::MacAddr => unimplemented!("MacAddr is not available in MySQL."), @@ -165,7 +154,7 @@ impl TableBuilder for MysqlQueryBuilder { } TableAlterOption::DropForeignKey(name) => { let mut foreign_key = TableForeignKey::new(); - foreign_key.name(&name.to_string()); + foreign_key.name(name.to_string()); let drop = ForeignKeyDropStatement { foreign_key, table: None, diff --git a/src/backend/postgres/index.rs b/src/backend/postgres/index.rs index 3f1040e..0ff5b06 100644 --- a/src/backend/postgres/index.rs +++ b/src/backend/postgres/index.rs @@ -64,6 +64,7 @@ impl IndexBuilder for PostgresQueryBuilder { if create.nulls_not_distinct { write!(sql, " NULLS NOT DISTINCT").unwrap(); } + self.prepare_filter(&create.r#where, sql); } fn prepare_table_ref_index_stmt(&self, table_ref: &TableRef, sql: &mut dyn SqlWriter) { @@ -128,4 +129,8 @@ impl IndexBuilder for PostgresQueryBuilder { write!(sql, "UNIQUE ").unwrap(); } } + + fn prepare_filter(&self, condition: &ConditionHolder, sql: &mut dyn SqlWriter) { + self.prepare_condition(condition, "WHERE", sql); + } } diff --git a/src/backend/postgres/query.rs b/src/backend/postgres/query.rs index f7cce9d..f065011 100644 --- a/src/backend/postgres/query.rs +++ b/src/backend/postgres/query.rs @@ -90,6 +90,12 @@ impl QueryBuilder for PostgresQueryBuilder { PgBinOper::CastJsonField => "->>", PgBinOper::Regex => "~", PgBinOper::RegexCaseInsensitive => "~*", + #[cfg(feature = "postgres-vector")] + PgBinOper::EuclideanDistance => "<->", + #[cfg(feature = "postgres-vector")] + PgBinOper::NegativeInnerProduct => "<#>", + #[cfg(feature = "postgres-vector")] + PgBinOper::CosineDistance => "<=>", } ) .unwrap(), @@ -116,6 +122,8 @@ impl QueryBuilder for PostgresQueryBuilder { PgFunction::TsRankCd => "TS_RANK_CD", PgFunction::StartsWith => "STARTS_WITH", PgFunction::GenRandomUUID => "GEN_RANDOM_UUID", + PgFunction::JsonBuildObject => "JSON_BUILD_OBJECT", + PgFunction::JsonAgg => "JSON_AGG", #[cfg(feature = "postgres-array")] PgFunction::Any => "ANY", #[cfg(feature = "postgres-array")] diff --git a/src/backend/postgres/table.rs b/src/backend/postgres/table.rs index 11fc6a4..a1a3e2f 100644 --- a/src/backend/postgres/table.rs +++ b/src/backend/postgres/table.rs @@ -18,8 +18,8 @@ impl TableBuilder for PostgresQueryBuilder { None => "char".into(), }, ColumnType::String(length) => match length { - Some(length) => format!("varchar({length})"), - None => "varchar".into(), + StringLen::N(length) => format!("varchar({length})"), + _ => "varchar".into(), }, ColumnType::Text => "text".into(), ColumnType::TinyInteger | ColumnType::TinyUnsigned => "smallint".into(), @@ -47,11 +47,11 @@ impl TableBuilder for PostgresQueryBuilder { } typ } - ColumnType::Binary(_) => "bytea".into(), - ColumnType::VarBinary(length) => format!("bit varying({length})"), + ColumnType::Binary(_) | ColumnType::VarBinary(_) | ColumnType::Blob => + "bytea".into(), ColumnType::Bit(length) => { match length { - Some(length) => format!("varbit({length})"), + Some(length) => format!("bit({length})"), None => "bit".into(), } } @@ -71,12 +71,16 @@ impl TableBuilder for PostgresQueryBuilder { self.prepare_column_type(elem_type, &mut sql); format!("{sql}[]") } + ColumnType::Vector(size) => match size { + Some(size) => format!("vector({size})"), + None => "vector".into(), + }, ColumnType::Custom(iden) => iden.to_string(), ColumnType::Enum { name, .. } => name.to_string(), ColumnType::Cidr => "cidr".into(), ColumnType::Inet => "inet".into(), ColumnType::MacAddr => "macaddr".into(), - ColumnType::Year(_) => unimplemented!("Year is not available in Postgres."), + ColumnType::Year => unimplemented!("Year is not available in Postgres."), ColumnType::LTree => "ltree".into(), } ) @@ -192,7 +196,7 @@ impl TableBuilder for PostgresQueryBuilder { } TableAlterOption::DropForeignKey(name) => { let mut foreign_key = TableForeignKey::new(); - foreign_key.name(&name.to_string()); + foreign_key.name(name.to_string()); let drop = ForeignKeyDropStatement { foreign_key, table: None, diff --git a/src/backend/postgres/types.rs b/src/backend/postgres/types.rs index db5a43b..3ec126d 100644 --- a/src/backend/postgres/types.rs +++ b/src/backend/postgres/types.rs @@ -88,22 +88,27 @@ impl PostgresQueryBuilder { fn prepare_alter_type_opt(&self, opt: &TypeAlterOpt, sql: &mut dyn SqlWriter) { match opt { - TypeAlterOpt::Add(value, placement) => { + TypeAlterOpt::Add { + value, + placement, + if_not_exists, + } => { write!(sql, " ADD VALUE ").unwrap(); - match placement { - Some(add_option) => match add_option { + if *if_not_exists { + write!(sql, "IF NOT EXISTS ").unwrap(); + } + self.prepare_value(&value.to_string().into(), sql); + if let Some(add_option) = placement { + match add_option { TypeAlterAddOpt::Before(before_value) => { - self.prepare_value(&value.to_string().into(), sql); write!(sql, " BEFORE ").unwrap(); self.prepare_value(&before_value.to_string().into(), sql); } TypeAlterAddOpt::After(after_value) => { - self.prepare_value(&value.to_string().into(), sql); write!(sql, " AFTER ").unwrap(); self.prepare_value(&after_value.to_string().into(), sql); } - }, - None => self.prepare_value(&value.to_string().into(), sql), + } } } TypeAlterOpt::Rename(new_name) => { diff --git a/src/backend/query_builder.rs b/src/backend/query_builder.rs index a532cd2..41cac86 100644 --- a/src/backend/query_builder.rs +++ b/src/backend/query_builder.rs @@ -23,15 +23,15 @@ pub trait QueryBuilder: if let Some(table) = &insert.table { write!(sql, " INTO ").unwrap(); self.prepare_table_ref(table, sql); - write!(sql, " ").unwrap(); } - self.prepare_output(&insert.returning, sql); - if insert.default_values.is_some() && insert.columns.is_empty() && insert.source.is_none() { + self.prepare_output(&insert.returning, sql); + write!(sql, " ").unwrap(); let num_rows = insert.default_values.unwrap(); self.insert_default_values(num_rows, sql); } else { + write!(sql, " ").unwrap(); write!(sql, "(").unwrap(); insert.columns.iter().fold(true, |first, col| { if !first { @@ -42,6 +42,8 @@ pub trait QueryBuilder: }); write!(sql, ")").unwrap(); + self.prepare_output(&insert.returning, sql); + if let Some(source) = &insert.source { write!(sql, " ").unwrap(); match source { @@ -469,12 +471,9 @@ pub trait QueryBuilder: None => {} }; - match &select_expr.alias { - Some(alias) => { - write!(sql, " AS ").unwrap(); - alias.prepare(sql.as_writer(), self.quote()); - } - None => {} + if let Some(alias) = &select_expr.alias { + write!(sql, " AS ").unwrap(); + alias.prepare(sql.as_writer(), self.quote()); }; } @@ -590,6 +589,8 @@ pub trait QueryBuilder: BinOper::As => "AS", BinOper::Escape => "ESCAPE", BinOper::Custom(raw) => raw, + BinOper::BitAnd => "&", + BinOper::BitOr => "|", #[allow(unreachable_patterns)] _ => unimplemented!(), } @@ -665,6 +666,8 @@ pub trait QueryBuilder: Function::Coalesce => "COALESCE", Function::Count => "COUNT", Function::IfNull => self.if_null_function(), + Function::Greatest => self.greatest_function(), + Function::Least => self.least_function(), Function::CharLength => self.char_length_function(), Function::Cast => "CAST", Function::Lower => "LOWER", @@ -674,6 +677,7 @@ pub trait QueryBuilder: Function::Custom(_) => "", Function::Random => self.random_function(), Function::Round => "ROUND", + Function::Md5 => "MD5", #[cfg(feature = "backend-postgres")] Function::PgFunction(_) => unimplemented!(), } @@ -774,15 +778,6 @@ pub trait QueryBuilder: "Cannot build a with query that has no common table expression!" ); - if with_clause.recursive { - assert_eq!( - with_clause.cte_expressions.len(), - 1, - "Cannot build a recursive query with more than one common table! \ - A recursive with query must have a single cte inside it that has a union query of \ - two queries!" - ); - } for cte in &with_clause.cte_expressions { if !cte_first { write!(sql, ", ").unwrap(); @@ -882,7 +877,6 @@ pub trait QueryBuilder: JoinType::LeftJoin => "LEFT JOIN", JoinType::RightJoin => "RIGHT JOIN", JoinType::FullOuterJoin => "FULL OUTER JOIN", - JoinType::StraightJoin => "STRAIGHT_JOIN", } ) .unwrap() @@ -990,6 +984,10 @@ pub trait QueryBuilder: /// Convert a SQL value into syntax-specific string fn value_to_string(&self, v: &Value) -> String { + self.value_to_string_common(v) + } + + fn value_to_string_common(&self, v: &Value) -> String { let mut s = String::new(); match v { Value::Bool(None) @@ -1040,6 +1038,8 @@ pub trait QueryBuilder: Value::MacAddress(None) => write!(s, "NULL").unwrap(), #[cfg(feature = "postgres-array")] Value::Array(_, None) => write!(s, "NULL").unwrap(), + #[cfg(feature = "postgres-vector")] + Value::Vector(None) => write!(s, "NULL").unwrap(), Value::Bool(Some(b)) => write!(s, "{}", if *b { "TRUE" } else { "FALSE" }).unwrap(), Value::TinyInt(Some(v)) => write!(s, "{v}").unwrap(), Value::SmallInt(Some(v)) => write!(s, "{v}").unwrap(), @@ -1113,6 +1113,17 @@ pub trait QueryBuilder: .join(",") ) .unwrap(), + #[cfg(feature = "postgres-vector")] + Value::Vector(Some(v)) => { + write!(s, "'[").unwrap(); + for (i, &element) in v.as_slice().iter().enumerate() { + if i != 0 { + write!(s, ",").unwrap(); + } + write!(s, "{element}").unwrap(); + } + write!(s, "]'").unwrap(); + } #[cfg(feature = "with-ipnetwork")] Value::IpNetwork(Some(v)) => write!(s, "'{v}'").unwrap(), #[cfg(feature = "with-mac_address")] @@ -1169,10 +1180,18 @@ pub trait QueryBuilder: &self, on_conflict_action: &Option, sql: &mut dyn SqlWriter, + ) { + self.prepare_on_conflict_action_common(on_conflict_action, sql); + } + + fn prepare_on_conflict_action_common( + &self, + on_conflict_action: &Option, + sql: &mut dyn SqlWriter, ) { if let Some(action) = on_conflict_action { match action { - OnConflictAction::DoNothing => { + OnConflictAction::DoNothing(_) => { write!(sql, " DO NOTHING").unwrap(); } OnConflictAction::Update(update_strats) => { @@ -1442,6 +1461,18 @@ pub trait QueryBuilder: "IFNULL" } + #[doc(hidden)] + /// The name of the function that represents the "greatest" function. + fn greatest_function(&self) -> &str { + "GREATEST" + } + + #[doc(hidden)] + /// The name of the function that represents the "least" function. + fn least_function(&self) -> &str { + "LEAST" + } + #[doc(hidden)] /// The name of the function that returns the char length. fn char_length_function(&self) -> &str { diff --git a/src/backend/sqlite/index.rs b/src/backend/sqlite/index.rs index 183a0ef..b549b0d 100644 --- a/src/backend/sqlite/index.rs +++ b/src/backend/sqlite/index.rs @@ -32,6 +32,7 @@ impl IndexBuilder for SqliteQueryBuilder { write!(sql, " ").unwrap(); self.prepare_index_columns(&create.index.columns, sql); + self.prepare_filter(&create.r#where, sql); } fn prepare_table_ref_index_stmt(&self, table_ref: &TableRef, sql: &mut dyn SqlWriter) { @@ -69,4 +70,8 @@ impl IndexBuilder for SqliteQueryBuilder { } fn write_column_index_prefix(&self, _col_prefix: &Option, _sql: &mut dyn SqlWriter) {} + + fn prepare_filter(&self, condition: &ConditionHolder, sql: &mut dyn SqlWriter) { + self.prepare_condition(condition, "WHERE", sql); + } } diff --git a/src/backend/sqlite/query.rs b/src/backend/sqlite/query.rs index ece86a7..0a06229 100644 --- a/src/backend/sqlite/query.rs +++ b/src/backend/sqlite/query.rs @@ -76,6 +76,14 @@ impl QueryBuilder for SqliteQueryBuilder { sql.push_param(value.clone(), self as _); } + fn greatest_function(&self) -> &str { + "MAX" + } + + fn least_function(&self) -> &str { + "MIN" + } + fn char_length_function(&self) -> &str { "LENGTH" } diff --git a/src/backend/sqlite/table.rs b/src/backend/sqlite/table.rs index 3a5a327..429a457 100644 --- a/src/backend/sqlite/table.rs +++ b/src/backend/sqlite/table.rs @@ -47,7 +47,7 @@ impl TableBuilder for SqliteQueryBuilder { } fn prepare_table_drop_opt(&self, _drop_opt: &TableDropOpt, _sql: &mut dyn SqlWriter) { - // SQLite does not support table drop options + // Sqlite does not support table drop options } fn prepare_table_truncate_statement( @@ -131,58 +131,64 @@ impl SqliteQueryBuilder { "{}", match column_type { ColumnType::Char(length) => match length { - Some(length) => format!("text({length})"), - None => "text".into(), + Some(length) => format!("char({length})"), + None => "char".into(), }, ColumnType::String(length) => match length { - Some(length) => format!("text({length})"), - None => "text".into(), + StringLen::N(length) => format!("varchar({length})"), + _ => "varchar".into(), }, ColumnType::Text => "text".into(), - ColumnType::TinyInteger | ColumnType::TinyUnsigned => "integer".into(), - ColumnType::SmallInteger | ColumnType::SmallUnsigned => "integer".into(), + ColumnType::TinyInteger | ColumnType::TinyUnsigned => integer("tinyint").into(), + ColumnType::SmallInteger | ColumnType::SmallUnsigned => integer("smallint").into(), ColumnType::Integer | ColumnType::Unsigned => "integer".into(), #[allow(clippy::if_same_then_else)] ColumnType::BigInteger | ColumnType::BigUnsigned => if is_auto_increment { "integer" - } else if cfg!(feature = "option-sqlite-exact-column-type") { - "integer" } else { - "bigint" + integer("bigint") } .into(), - ColumnType::Float => "real".into(), - ColumnType::Double => "real".into(), + ColumnType::Float => "float".into(), + ColumnType::Double => "double".into(), ColumnType::Decimal(precision) => match precision { - Some((precision, scale)) => format!("real({precision}, {scale})"), + Some((precision, scale)) => { + if precision > &16 { + panic!("precision cannot be larger than 16"); + } + format!("real({precision}, {scale})") + } None => "real".into(), }, - ColumnType::DateTime => "text".into(), - ColumnType::Timestamp => "text".into(), - ColumnType::TimestampWithTimeZone => "text".into(), - ColumnType::Time => "text".into(), - ColumnType::Date => "text".into(), - ColumnType::Interval(_, _) => "unsupported".into(), - ColumnType::Binary(blob_size) => match blob_size { - BlobSize::Blob(Some(length)) => format!("binary({length})"), - _ => "blob".into(), + ColumnType::DateTime => "datetime_text".into(), + ColumnType::Timestamp => "timestamp_text".into(), + ColumnType::TimestampWithTimeZone => "timestamp_with_timezone_text".into(), + ColumnType::Time => "time_text".into(), + ColumnType::Date => "date_text".into(), + ColumnType::Interval(_, _) => + unimplemented!("Interval is not available in Sqlite."), + ColumnType::Binary(length) => format!("blob({length})"), + ColumnType::VarBinary(length) => match length { + StringLen::N(length) => format!("varbinary_blob({length})"), + _ => "varbinary_blob".into(), }, - ColumnType::VarBinary(length) => format!("binary({length})"), + ColumnType::Blob => "blob".into(), ColumnType::Boolean => "boolean".into(), ColumnType::Money(precision) => match precision { - Some((precision, scale)) => format!("integer({precision}, {scale})"), - None => "integer".into(), + Some((precision, scale)) => format!("real_money({precision}, {scale})"), + None => "real_money".into(), }, - ColumnType::Json => "text".into(), - ColumnType::JsonBinary => "text".into(), - ColumnType::Uuid => "text(36)".into(), + ColumnType::Json => "json_text".into(), + ColumnType::JsonBinary => "jsonb_text".into(), + ColumnType::Uuid => "uuid_text".into(), ColumnType::Custom(iden) => iden.to_string(), - ColumnType::Enum { .. } => "text".into(), + ColumnType::Enum { .. } => "enum_text".into(), ColumnType::Array(_) => unimplemented!("Array is not available in Sqlite."), + ColumnType::Vector(_) => unimplemented!("Vector is not available in Sqlite."), ColumnType::Cidr => unimplemented!("Cidr is not available in Sqlite."), ColumnType::Inet => unimplemented!("Inet is not available in Sqlite."), ColumnType::MacAddr => unimplemented!("MacAddr is not available in Sqlite."), - ColumnType::Year(_) => unimplemented!("Year is not available in Sqlite."), + ColumnType::Year => unimplemented!("Year is not available in Sqlite."), ColumnType::Bit(_) => unimplemented!("Bit is not available in Sqlite."), ColumnType::VarBit(_) => unimplemented!("VarBit is not available in Sqlite."), ColumnType::LTree => unimplemented!("LTree is not available in Sqlite."), @@ -191,3 +197,11 @@ impl SqliteQueryBuilder { .unwrap() } } + +fn integer(ty: &str) -> &str { + if cfg!(feature = "option-sqlite-exact-column-type") { + "integer" + } else { + ty + } +} diff --git a/src/expr.rs b/src/expr.rs index 74311ff..d783397 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -37,52 +37,1375 @@ pub enum SimpleExpr { Constant(Value), } -pub(crate) mod private { - use crate::{BinOper, LikeExpr, SimpleExpr, UnOper}; - - pub trait Expression: Sized { - fn un_op(self, o: UnOper) -> SimpleExpr; - - fn bin_op(self, op: O, right: T) -> SimpleExpr - where - O: Into, - T: Into; - - fn like_like(self, op: O, like: LikeExpr) -> SimpleExpr - where - O: Into, - { - self.bin_op( - op, - match like.escape { - Some(escape) => SimpleExpr::Binary( - Box::new(like.pattern.into()), - BinOper::Escape, - Box::new(SimpleExpr::Constant(escape.into())), - ), - None => like.pattern.into(), - }, - ) - } +/// "Operator" methods for building complex expressions. +/// +/// ## Examples +/// +/// ```no_run +/// use sea_query::*; +/// +/// let expr = 1_i32.cast_as(Alias::new("REAL")); +/// +/// let expr = Func::char_length("abc").eq(3_i32); +/// +/// let expr = Expr::current_date() +/// .cast_as(Alias::new("TEXT")) +/// .like("2024%"); +/// ``` +pub trait ExprTrait: Sized { + /// Express an arithmetic addition operation. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(1.add(1).eq(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 + 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 + 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 + 1 = 2"# + /// ); + /// ``` + fn add(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::Add, right) + } + + /// Express a `AS enum` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::insert() + /// .into_table(Char::Table) + /// .columns([Char::FontSize]) + /// .values_panic(["large".as_enum(Alias::new("FontSizeEnum"))]) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"INSERT INTO `character` (`font_size`) VALUES ('large')"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"INSERT INTO "character" ("font_size") VALUES (CAST('large' AS FontSizeEnum))"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"INSERT INTO "character" ("font_size") VALUES ('large')"# + /// ); + /// ``` + #[allow(clippy::wrong_self_convention)] + fn as_enum(self, type_name: N) -> SimpleExpr + where + N: IntoIden; + + fn and(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::And, right) + } + + /// Express a `BETWEEN` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::SizeW).into_column_ref().between(1, 10)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` BETWEEN 1 AND 10"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" BETWEEN 1 AND 10"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" BETWEEN 1 AND 10"# + /// ); + /// ``` + fn between(self, a: A, b: B) -> SimpleExpr + where + A: Into, + B: Into, + { + self.binary( + BinOper::Between, + SimpleExpr::Binary(Box::new(a.into()), BinOper::And, Box::new(b.into())), + ) + } + + /// Create any binary operation + /// + /// # Examples + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .cond_where(all![ + /// Char::SizeW.into_column_ref().binary(BinOper::SmallerThan, 10), + /// Char::SizeW.into_column_ref().binary(BinOper::GreaterThan, Char::SizeH.into_column_ref()) + /// ]) + /// .to_owned(); + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` < 10 AND `size_w` > `size_h`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w" < 10 AND "size_w" > "size_h""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w" < 10 AND "size_w" > "size_h""# + /// ); + /// ``` + fn binary(self, op: O, right: R) -> SimpleExpr + where + O: Into, + R: Into; + + /// Express a `CAST AS` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .expr("1".cast_as(Alias::new("integer"))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT CAST('1' AS integer)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT CAST('1' AS integer)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT CAST('1' AS integer)"# + /// ); + /// ``` + fn cast_as(self, type_name: N) -> SimpleExpr + where + N: IntoIden; + + /// Express an arithmetic division operation. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(1.div(1).eq(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 / 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 / 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 / 1 = 2"# + /// ); + /// ``` + fn div(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::Div, right) + } + + /// Express an equal (`=`) expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// // Sometimes, you'll have to qualify the call because of conflicting std traits. + /// .and_where(ExprTrait::eq("What!", "Nothing")) + /// .and_where(Char::Id.into_column_ref().eq(1)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 'What!' = 'Nothing' AND `id` = 1"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 'What!' = 'Nothing' AND "id" = 1"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 'What!' = 'Nothing' AND "id" = 1"# + /// ); + /// ``` + fn eq(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::Equal, right) + } + + /// Express a equal expression between two table columns, + /// you will mainly use this to relate identical value between two table columns. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::FontId).into_column_ref().equals((Font::Table, Font::Id))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`font_id` = `font`.`id`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."font_id" = "font"."id""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."font_id" = "font"."id""# + /// ); + /// ``` + fn equals(self, col: C) -> SimpleExpr + where + C: IntoColumnRef, + { + self.binary(BinOper::Equal, col.into_column_ref()) + } + + /// Express a greater than (`>`) expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::SizeW).into_column_ref().gt(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` > 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" > 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" > 2"# + /// ); + /// ``` + fn gt(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::GreaterThan, right) + } + + /// Express a greater than or equal (`>=`) expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::SizeW).into_column_ref().gte(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` >= 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" >= 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" >= 2"# + /// ); + /// ``` + fn gte(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::GreaterThanOrEqual, right) + } + + /// Express a `IN` sub-query expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Char::SizeW.into_column_ref().in_subquery( + /// Query::select() + /// .expr(Expr::cust("3 + 2 * 2")) + /// .take() + /// )) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` IN (SELECT 3 + 2 * 2)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w" IN (SELECT 3 + 2 * 2)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w" IN (SELECT 3 + 2 * 2)"# + /// ); + /// ``` + fn in_subquery(self, sel: SelectStatement) -> SimpleExpr { + self.binary( + BinOper::In, + SimpleExpr::SubQuery(None, Box::new(sel.into_sub_query_statement())), + ) + } + + /// Express a `IN` sub expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::FontId]) + /// .from(Char::Table) + /// .and_where( + /// ExprTrait::in_tuples( + /// Expr::tuple([ + /// Expr::col(Char::Character).into(), + /// Expr::col(Char::FontId).into(), + /// ]), + /// [(1, String::from("1")), (2, String::from("2"))] + /// ) + /// ) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `font_id` FROM `character` WHERE (`character`, `font_id`) IN ((1, '1'), (2, '2'))"# + /// ); + /// + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "font_id" FROM "character" WHERE ("character", "font_id") IN ((1, '1'), (2, '2'))"# + /// ); + /// + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "font_id" FROM "character" WHERE ("character", "font_id") IN ((1, '1'), (2, '2'))"# + /// ); + /// ``` + fn in_tuples(self, v: I) -> SimpleExpr + where + V: IntoValueTuple, + I: IntoIterator, + { + self.binary( + BinOper::In, + SimpleExpr::Tuple( + v.into_iter() + .map(|m| SimpleExpr::Values(m.into_value_tuple().into_iter().collect())) + .collect(), + ), + ) + } + + /// Express a `IS` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::Ascii).into_column_ref().is(true)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`ascii` IS TRUE"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."ascii" IS TRUE"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."ascii" IS TRUE"# + /// ); + /// ``` + fn is(self, right: R) -> SimpleExpr + where + R: Into, + { + self.binary(BinOper::Is, right) + } + + /// Express a `IN` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Id]) + /// .from(Char::Table) + /// .and_where( + /// (Char::Table, Char::SizeW) + /// .into_column_ref() + /// .is_in([1, 2, 3]), + /// ) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `id` FROM `character` WHERE `character`.`size_w` IN (1, 2, 3)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "id" FROM "character" WHERE "character"."size_w" IN (1, 2, 3)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "id" FROM "character" WHERE "character"."size_w" IN (1, 2, 3)"# + /// ); + /// ``` + /// Empty value list + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Id]) + /// .from(Char::Table) + /// .and_where( + /// (Char::Table, Char::SizeW) + /// .into_column_ref() + /// .is_in(Vec::::new()), + /// ) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `id` FROM `character` WHERE 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "id" FROM "character" WHERE 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "id" FROM "character" WHERE 1 = 2"# + /// ); + /// ``` + #[allow(clippy::wrong_self_convention)] + fn is_in(self, v: I) -> SimpleExpr + where + V: Into, + I: IntoIterator, + { + self.binary( + BinOper::In, + SimpleExpr::Tuple(v.into_iter().map(|v| v.into()).collect()), + ) + } + + /// Express a `IS NOT` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::Ascii).into_column_ref().is_not(true)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`ascii` IS NOT TRUE"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."ascii" IS NOT TRUE"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."ascii" IS NOT TRUE"# + /// ); + /// ``` + #[allow(clippy::wrong_self_convention)] + fn is_not(self, right: R) -> SimpleExpr + where + R: Into, + { + self.binary(BinOper::IsNot, right) + } + + /// Express a `NOT IN` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Id]) + /// .from(Char::Table) + /// .and_where( + /// (Char::Table, Char::SizeW) + /// .into_column_ref() + /// .is_not_in([1, 2, 3]), + /// ) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `id` FROM `character` WHERE `character`.`size_w` NOT IN (1, 2, 3)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "id" FROM "character" WHERE "character"."size_w" NOT IN (1, 2, 3)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "id" FROM "character" WHERE "character"."size_w" NOT IN (1, 2, 3)"# + /// ); + /// ``` + /// Empty value list + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Id]) + /// .from(Char::Table) + /// .and_where( + /// (Char::Table, Char::SizeW) + /// .into_column_ref() + /// .is_not_in(Vec::::new()), + /// ) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `id` FROM `character` WHERE 1 = 1"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "id" FROM "character" WHERE 1 = 1"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "id" FROM "character" WHERE 1 = 1"# + /// ); + /// ``` + #[allow(clippy::wrong_self_convention)] + fn is_not_in(self, v: I) -> SimpleExpr + where + V: Into, + I: IntoIterator, + { + self.binary( + BinOper::NotIn, + SimpleExpr::Tuple(v.into_iter().map(|v| v.into()).collect()), + ) + } + + /// Express a `IS NOT NULL` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::SizeW).into_column_ref().is_not_null()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` IS NOT NULL"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" IS NOT NULL"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" IS NOT NULL"# + /// ); + /// ``` + #[allow(clippy::wrong_self_convention)] + fn is_not_null(self) -> SimpleExpr { + self.binary(BinOper::IsNot, Keyword::Null) + } + + /// Express a `IS NULL` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::SizeW).into_column_ref().is_null()) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` IS NULL"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" IS NULL"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" IS NULL"# + /// ); + /// ``` + #[allow(clippy::wrong_self_convention)] + fn is_null(self) -> SimpleExpr { + self.binary(BinOper::Is, Keyword::Null) + } + + /// Express a bitwise left shift. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(1.left_shift(1).eq(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 << 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 << 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 << 1 = 2"# + /// ); + /// ``` + fn left_shift(self, right: R) -> SimpleExpr + where + R: Into, + { + self.binary(BinOper::LShift, right) + } + + /// Express a `LIKE` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::Character).into_column_ref().like("Ours'%")) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`character` LIKE 'Ours\'%'"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."character" LIKE E'Ours\'%'"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."character" LIKE 'Ours''%'"# + /// ); + /// ``` + /// + /// Like with ESCAPE + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::Character).into_column_ref().like(LikeExpr::new(r"|_Our|_").escape('|'))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`character` LIKE '|_Our|_' ESCAPE '|'"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."character" LIKE '|_Our|_' ESCAPE '|'"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."character" LIKE '|_Our|_' ESCAPE '|'"# + /// ); + /// ``` + fn like(self, like: L) -> SimpleExpr + where + L: IntoLikeExpr, + { + ExprTrait::binary(self, BinOper::Like, like.into_like_expr()) + } + + /// Express a less than (`<`) expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::SizeW).into_column_ref().lt(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` < 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" < 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" < 2"# + /// ); + /// ``` + fn lt(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::SmallerThan, right) + } + + /// Express a less than or equal (`<=`) expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::SizeW).into_column_ref().lte(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` <= 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" <= 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" <= 2"# + /// ); + /// ``` + fn lte(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::SmallerThanOrEqual, right) + } + + /// Express an arithmetic modulo operation. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(1.modulo(1).eq(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 % 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 % 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 % 1 = 2"# + /// ); + /// ``` + fn modulo(self, right: R) -> SimpleExpr + where + R: Into, + { + self.binary(BinOper::Mod, right) + } + + /// Express an arithmetic multiplication operation. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(1.mul(1).eq(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 * 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 * 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 * 1 = 2"# + /// ); + /// ``` + fn mul(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::Mul, right) + } + + /// Express a not equal (`<>`) expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// // Sometimes, you'll have to qualify the call because of conflicting std traits. + /// .and_where(ExprTrait::ne("Morning", "Good")) + /// .and_where(Char::Id.into_column_ref().ne(1)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 'Morning' <> 'Good' AND `id` <> 1"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 'Morning' <> 'Good' AND "id" <> 1"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 'Morning' <> 'Good' AND "id" <> 1"# + /// ); + /// ``` + fn ne(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::NotEqual, right) + } + + /// Negates an expression with `NOT`. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(ExprTrait::not(Expr::col((Char::Table, Char::SizeW)).is_null())) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE NOT `character`.`size_w` IS NULL"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE NOT "character"."size_w" IS NULL"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE NOT "character"."size_w" IS NULL"# + /// ); + /// ``` + fn not(self) -> SimpleExpr { + self.unary(UnOper::Not) + } + + /// Express a `NOT BETWEEN` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::SizeW).into_column_ref().not_between(1, 10)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`size_w` NOT BETWEEN 1 AND 10"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" NOT BETWEEN 1 AND 10"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."size_w" NOT BETWEEN 1 AND 10"# + /// ); + /// ``` + fn not_between(self, a: A, b: B) -> SimpleExpr + where + A: Into, + B: Into, + { + self.binary( + BinOper::NotBetween, + SimpleExpr::Binary(Box::new(a.into()), BinOper::And, Box::new(b.into())), + ) + } + + /// Express a not equal expression between two table columns, + /// you will mainly use this to relate identical value between two table columns. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::FontId).into_column_ref().not_equals((Font::Table, Font::Id))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`font_id` <> `font`.`id`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."font_id" <> "font"."id""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."font_id" <> "font"."id""# + /// ); + /// ``` + fn not_equals(self, col: C) -> SimpleExpr + where + C: IntoColumnRef, + { + self.binary(BinOper::NotEqual, col.into_column_ref()) + } + + /// Express a `NOT IN` sub-query expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Char::SizeW.into_column_ref().not_in_subquery( + /// Query::select() + /// .expr(Expr::cust("3 + 2 * 2")) + /// .take() + /// )) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `size_w` NOT IN (SELECT 3 + 2 * 2)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w" NOT IN (SELECT 3 + 2 * 2)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "size_w" NOT IN (SELECT 3 + 2 * 2)"# + /// ); + /// ``` + fn not_in_subquery(self, sel: SelectStatement) -> SimpleExpr { + self.binary( + BinOper::NotIn, + SimpleExpr::SubQuery(None, Box::new(sel.into_sub_query_statement())), + ) + } + + /// Express a `NOT LIKE` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where((Char::Table, Char::Character).into_column_ref().not_like("Ours'%")) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`character` NOT LIKE 'Ours\'%'"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."character" NOT LIKE E'Ours\'%'"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."character" NOT LIKE 'Ours''%'"# + /// ); + /// ``` + fn not_like(self, like: L) -> SimpleExpr + where + L: IntoLikeExpr, + { + ExprTrait::binary(self, BinOper::NotLike, like.into_like_expr()) + } + + /// Express a logical `OR` operation. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(false.or(true)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE FALSE OR TRUE"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE FALSE OR TRUE"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE FALSE OR TRUE"# + /// ); + /// ``` + fn or(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::Or, right) + } + + /// Express a bitwise right shift. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(1.right_shift(1).eq(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 >> 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 >> 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 >> 1 = 2"# + /// ); + /// ``` + fn right_shift(self, right: R) -> SimpleExpr + where + R: Into, + { + self.binary(BinOper::RShift, right) + } + + /// Express an arithmetic subtraction operation. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(1.sub(1).eq(2)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE 1 - 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 - 1 = 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE 1 - 1 = 2"# + /// ); + /// ``` + fn sub(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::Sub, right) + } + + /// Apply any unary operator to the expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(Expr::col((Char::Table, Char::SizeW)).is_null().unary(UnOper::Not)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE NOT `character`.`size_w` IS NULL"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE NOT "character"."size_w" IS NULL"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE NOT "character"."size_w" IS NULL"# + /// ); + /// ``` + fn unary(self, o: UnOper) -> SimpleExpr; + + /// Express a bitwise AND operation. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .expr(1.bit_and(2).eq(3)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT (1 & 2) = 3"# + /// ); + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(1.bit_and(1).eq(1)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE (1 & 1) = 1"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE (1 & 1) = 1"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE (1 & 1) = 1"# + /// ); + /// ``` + fn bit_and(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::BitAnd, right) } -} -use private::Expression; + /// Express a bitwise OR operation. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .columns([Char::Character, Char::SizeW, Char::SizeH]) + /// .from(Char::Table) + /// .and_where(1.bit_or(1).eq(1)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE (1 | 1) = 1"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE (1 | 1) = 1"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE (1 | 1) = 1"# + /// ); + /// ``` + fn bit_or(self, right: R) -> SimpleExpr + where + R: Into, + { + ExprTrait::binary(self, BinOper::BitOr, right) + } +} -impl Expression for Expr { - fn un_op(mut self, o: UnOper) -> SimpleExpr { - self.uopr = Some(o); - self.into() +/// This generic implementation covers all expression types, +/// including [ColumnRef], [Value], [FunctionCall], [SimpleExpr]... +impl ExprTrait for T +where + T: Into, +{ + fn as_enum(self, type_name: N) -> SimpleExpr + where + N: IntoIden, + { + SimpleExpr::AsEnum(type_name.into_iden(), Box::new(self.into())) } - fn bin_op(mut self, op: O, right: T) -> SimpleExpr + fn binary(self, op: O, right: R) -> SimpleExpr where O: Into, - T: Into, + R: Into, + { + SimpleExpr::Binary(Box::new(self.into()), op.into(), Box::new(right.into())) + } + + fn cast_as(self, type_name: N) -> SimpleExpr + where + N: IntoIden, { - self.bopr = Some(op.into()); - self.right = Some(right.into()); - self.into() + SimpleExpr::FunctionCall(Func::cast_as(self, type_name)) + } + + fn unary(self, op: UnOper) -> SimpleExpr { + SimpleExpr::Unary(op, Box::new(self.into())) } } @@ -267,6 +1590,29 @@ impl Expr { /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE IFNULL("size_w", 0) > 2"# /// ); /// ``` + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .column(Char::Character) + /// .from(Char::Table) + /// .and_where(Expr::expr(Func::lower(Expr::col(Char::Character))).is_in(["a", "b"])) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character` FROM `character` WHERE LOWER(`character`) IN ('a', 'b')"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character" FROM "character" WHERE LOWER("character") IN ('a', 'b')"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character" FROM "character" WHERE LOWER("character") IN ('a', 'b')"# + /// ); + /// ``` #[allow(clippy::self_named_constructors)] pub fn expr(expr: T) -> Self where @@ -480,6 +1826,8 @@ impl Expr { /// Express an equal (`=`) expression. /// + /// This is equivalent to a newer [ExprTrait::eq] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -509,11 +1857,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::Equal, v) + ExprTrait::eq(self, v) } /// Express a not equal (`<>`) expression. /// + /// This is equivalent to a newer [ExprTrait::ne] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -543,12 +1893,14 @@ impl Expr { where V: Into, { - self.binary(BinOper::NotEqual, v) + ExprTrait::ne(self, v) } /// Express a equal expression between two table columns, /// you will mainly use this to relate identical value between two table columns. /// + /// This is equivalent to a newer [ExprTrait::equals] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -577,12 +1929,14 @@ impl Expr { where C: IntoColumnRef, { - self.binary(BinOper::Equal, col.into_column_ref()) + ExprTrait::equals(self, col) } /// Express a not equal expression between two table columns, /// you will mainly use this to relate identical value between two table columns. /// + /// This is equivalent to a newer [ExprTrait::not_equals] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -591,31 +1945,33 @@ impl Expr { /// let query = Query::select() /// .columns([Char::Character, Char::SizeW, Char::SizeH]) /// .from(Char::Table) - /// .and_where(Expr::col((Char::Table, Char::FontId)).equals((Font::Table, Font::Id))) + /// .and_where(Expr::col((Char::Table, Char::FontId)).not_equals((Font::Table, Font::Id))) /// .to_owned(); /// /// assert_eq!( /// query.to_string(MysqlQueryBuilder), - /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`font_id` = `font`.`id`"# + /// r#"SELECT `character`, `size_w`, `size_h` FROM `character` WHERE `character`.`font_id` <> `font`.`id`"# /// ); /// assert_eq!( /// query.to_string(PostgresQueryBuilder), - /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."font_id" = "font"."id""# + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."font_id" <> "font"."id""# /// ); /// assert_eq!( /// query.to_string(SqliteQueryBuilder), - /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."font_id" = "font"."id""# + /// r#"SELECT "character", "size_w", "size_h" FROM "character" WHERE "character"."font_id" <> "font"."id""# /// ); /// ``` pub fn not_equals(self, col: C) -> SimpleExpr where C: IntoColumnRef, { - self.binary(BinOper::NotEqual, col.into_column_ref()) + ExprTrait::not_equals(self, col) } /// Express a greater than (`>`) expression. /// + /// This is equivalent to a newer [ExprTrait::gt] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -644,11 +2000,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::GreaterThan, v.into()) + ExprTrait::gt(self, v) } /// Express a greater than or equal (`>=`) expression. /// + /// This is equivalent to a newer [ExprTrait::gte] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -677,11 +2035,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::GreaterThanOrEqual, v) + ExprTrait::gte(self, v) } /// Express a less than (`<`) expression. /// + /// This is equivalent to a newer [ExprTrait::lt] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -710,11 +2070,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::SmallerThan, v) + ExprTrait::lt(self, v) } /// Express a less than or equal (`<=`) expression. /// + /// This is equivalent to a newer [ExprTrait::lte] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -743,11 +2105,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::SmallerThanOrEqual, v) + ExprTrait::lte(self, v) } /// Express an arithmetic addition operation. /// + /// This is equivalent to a newer [ExprTrait::add] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -777,11 +2141,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::Add, v) + ExprTrait::add(self, v) } /// Express an arithmetic subtraction operation. /// + /// This is equivalent to a newer [ExprTrait::sub] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -811,11 +2177,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::Sub, v) + ExprTrait::sub(self, v) } /// Express an arithmetic multiplication operation. /// + /// This is equivalent to a newer [ExprTrait::mul] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -845,11 +2213,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::Mul, v) + ExprTrait::mul(self, v) } /// Express an arithmetic division operation. /// + /// This is equivalent to a newer [ExprTrait::div] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -879,11 +2249,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::Div, v) + ExprTrait::div(self, v) } /// Express an arithmetic modulo operation. /// + /// This is equivalent to a newer [ExprTrait::modulo] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -913,11 +2285,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::Mod, v) + ExprTrait::modulo(self, v) } /// Express a bitwise left shift. /// + /// This is equivalent to a newer [ExprTrait::left_shift] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -947,11 +2321,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::LShift, v) + ExprTrait::left_shift(self, v) } /// Express a bitwise right shift. /// + /// This is equivalent to a newer [ExprTrait::right_shift] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -981,11 +2357,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::RShift, v) + ExprTrait::right_shift(self, v) } /// Express a `BETWEEN` expression. /// + /// This is equivalent to a newer [ExprTrait::between] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1014,11 +2392,13 @@ impl Expr { where V: Into, { - self.between_or_not_between(BinOper::Between, a, b) + ExprTrait::between(self, a, b) } /// Express a `NOT BETWEEN` expression. /// + /// This is equivalent to a newer [ExprTrait::not_between] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1047,21 +2427,13 @@ impl Expr { where V: Into, { - self.between_or_not_between(BinOper::NotBetween, a, b) - } - - fn between_or_not_between(self, op: BinOper, a: V, b: V) -> SimpleExpr - where - V: Into, - { - self.binary( - op, - SimpleExpr::Binary(Box::new(a.into()), BinOper::And, Box::new(b.into())), - ) + ExprTrait::not_between(self, a, b) } /// Express a `LIKE` expression. /// + /// This is equivalent to a newer [ExprTrait::like] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1112,16 +2484,20 @@ impl Expr { /// ); /// ``` pub fn like(self, like: L) -> SimpleExpr { - self.like_like(BinOper::Like, like.into_like_expr()) + ExprTrait::like(self, like) } - /// Express a `NOT LIKE` expression + /// Express a `NOT LIKE` expression. + /// + /// This is equivalent to a newer [ExprTrait::not_like] and may require more some wrapping beforehand. pub fn not_like(self, like: L) -> SimpleExpr { - self.like_like(BinOper::NotLike, like.into_like_expr()) + ExprTrait::not_like(self, like) } /// Express a `IS NULL` expression. /// + /// This is equivalent to a newer [ExprTrait::is_null] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1148,11 +2524,13 @@ impl Expr { /// ``` #[allow(clippy::wrong_self_convention)] pub fn is_null(self) -> SimpleExpr { - self.binary(BinOper::Is, Keyword::Null) + ExprTrait::is_null(self) } /// Express a `IS` expression. /// + /// This is equivalent to a newer [ExprTrait::is] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1181,11 +2559,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::Is, v) + ExprTrait::is(self, v) } /// Express a `IS NOT NULL` expression. /// + /// This is equivalent to a newer [ExprTrait::is_not_null] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1212,11 +2592,13 @@ impl Expr { /// ``` #[allow(clippy::wrong_self_convention)] pub fn is_not_null(self) -> SimpleExpr { - self.binary(BinOper::IsNot, Keyword::Null) + ExprTrait::is_not_null(self) } /// Express a `IS NOT` expression. /// + /// This is equivalent to a newer [ExprTrait::is_not] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1245,11 +2627,13 @@ impl Expr { where V: Into, { - self.binary(BinOper::IsNot, v) + ExprTrait::is_not(self, v) } /// Create any binary operation /// + /// This is equivalent to a newer [ExprTrait::binary] and may require more some wrapping beforehand. + /// /// # Examples /// ``` /// use sea_query::{*, tests_cfg::*}; @@ -1280,11 +2664,13 @@ impl Expr { O: Into, T: Into, { - self.bin_op(op, right) + ExprTrait::binary(self, op, right) } /// Negates an expression with `NOT`. /// + /// This is equivalent to a newer [ExprTrait::not] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1311,7 +2697,7 @@ impl Expr { /// ``` #[allow(clippy::should_implement_trait)] pub fn not(self) -> SimpleExpr { - self.un_op(UnOper::Not) + ExprTrait::not(self) } /// Express a `MAX` function. @@ -1493,6 +2879,8 @@ impl Expr { /// Express a `IN` expression. /// + /// This is equivalent to a newer [ExprTrait::is_in] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1541,18 +2929,18 @@ impl Expr { /// ); /// ``` #[allow(clippy::wrong_self_convention)] - pub fn is_in(mut self, v: I) -> SimpleExpr + pub fn is_in(self, v: I) -> SimpleExpr where V: Into, I: IntoIterator, { - self.bopr = Some(BinOper::In); - self.right = Some(SimpleExpr::Tuple(v.into_iter().map(|v| v.into()).collect())); - self.into() + ExprTrait::is_in(self, v) } /// Express a `IN` sub expression. /// + /// This is equivalent to a newer [ExprTrait::in_tuples] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1586,22 +2974,18 @@ impl Expr { /// ); /// ``` #[allow(clippy::wrong_self_convention)] - pub fn in_tuples(mut self, v: I) -> SimpleExpr + pub fn in_tuples(self, v: I) -> SimpleExpr where V: IntoValueTuple, I: IntoIterator, { - self.bopr = Some(BinOper::In); - self.right = Some(SimpleExpr::Tuple( - v.into_iter() - .map(|m| SimpleExpr::Values(m.into_value_tuple().into_iter().collect())) - .collect(), - )); - self.into() + ExprTrait::in_tuples(self, v) } /// Express a `NOT IN` expression. /// + /// This is equivalent to a newer [ExprTrait::is_not_in] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1650,18 +3034,18 @@ impl Expr { /// ); /// ``` #[allow(clippy::wrong_self_convention)] - pub fn is_not_in(mut self, v: I) -> SimpleExpr + pub fn is_not_in(self, v: I) -> SimpleExpr where V: Into, I: IntoIterator, { - self.bopr = Some(BinOper::NotIn); - self.right = Some(SimpleExpr::Tuple(v.into_iter().map(|v| v.into()).collect())); - self.into() + ExprTrait::is_not_in(self, v) } /// Express a `IN` sub-query expression. /// + /// This is equivalent to a newer [ExprTrait::in_subquery] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1691,17 +3075,14 @@ impl Expr { /// ); /// ``` #[allow(clippy::wrong_self_convention)] - pub fn in_subquery(mut self, sel: SelectStatement) -> SimpleExpr { - self.bopr = Some(BinOper::In); - self.right = Some(SimpleExpr::SubQuery( - None, - Box::new(sel.into_sub_query_statement()), - )); - self.into() + pub fn in_subquery(self, sel: SelectStatement) -> SimpleExpr { + ExprTrait::in_subquery(self, sel) } /// Express a `NOT IN` sub-query expression. /// + /// This is equivalent to a newer [ExprTrait::not_in_subquery] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1731,13 +3112,8 @@ impl Expr { /// ); /// ``` #[allow(clippy::wrong_self_convention)] - pub fn not_in_subquery(mut self, sel: SelectStatement) -> SimpleExpr { - self.bopr = Some(BinOper::NotIn); - self.right = Some(SimpleExpr::SubQuery( - None, - Box::new(sel.into_sub_query_statement()), - )); - self.into() + pub fn not_in_subquery(self, sel: SelectStatement) -> SimpleExpr { + ExprTrait::not_in_subquery(self, sel) } /// Express a `EXISTS` sub-query expression. @@ -1844,6 +3220,8 @@ impl Expr { /// Express a `AS enum` expression. /// + /// This is equivalent to a newer [ExprTrait::as_enum] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1890,7 +3268,7 @@ impl Expr { where T: IntoIden, { - SimpleExpr::AsEnum(type_name.into_iden(), Box::new(self.into())) + ExprTrait::as_enum(self, type_name) } /// Adds new `CASE WHEN` to existing case statement. @@ -1927,6 +3305,8 @@ impl Expr { /// Express a `CAST AS` expression. /// + /// This is equivalent to a newer [ExprTrait::cast_as] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -1953,11 +3333,10 @@ impl Expr { where T: IntoIden, { - let func = Func::cast_as(self, type_name); - SimpleExpr::FunctionCall(func) + ExprTrait::cast_as(self, type_name) } - /// Keyword `CURRENT_TIMESTAMP`. + /// Keyword `CURRENT_DATE`. /// /// # Examples /// @@ -2092,23 +3471,24 @@ impl From for SimpleExpr { } } -impl Expression for SimpleExpr { - fn un_op(self, op: UnOper) -> SimpleExpr { - SimpleExpr::Unary(op, Box::new(self)) - } - - fn bin_op(self, op: O, right: T) -> SimpleExpr - where - O: Into, - T: Into, - { - SimpleExpr::Binary(Box::new(self), op.into(), Box::new(right.into())) +impl From for SimpleExpr { + fn from(like: LikeExpr) -> Self { + match like.escape { + Some(escape) => SimpleExpr::Binary( + Box::new(like.pattern.into()), + BinOper::Escape, + Box::new(SimpleExpr::Constant(escape.into())), + ), + None => like.pattern.into(), + } } } impl SimpleExpr { /// Negates an expression with `NOT`. /// + /// This is equivalent to a newer [ExprTrait::not] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -2135,7 +3515,7 @@ impl SimpleExpr { /// ``` #[allow(clippy::should_implement_trait)] pub fn not(self) -> SimpleExpr { - self.un_op(UnOper::Not) + ExprTrait::not(self) } /// Express a logical `AND` operation. @@ -2168,11 +3548,13 @@ impl SimpleExpr { /// ); /// ``` pub fn and(self, right: SimpleExpr) -> Self { - self.binary(BinOper::And, right) + ExprTrait::and(self, right) } /// Express a logical `OR` operation. /// + /// This is equivalent to a newer [ExprTrait::or] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -2199,11 +3581,13 @@ impl SimpleExpr { /// ); /// ``` pub fn or(self, right: SimpleExpr) -> Self { - self.binary(BinOper::Or, right) + ExprTrait::or(self, right) } /// Express an equal (`=`) expression. /// + /// This is equivalent to a newer [ExprTrait::eq] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -2232,11 +3616,13 @@ impl SimpleExpr { where V: Into, { - self.binary(BinOper::Equal, v) + ExprTrait::eq(self, v) } /// Express a not equal (`<>`) expression. /// + /// This is equivalent to a newer [ExprTrait::ne] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -2265,11 +3651,13 @@ impl SimpleExpr { where V: Into, { - self.binary(BinOper::NotEqual, v) + ExprTrait::ne(self, v) } /// Perform addition with another [`SimpleExpr`]. /// + /// This is equivalent to a newer [ExprTrait::add] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -2302,11 +3690,13 @@ impl SimpleExpr { where T: Into, { - self.binary(BinOper::Add, right) + ExprTrait::add(self, right) } /// Perform multiplication with another [`SimpleExpr`]. /// + /// This is equivalent to a newer [ExprTrait::mul] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -2339,11 +3729,13 @@ impl SimpleExpr { where T: Into, { - self.binary(BinOper::Mul, right.into()) + ExprTrait::mul(self, right) } /// Perform division with another [`SimpleExpr`]. /// + /// This is equivalent to a newer [ExprTrait::div] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -2376,11 +3768,13 @@ impl SimpleExpr { where T: Into, { - self.binary(BinOper::Div, right.into()) + ExprTrait::div(self, right) } /// Perform subtraction with another [`SimpleExpr`]. /// + /// This is equivalent to a newer [ExprTrait::sub] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -2413,11 +3807,13 @@ impl SimpleExpr { where T: Into, { - self.binary(BinOper::Sub, right) + ExprTrait::sub(self, right) } /// Express a `CAST AS` expression. /// + /// This is equivalent to a newer [ExprTrait::cast_as] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -2444,12 +3840,45 @@ impl SimpleExpr { where T: IntoIden, { - let func = Func::cast_as(self, type_name); + ExprTrait::cast_as(self, type_name) + } + + /// Express a case-sensitive `CAST AS` expression. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .expr(Expr::value("1").cast_as_quoted(Alias::new("MyType"), '"'.into())) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT CAST('1' AS "MyType")"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT CAST('1' AS "MyType")"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT CAST('1' AS "MyType")"# + /// ); + /// ``` + pub fn cast_as_quoted(self, type_name: T, q: Quote) -> Self + where + T: IntoIden, + { + let func = Func::cast_as_quoted(self, type_name, q); Self::FunctionCall(func) } /// Create any binary operation /// + /// This is equivalent to a newer [ExprTrait::add] and may require more some wrapping beforehand. + /// /// # Examples /// ``` /// use sea_query::{*, tests_cfg::*}; @@ -2479,11 +3908,13 @@ impl SimpleExpr { O: Into, T: Into, { - self.bin_op(op, right) + ExprTrait::binary(self, op, right) } /// Express a `LIKE` expression. /// + /// This is equivalent to a newer [ExprTrait::like] and may require more some wrapping beforehand. + /// /// # Examples /// /// ``` @@ -2509,12 +3940,14 @@ impl SimpleExpr { /// ); /// ``` pub fn like(self, like: L) -> Self { - self.like_like(BinOper::Like, like.into_like_expr()) + ExprTrait::like(self, like) } - /// Express a `NOT LIKE` expression + /// Express a `NOT LIKE` expression. + /// + /// This is equivalent to a newer [ExprTrait::not_like] and may require more some wrapping beforehand. pub fn not_like(self, like: L) -> Self { - self.like_like(BinOper::NotLike, like.into_like_expr()) + ExprTrait::not_like(self, like) } pub(crate) fn is_binary(&self) -> bool { diff --git a/src/extension/mysql/column.rs b/src/extension/mysql/column.rs new file mode 100644 index 0000000..232f8a9 --- /dev/null +++ b/src/extension/mysql/column.rs @@ -0,0 +1,19 @@ +use crate::Iden; + +#[derive(Debug, Copy, Clone)] +pub enum MySqlType { + TinyBlob, + MediumBlob, + LongBlob, +} + +impl Iden for MySqlType { + fn unquoted(&self, s: &mut dyn std::fmt::Write) { + let ty = match self { + Self::TinyBlob => "tinyblob", + Self::MediumBlob => "mediumblob", + Self::LongBlob => "longblob", + }; + write!(s, "{ty}").unwrap(); + } +} diff --git a/src/extension/mysql/mod.rs b/src/extension/mysql/mod.rs index dd8cc4f..9841ca6 100644 --- a/src/extension/mysql/mod.rs +++ b/src/extension/mysql/mod.rs @@ -1,5 +1,7 @@ +mod column; mod index; mod select; +pub use column::*; pub use index::*; pub use select::*; diff --git a/src/extension/postgres/expr.rs b/src/extension/postgres/expr.rs index a8ca292..8c4e894 100644 --- a/src/extension/postgres/expr.rs +++ b/src/extension/postgres/expr.rs @@ -1,7 +1,9 @@ use super::PgBinOper; -use crate::{expr::private::Expression, Expr, IntoLikeExpr, SimpleExpr}; +use crate::{ + ColumnRef, Expr, ExprTrait, FunctionCall, IntoLikeExpr, Keyword, LikeExpr, SimpleExpr, Value, +}; -pub trait PgExpr: Expression { +pub trait PgExpr: ExprTrait { /// Express an postgres concatenate (`||`) expression. /// /// # Examples @@ -24,7 +26,7 @@ pub trait PgExpr: Expression { where T: Into, { - self.bin_op(PgBinOper::Concatenate, right) + self.binary(PgBinOper::Concatenate, right) } /// Alias of [`PgExpr::concatenate`] fn concat(self, right: T) -> SimpleExpr @@ -57,7 +59,7 @@ pub trait PgExpr: Expression { where T: Into, { - self.bin_op(PgBinOper::Matches, expr) + self.binary(PgBinOper::Matches, expr) } /// Express an postgres fulltext search contains (`@>`) expression. @@ -83,7 +85,7 @@ pub trait PgExpr: Expression { where T: Into, { - self.bin_op(PgBinOper::Contains, expr) + self.binary(PgBinOper::Contains, expr) } /// Express an postgres fulltext search contained (`<@`) expression. @@ -109,7 +111,7 @@ pub trait PgExpr: Expression { where T: Into, { - self.bin_op(PgBinOper::Contained, expr) + self.binary(PgBinOper::Contained, expr) } /// Express a `ILIKE` expression. @@ -134,7 +136,7 @@ pub trait PgExpr: Expression { where L: IntoLikeExpr, { - self.like_like(PgBinOper::ILike, like.into_like_expr()) + self.binary(PgBinOper::ILike, like.into_like_expr()) } /// Express a `NOT ILIKE` expression @@ -142,7 +144,7 @@ pub trait PgExpr: Expression { where L: IntoLikeExpr, { - self.like_like(PgBinOper::NotILike, like.into_like_expr()) + self.binary(PgBinOper::NotILike, like.into_like_expr()) } /// Express a postgres retrieves JSON field as JSON value (`->`). @@ -167,7 +169,7 @@ pub trait PgExpr: Expression { where T: Into, { - self.bin_op(PgBinOper::GetJsonField, right) + self.binary(PgBinOper::GetJsonField, right) } /// Express a postgres retrieves JSON field and casts it to an appropriate SQL type (`->>`). @@ -192,10 +194,16 @@ pub trait PgExpr: Expression { where T: Into, { - self.bin_op(PgBinOper::CastJsonField, right) + self.binary(PgBinOper::CastJsonField, right) } } +// TODO: https://github.com/SeaQL/sea-query/discussions/795: +// replace all of this with `impl PgExpr for T where T: ExprTrait {}` impl PgExpr for Expr {} - impl PgExpr for SimpleExpr {} +impl PgExpr for FunctionCall {} +impl PgExpr for ColumnRef {} +impl PgExpr for Keyword {} +impl PgExpr for LikeExpr {} +impl PgExpr for Value {} diff --git a/src/extension/postgres/func.rs b/src/extension/postgres/func.rs index 642d087..67fc0b2 100644 --- a/src/extension/postgres/func.rs +++ b/src/extension/postgres/func.rs @@ -14,6 +14,8 @@ pub enum PgFunction { TsRankCd, StartsWith, GenRandomUUID, + JsonBuildObject, + JsonAgg, #[cfg(feature = "postgres-array")] Any, #[cfg(feature = "postgres-array")] @@ -350,4 +352,59 @@ impl PgFunc { pub fn gen_random_uuid() -> FunctionCall { FunctionCall::new(Function::PgFunction(PgFunction::GenRandomUUID)) } + + /// Call the `JSON_BUILD_OBJECT` function. Postgres only. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .expr(PgFunc::json_build_object(vec![ + /// (Expr::val("a"), Expr::val(1)), + /// (Expr::val("b"), Expr::val("2")), + /// ])) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT JSON_BUILD_OBJECT('a', 1, 'b', '2')"# + /// ); + /// ``` + pub fn json_build_object(pairs: Vec<(T, T)>) -> FunctionCall + where + T: Into, + { + let mut args = vec![]; + for (key, value) in pairs { + args.push(key.into()); + args.push(value.into()); + } + FunctionCall::new(Function::PgFunction(PgFunction::JsonBuildObject)).args(args) + } + + /// Call the `JSON_AGG` function. Postgres only. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .from(Char::Table) + /// .expr(PgFunc::json_agg(Expr::col(Char::SizeW))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT JSON_AGG("size_w") FROM "character""# + /// ); + /// ``` + pub fn json_agg(expr: T) -> FunctionCall + where + T: Into, + { + FunctionCall::new(Function::PgFunction(PgFunction::JsonAgg)).arg(expr) + } } diff --git a/src/extension/postgres/mod.rs b/src/extension/postgres/mod.rs index acd0c7d..930b23e 100644 --- a/src/extension/postgres/mod.rs +++ b/src/extension/postgres/mod.rs @@ -1,7 +1,6 @@ pub use expr::*; pub use extension::*; pub use func::*; -pub use interval::*; pub use ltree::*; pub use types::*; @@ -38,6 +37,12 @@ pub enum PgBinOper { Regex, /// `~*`. Regex operator with case insensitive matching. RegexCaseInsensitive, + #[cfg(feature = "postgres-vector")] + EuclideanDistance, + #[cfg(feature = "postgres-vector")] + NegativeInnerProduct, + #[cfg(feature = "postgres-vector")] + CosineDistance, } impl From for BinOper { diff --git a/src/extension/postgres/types.rs b/src/extension/postgres/types.rs index 3aff184..70f69d0 100644 --- a/src/extension/postgres/types.rs +++ b/src/extension/postgres/types.rs @@ -88,7 +88,11 @@ pub enum TypeDropOpt { #[derive(Debug, Clone)] pub enum TypeAlterOpt { - Add(DynIden, Option), + Add { + value: DynIden, + placement: Option, + if_not_exists: bool, + }, Rename(DynIden), RenameValue(DynIden, DynIden), } @@ -361,7 +365,11 @@ impl TypeAlterStatement { where T: IntoIden, { - self.alter_option(TypeAlterOpt::Add(value.into_iden(), None)) + self.alter_option(TypeAlterOpt::Add { + value: value.into_iden(), + placement: None, + if_not_exists: false, + }) } /// Add a enum value before an existing value @@ -398,6 +406,28 @@ impl TypeAlterStatement { self } + /// Add a enum value if not already exists + /// + /// ``` + /// use sea_query::{extension::postgres::Type, tests_cfg::*, *}; + /// + /// assert_eq!( + /// Type::alter() + /// .name(Font::Table) + /// .add_value(Alias::new("weight")) + /// .if_not_exists() + /// .after(Font::Variant) + /// .to_string(PostgresQueryBuilder), + /// r#"ALTER TYPE "font" ADD VALUE IF NOT EXISTS 'weight' AFTER 'variant'"# + /// ) + /// ``` + pub fn if_not_exists(mut self) -> Self { + if let Some(option) = self.option { + self.option = Some(option.if_not_exists()); + } + self + } + pub fn rename_to(self, name: T) -> Self where T: IntoIden, @@ -442,9 +472,15 @@ impl TypeAlterOpt { T: IntoIden, { match self { - TypeAlterOpt::Add(iden, _) => { - Self::Add(iden, Some(TypeAlterAddOpt::Before(value.into_iden()))) - } + TypeAlterOpt::Add { + value: iden, + if_not_exists, + .. + } => Self::Add { + value: iden, + if_not_exists, + placement: Some(TypeAlterAddOpt::Before(value.into_iden())), + }, _ => self, } } @@ -455,9 +491,29 @@ impl TypeAlterOpt { T: IntoIden, { match self { - TypeAlterOpt::Add(iden, _) => { - Self::Add(iden, Some(TypeAlterAddOpt::After(value.into_iden()))) - } + TypeAlterOpt::Add { + value: iden, + if_not_exists, + .. + } => Self::Add { + value: iden, + if_not_exists, + placement: Some(TypeAlterAddOpt::After(value.into_iden())), + }, + _ => self, + } + } + + /// Changes only `ADD VALUE x` options into `ADD VALUE IF NOT EXISTS x` options, does nothing otherwise + pub fn if_not_exists(self) -> Self { + match self { + TypeAlterOpt::Add { + value, placement, .. + } => Self::Add { + value, + placement, + if_not_exists: true, + }, _ => self, } } diff --git a/src/extension/sqlite/expr.rs b/src/extension/sqlite/expr.rs index 1623f5c..738fff4 100644 --- a/src/extension/sqlite/expr.rs +++ b/src/extension/sqlite/expr.rs @@ -1,8 +1,8 @@ -use crate::{expr::private::Expression, Expr, SimpleExpr}; +use crate::{ColumnRef, Expr, ExprTrait, FunctionCall, Keyword, LikeExpr, SimpleExpr, Value}; use super::SqliteBinOper; -pub trait SqliteExpr: Expression { +pub trait SqliteExpr: ExprTrait { /// Express an sqlite `GLOB` operator. /// /// # Examples @@ -25,7 +25,7 @@ pub trait SqliteExpr: Expression { where T: Into, { - self.bin_op(SqliteBinOper::Glob, right) + self.binary(SqliteBinOper::Glob, right) } /// Express an sqlite `MATCH` operator. @@ -50,7 +50,7 @@ pub trait SqliteExpr: Expression { where T: Into, { - self.bin_op(SqliteBinOper::Match, right) + self.binary(SqliteBinOper::Match, right) } /// Express an sqlite retrieves JSON field as JSON value (`->`). @@ -75,7 +75,7 @@ pub trait SqliteExpr: Expression { where T: Into, { - self.bin_op(SqliteBinOper::GetJsonField, right) + self.binary(SqliteBinOper::GetJsonField, right) } /// Express an sqlite retrieves JSON field and casts it to an appropriate SQL type (`->>`). @@ -100,10 +100,16 @@ pub trait SqliteExpr: Expression { where T: Into, { - self.bin_op(SqliteBinOper::CastJsonField, right) + self.binary(SqliteBinOper::CastJsonField, right) } } +// TODO: https://github.com/SeaQL/sea-query/discussions/795: +// replace all of this with `impl PgExpr for T where T: ExprTrait {}` impl SqliteExpr for Expr {} - impl SqliteExpr for SimpleExpr {} +impl SqliteExpr for FunctionCall {} +impl SqliteExpr for ColumnRef {} +impl SqliteExpr for Keyword {} +impl SqliteExpr for LikeExpr {} +impl SqliteExpr for Value {} diff --git a/src/func.rs b/src/func.rs index 24b17d1..7c81264 100644 --- a/src/func.rs +++ b/src/func.rs @@ -15,6 +15,8 @@ pub enum Function { Abs, Count, IfNull, + Greatest, + Least, CharLength, Cast, Custom(DynIden), @@ -25,6 +27,7 @@ pub enum Function { BitOr, Random, Round, + Md5, #[cfg(feature = "backend-postgres")] PgFunction(PgFunction), } @@ -38,8 +41,8 @@ pub struct FunctionCall { } #[derive(Debug, Default, Copy, Clone, PartialEq)] -pub(crate) struct FuncArgMod { - pub(crate) distinct: bool, +pub struct FuncArgMod { + pub distinct: bool, } impl FunctionCall { @@ -85,6 +88,10 @@ impl FunctionCall { pub fn get_args(&self) -> &[SimpleExpr] { &self.args } + + pub fn get_mods(&self) -> &[FuncArgMod] { + &self.mods + } } /// Function call helper. @@ -387,6 +394,76 @@ impl Func { FunctionCall::new(Function::CharLength).arg(expr) } + /// Call `GREATEST` function. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .expr(Func::greatest([ + /// Expr::col(Char::SizeW).into(), + /// Expr::col(Char::SizeH).into(), + /// ])) + /// .from(Char::Table) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT GREATEST(`size_w`, `size_h`) FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT GREATEST("size_w", "size_h") FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT MAX("size_w", "size_h") FROM "character""# + /// ); + /// ``` + pub fn greatest(args: I) -> FunctionCall + where + I: IntoIterator, + { + FunctionCall::new(Function::Greatest).args(args) + } + + /// Call `LEAST` function. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .expr(Func::least([ + /// Expr::col(Char::SizeW).into(), + /// Expr::col(Char::SizeH).into(), + /// ])) + /// .from(Char::Table) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT LEAST(`size_w`, `size_h`) FROM `character`"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT LEAST("size_w", "size_h") FROM "character""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT MIN("size_w", "size_h") FROM "character""# + /// ); + /// ``` + pub fn least(args: I) -> FunctionCall + where + I: IntoIterator, + { + FunctionCall::new(Function::Least).args(args) + } + /// Call `IF NULL` function. /// /// # Examples @@ -459,6 +536,46 @@ impl Func { )) } + /// Call `CAST` function with a case-sensitive custom type. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .expr(Func::cast_as_quoted( + /// "hello", + /// Alias::new("MyType"), + /// '"'.into(), + /// )) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT CAST('hello' AS "MyType")"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT CAST('hello' AS "MyType")"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT CAST('hello' AS "MyType")"# + /// ); + /// ``` + pub fn cast_as_quoted(expr: V, iden: I, q: Quote) -> FunctionCall + where + V: Into, + I: IntoIden, + { + let expr: SimpleExpr = expr.into(); + let mut quoted_type = String::new(); + iden.into_iden().prepare(&mut quoted_type, q); + FunctionCall::new(Function::Cast) + .arg(expr.binary(BinOper::As, Expr::cust(quoted_type.as_str()))) + } + /// Call `COALESCE` function. /// /// # Examples @@ -721,4 +838,33 @@ impl Func { pub fn random() -> FunctionCall { FunctionCall::new(Function::Random) } + + /// Call `MD5` function, this is only available in Postgres and MySQL. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .expr(Func::md5(Expr::col((Char::Table, Char::Character)))) + /// .from(Char::Table) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT MD5(`character`.`character`) FROM `character`"# + /// ); + /// + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT MD5("character"."character") FROM "character""# + /// ); + /// ``` + pub fn md5(expr: T) -> FunctionCall + where + T: Into, + { + FunctionCall::new(Function::Md5).arg(expr) + } } diff --git a/src/index/create.rs b/src/index/create.rs index ce094d4..714325f 100644 --- a/src/index/create.rs +++ b/src/index/create.rs @@ -1,6 +1,7 @@ use inherent::inherent; use crate::{backend::SchemaBuilder, types::*, SchemaStatementBuilder}; +use crate::{ConditionHolder, ConditionalStatement, IntoCondition}; use super::common::*; @@ -100,6 +101,31 @@ use super::common::*; /// r#"CREATE INDEX "idx-glyph-aspect" ON "glyph" ("aspect" DESC)"# /// ); /// ``` +/// Index on multi-columns +/// ``` +/// use sea_query::{tests_cfg::*, *}; +/// +/// let index = Index::create() +/// .name("idx-glyph-aspect") +/// .table(Glyph::Table) +/// .col((Glyph::Image, IndexOrder::Asc)) +/// .col((Glyph::Aspect, IndexOrder::Desc)) +/// .unique() +/// .to_owned(); +/// +/// assert_eq!( +/// index.to_string(MysqlQueryBuilder), +/// r#"CREATE UNIQUE INDEX `idx-glyph-aspect` ON `glyph` (`image` ASC, `aspect` DESC)"# +/// ); +/// assert_eq!( +/// index.to_string(PostgresQueryBuilder), +/// r#"CREATE UNIQUE INDEX "idx-glyph-aspect" ON "glyph" ("image" ASC, "aspect" DESC)"# +/// ); +/// assert_eq!( +/// index.to_string(SqliteQueryBuilder), +/// r#"CREATE UNIQUE INDEX "idx-glyph-aspect" ON "glyph" ("image" ASC, "aspect" DESC)"# +/// ); +/// ``` /// Index with prefix and order /// ``` /// use sea_query::{tests_cfg::*, *}; @@ -123,6 +149,27 @@ use super::common::*; /// r#"CREATE INDEX "idx-glyph-aspect" ON "glyph" ("aspect" ASC)"# /// ); /// ``` +/// +/// Partial Index with prefix and order +/// ``` +/// use sea_query::{tests_cfg::*, *}; +/// +/// let index = Index::create() +/// .name("idx-glyph-aspect") +/// .table(Glyph::Table) +/// .col((Glyph::Aspect, 64, IndexOrder::Asc)) +/// .and_where(Expr::col((Glyph::Table, Glyph::Aspect)).is_in(vec![3, 4])) +/// .to_owned(); +/// +/// assert_eq!( +/// index.to_string(PostgresQueryBuilder), +/// r#"CREATE INDEX "idx-glyph-aspect" ON "glyph" ("aspect" (64) ASC) WHERE "glyph"."aspect" IN (3, 4)"# +/// ); +/// assert_eq!( +/// index.to_string(SqliteQueryBuilder), +/// r#"CREATE INDEX "idx-glyph-aspect" ON "glyph" ("aspect" ASC) WHERE "glyph"."aspect" IN (3, 4)"# +/// ); +/// ``` #[derive(Default, Debug, Clone)] pub struct IndexCreateStatement { pub(crate) table: Option, @@ -132,6 +179,7 @@ pub struct IndexCreateStatement { pub(crate) nulls_not_distinct: bool, pub(crate) index_type: Option, pub(crate) if_not_exists: bool, + pub(crate) r#where: ConditionHolder, } /// Specification of a table index @@ -146,7 +194,16 @@ pub enum IndexType { impl IndexCreateStatement { /// Construct a new [`IndexCreateStatement`] pub fn new() -> Self { - Self::default() + Self { + table: None, + index: Default::default(), + primary: false, + unique: false, + nulls_not_distinct: false, + index_type: None, + if_not_exists: false, + r#where: ConditionHolder::new(), + } } /// Create index if index not exists @@ -238,6 +295,7 @@ impl IndexCreateStatement { nulls_not_distinct: self.nulls_not_distinct, index_type: self.index_type.take(), if_not_exists: self.if_not_exists, + r#where: self.r#where.clone(), } } } @@ -258,3 +316,18 @@ impl SchemaStatementBuilder for IndexCreateStatement { pub fn to_string(&self, schema_builder: T) -> String; } + +impl ConditionalStatement for IndexCreateStatement { + fn and_or_where(&mut self, condition: LogicalChainOper) -> &mut Self { + self.r#where.add_and_or(condition); + self + } + + fn cond_where(&mut self, condition: C) -> &mut Self + where + C: IntoCondition, + { + self.r#where.add_condition(condition.into_condition()); + self + } +} diff --git a/src/lib.rs b/src/lib.rs index 1798743..15e1a18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ //! //! ### Feature flags //! -//! Macro: `derive` `attr` +//! Macro: `derive` //! //! Async support: `thread-safe` (use `Arc` inplace of `Rc`) //! @@ -180,10 +180,9 @@ //! ``` //! //! If you're okay with running another procedural macro, you can activate -//! the `derive` or `attr` feature on the crate to save you some boilerplate. +//! the `derive` feature on the crate to save you some boilerplate. //! For more usage information, look at -//! [the derive examples](https://github.com/SeaQL/sea-query/tree/master/sea-query-derive/tests/pass) -//! or [the attribute examples](https://github.com/SeaQL/sea-query/tree/master/sea-query-attr/tests/pass). +//! [the derive examples](https://github.com/SeaQL/sea-query/tree/master/sea-query-derive/tests/pass). //! //! ```rust //! #[cfg(feature = "derive")] @@ -203,7 +202,7 @@ //! ``` //! //! ```rust -//! #[cfg(feature = "attr")] +//! #[cfg(feature = "derive")] //! # fn test() { //! use sea_query::{enum_def, Iden}; //! @@ -223,7 +222,7 @@ //! assert_eq!(CharacterIden::Table.to_string(), "character"); //! assert_eq!(CharacterIden::Foo.to_string(), "foo"); //! # } -//! # #[cfg(feature = "attr")] +//! # #[cfg(feature = "derive")] //! # test(); //! ``` //! @@ -586,7 +585,7 @@ //! r#"CREATE TABLE IF NOT EXISTS "character" ("#, //! r#""id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,"#, //! r#""font_size" integer NOT NULL,"#, -//! r#""character" text NOT NULL,"#, +//! r#""character" varchar NOT NULL,"#, //! r#""size_w" integer NOT NULL,"#, //! r#""size_h" integer NOT NULL,"#, //! r#""font_id" integer DEFAULT NULL,"#, @@ -846,7 +845,7 @@ pub use types::*; pub use value::*; #[cfg(feature = "derive")] -pub use sea_query_derive::{Iden, IdenStatic}; +pub use sea_query_derive::{enum_def, Iden, IdenStatic}; -#[cfg(feature = "attr")] -pub use sea_query_attr::enum_def; +#[cfg(all(feature = "attr", not(feature = "derive")))] +pub use sea_query_derive::enum_def; diff --git a/src/prepare.rs b/src/prepare.rs index 646e427..db45361 100644 --- a/src/prepare.rs +++ b/src/prepare.rs @@ -53,9 +53,9 @@ impl Write for SqlWriterValues { } } -impl ToString for SqlWriterValues { - fn to_string(&self) -> String { - self.string.clone() +impl std::fmt::Display for SqlWriterValues { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.string) } } @@ -117,7 +117,7 @@ where #[cfg(test)] #[cfg(feature = "backend-mysql")] -mod tests { +mod tests_mysql { use super::*; use pretty_assertions::assert_eq; @@ -151,6 +151,21 @@ mod tests { #[test] fn inject_parameters_4() { + assert_eq!( + inject_parameters("?", [vec![0xABu8, 0xCD, 0xEF].into()], &MysqlQueryBuilder), + "x'ABCDEF'" + ); + } +} + +#[cfg(test)] +#[cfg(feature = "backend-postgres")] +mod tests_postgres { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn inject_parameters_5() { assert_eq!( inject_parameters( "WHERE A = $1 AND C = $2", @@ -162,7 +177,7 @@ mod tests { } #[test] - fn inject_parameters_5() { + fn inject_parameters_6() { assert_eq!( inject_parameters( "WHERE A = $2 AND C = $1", @@ -174,18 +189,10 @@ mod tests { } #[test] - fn inject_parameters_6() { + fn inject_parameters_7() { assert_eq!( inject_parameters("WHERE A = $1", ["B'C".into()], &PostgresQueryBuilder), "WHERE A = E'B\\'C'" ); } - - #[test] - fn inject_parameters_7() { - assert_eq!( - inject_parameters("?", [vec![0xABu8, 0xCD, 0xEF].into()], &MysqlQueryBuilder), - "x'ABCDEF'" - ); - } } diff --git a/src/query/insert.rs b/src/query/insert.rs index 93ca39e..b82f536 100644 --- a/src/query/insert.rs +++ b/src/query/insert.rs @@ -278,6 +278,44 @@ impl InsertStatement { self.values(values).unwrap() } + /// Add rows to be inserted from an iterator, variation of [`InsertStatement::values_panic`]. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let rows = vec![[2.1345.into(), "24B".into()], [5.15.into(), "12A".into()]]; + /// + /// let query = Query::insert() + /// .into_table(Glyph::Table) + /// .columns([Glyph::Aspect, Glyph::Image]) + /// .values_from_panic(rows) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (2.1345, '24B'), (5.15, '12A')"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2.1345, '24B'), (5.15, '12A')"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2.1345, '24B'), (5.15, '12A')"# + /// ); + /// ``` + pub fn values_from_panic(&mut self, values_iter: impl IntoIterator) -> &mut Self + where + I: IntoIterator, + { + values_iter.into_iter().for_each(|values| { + self.values_panic(values); + }); + self + } + /// ON CONFLICT expression /// /// # Examples diff --git a/src/query/on_conflict.rs b/src/query/on_conflict.rs index 47d45c0..aee8fb7 100644 --- a/src/query/on_conflict.rs +++ b/src/query/on_conflict.rs @@ -21,7 +21,7 @@ pub enum OnConflictTarget { #[derive(Debug, Clone, PartialEq)] pub enum OnConflictAction { /// Do nothing - DoNothing, + DoNothing(Vec), /// Update column value of existing row Update(Vec), } @@ -137,8 +137,114 @@ impl OnConflict { self } + /// Set ON CONFLICT do nothing. + /// + /// Please use [`Self::do_nothing_on()`] and provide primary keys if you are using MySQL. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::insert() + /// .into_table(Glyph::Table) + /// .columns([Glyph::Aspect, Glyph::Image]) + /// .values_panic(["abcd".into(), 3.1415.into()]) + /// .on_conflict( + /// OnConflict::columns([Glyph::Id, Glyph::Aspect]) + /// .do_nothing() + /// .to_owned(), + /// ) + /// .to_owned(); + /// + /// // Sadly this is not valid today. + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// [ + /// r#"INSERT INTO `glyph` (`aspect`, `image`)"#, + /// r#"VALUES ('abcd', 3.1415)"#, + /// r#"ON DUPLICATE KEY IGNORE"#, + /// ] + /// .join(" ") + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// [ + /// r#"INSERT INTO "glyph" ("aspect", "image")"#, + /// r#"VALUES ('abcd', 3.1415)"#, + /// r#"ON CONFLICT ("id", "aspect") DO NOTHING"#, + /// ] + /// .join(" ") + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// [ + /// r#"INSERT INTO "glyph" ("aspect", "image")"#, + /// r#"VALUES ('abcd', 3.1415)"#, + /// r#"ON CONFLICT ("id", "aspect") DO NOTHING"#, + /// ] + /// .join(" ") + /// ); + /// ``` pub fn do_nothing(&mut self) -> &mut Self { - self.action = Some(OnConflictAction::DoNothing); + self.action = Some(OnConflictAction::DoNothing(vec![])); + self + } + + /// Set ON CONFLICT do nothing, but with MySQL specific polyfill. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::insert() + /// .into_table(Glyph::Table) + /// .columns([Glyph::Aspect, Glyph::Image]) + /// .values_panic(["abcd".into(), 3.1415.into()]) + /// .on_conflict( + /// OnConflict::columns([Glyph::Id, Glyph::Aspect]) + /// .do_nothing_on([Glyph::Id]) + /// .to_owned(), + /// ) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// [ + /// r#"INSERT INTO `glyph` (`aspect`, `image`)"#, + /// r#"VALUES ('abcd', 3.1415)"#, + /// r#"ON DUPLICATE KEY UPDATE `id` = `id`"#, + /// ] + /// .join(" ") + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// [ + /// r#"INSERT INTO "glyph" ("aspect", "image")"#, + /// r#"VALUES ('abcd', 3.1415)"#, + /// r#"ON CONFLICT ("id", "aspect") DO NOTHING"#, + /// ] + /// .join(" ") + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// [ + /// r#"INSERT INTO "glyph" ("aspect", "image")"#, + /// r#"VALUES ('abcd', 3.1415)"#, + /// r#"ON CONFLICT ("id", "aspect") DO NOTHING"#, + /// ] + /// .join(" ") + /// ); + /// ``` + pub fn do_nothing_on(&mut self, pk_cols: I) -> &mut Self + where + C: IntoIden, + I: IntoIterator, + { + self.action = Some(OnConflictAction::DoNothing( + pk_cols.into_iter().map(IntoIden::into_iden).collect(), + )); self } @@ -247,7 +353,7 @@ impl OnConflict { Some(OnConflictAction::Update(v)) => { v.append(&mut update_strats); } - Some(OnConflictAction::DoNothing) | None => { + Some(OnConflictAction::DoNothing(_)) | None => { self.action = Some(OnConflictAction::Update(update_strats)); } }; @@ -302,7 +408,7 @@ impl OnConflict { Some(OnConflictAction::Update(v)) => { v.append(&mut update_exprs); } - Some(OnConflictAction::DoNothing) | None => { + Some(OnConflictAction::DoNothing(_)) | None => { self.action = Some(OnConflictAction::Update(update_exprs)); } }; diff --git a/src/query/select.rs b/src/query/select.rs index 4501e5a..debd080 100644 --- a/src/query/select.rs +++ b/src/query/select.rs @@ -214,6 +214,75 @@ impl SelectStatement { self } + /// A shorthand to express if ... else ... when constructing the select statement. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::select() + /// .column(Char::Character) + /// .from(Char::Table) + /// .apply_if(Some(5), |q, v| { + /// q.and_where(Expr::col(Char::FontId).eq(v)); + /// }) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character` FROM `character` WHERE `font_id` = 5"# + /// ); + /// ``` + pub fn apply_if(&mut self, val: Option, if_some: F) -> &mut Self + where + Self: Sized, + F: FnOnce(&mut Self, T), + { + if let Some(val) = val { + if_some(self, val); + } + self + } + + /// Construct part of the select statement in another function. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let common_expr = |q: &mut SelectStatement| { + /// q.and_where(Expr::col(Char::FontId).eq(5)); + /// }; + /// + /// let query = Query::select() + /// .column(Char::Character) + /// .from(Char::Table) + /// .apply(common_expr) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT `character` FROM `character` WHERE `font_id` = 5"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT "character" FROM "character" WHERE "font_id" = 5"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT "character" FROM "character" WHERE "font_id" = 5"# + /// ); + /// ``` + pub fn apply(&mut self, func: F) -> &mut Self + where + F: FnOnce(&mut Self), + { + func(self); + self + } + /// Clear the select list pub fn clear_selects(&mut self) -> &mut Self { self.selects = Vec::new(); diff --git a/src/query/update.rs b/src/query/update.rs index b267e81..a514127 100644 --- a/src/query/update.rs +++ b/src/query/update.rs @@ -132,6 +132,24 @@ impl UpdateStatement { /// query.to_string(SqliteQueryBuilder), /// r#"UPDATE "glyph" SET "aspect" = 60 * 24 * 24, "image" = '24B0E11951B03B07F8300FD003983F03F0780060'"# /// ); + /// + /// let query = Query::update() + /// .table(Glyph::Table) + /// .value(Glyph::Aspect, Expr::value(Value::Int(None))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = NULL"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"UPDATE "glyph" SET "aspect" = NULL"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"UPDATE "glyph" SET "aspect" = NULL"# + /// ); /// ``` pub fn value(&mut self, col: C, value: T) -> &mut Self where diff --git a/src/query/with.rs b/src/query/with.rs index 85815df..94933f9 100644 --- a/src/query/with.rs +++ b/src/query/with.rs @@ -114,7 +114,7 @@ impl CommonTableExpression { pub fn from_select(select: SelectStatement) -> Self { let mut cte = Self::default(); cte.try_set_cols_from_selects(&select.selects); - if let Some(from) = select.from.get(0) { + if let Some(from) = select.from.first() { match from { TableRef::Table(iden) => cte.set_table_name_from_select(iden), TableRef::SchemaTable(_, iden) => cte.set_table_name_from_select(iden), diff --git a/src/table/alter.rs b/src/table/alter.rs index 8b8192e..74b14d1 100644 --- a/src/table/alter.rs +++ b/src/table/alter.rs @@ -1,4 +1,7 @@ -use crate::{backend::SchemaBuilder, types::*, ColumnDef, SchemaStatementBuilder, TableForeignKey}; +use crate::{ + backend::SchemaBuilder, types::*, ColumnDef, IntoColumnDef, SchemaStatementBuilder, + TableForeignKey, +}; use inherent::inherent; /// Alter a table @@ -100,10 +103,10 @@ impl TableAlterStatement { /// r#"ALTER TABLE "font" ADD COLUMN "new_col" integer NOT NULL DEFAULT 100"#, /// ); /// ``` - pub fn add_column(&mut self, column_def: &mut ColumnDef) -> &mut Self { + pub fn add_column(&mut self, column_def: C) -> &mut Self { self.options .push(TableAlterOption::AddColumn(AddColumnOption { - column: column_def.take(), + column: column_def.into_column_def(), if_not_exists: false, })); self @@ -139,10 +142,10 @@ impl TableAlterStatement { /// r#"ALTER TABLE "font" ADD COLUMN "new_col" integer NOT NULL DEFAULT 100"#, /// ); /// ``` - pub fn add_column_if_not_exists(&mut self, column_def: &mut ColumnDef) -> &mut Self { + pub fn add_column_if_not_exists(&mut self, column_def: C) -> &mut Self { self.options .push(TableAlterOption::AddColumn(AddColumnOption { - column: column_def.take(), + column: column_def.into_column_def(), if_not_exists: true, })); self @@ -179,8 +182,8 @@ impl TableAlterStatement { /// ); /// // Sqlite not support modifying table column /// ``` - pub fn modify_column(&mut self, column_def: &mut ColumnDef) -> &mut Self { - self.add_alter_option(TableAlterOption::ModifyColumn(column_def.take())) + pub fn modify_column(&mut self, column_def: C) -> &mut Self { + self.add_alter_option(TableAlterOption::ModifyColumn(column_def.into_column_def())) } /// Rename a column in an existing table diff --git a/src/table/column.rs b/src/table/column.rs index 7ed0f71..4f6d2ba 100644 --- a/src/table/column.rs +++ b/src/table/column.rs @@ -9,13 +9,59 @@ pub struct ColumnDef { pub(crate) spec: Vec, } +pub trait IntoColumnDef { + fn into_column_def(self) -> ColumnDef; +} + /// All column types +/// +/// | ColumnType | MySQL data type | PostgreSQL data type | SQLite data type | +/// |-----------------------|-------------------|-----------------------------|------------------------------| +/// | Char | char | char | char | +/// | String | varchar | varchar | varchar | +/// | Text | text | text | text | +/// | TinyInteger | tinyint | smallint | tinyint | +/// | SmallInteger | smallint | smallint | smallint | +/// | Integer | int | integer | integer | +/// | BigInteger | bigint | bigint | integer | +/// | TinyUnsigned | tinyint unsigned | smallint | tinyint | +/// | SmallUnsigned | smallint unsigned | smallint | smallint | +/// | Unsigned | int unsigned | integer | integer | +/// | BigUnsigned | bigint unsigned | bigint | integer | +/// | Float | float | real | float | +/// | Double | double | double precision | double | +/// | Decimal | decimal | decimal | real | +/// | DateTime | datetime | timestamp without time zone | datetime_text | +/// | Timestamp | timestamp | timestamp | timestamp_text | +/// | TimestampWithTimeZone | timestamp | timestamp with time zone | timestamp_with_timezone_text | +/// | Time | time | time | time_text | +/// | Date | date | date | date_text | +/// | Year | year | N/A | N/A | +/// | Interval | N/A | interval | N/A | +/// | Blob | blob | bytea | blob | +/// | Binary | binary | bytea | blob | +/// | VarBinary | varbinary | bytea | varbinary_blob | +/// | Bit | bit | bit | N/A | +/// | VarBit | bit | varbit | N/A | +/// | Boolean | bool | bool | boolean | +/// | Money | decimal | money | real_money | +/// | Json | json | json | json_text | +/// | JsonBinary | json | jsonb | jsonb_text | +/// | Uuid | binary(16) | uuid | uuid_text | +/// | Enum | ENUM(...) | ENUM_NAME | enum_text | +/// | Array | N/A | DATA_TYPE[] | N/A | +/// | Vector | N/A | vector | N/A | +/// | Cidr | N/A | cidr | N/A | +/// | Inet | N/A | inet | N/A | +/// | MacAddr | N/A | macaddr | N/A | +/// | LTree | N/A | ltree | N/A | #[non_exhaustive] #[derive(Debug, Clone)] pub enum ColumnType { Char(Option), - String(Option), + String(StringLen), Text, + Blob, TinyInteger, SmallInteger, Integer, @@ -32,10 +78,10 @@ pub enum ColumnType { TimestampWithTimeZone, Time, Date, - Year(Option), + Year, Interval(Option, Option), - Binary(BlobSize), - VarBinary(u32), + Binary(u32), + VarBinary(StringLen), Bit(Option), VarBit(u32), Boolean, @@ -49,19 +95,29 @@ pub enum ColumnType { variants: Vec, }, Array(RcOrArc), + Vector(Option), Cidr, Inet, MacAddr, LTree, } +/// Length for var-char/binary; default to 255 +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum StringLen { + /// String size + N(u32), + Max, + #[default] + None, +} + impl PartialEq for ColumnType { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Char(l0), Self::Char(r0)) => l0 == r0, (Self::String(l0), Self::String(r0)) => l0 == r0, (Self::Decimal(l0), Self::Decimal(r0)) => l0 == r0, - (Self::Year(l0), Self::Year(r0)) => l0 == r0, (Self::Interval(l0, l1), Self::Interval(r0, r1)) => l0 == r0 && l1 == r1, (Self::Binary(l0), Self::Binary(r0)) => l0 == r0, (Self::VarBinary(l0), Self::VarBinary(r0)) => l0 == r0, @@ -98,6 +154,17 @@ impl ColumnType { { ColumnType::Custom(Alias::new(ty).into_iden()) } + + pub fn string(length: Option) -> ColumnType { + match length { + Some(s) => ColumnType::String(StringLen::N(s)), + None => ColumnType::String(StringLen::None), + } + } + + pub fn var_binary(length: u32) -> ColumnType { + ColumnType::VarBinary(StringLen::N(length)) + } } /// All column specification keywords @@ -133,22 +200,6 @@ pub enum PgInterval { MinuteToSecond, } -// All MySQL year type field length sizes -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum MySqlYear { - Two, - Four, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum BlobSize { - Tiny, - /// MySQL & SQLite support `binary(length)` column type - Blob(Option), - Medium, - Long, -} - impl ColumnDef { /// Construct a table column pub fn new(name: T) -> Self @@ -266,13 +317,13 @@ impl ColumnDef { /// Set column type as string with custom length pub fn string_len(&mut self, length: u32) -> &mut Self { - self.types = Some(ColumnType::String(Some(length))); + self.types = Some(ColumnType::String(StringLen::N(length))); self } /// Set column type as string pub fn string(&mut self) -> &mut Self { - self.types = Some(ColumnType::String(None)); + self.types = Some(ColumnType::String(Default::default())); self } @@ -405,6 +456,12 @@ impl ColumnDef { self } + #[cfg(feature = "postgres-vector")] + pub fn vector(&mut self, size: Option) -> &mut Self { + self.types = Some(ColumnType::Vector(size)); + self + } + /// Set column type as timestamp pub fn timestamp(&mut self) -> &mut Self { self.types = Some(ColumnType::Timestamp); @@ -431,32 +488,25 @@ impl ColumnDef { /// Set column type as year /// Only MySQL supports year - pub fn year(&mut self, length: Option) -> &mut Self { - self.types = Some(ColumnType::Year(length)); + pub fn year(&mut self) -> &mut Self { + self.types = Some(ColumnType::Year); self } /// Set column type as binary with custom length pub fn binary_len(&mut self, length: u32) -> &mut Self { - self.types = Some(ColumnType::Binary(BlobSize::Blob(Some(length)))); + self.types = Some(ColumnType::Binary(length)); self } - /// Set column type as binary + /// Set column type as binary with default length of 1 pub fn binary(&mut self) -> &mut Self { - self.types = Some(ColumnType::Binary(BlobSize::Blob(None))); - self - } - - /// Set column type as blob, but when given BlobSize::Blob(size) argument, this column map to binary(size) type instead. - pub fn blob(&mut self, size: BlobSize) -> &mut Self { - self.types = Some(ColumnType::Binary(size)); - self + self.binary_len(1) } /// Set column type as binary with variable length pub fn var_binary(&mut self, length: u32) -> &mut Self { - self.types = Some(ColumnType::VarBinary(length)); + self.types = Some(ColumnType::VarBinary(StringLen::N(length))); self } @@ -472,6 +522,12 @@ impl ColumnDef { self } + /// Set column type as blob + pub fn blob(&mut self) -> &mut Self { + self.types = Some(ColumnType::Blob); + self + } + /// Set column type as boolean pub fn boolean(&mut self) -> &mut Self { self.types = Some(ColumnType::Boolean); @@ -695,3 +751,15 @@ impl ColumnDef { } } } + +impl IntoColumnDef for &mut ColumnDef { + fn into_column_def(self) -> ColumnDef { + self.take() + } +} + +impl IntoColumnDef for ColumnDef { + fn into_column_def(self) -> ColumnDef { + self + } +} diff --git a/src/table/create.rs b/src/table/create.rs index 022e194..64d1932 100644 --- a/src/table/create.rs +++ b/src/table/create.rs @@ -1,8 +1,8 @@ use inherent::inherent; use crate::{ - backend::SchemaBuilder, foreign_key::*, index::*, types::*, ColumnDef, SchemaStatementBuilder, - SimpleExpr, + backend::SchemaBuilder, foreign_key::*, index::*, types::*, ColumnDef, IntoColumnDef, + SchemaStatementBuilder, SimpleExpr, }; /// Create a table @@ -70,7 +70,7 @@ use crate::{ /// r#"CREATE TABLE IF NOT EXISTS "character" ("#, /// r#""id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,"#, /// r#""font_size" integer NOT NULL,"#, -/// r#""character" text NOT NULL,"#, +/// r#""character" varchar NOT NULL,"#, /// r#""size_w" integer NOT NULL,"#, /// r#""size_h" integer NOT NULL,"#, /// r#""font_id" integer DEFAULT NULL,"#, @@ -136,9 +136,9 @@ impl TableCreateStatement { } /// Add a new table column - pub fn col(&mut self, column: &mut ColumnDef) -> &mut Self { - let mut column = column.take(); - column.table = self.table.clone(); + pub fn col(&mut self, column: C) -> &mut Self { + let mut column = column.into_column_def(); + column.table.clone_from(&self.table); self.columns.push(column); self } @@ -215,7 +215,7 @@ impl TableCreateStatement { /// [ /// r#"CREATE TABLE "glyph" ("#, /// r#""id" integer NOT NULL,"#, - /// r#""image" text NOT NULL,"#, + /// r#""image" varchar NOT NULL,"#, /// r#"PRIMARY KEY ("id", "image")"#, /// r#")"#, /// ] diff --git a/src/token.rs b/src/token.rs index 44ddf62..1d0ee97 100644 --- a/src/token.rs +++ b/src/token.rs @@ -454,7 +454,7 @@ mod tests { #[test] fn test_9() { - let string = r#"[ab] "#; + let string = r"[ab] "; let tokenizer = Tokenizer::new(string); let tokens: Vec = tokenizer.iter().collect(); assert_eq!( @@ -491,7 +491,7 @@ mod tests { #[test] fn test_11() { - let string = r#" `a``b` "#; + let string = r" `a``b` "; let tokenizer = Tokenizer::new(string); let tokens: Vec = tokenizer.iter().collect(); assert_eq!( @@ -510,7 +510,7 @@ mod tests { #[test] fn test_12() { - let string = r#" 'a''b' "#; + let string = r" 'a''b' "; let tokenizer = Tokenizer::new(string); let tokens: Vec = tokenizer.iter().collect(); assert_eq!( @@ -529,7 +529,7 @@ mod tests { #[test] fn test_13() { - let string = r#"(?)"#; + let string = r"(?)"; let tokenizer = Tokenizer::new(string); let tokens: Vec = tokenizer.iter().collect(); assert_eq!( @@ -548,7 +548,7 @@ mod tests { #[test] fn test_14() { - let string = r#"($1 = $2)"#; + let string = r"($1 = $2)"; let tokenizer = Tokenizer::new(string); let tokens: Vec = tokenizer.iter().collect(); assert_eq!( diff --git a/src/types.rs b/src/types.rs index 8cf814b..5ef4d11 100644 --- a/src/types.rs +++ b/src/types.rs @@ -181,6 +181,8 @@ pub enum BinOper { Mul, Div, Mod, + BitAnd, + BitOr, LShift, RShift, As, @@ -208,7 +210,6 @@ pub enum JoinType { LeftJoin, RightJoin, FullOuterJoin, - StraightJoin, } /// Nulls order @@ -242,7 +243,7 @@ pub enum Order { } /// Helper for create name alias -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Alias(String); /// Null Alias @@ -343,11 +344,11 @@ impl Quote { } pub fn left(&self) -> char { - char::try_from(self.0).unwrap() + char::from(self.0) } pub fn right(&self) -> char { - char::try_from(self.1).unwrap() + char::from(self.1) } } @@ -624,10 +625,7 @@ mod tests { .to_owned(); #[cfg(feature = "backend-mysql")] - assert_eq!( - query.to_string(MysqlQueryBuilder), - r#"SELECT `hello-World_`"# - ); + assert_eq!(query.to_string(MysqlQueryBuilder), r"SELECT `hello-World_`"); #[cfg(feature = "backend-postgres")] assert_eq!( query.to_string(PostgresQueryBuilder), @@ -645,7 +643,7 @@ mod tests { let query = Query::select().column(Alias::new("hel`lo")).to_owned(); #[cfg(feature = "backend-mysql")] - assert_eq!(query.to_string(MysqlQueryBuilder), r#"SELECT `hel``lo`"#); + assert_eq!(query.to_string(MysqlQueryBuilder), r"SELECT `hel``lo`"); #[cfg(feature = "backend-sqlite")] assert_eq!(query.to_string(SqliteQueryBuilder), r#"SELECT "hel`lo""#); @@ -660,7 +658,7 @@ mod tests { let query = Query::select().column(Alias::new("hel``lo")).to_owned(); #[cfg(feature = "backend-mysql")] - assert_eq!(query.to_string(MysqlQueryBuilder), r#"SELECT `hel````lo`"#); + assert_eq!(query.to_string(MysqlQueryBuilder), r"SELECT `hel````lo`"); #[cfg(feature = "backend-sqlite")] assert_eq!(query.to_string(SqliteQueryBuilder), r#"SELECT "hel``lo""#); diff --git a/src/value.rs b/src/value.rs index 7049d74..7a7ac30 100644 --- a/src/value.rs +++ b/src/value.rs @@ -31,7 +31,7 @@ use std::net::IpAddr; #[cfg(feature = "with-mac_address")] use mac_address::MacAddress; -use crate::{BlobSize, ColumnType, CommonSqlQueryBuilder, QueryBuilder}; +use crate::{ColumnType, CommonSqlQueryBuilder, QueryBuilder, StringLen}; /// [`Value`] types variant for Postgres array #[derive(Clone, Debug, Eq, PartialEq, Hash)] @@ -124,11 +124,6 @@ pub enum ArrayType { /// implementation of NaN != NaN. #[derive(Clone, Debug)] #[cfg_attr(not(feature = "hashable-value"), derive(PartialEq))] -#[cfg_attr( - feature = "hashable-value", - derive(derivative::Derivative), - derivative(Hash, PartialEq, Eq) -)] pub enum Value { Bool(Option), TinyInt(Option), @@ -139,26 +134,8 @@ pub enum Value { SmallUnsigned(Option), Unsigned(Option), BigUnsigned(Option), - Float( - #[cfg_attr( - feature = "hashable-value", - derivative( - Hash(hash_with = "hashable_value::hash_f32"), - PartialEq(compare_with = "hashable_value::cmp_f32") - ) - )] - Option, - ), - Double( - #[cfg_attr( - feature = "hashable-value", - derivative( - Hash(hash_with = "hashable_value::hash_f64"), - PartialEq(compare_with = "hashable_value::cmp_f64") - ) - )] - Option, - ), + Float(Option), + Double(Option), String(Option>), Char(Option), @@ -167,16 +144,7 @@ pub enum Value { #[cfg(feature = "with-json")] #[cfg_attr(docsrs, doc(cfg(feature = "with-json")))] - Json( - #[cfg_attr( - feature = "hashable-value", - derivative( - Hash(hash_with = "hashable_value::hash_json"), - PartialEq(compare_with = "hashable_value::cmp_json") - ) - )] - Option>, - ), + Json(Option>), #[cfg(feature = "with-chrono")] #[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))] @@ -234,6 +202,10 @@ pub enum Value { #[cfg_attr(docsrs, doc(cfg(feature = "postgres-array")))] Array(ArrayType, Option>>), + #[cfg(feature = "postgres-vector")] + #[cfg_attr(docsrs, doc(cfg(feature = "postgres-vector")))] + Vector(Option>), + #[cfg(feature = "with-ipnetwork")] #[cfg_attr(docsrs, doc(cfg(feature = "with-ipnetwork")))] IpNetwork(Option>), @@ -265,6 +237,10 @@ pub trait ValueType: Sized { fn array_type() -> ArrayType; fn column_type() -> ColumnType; + + fn enum_type_name() -> Option<&'static str> { + None + } } #[derive(Debug)] @@ -318,6 +294,107 @@ impl Value { { T::expect(self, msg) } + + /// Get the null variant of self + /// + /// ``` + /// use sea_query::Value; + /// + /// let v = Value::Int(Some(2)); + /// let n = v.as_null(); + /// + /// assert_eq!(n, Value::Int(None)); + /// ``` + pub fn as_null(&self) -> Self { + match self { + Self::Bool(_) => Self::Bool(None), + Self::TinyInt(_) => Self::TinyInt(None), + Self::SmallInt(_) => Self::SmallInt(None), + Self::Int(_) => Self::Int(None), + Self::BigInt(_) => Self::BigInt(None), + Self::TinyUnsigned(_) => Self::TinyUnsigned(None), + Self::SmallUnsigned(_) => Self::SmallUnsigned(None), + Self::Unsigned(_) => Self::Unsigned(None), + Self::BigUnsigned(_) => Self::BigUnsigned(None), + Self::Float(_) => Self::Float(None), + Self::Double(_) => Self::Double(None), + Self::String(_) => Self::String(None), + Self::Char(_) => Self::Char(None), + Self::Bytes(_) => Self::Bytes(None), + + #[cfg(feature = "with-json")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-json")))] + Self::Json(_) => Self::Json(None), + + #[cfg(feature = "with-chrono")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))] + Self::ChronoDate(_) => Self::ChronoDate(None), + + #[cfg(feature = "with-chrono")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))] + Self::ChronoTime(_) => Self::ChronoTime(None), + + #[cfg(feature = "with-chrono")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))] + Self::ChronoDateTime(_) => Self::ChronoDateTime(None), + + #[cfg(feature = "with-chrono")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))] + Self::ChronoDateTimeUtc(_) => Self::ChronoDateTimeUtc(None), + + #[cfg(feature = "with-chrono")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))] + Self::ChronoDateTimeLocal(_) => Self::ChronoDateTimeLocal(None), + + #[cfg(feature = "with-chrono")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))] + Self::ChronoDateTimeWithTimeZone(_) => Self::ChronoDateTimeWithTimeZone(None), + + #[cfg(feature = "with-time")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-time")))] + Self::TimeDate(_) => Self::TimeDate(None), + + #[cfg(feature = "with-time")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-time")))] + Self::TimeTime(_) => Self::TimeTime(None), + + #[cfg(feature = "with-time")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-time")))] + Self::TimeDateTime(_) => Self::TimeDateTime(None), + + #[cfg(feature = "with-time")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-time")))] + Self::TimeDateTimeWithTimeZone(_) => Self::TimeDateTimeWithTimeZone(None), + + #[cfg(feature = "with-uuid")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-uuid")))] + Self::Uuid(_) => Self::Uuid(None), + + #[cfg(feature = "with-rust_decimal")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-rust_decimal")))] + Self::Decimal(_) => Self::Decimal(None), + + #[cfg(feature = "with-bigdecimal")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-bigdecimal")))] + Self::BigDecimal(_) => Self::BigDecimal(None), + + #[cfg(feature = "postgres-array")] + #[cfg_attr(docsrs, doc(cfg(feature = "postgres-array")))] + Self::Array(ty, _) => Self::Array(ty.clone(), None), + + #[cfg(feature = "postgres-vector")] + #[cfg_attr(docsrs, doc(cfg(feature = "postgres-vector")))] + Self::Vector(_) => Self::Vector(None), + + #[cfg(feature = "with-ipnetwork")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-ipnetwork")))] + Self::IpNetwork(_) => Self::IpNetwork(None), + + #[cfg(feature = "with-mac_address")] + #[cfg_attr(docsrs, doc(cfg(feature = "with-mac_address")))] + Self::MacAddress(_) => Self::MacAddress(None), + } + } } macro_rules! type_to_value { @@ -495,12 +572,12 @@ impl ValueType for Cow<'_, str> { } fn column_type() -> ColumnType { - ColumnType::String(None) + ColumnType::String(StringLen::None) } } -type_to_box_value!(Vec, Bytes, Binary(BlobSize::Blob(None))); -type_to_box_value!(String, String, String(None)); +type_to_box_value!(Vec, Bytes, VarBinary(StringLen::None)); +type_to_box_value!(String, String, String(StringLen::None)); #[cfg(feature = "with-json")] #[cfg_attr(docsrs, doc(cfg(feature = "with-json")))] @@ -893,6 +970,45 @@ pub mod with_array { } } +#[cfg(feature = "postgres-vector")] +#[cfg_attr(docsrs, doc(cfg(feature = "postgres-vector")))] +pub mod with_vector { + use super::*; + + impl From for Value { + fn from(x: pgvector::Vector) -> Value { + Value::Vector(Some(Box::new(x))) + } + } + + impl Nullable for pgvector::Vector { + fn null() -> Value { + Value::Vector(None) + } + } + + impl ValueType for pgvector::Vector { + fn try_from(v: Value) -> Result { + match v { + Value::Vector(Some(x)) => Ok(*x), + _ => Err(ValueTypeErr), + } + } + + fn type_name() -> String { + stringify!(Vector).to_owned() + } + + fn array_type() -> ArrayType { + unimplemented!("Vector does not have array type") + } + + fn column_type() -> ColumnType { + ColumnType::Vector(None) + } + } +} + #[allow(unused_macros)] macro_rules! box_to_opt_ref { ( $v: expr ) => { @@ -1392,6 +1508,8 @@ pub fn sea_value_to_json_value(value: &Value) -> Json { Value::Uuid(None) => Json::Null, #[cfg(feature = "postgres-array")] Value::Array(_, None) => Json::Null, + #[cfg(feature = "postgres-vector")] + Value::Vector(None) => Json::Null, #[cfg(feature = "with-ipnetwork")] Value::IpNetwork(None) => Json::Null, #[cfg(feature = "with-mac_address")] @@ -1447,6 +1565,8 @@ pub fn sea_value_to_json_value(value: &Value) -> Json { Value::Array(_, Some(v)) => { Json::Array(v.as_ref().iter().map(sea_value_to_json_value).collect()) } + #[cfg(feature = "postgres-vector")] + Value::Vector(Some(v)) => Json::Array(v.as_slice().iter().map(|&v| v.into()).collect()), #[cfg(feature = "with-ipnetwork")] Value::IpNetwork(Some(_)) => CommonSqlQueryBuilder.value_to_string(value).into(), #[cfg(feature = "with-mac_address")] @@ -1918,23 +2038,172 @@ mod tests { mod hashable_value { use super::*; use ordered_float::OrderedFloat; - use std::hash::{Hash, Hasher}; + use std::{ + hash::{Hash, Hasher}, + mem, + }; + + impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Bool(l), Self::Bool(r)) => l == r, + (Self::TinyInt(l), Self::TinyInt(r)) => l == r, + (Self::SmallInt(l), Self::SmallInt(r)) => l == r, + (Self::Int(l), Self::Int(r)) => l == r, + (Self::BigInt(l), Self::BigInt(r)) => l == r, + (Self::TinyUnsigned(l), Self::TinyUnsigned(r)) => l == r, + (Self::SmallUnsigned(l), Self::SmallUnsigned(r)) => l == r, + (Self::Unsigned(l), Self::Unsigned(r)) => l == r, + (Self::BigUnsigned(l), Self::BigUnsigned(r)) => l == r, + (Self::Float(l), Self::Float(r)) => cmp_f32(l, r), + (Self::Double(l), Self::Double(r)) => cmp_f64(l, r), + (Self::String(l), Self::String(r)) => l == r, + (Self::Char(l), Self::Char(r)) => l == r, + (Self::Bytes(l), Self::Bytes(r)) => l == r, + + #[cfg(feature = "with-json")] + (Self::Json(l), Self::Json(r)) => cmp_json(l, r), + + #[cfg(feature = "with-chrono")] + (Self::ChronoDate(l), Self::ChronoDate(r)) => l == r, + #[cfg(feature = "with-chrono")] + (Self::ChronoTime(l), Self::ChronoTime(r)) => l == r, + #[cfg(feature = "with-chrono")] + (Self::ChronoDateTime(l), Self::ChronoDateTime(r)) => l == r, + #[cfg(feature = "with-chrono")] + (Self::ChronoDateTimeUtc(l), Self::ChronoDateTimeUtc(r)) => l == r, + #[cfg(feature = "with-chrono")] + (Self::ChronoDateTimeLocal(l), Self::ChronoDateTimeLocal(r)) => l == r, + #[cfg(feature = "with-chrono")] + (Self::ChronoDateTimeWithTimeZone(l), Self::ChronoDateTimeWithTimeZone(r)) => { + l == r + } + + #[cfg(feature = "with-time")] + (Self::TimeDate(l), Self::TimeDate(r)) => l == r, + #[cfg(feature = "with-time")] + (Self::TimeTime(l), Self::TimeTime(r)) => l == r, + #[cfg(feature = "with-time")] + (Self::TimeDateTime(l), Self::TimeDateTime(r)) => l == r, + #[cfg(feature = "with-time")] + (Self::TimeDateTimeWithTimeZone(l), Self::TimeDateTimeWithTimeZone(r)) => l == r, + + #[cfg(feature = "with-uuid")] + (Self::Uuid(l), Self::Uuid(r)) => l == r, + + #[cfg(feature = "with-rust_decimal")] + (Self::Decimal(l), Self::Decimal(r)) => l == r, + + #[cfg(feature = "with-bigdecimal")] + (Self::BigDecimal(l), Self::BigDecimal(r)) => l == r, + + #[cfg(feature = "postgres-array")] + (Self::Array(ty_l, values_l), Self::Array(ty_r, values_r)) => { + ty_l == ty_r && values_l == values_r + } + + #[cfg(feature = "postgres-vector")] + (Self::Vector(l), Self::Vector(r)) => cmp_vector(l, r), + + #[cfg(feature = "with-ipnetwork")] + (Self::IpNetwork(l), Self::IpNetwork(r)) => l == r, + + #[cfg(feature = "with-mac_address")] + (Self::MacAddress(l), Self::MacAddress(r)) => l == r, + + _ => false, + } + } + } + + impl Eq for Value {} + + impl Hash for Value { + fn hash(&self, state: &mut H) { + mem::discriminant(self).hash(state); + match self { + Value::Bool(v) => v.hash(state), + Value::TinyInt(v) => v.hash(state), + Value::SmallInt(v) => v.hash(state), + Value::Int(v) => v.hash(state), + Value::BigInt(v) => v.hash(state), + Value::TinyUnsigned(v) => v.hash(state), + Value::SmallUnsigned(v) => v.hash(state), + Value::Unsigned(v) => v.hash(state), + Value::BigUnsigned(v) => v.hash(state), + Value::Float(v) => hash_f32(v, state), + Value::Double(v) => hash_f64(v, state), + Value::String(v) => v.hash(state), + Value::Char(v) => v.hash(state), + Value::Bytes(v) => v.hash(state), + + #[cfg(feature = "with-json")] + Value::Json(value) => hash_json(value, state), + + #[cfg(feature = "with-chrono")] + Value::ChronoDate(naive_date) => naive_date.hash(state), + #[cfg(feature = "with-chrono")] + Value::ChronoTime(naive_time) => naive_time.hash(state), + #[cfg(feature = "with-chrono")] + Value::ChronoDateTime(naive_date_time) => naive_date_time.hash(state), + #[cfg(feature = "with-chrono")] + Value::ChronoDateTimeUtc(date_time) => date_time.hash(state), + #[cfg(feature = "with-chrono")] + Value::ChronoDateTimeLocal(date_time) => date_time.hash(state), + #[cfg(feature = "with-chrono")] + Value::ChronoDateTimeWithTimeZone(date_time) => date_time.hash(state), + + #[cfg(feature = "with-time")] + Value::TimeDate(date) => date.hash(state), + #[cfg(feature = "with-time")] + Value::TimeTime(time) => time.hash(state), + #[cfg(feature = "with-time")] + Value::TimeDateTime(primitive_date_time) => primitive_date_time.hash(state), + #[cfg(feature = "with-time")] + Value::TimeDateTimeWithTimeZone(offset_date_time) => offset_date_time.hash(state), + + #[cfg(feature = "with-uuid")] + Value::Uuid(uuid) => uuid.hash(state), + + #[cfg(feature = "with-rust_decimal")] + Value::Decimal(decimal) => decimal.hash(state), + + #[cfg(feature = "with-bigdecimal")] + Value::BigDecimal(big_decimal) => big_decimal.hash(state), + + #[cfg(feature = "postgres-array")] + Value::Array(array_type, vec) => { + array_type.hash(state); + vec.hash(state); + } - pub fn hash_f32(v: &Option, state: &mut H) { + #[cfg(feature = "postgres-vector")] + Value::Vector(vector) => hash_vector(vector, state), + + #[cfg(feature = "with-ipnetwork")] + Value::IpNetwork(ip_network) => ip_network.hash(state), + + #[cfg(feature = "with-mac_address")] + Value::MacAddress(mac_address) => mac_address.hash(state), + } + } + } + + fn hash_f32(v: &Option, state: &mut H) { match v { Some(v) => OrderedFloat(*v).hash(state), None => "null".hash(state), } } - pub fn hash_f64(v: &Option, state: &mut H) { + fn hash_f64(v: &Option, state: &mut H) { match v { Some(v) => OrderedFloat(*v).hash(state), None => "null".hash(state), } } - pub fn cmp_f32(l: &Option, r: &Option) -> bool { + fn cmp_f32(l: &Option, r: &Option) -> bool { match (l, r) { (Some(l), Some(r)) => OrderedFloat(*l).eq(&OrderedFloat(*r)), (None, None) => true, @@ -1942,7 +2211,7 @@ mod hashable_value { } } - pub fn cmp_f64(l: &Option, r: &Option) -> bool { + fn cmp_f64(l: &Option, r: &Option) -> bool { match (l, r) { (Some(l), Some(r)) => OrderedFloat(*l).eq(&OrderedFloat(*r)), (None, None) => true, @@ -1951,7 +2220,7 @@ mod hashable_value { } #[cfg(feature = "with-json")] - pub fn hash_json(v: &Option>, state: &mut H) { + fn hash_json(v: &Option>, state: &mut H) { match v { Some(v) => serde_json::to_string(v).unwrap().hash(state), None => "null".hash(state), @@ -1959,7 +2228,7 @@ mod hashable_value { } #[cfg(feature = "with-json")] - pub fn cmp_json(l: &Option>, r: &Option>) -> bool { + fn cmp_json(l: &Option>, r: &Option>) -> bool { match (l, r) { (Some(l), Some(r)) => serde_json::to_string(l) .unwrap() @@ -1969,6 +2238,38 @@ mod hashable_value { } } + #[cfg(feature = "postgres-vector")] + fn hash_vector(v: &Option>, state: &mut H) { + match v { + Some(v) => { + for &value in v.as_slice().iter() { + hash_f32(&Some(value), state); + } + } + None => "null".hash(state), + } + } + + #[cfg(feature = "postgres-vector")] + fn cmp_vector(l: &Option>, r: &Option>) -> bool { + match (l, r) { + (Some(l), Some(r)) => { + let (l, r) = (l.as_slice(), r.as_slice()); + if l.len() != r.len() { + return false; + } + for (l, r) in l.iter().zip(r.iter()) { + if !cmp_f32(&Some(*l), &Some(*r)) { + return false; + } + } + true + } + (None, None) => true, + _ => false, + } + } + #[test] fn test_hash_value_0() { let hash_set: std::collections::HashSet = [ diff --git a/tests/common.rs b/tests/common.rs index 65a24eb..a3f9086 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -11,6 +11,7 @@ use sea_query::Iden; /// /// [`Iden`]: crate::types::Iden #[derive(Debug)] +#[allow(dead_code)] pub enum BinaryType { Table, BinaryLen, diff --git a/tests/mysql/mod.rs b/tests/mysql/mod.rs index fc7388c..d717774 100644 --- a/tests/mysql/mod.rs +++ b/tests/mysql/mod.rs @@ -1,4 +1,4 @@ -use sea_query::{tests_cfg::*, *}; +use sea_query::{extension::mysql::*, tests_cfg::*, *}; mod foreign_key; mod index; diff --git a/tests/mysql/query.rs b/tests/mysql/query.rs index 4994ab1..3e5344a 100644 --- a/tests/mysql/query.rs +++ b/tests/mysql/query.rs @@ -518,7 +518,7 @@ fn select_35() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE `aspect` IS NULL"# + r"SELECT `id` FROM `glyph` WHERE `aspect` IS NULL" ); assert_eq!(values.0, vec![]); } @@ -533,7 +533,7 @@ fn select_36() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE `aspect` IS NULL"# + r"SELECT `id` FROM `glyph` WHERE `aspect` IS NULL" ); assert_eq!(values.0, vec![]); } @@ -546,7 +546,7 @@ fn select_37() { .cond_where(Cond::any().add(Cond::all()).add(Cond::any())) .build(MysqlQueryBuilder); - assert_eq!(statement, r#"SELECT `id` FROM `glyph` WHERE TRUE OR FALSE"#); + assert_eq!(statement, r"SELECT `id` FROM `glyph` WHERE TRUE OR FALSE"); assert_eq!(values.0, vec![]); } @@ -565,7 +565,7 @@ fn select_37a() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE NOT ((NOT TRUE) AND (NOT FALSE))"# + r"SELECT `id` FROM `glyph` WHERE NOT ((NOT TRUE) AND (NOT FALSE))" ); assert_eq!(values.0, vec![]); } @@ -584,7 +584,7 @@ fn select_38() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE `aspect` IS NULL OR `aspect` IS NOT NULL"# + r"SELECT `id` FROM `glyph` WHERE `aspect` IS NULL OR `aspect` IS NOT NULL" ); assert_eq!(values.0, vec![]); } @@ -603,7 +603,7 @@ fn select_39() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE `aspect` IS NULL AND `aspect` IS NOT NULL"# + r"SELECT `id` FROM `glyph` WHERE `aspect` IS NULL AND `aspect` IS NOT NULL" ); assert_eq!(values.0, vec![]); } @@ -624,7 +624,7 @@ fn select_40() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE `aspect` IS NULL OR (`aspect` IS NOT NULL AND `aspect` < 8)"# + r"SELECT `id` FROM `glyph` WHERE `aspect` IS NULL OR (`aspect` IS NOT NULL AND `aspect` < 8)" ); } @@ -656,7 +656,7 @@ fn select_42() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE `aspect` < 8 AND `aspect` IS NOT NULL"# + r"SELECT `id` FROM `glyph` WHERE `aspect` < 8 AND `aspect` IS NOT NULL" ); } @@ -685,7 +685,7 @@ fn select_44() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE NOT `aspect` < 8"# + r"SELECT `id` FROM `glyph` WHERE NOT `aspect` < 8" ); } @@ -704,7 +704,7 @@ fn select_45() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE NOT (`aspect` < 8 OR `aspect` IS NOT NULL)"# + r"SELECT `id` FROM `glyph` WHERE NOT (`aspect` < 8 OR `aspect` IS NOT NULL)" ); } @@ -722,7 +722,7 @@ fn select_46() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE NOT `aspect` < 8"# + r"SELECT `id` FROM `glyph` WHERE NOT `aspect` < 8" ); } @@ -741,7 +741,7 @@ fn select_47() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE NOT (`aspect` < 8 AND `aspect` IS NOT NULL)"# + r"SELECT `id` FROM `glyph` WHERE NOT (`aspect` < 8 AND `aspect` IS NOT NULL)" ); } @@ -760,7 +760,7 @@ fn select_48() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE (`aspect`, 100) < (8, 100)"# + r"SELECT `id` FROM `glyph` WHERE (`aspect`, 100) < (8, 100)" ); } @@ -782,7 +782,7 @@ fn select_48a() { assert_eq!( statement, - r#"SELECT `id` FROM `glyph` WHERE (`aspect`, '100') IN ((8, '100'))"# + r"SELECT `id` FROM `glyph` WHERE (`aspect`, '100') IN ((8, '100'))" ); } @@ -793,7 +793,7 @@ fn select_49() { .from(Char::Table) .to_string(MysqlQueryBuilder); - assert_eq!(statement, r#"SELECT * FROM `character`"#); + assert_eq!(statement, r"SELECT * FROM `character`"); } #[test] @@ -810,7 +810,7 @@ fn select_50() { assert_eq!( statement, - r#"SELECT `character`.*, `font`.`name` FROM `character` INNER JOIN `font` ON `character`.`font_id` = `font`.`id`"# + r"SELECT `character`.*, `font`.`name` FROM `character` INNER JOIN `font` ON `character`.`font_id` = `font`.`id`" ) } @@ -829,13 +829,13 @@ fn select_51() { ) .to_string(MysqlQueryBuilder), [ - r#"SELECT `aspect`"#, - r#"FROM `glyph`"#, - r#"WHERE IFNULL(`aspect`, 0) > 2"#, - r#"ORDER BY `image` IS NULL DESC,"#, - r#"`image` DESC,"#, - r#"`glyph`.`aspect` IS NULL ASC,"#, - r#"`glyph`.`aspect` ASC"#, + r"SELECT `aspect`", + r"FROM `glyph`", + r"WHERE IFNULL(`aspect`, 0) > 2", + r"ORDER BY `image` IS NULL DESC,", + r"`image` DESC,", + r"`glyph`.`aspect` IS NULL ASC,", + r"`glyph`.`aspect` ASC", ] .join(" ") ); @@ -854,13 +854,13 @@ fn select_52() { ]) .to_string(MysqlQueryBuilder), [ - r#"SELECT `aspect`"#, - r#"FROM `glyph`"#, - r#"WHERE IFNULL(`aspect`, 0) > 2"#, - r#"ORDER BY `id` IS NULL DESC,"#, - r#"`id` ASC,"#, - r#"`aspect` IS NULL ASC,"#, - r#"`aspect` DESC"#, + r"SELECT `aspect`", + r"FROM `glyph`", + r"WHERE IFNULL(`aspect`, 0) > 2", + r"ORDER BY `id` IS NULL DESC,", + r"`id` ASC,", + r"`aspect` IS NULL ASC,", + r"`aspect` DESC", ] .join(" ") ); @@ -883,13 +883,13 @@ fn select_53() { ]) .to_string(MysqlQueryBuilder), [ - r#"SELECT `aspect`"#, - r#"FROM `glyph`"#, - r#"WHERE IFNULL(`aspect`, 0) > 2"#, - r#"ORDER BY `glyph`.`id` IS NULL DESC,"#, - r#"`glyph`.`id` ASC,"#, - r#"`glyph`.`aspect` IS NULL ASC,"#, - r#"`glyph`.`aspect` DESC"#, + r"SELECT `aspect`", + r"FROM `glyph`", + r"WHERE IFNULL(`aspect`, 0) > 2", + r"ORDER BY `glyph`.`id` IS NULL DESC,", + r"`glyph`.`id` ASC,", + r"`glyph`.`aspect` IS NULL ASC,", + r"`glyph`.`aspect` DESC", ] .join(" ") ); @@ -906,7 +906,7 @@ fn select_54() { assert_eq!( statement, - r#"SELECT * FROM `character`, `font` WHERE `font`.`id` = `character`.`font_id`"# + r"SELECT * FROM `character`, `font` WHERE `font`.`id` = `character`.`font_id`" ); } @@ -924,16 +924,16 @@ fn select_55() { .order_by((Glyph::Table, Glyph::Aspect), Order::Asc) .to_string(MysqlQueryBuilder), [ - r#"SELECT `aspect`"#, - r#"FROM `glyph`"#, - r#"WHERE IFNULL(`aspect`, 0) > 2"#, - r#"ORDER BY CASE"#, - r#"WHEN `id`=4 THEN 0"#, - r#"WHEN `id`=5 THEN 1"#, - r#"WHEN `id`=1 THEN 2"#, - r#"WHEN `id`=3 THEN 3"#, - r#"ELSE 4 END,"#, - r#"`glyph`.`aspect` ASC"#, + r"SELECT `aspect`", + r"FROM `glyph`", + r"WHERE IFNULL(`aspect`, 0) > 2", + r"ORDER BY CASE", + r"WHEN `id`=4 THEN 0", + r"WHEN `id`=5 THEN 1", + r"WHEN `id`=1 THEN 2", + r"WHEN `id`=3 THEN 3", + r"ELSE 4 END,", + r"`glyph`.`aspect` ASC", ] .join(" ") ); @@ -953,15 +953,15 @@ fn select_56() { ) .to_string(MysqlQueryBuilder), [ - r#"SELECT `aspect`"#, - r#"FROM `glyph`"#, - r#"WHERE IFNULL(`aspect`, 0) > 2"#, - r#"ORDER BY `glyph`.`aspect` ASC,"#, - r#"CASE WHEN `id`=4 THEN 0"#, - r#"WHEN `id`=5 THEN 1"#, - r#"WHEN `id`=1 THEN 2"#, - r#"WHEN `id`=3 THEN 3"#, - r#"ELSE 4 END"#, + r"SELECT `aspect`", + r"FROM `glyph`", + r"WHERE IFNULL(`aspect`, 0) > 2", + r"ORDER BY `glyph`.`aspect` ASC,", + r"CASE WHEN `id`=4 THEN 0", + r"WHEN `id`=5 THEN 1", + r"WHEN `id`=1 THEN 2", + r"WHEN `id`=3 THEN 3", + r"ELSE 4 END", ] .join(" ") ); @@ -982,7 +982,7 @@ fn select_57() { assert_eq!( query.to_string(MysqlQueryBuilder), - r#"SELECT (CASE WHEN (`glyph`.`aspect` > 0) THEN 'positive' WHEN (`glyph`.`aspect` < 0) THEN 'negative' ELSE 'zero' END) AS `polarity` FROM `glyph`"# + r"SELECT (CASE WHEN (`glyph`.`aspect` > 0) THEN 'positive' WHEN (`glyph`.`aspect` < 0) THEN 'negative' ELSE 'zero' END) AS `polarity` FROM `glyph`" ); } @@ -1049,18 +1049,28 @@ fn select_61() { .offset(100) .to_string(MysqlQueryBuilder), [ - r#"SELECT `character`, `size_w`, `size_h`"#, - r#"FROM `character`"#, - r#"IGNORE INDEX FOR JOIN (`IDX_123456`)"#, - r#"USE INDEX FOR GROUP BY (`IDX_789ABC`)"#, - r#"FORCE INDEX FOR ORDER BY (`IDX_DEFGHI`)"#, - r#"LIMIT 10"#, - r#"OFFSET 100"#, + r"SELECT `character`, `size_w`, `size_h`", + r"FROM `character`", + r"IGNORE INDEX FOR JOIN (`IDX_123456`)", + r"USE INDEX FOR GROUP BY (`IDX_789ABC`)", + r"FORCE INDEX FOR ORDER BY (`IDX_DEFGHI`)", + r"LIMIT 10", + r"OFFSET 100", ] .join(" ") ); } +#[test] +fn md5_fn() { + assert_eq!( + Query::select() + .expr(Func::md5(Expr::val("test"))) + .to_string(MysqlQueryBuilder), + r#"SELECT MD5('test')"# + ); +} + #[test] #[allow(clippy::approx_constant)] fn insert_2() { @@ -1193,7 +1203,7 @@ fn insert_from_select() { .unwrap() .to_owned() .to_string(MysqlQueryBuilder), - r#"INSERT INTO `glyph` (`aspect`, `image`) SELECT `aspect`, `image` FROM `glyph` WHERE `image` LIKE '%'"# + r"INSERT INTO `glyph` (`aspect`, `image`) SELECT `aspect`, `image` FROM `glyph` WHERE `image` LIKE '%'" ); } @@ -1215,9 +1225,9 @@ fn insert_on_conflict_0() { ) .to_string(MysqlQueryBuilder), [ - r#"INSERT INTO `glyph` (`aspect`, `image`)"#, - r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, - r#"ON DUPLICATE KEY UPDATE `aspect` = VALUES(`aspect`), `image` = VALUES(`image`)"#, + r"INSERT INTO `glyph` (`aspect`, `image`)", + r"VALUES ('04108048005887010020060000204E0180400400', 3.1415)", + r"ON DUPLICATE KEY UPDATE `aspect` = VALUES(`aspect`), `image` = VALUES(`image`)", ] .join(" ") ); @@ -1241,9 +1251,9 @@ fn insert_on_conflict_1() { ) .to_string(MysqlQueryBuilder), [ - r#"INSERT INTO `glyph` (`aspect`, `image`)"#, - r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, - r#"ON DUPLICATE KEY UPDATE `aspect` = VALUES(`aspect`)"#, + r"INSERT INTO `glyph` (`aspect`, `image`)", + r"VALUES ('04108048005887010020060000204E0180400400', 3.1415)", + r"ON DUPLICATE KEY UPDATE `aspect` = VALUES(`aspect`)", ] .join(" ") ); @@ -1267,9 +1277,9 @@ fn insert_on_conflict_2() { ) .to_string(MysqlQueryBuilder), [ - r#"INSERT INTO `glyph` (`aspect`, `image`)"#, - r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, - r#"ON DUPLICATE KEY UPDATE `aspect` = VALUES(`aspect`), `image` = VALUES(`image`)"#, + r"INSERT INTO `glyph` (`aspect`, `image`)", + r"VALUES ('04108048005887010020060000204E0180400400', 3.1415)", + r"ON DUPLICATE KEY UPDATE `aspect` = VALUES(`aspect`), `image` = VALUES(`image`)", ] .join(" ") ); @@ -1296,9 +1306,9 @@ fn insert_on_conflict_3() { ) .to_string(MysqlQueryBuilder), [ - r#"INSERT INTO `glyph` (`aspect`, `image`)"#, - r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, - r#"ON DUPLICATE KEY UPDATE `aspect` = '04108048005887010020060000204E0180400400', `image` = 3.1415"#, + r"INSERT INTO `glyph` (`aspect`, `image`)", + r"VALUES ('04108048005887010020060000204E0180400400', 3.1415)", + r"ON DUPLICATE KEY UPDATE `aspect` = '04108048005887010020060000204E0180400400', `image` = 3.1415", ] .join(" ") ); @@ -1322,9 +1332,9 @@ fn insert_on_conflict_4() { ) .to_string(MysqlQueryBuilder), [ - r#"INSERT INTO `glyph` (`aspect`, `image`)"#, - r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, - r#"ON DUPLICATE KEY UPDATE `image` = 1 + 2"#, + r"INSERT INTO `glyph` (`aspect`, `image`)", + r"VALUES ('04108048005887010020060000204E0180400400', 3.1415)", + r"ON DUPLICATE KEY UPDATE `image` = 1 + 2", ] .join(" ") ); @@ -1349,9 +1359,9 @@ fn insert_on_conflict_5() { ) .to_string(MysqlQueryBuilder), [ - r#"INSERT INTO `glyph` (`aspect`, `image`)"#, - r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, - r#"ON DUPLICATE KEY UPDATE `aspect` = '04108048005887010020060000204E0180400400', `image` = VALUES(`image`)"#, + r"INSERT INTO `glyph` (`aspect`, `image`)", + r"VALUES ('04108048005887010020060000204E0180400400', 3.1415)", + r"ON DUPLICATE KEY UPDATE `aspect` = '04108048005887010020060000204E0180400400', `image` = VALUES(`image`)", ] .join(" ") ); @@ -1376,9 +1386,32 @@ fn insert_on_conflict_6() { ) .to_string(MysqlQueryBuilder), [ - r#"INSERT INTO `glyph` (`aspect`, `image`)"#, - r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, - r#"ON DUPLICATE KEY UPDATE `aspect` = VALUES(`aspect`), `image` = 1 + 2"#, + r"INSERT INTO `glyph` (`aspect`, `image`)", + r"VALUES ('04108048005887010020060000204E0180400400', 3.1415)", + r"ON DUPLICATE KEY UPDATE `aspect` = VALUES(`aspect`), `image` = 1 + 2", + ] + .join(" ") + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_on_conflict_do_nothing_on() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns([Glyph::Aspect, Glyph::Image]) + .values_panic(["abcd".into(), 3.1415.into()]) + .on_conflict( + OnConflict::columns([Glyph::Id, Glyph::Aspect]) + .do_nothing_on([Glyph::Id]) + .to_owned(), + ) + .to_string(MysqlQueryBuilder), + [ + r"INSERT INTO `glyph` (`aspect`, `image`)", + r"VALUES ('abcd', 3.1415)", + r"ON DUPLICATE KEY UPDATE `id` = `id`", ] .join(" ") ); diff --git a/tests/mysql/table.rs b/tests/mysql/table.rs index 213d67b..5f71f4e 100644 --- a/tests/mysql/table.rs +++ b/tests/mysql/table.rs @@ -165,18 +165,17 @@ fn create_6() { .table(BinaryType::Table) .col(ColumnDef::new(BinaryType::BinaryLen).binary_len(32)) .col(ColumnDef::new(BinaryType::Binary).binary()) - .col(ColumnDef::new(BinaryType::BlobSize).blob(BlobSize::Blob(Some(32)))) - .col(ColumnDef::new(BinaryType::TinyBlob).blob(BlobSize::Tiny)) - .col(ColumnDef::new(BinaryType::Blob).blob(BlobSize::Blob(None))) - .col(ColumnDef::new(BinaryType::MediumBlob).blob(BlobSize::Medium)) - .col(ColumnDef::new(BinaryType::LongBlob).blob(BlobSize::Long)) + .col(ColumnDef::new(BinaryType::Blob).blob()) + .col(ColumnDef::new(BinaryType::TinyBlob).custom(MySqlType::TinyBlob)) + .col(ColumnDef::new(BinaryType::MediumBlob).custom(MySqlType::MediumBlob)) + .col(ColumnDef::new(BinaryType::LongBlob).custom(MySqlType::LongBlob)) .to_string(MysqlQueryBuilder), [ "CREATE TABLE `binary_type` (", "`binlen` binary(32),", - "`bin` blob, `defb` binary(32),", - "`tb` tinyblob,", + "`bin` binary(1),", "`b` blob,", + "`tb` tinyblob,", "`mb` mediumblob,", "`lb` longblob", ")", @@ -190,13 +189,15 @@ fn create_7() { assert_eq!( Table::create() .table(Char::Table) + .col(ColumnDef::new(BinaryType::Blob).blob()) .col(ColumnDef::new(Char::Character).binary()) .col(ColumnDef::new(Char::FontSize).binary_len(10)) .col(ColumnDef::new(Char::SizeW).var_binary(10)) .to_string(MysqlQueryBuilder), [ "CREATE TABLE `character` (", - "`character` blob,", + "`b` blob,", + "`character` binary(1),", "`font_size` binary(10),", "`size_w` varbinary(10)", ")", @@ -210,9 +211,9 @@ fn create_8() { assert_eq!( Table::create() .table(Font::Table) - .col(ColumnDef::new(Font::Variant).year(Some(MySqlYear::Four))) + .col(ColumnDef::new(Font::Variant).year()) .to_string(MysqlQueryBuilder), - "CREATE TABLE `font` ( `variant` year(4) )" + "CREATE TABLE `font` ( `variant` year )" ); } @@ -227,6 +228,20 @@ fn create_9() { ); } +#[test] +fn create_10() { + assert_eq!( + Table::create() + .table(Glyph::Table) + .col(ColumnDef::new(Glyph::Id).enumeration( + Alias::new("tea"), + [Alias::new("EverydayTea"), Alias::new("BreakfastTea")] + ),) + .to_string(MysqlQueryBuilder), + "CREATE TABLE `glyph` ( `id` ENUM('EverydayTea', 'BreakfastTea') )" + ); +} + #[test] fn drop_1() { assert_eq!( @@ -344,7 +359,7 @@ fn create_with_check_constraint() { .check(Expr::col(Glyph::Id).lt(20)) .check(Expr::col(Glyph::Id).ne(15)) .to_string(MysqlQueryBuilder), - r#"CREATE TABLE `glyph` ( `id` int NOT NULL CHECK (`id` > 10), CHECK (`id` < 20), CHECK (`id` <> 15) )"#, + r"CREATE TABLE `glyph` ( `id` int NOT NULL CHECK (`id` > 10), CHECK (`id` < 20), CHECK (`id` <> 15) )", ); } @@ -361,6 +376,6 @@ fn alter_with_check_constraint() { .check(Expr::col(Glyph::Aspect).gt(100)) ) .to_string(MysqlQueryBuilder), - r#"ALTER TABLE `glyph` ADD COLUMN `aspect` int NOT NULL DEFAULT 101 CHECK (`aspect` > 100)"#, + r"ALTER TABLE `glyph` ADD COLUMN `aspect` int NOT NULL DEFAULT 101 CHECK (`aspect` > 100)", ); } diff --git a/tests/postgres/index.rs b/tests/postgres/index.rs index a71e92b..d51eb19 100644 --- a/tests/postgres/index.rs +++ b/tests/postgres/index.rs @@ -83,6 +83,21 @@ fn create_6() { ); } +#[test] +fn create_7() { + assert_eq!( + Index::create() + .unique() + .nulls_not_distinct() + .name("partial-index-glyph-image-not-null") + .table(Glyph::Table) + .col(Glyph::Image) + .and_where(Expr::col(Glyph::Image).is_not_null()) + .to_string(PostgresQueryBuilder), + r#"CREATE UNIQUE INDEX "partial-index-glyph-image-not-null" ON "glyph" ("image") NULLS NOT DISTINCT WHERE "image" IS NOT NULL"# + ); +} + #[test] fn drop_1() { assert_eq!( diff --git a/tests/postgres/query.rs b/tests/postgres/query.rs index 5df9ed2..804818b 100644 --- a/tests/postgres/query.rs +++ b/tests/postgres/query.rs @@ -1517,6 +1517,52 @@ fn insert_on_conflict_9() { ); } +#[test] +#[allow(clippy::approx_constant)] +fn insert_on_conflict_do_nothing() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns([Glyph::Aspect, Glyph::Image]) + .values_panic(["abcd".into(), 3.1415.into()]) + .on_conflict( + OnConflict::columns([Glyph::Id, Glyph::Aspect]) + .do_nothing() + .to_owned(), + ) + .to_string(PostgresQueryBuilder), + [ + r#"INSERT INTO "glyph" ("aspect", "image")"#, + r#"VALUES ('abcd', 3.1415)"#, + r#"ON CONFLICT ("id", "aspect") DO NOTHING"#, + ] + .join(" ") + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_on_conflict_do_nothing_on() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns([Glyph::Aspect, Glyph::Image]) + .values_panic(["abcd".into(), 3.1415.into()]) + .on_conflict( + OnConflict::columns([Glyph::Id, Glyph::Aspect]) + .do_nothing_on([Glyph::Id]) + .to_owned(), + ) + .to_string(PostgresQueryBuilder), + [ + r#"INSERT INTO "glyph" ("aspect", "image")"#, + r#"VALUES ('abcd', 3.1415)"#, + r#"ON CONFLICT ("id", "aspect") DO NOTHING"#, + ] + .join(" ") + ); +} + #[test] #[allow(clippy::approx_constant)] fn insert_returning_all_columns() { @@ -1879,6 +1925,16 @@ fn sub_query_with_fn() { ); } +#[test] +fn md5_fn() { + assert_eq!( + Query::select() + .expr(Func::md5(Expr::val("test"))) + .to_string(PostgresQueryBuilder), + r#"SELECT MD5('test')"# + ); +} + #[test] fn select_array_contains_bin_oper() { assert_eq!( @@ -2064,3 +2120,18 @@ fn test_issue_674_nested_logical_panic() { r#"SELECT "character" FROM "character" WHERE TRUE AND (TRUE AND TRUE AND TRUE)"# ); } + +#[test] +#[cfg(feature = "postgres-vector")] +fn test_pgvector_select() { + assert_eq!( + Query::select() + .columns([Char::Character]) + .from(Char::Table) + .and_where( + Expr::col(Char::Character).eq(Expr::val(pgvector::Vector::from(vec![1.0, 2.0]))) + ) + .to_string(PostgresQueryBuilder), + r#"SELECT "character" FROM "character" WHERE "character" = '[1,2]'"# + ); +} diff --git a/tests/postgres/table.rs b/tests/postgres/table.rs index c63e9e5..d26fa2c 100644 --- a/tests/postgres/table.rs +++ b/tests/postgres/table.rs @@ -260,21 +260,11 @@ fn create_12() { .table(BinaryType::Table) .col(ColumnDef::new(BinaryType::BinaryLen).binary_len(32)) .col(ColumnDef::new(BinaryType::Binary).binary()) - .col(ColumnDef::new(BinaryType::BlobSize).blob(BlobSize::Blob(Some(32)))) - .col(ColumnDef::new(BinaryType::TinyBlob).blob(BlobSize::Tiny)) - .col(ColumnDef::new(BinaryType::Blob).blob(BlobSize::Blob(None))) - .col(ColumnDef::new(BinaryType::MediumBlob).blob(BlobSize::Medium)) - .col(ColumnDef::new(BinaryType::LongBlob).blob(BlobSize::Long)) .to_string(PostgresQueryBuilder), [ r#"CREATE TABLE "binary_type" ("#, r#""binlen" bytea,"#, - r#""bin" bytea,"#, - r#""defb" bytea,"#, - r#""tb" bytea,"#, - r#""b" bytea,"#, - r#""mb" bytea,"#, - r#""lb" bytea"#, + r#""bin" bytea"#, r#")"#, ] .join(" ") @@ -294,7 +284,7 @@ fn create_13() { r#"CREATE TABLE "character" ("#, r#""character" bytea,"#, r#""font_size" bytea,"#, - r#""size_w" bit varying(10)"#, + r#""size_w" bytea"#, r#")"#, ] .join(" ") diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index eb6d70b..4a69dff 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -24,6 +24,38 @@ fn create_2() { ); } +#[test] +fn create_3() { + assert_eq!( + Type::create() + .as_enum(Tea::Enum) + .values([Tea::EverydayTea, Tea::BreakfastTea]) + .to_string(PostgresQueryBuilder), + r#"CREATE TYPE "tea" AS ENUM ('EverydayTea', 'BreakfastTea')"# + ); + + enum Tea { + Enum, + EverydayTea, + BreakfastTea, + } + + impl sea_query::Iden for Tea { + fn unquoted(&self, s: &mut dyn std::fmt::Write) { + write!( + s, + "{}", + match self { + Self::Enum => "tea", + Self::EverydayTea => "EverydayTea", + Self::BreakfastTea => "BreakfastTea", + } + ) + .unwrap(); + } + } +} + #[test] fn drop_1() { assert_eq!( diff --git a/tests/sqlite/index.rs b/tests/sqlite/index.rs index f1c776d..95f9c89 100644 --- a/tests/sqlite/index.rs +++ b/tests/sqlite/index.rs @@ -42,6 +42,21 @@ fn create_3() { ); } +#[test] +fn create_4() { + assert_eq!( + Index::create() + .if_not_exists() + .unique() + .name("partial-index-glyph-image-not-null") + .table(Glyph::Table) + .col(Glyph::Image) + .and_where(Expr::col(Glyph::Image).is_not_null()) + .to_string(SqliteQueryBuilder), + r#"CREATE UNIQUE INDEX IF NOT EXISTS "partial-index-glyph-image-not-null" ON "glyph" ("image") WHERE "image" IS NOT NULL"# + ); +} + #[test] fn drop_1() { assert_eq!( diff --git a/tests/sqlite/query.rs b/tests/sqlite/query.rs index 132ff98..e248144 100644 --- a/tests/sqlite/query.rs +++ b/tests/sqlite/query.rs @@ -434,7 +434,7 @@ fn select_31() { Query::select() .expr((1..10_i32).fold(Expr::value(0), |expr, i| { expr.add(i) })) .to_string(SqliteQueryBuilder), - r#"SELECT 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9"# + r"SELECT 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9" ); } @@ -1407,7 +1407,7 @@ fn insert_on_conflict_7() { .update_column(Glyph::Aspect) .to_owned() ) - .to_string(PostgresQueryBuilder), + .to_string(SqliteQueryBuilder), [ r#"INSERT INTO "glyph" ("aspect", "image")"#, r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, @@ -1434,7 +1434,7 @@ fn insert_on_conflict_8() { .update_column(Glyph::Aspect) .to_owned() ) - .to_string(PostgresQueryBuilder), + .to_string(SqliteQueryBuilder), [ r#"INSERT INTO "glyph" ("aspect", "image")"#, r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, @@ -1461,7 +1461,7 @@ fn insert_on_conflict_9() { .update_column(Glyph::Aspect) .to_owned() ) - .to_string(PostgresQueryBuilder), + .to_string(SqliteQueryBuilder), [ r#"INSERT INTO "glyph" ("aspect", "image")"#, r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, @@ -1471,6 +1471,52 @@ fn insert_on_conflict_9() { ); } +#[test] +#[allow(clippy::approx_constant)] +fn insert_on_conflict_do_nothing() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns([Glyph::Aspect, Glyph::Image]) + .values_panic(["abcd".into(), 3.1415.into()]) + .on_conflict( + OnConflict::columns([Glyph::Id, Glyph::Aspect]) + .do_nothing() + .to_owned(), + ) + .to_string(SqliteQueryBuilder), + [ + r#"INSERT INTO "glyph" ("aspect", "image")"#, + r#"VALUES ('abcd', 3.1415)"#, + r#"ON CONFLICT ("id", "aspect") DO NOTHING"#, + ] + .join(" ") + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_on_conflict_do_nothing_on() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns([Glyph::Aspect, Glyph::Image]) + .values_panic(["abcd".into(), 3.1415.into()]) + .on_conflict( + OnConflict::columns([Glyph::Id, Glyph::Aspect]) + .do_nothing_on([Glyph::Id]) + .to_owned(), + ) + .to_string(SqliteQueryBuilder), + [ + r#"INSERT INTO "glyph" ("aspect", "image")"#, + r#"VALUES ('abcd', 3.1415)"#, + r#"ON CONFLICT ("id", "aspect") DO NOTHING"#, + ] + .join(" ") + ); +} + #[test] #[allow(clippy::approx_constant)] fn insert_returning_all_columns() { @@ -1746,3 +1792,47 @@ fn sub_query_with_fn() { r#"SELECT ARRAY((SELECT * FROM "character"))"# ); } + +#[test] +fn recursive_with_multiple_ctes() { + let sub_select1 = Query::select() + .column(Asterisk) + .from(Char::Table) + .to_owned(); + let sub_select1_name = SeaRc::new(Alias::new("sub1")); + let mut sub_select1_cte = CommonTableExpression::new(); + sub_select1_cte.table_name(sub_select1_name.clone()); + sub_select1_cte.column(SeaRc::new(Alias::new("a"))); + sub_select1_cte.query(sub_select1); + let sub_select2 = Query::select() + .column(Asterisk) + .from(Char::Table) + .to_owned(); + let sub_select2_name = SeaRc::new(Alias::new("sub2")); + let mut sub_select2_cte = CommonTableExpression::new(); + sub_select2_cte.table_name(sub_select2_name.clone()); + sub_select2_cte.column(SeaRc::new(Alias::new("b"))); + sub_select2_cte.query(sub_select2); + + let mut with = WithClause::new(); + with.recursive(true) + .cte(sub_select1_cte) + .cte(sub_select2_cte); + + let mut main_sel2 = Query::select(); + main_sel2 + .expr(Expr::col(Asterisk)) + .from(TableRef::Table(sub_select2_name)); + let mut main_sel1 = Query::select(); + main_sel1 + .expr(Expr::col(Asterisk)) + .from(TableRef::Table(sub_select1_name)) + .union(UnionType::All, main_sel2); + + let query = with.query(main_sel1); + + assert_eq!( + query.to_string(SqliteQueryBuilder), + r#"WITH RECURSIVE "sub1" ("a") AS (SELECT * FROM "character") , "sub2" ("b") AS (SELECT * FROM "character") SELECT * FROM "sub1" UNION ALL SELECT * FROM "sub2""# + ); +} diff --git a/tests/sqlite/table.rs b/tests/sqlite/table.rs index b79af85..12f2be7 100644 --- a/tests/sqlite/table.rs +++ b/tests/sqlite/table.rs @@ -19,7 +19,7 @@ fn create_1() { [ r#"CREATE TABLE "glyph" ("#, r#""id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,"#, - r#""aspect" real NOT NULL,"#, + r#""aspect" double NOT NULL,"#, r#""image" text"#, r#")"#, ] @@ -46,9 +46,9 @@ fn create_2() { [ r#"CREATE TABLE "font" ("#, r#""id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,"#, - r#""name" text NOT NULL,"#, - r#""variant" text NOT NULL,"#, - r#""language" text NOT NULL"#, + r#""name" varchar NOT NULL,"#, + r#""variant" varchar NOT NULL,"#, + r#""language" varchar NOT NULL"#, r#")"#, ] .join(" ") @@ -89,7 +89,7 @@ fn create_3() { r#"CREATE TABLE IF NOT EXISTS "character" ("#, r#""id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,"#, r#""font_size" integer NOT NULL,"#, - r#""character" text NOT NULL,"#, + r#""character" varchar NOT NULL,"#, r#""size_w" integer NOT NULL,"#, r#""size_h" integer NOT NULL,"#, r#""font_id" integer DEFAULT NULL,"#, @@ -107,21 +107,11 @@ fn create_4() { .table(BinaryType::Table) .col(ColumnDef::new(BinaryType::BinaryLen).binary_len(32)) .col(ColumnDef::new(BinaryType::Binary).binary()) - .col(ColumnDef::new(BinaryType::BlobSize).blob(BlobSize::Blob(Some(32)))) - .col(ColumnDef::new(BinaryType::TinyBlob).blob(BlobSize::Tiny)) - .col(ColumnDef::new(BinaryType::Blob).blob(BlobSize::Blob(None))) - .col(ColumnDef::new(BinaryType::MediumBlob).blob(BlobSize::Medium)) - .col(ColumnDef::new(BinaryType::LongBlob).blob(BlobSize::Long)) .to_string(SqliteQueryBuilder), [ r#"CREATE TABLE "binary_type" ("#, - r#""binlen" binary(32),"#, - r#""bin" blob,"#, - r#""defb" binary(32),"#, - r#""tb" blob,"#, - r#""b" blob,"#, - r#""mb" blob,"#, - r#""lb" blob"#, + r#""binlen" blob(32),"#, + r#""bin" blob(1)"#, r#")"#, ] .join(" ") @@ -139,9 +129,9 @@ fn create_5() { .to_string(SqliteQueryBuilder), [ r#"CREATE TABLE "character" ("#, - r#""character" blob,"#, - r#""font_size" binary(10),"#, - r#""size_w" binary(10)"#, + r#""character" blob(1),"#, + r#""font_size" blob(10),"#, + r#""size_w" varbinary_blob(10)"#, r#")"#, ] .join(" ") @@ -229,7 +219,7 @@ fn create_with_unique_index() { r#"CREATE TABLE IF NOT EXISTS "character" ("#, r#""id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,"#, r#""font_size" integer NOT NULL,"#, - r#""character" text NOT NULL,"#, + r#""character" varchar NOT NULL,"#, r#""size_w" integer NOT NULL,"#, r#""size_h" integer NOT NULL,"#, r#""font_id" integer DEFAULT NULL,"#, @@ -274,7 +264,7 @@ fn create_with_primary_unique_index() { r#"CREATE TABLE IF NOT EXISTS "character" ("#, r#""id" integer NOT NULL,"#, r#""font_size" integer NOT NULL,"#, - r#""character" text NOT NULL,"#, + r#""character" varchar NOT NULL,"#, r#""size_w" integer NOT NULL,"#, r#""size_h" integer NOT NULL,"#, r#""font_id" integer DEFAULT NULL,"#, @@ -328,7 +318,7 @@ fn create_with_unique_index_constraint() { r#"CREATE TABLE IF NOT EXISTS "character" ("#, r#""id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,"#, r#""font_size" integer NOT NULL,"#, - r#""character" text NOT NULL,"#, + r#""character" varchar NOT NULL,"#, r#""size_w" integer NOT NULL,"#, r#""size_h" integer NOT NULL,"#, r#""font_id" integer DEFAULT NULL,"#,