diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 12a71c650b..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: I think I found a bug in SQLx -about: Create a bug-report issue. -title: '' -labels: 'bug' -assignees: '' ---- -### Bug Description -A clear and concise description of what the bug is. - -### Minimal Reproduction -A small code snippet or a link to a Github repo or Gist, with instructions on reproducing the bug. - -### Info -* SQLx version: [REQUIRED] -* SQLx features enabled: [REQUIRED] -* Database server and version: [REQUIRED] (MySQL / Postgres / SQLite ) -* Operating system: [REQUIRED] -* `rustc --version`: [REQUIRED] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..317e4a87ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,61 @@ +name: I think I found a bug in SQLx +description: Create a bug-report issue +labels: [bug] +body: + - type: textarea + id: related-issues + validations: + required: true + attributes: + label: I have found these related issues/pull requests + description: | + I have searched by clicking [HERE](https://github.com/launchbadge/sqlx/issues?q=) + for existing issues, these are the ones I've found, + and this is why I think this deserves a new issue. + placeholder: "Related to ..." + - type: textarea + id: description + validations: + required: true + attributes: + label: Description + description: Clear and concise description of what the bug is + - type: textarea + id: steps-to-reproduce + validations: + required: true + attributes: + label: Reproduction steps + description: A small code snippet or a link to a Github repo or Gist, with instructions on reproducing the bug. + - type: input + id: sqlx-version + attributes: + label: SQLx version + validations: + required: true + - type: input + id: sqlx-features + attributes: + label: Enabled SQLx features + validations: + required: true + - type: input + id: db-server-and-version + attributes: + label: Database server and version + placeholder: MySQL / Postgres / SQLite + validations: + required: true + - type: input + id: os-type + attributes: + label: Operating system + validations: + required: true + - type: input + id: rust-version + attributes: + label: Rust version + description: You can get this via running `rustc --version` + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 178ddfda5f..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: I have a feature request for SQLx -about: Create a feature-request issue. -title: '' -labels: 'enhancement' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..0f39efc46d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,37 @@ +name: I have a feature request for SQLx +description: Create a feature-request issue +labels: [enhancement] +body: + - type: textarea + id: related-issues + validations: + required: true + attributes: + label: I have found these related issues/pull requests + description: "Provide context for your pull request." + placeholder: | + Closes \#... + Relates to \#... + - type: textarea + id: feature-description + validations: + required: true + attributes: + label: Description + description: A clear and concise description of what the problem is + placeholder: You should add ... + - type: textarea + id: solution + validations: + required: true + attributes: + label: Prefered solution + description: A clear and concise description of what you want to happen. + placeholder: In my use-case, ... + - type: textarea + id: breaking-change + validations: + required: true + attributes: + label: Is this a breaking change? Why or why not? + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a86c28731e..6d193065c2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,2 +1,50 @@ + + ### Does your PR solve an issue? -### Delete this text and add "fixes #(issue number)" +Delete this text and add "fixes #(issue number)". + +Do *not* just list issue numbers here as they will not be automatically closed on merging this pull request unless prefixed with "fixes" or "closes". + +### Is this a breaking change? +Delete this text and answer yes/no and explain. + +If yes, this pull request will need to wait for the next major release (`0.{x + 1}.0`) + +Behavior changes _can_ be breaking if significant enough. +Consider [Hyrum's Law](https://www.hyrumslaw.com/): + +> With a sufficient number of users of an API, +> it does not matter what you promise in the contract: +> all observable behaviors of your system +> will be depended on by somebody. diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 280d1fc4f3..0dfbcbdf26 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -14,20 +14,20 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use latest Rust - run: rustup override set stable + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install + rustup override set stable - uses: Swatinem/rust-cache@v2 - with: - key: sqlx-cli - run: > - cargo build - -p sqlx-cli - --bin sqlx - --release - --no-default-features - --features mysql,postgres,sqlite + cargo build + -p sqlx-cli + --bin sqlx + --release + --no-default-features + --features mysql,postgres,sqlite - uses: actions/upload-artifact@v4 with: @@ -63,9 +63,10 @@ jobs: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: mysql-examples - name: Todos (Setup) working-directory: examples/mysql/todos @@ -98,7 +99,7 @@ jobs: name: sqlx-cli path: /home/runner/.local/bin - - run: | + - run: | ls -R /home/runner/.local/bin chmod +x $HOME/.local/bin/sqlx echo $HOME/.local/bin >> $GITHUB_PATH @@ -106,9 +107,8 @@ jobs: - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@v2 - with: - key: pg-examples + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install - name: Axum Social with Tests (Setup) working-directory: examples/postgres/axum-social-with-tests @@ -217,9 +217,10 @@ jobs: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: sqlite-examples - name: TODOs (Setup) env: diff --git a/.github/workflows/sqlx-cli.yml b/.github/workflows/sqlx-cli.yml index 3aeb3d7d33..8ec5db63d7 100644 --- a/.github/workflows/sqlx-cli.yml +++ b/.github/workflows/sqlx-cli.yml @@ -15,8 +15,9 @@ jobs: steps: - uses: actions/checkout@v4 - - run: | - rustup update + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install rustup component add clippy rustup toolchain install beta rustup component add --toolchain beta clippy @@ -32,29 +33,259 @@ jobs: --manifest-path sqlx-cli/Cargo.toml --target-dir target/beta/ - test: - name: Test + integration-test: + name: Integration Test runs-on: ${{ matrix.os }} strategy: matrix: # Note: macOS-latest uses M1 Silicon (ARM64) os: - - ubuntu-latest - # FIXME: migrations tests fail on Windows for whatever reason - # - windows-latest - - macOS-13 - - macOS-latest + - ubuntu-latest + # FIXME: migrations tests fail on Windows for whatever reason + # - windows-latest + - macOS-13 + - macOS-latest steps: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: ${{ runner.os }}-test - run: cargo test --manifest-path sqlx-cli/Cargo.toml + test-mysql: + name: Functional Test (MySQL) + runs-on: ubuntu-latest + # Deliberately not using `tests/docker-compose.yml` because that sets up the database automatically. + services: + mysql: + image: mysql:8 + ports: + - 3306:3306 + env: + MYSQL_ROOT_PASSWORD: password + env: + BASE_URL: mysql://root:password@localhost + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + + - uses: Swatinem/rust-cache@v2 + + - name: Install SQLx-CLI + run: + cargo install --locked --debug --path sqlx-cli + + - name: Basic Test + env: + DATABASE_URL: ${{ env.BASE_URL }}/test1 + run: | + sqlx db setup --source=tests/mysql/migrations + + sqlx mig info --source=tests/mysql/migrations + + sqlx db drop -y + + - name: Test .env + run: | + echo "DATABASE_URL=${{ env.BASE_URL }}/test2" > .env + + sqlx db setup --source=tests/mysql/migrations + + sqlx mig info --source=tests/mysql/migrations + + sqlx db drop -y + + - name: Test --no-dotenv + run: | + # Allow subcommands to fail + set +e + + echo "DATABASE_URL=${{ env.BASE_URL }}/test3" > .env + + ERROR=$(sqlx db setup --no-dotenv --source=tests/mysql/migrations) + + if [[ "$ERROR" == *"--database-url"* ]]; then + exit 0 + else + echo "Unexpected error from sqlx-cli: $ERROR" + exit 1 + fi + + - name: Test Reversible Migrations + env: + DATABASE_URL: ${{ env.BASE_URL }}/test4 + run: | + sqlx db setup --source=tests/mysql/migrations_reversible + + INFO_BEFORE=$(sqlx mig info --source=tests/mysql/migrations_reversible) + + sqlx mig revert --target-version=0 --source=tests/mysql/migrations_reversible + + INFO_AFTER=$(sqlx mig info --source=tests/mysql/migrations_reversible) + + if [[ "$INFO_BEFORE" == "$INFO_AFTER" ]]; then + echo "Error: migration info is identical before and after migrating: $INFO_BEFORE" + exit 1 + fi + + test-postgres: + name: Functional Test (PostgreSQL) + runs-on: ubuntu-latest + # Deliberately not using `tests/docker-compose.yml` because that sets up the database automatically. + services: + mysql: + image: postgres:17 + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: password + env: + BASE_URL: postgres://postgres:password@localhost + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + + - uses: Swatinem/rust-cache@v2 + + - name: Install SQLx-CLI + run: + cargo install --locked --debug --path sqlx-cli + + - name: Basic Test + env: + DATABASE_URL: ${{ env.BASE_URL }}/test1 + run: | + sqlx db setup --source=tests/postgres/migrations + + sqlx mig info --source=tests/postgres/migrations + + sqlx db drop -y + + - name: Test .env + run: | + echo "DATABASE_URL=${{ env.BASE_URL }}/test2" > .env + + sqlx db setup --source=tests/postgres/migrations + + sqlx mig info --source=tests/postgres/migrations + + sqlx db drop -y + + - name: Test --no-dotenv + run: | + # Allow subcommands to fail + set +e + + echo "DATABASE_URL=${{ env.BASE_URL }}/test3" > .env + + ERROR=$(sqlx db setup --no-dotenv --source=tests/postgres/migrations) + + if [[ "$ERROR" == *"--database-url"* ]]; then + exit 0 + else + echo "Unexpected error from sqlx-cli: $ERROR" + exit 1 + fi + + - name: Test Reversible Migrations + env: + DATABASE_URL: ${{ env.BASE_URL }}/test4 + run: | + sqlx db setup --source=tests/postgres/migrations_reversible + + INFO_BEFORE=$(sqlx mig info --source=tests/postgres/migrations_reversible) + + sqlx mig revert --target-version=0 --source=tests/postgres/migrations_reversible + + INFO_AFTER=$(sqlx mig info --source=tests/postgres/migrations_reversible) + + if [[ "$INFO_BEFORE" == "$INFO_AFTER" ]]; then + echo "Error: migration info is identical before and after migrating: $INFO_BEFORE" + exit 1 + fi + + test-sqlite: + name: Functional Test (SQLite) + runs-on: ubuntu-latest + env: + BASE_URL: sqlite://. + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + + - uses: Swatinem/rust-cache@v2 + + - name: Install SQLx-CLI + run: + cargo install --locked --debug --path sqlx-cli + + - name: Basic Test + env: + DATABASE_URL: ${{ env.BASE_URL }}/test1 + run: | + sqlx db setup --source=tests/sqlite/migrations + + sqlx mig info --source=tests/sqlite/migrations + + sqlx db drop -y + + - name: Test .env + run: | + echo "DATABASE_URL=${{ env.BASE_URL }}/test2" > .env + + sqlx db setup --source=tests/sqlite/migrations + + sqlx mig info --source=tests/sqlite/migrations + + sqlx db drop -y + + - name: Test --no-dotenv + run: | + # Allow subcommands to fail + set +e + + echo "DATABASE_URL=${{ env.BASE_URL }}/test3" > .env + + ERROR=$(sqlx db setup --no-dotenv --source=tests/sqlite/migrations) + + if [[ "$ERROR" == *"--database-url"* ]]; then + exit 0 + else + echo "Unexpected error from sqlx-cli: $ERROR" + exit 1 + fi + + - name: Test Reversible Migrations + env: + DATABASE_URL: ${{ env.BASE_URL }}/test4 + run: | + sqlx db setup --source=tests/sqlite/migrations_reversible + + INFO_BEFORE=$(sqlx mig info --source=tests/sqlite/migrations_reversible) + + sqlx mig revert --target-version=0 --source=tests/sqlite/migrations_reversible + + INFO_AFTER=$(sqlx mig info --source=tests/sqlite/migrations_reversible) + + if [[ "$INFO_BEFORE" == "$INFO_AFTER" ]]; then + echo "Error: migration info is identical before and after migrating: $INFO_BEFORE" + exit 1 + fi + build: name: Build runs-on: ${{ matrix.os }} @@ -85,12 +316,12 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use latest Rust - run: rustup override set stable + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install + rustup override set stable - uses: Swatinem/rust-cache@v2 - with: - key: ${{ runner.os }}-cli - run: cargo build --manifest-path sqlx-cli/Cargo.toml --bin cargo-sqlx ${{ matrix.args }} diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index 1a91e1fa83..1e3513b1ee 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -10,7 +10,7 @@ on: jobs: format: name: Format - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - run: rustup component add rustfmt @@ -18,28 +18,29 @@ jobs: check: name: Check - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - runtime: [async-std, tokio] - tls: [native-tls, rustls, none] + runtime: [ async-std, tokio ] + tls: [ native-tls, rustls, none ] steps: - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-check-${{ matrix.runtime }}-${{ matrix.tls }}" - - - run: | - rustup update + # Swatinem/rust-cache recommends setting up the rust toolchain first because it's used in cache keys + - name: Setup Rust + # https://blog.rust-lang.org/2025/03/02/Rustup-1.28.0.html + run: | + rustup show active-toolchain || rustup toolchain install rustup component add clippy rustup toolchain install beta rustup component add --toolchain beta clippy + - uses: Swatinem/rust-cache@v2 + - run: > cargo clippy --no-default-features - --features all-databases,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }},macros + --features all-databases,_unstable-all-types,sqlite-preupdate-hook,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }},macros -- -D warnings # Run beta for new warnings but don't break the build. @@ -47,31 +48,32 @@ jobs: - run: > cargo +beta clippy --no-default-features - --features all-databases,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }},macros + --features all-databases,_unstable-all-types,sqlite-preupdate-hook,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }},macros --target-dir target/beta/ check-minimal-versions: name: Check build using minimal versions - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - run: rustup update - - run: rustup toolchain install nightly + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install + rustup toolchain install nightly - run: cargo +nightly generate-lockfile -Z minimal-versions - run: cargo build --all-features test: name: Unit Tests - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@v2 - with: - key: ${{ runner.os }}-test + # https://blog.rust-lang.org/2025/03/02/Rustup-1.28.0.html + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install - - name: Install Rust - run: rustup update + - uses: Swatinem/rust-cache@v2 - name: Test sqlx-core run: > @@ -113,20 +115,22 @@ jobs: sqlite: name: SQLite - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - runtime: [async-std, tokio] - linking: [sqlite, sqlite-unbundled] + runtime: [ async-std, tokio ] + linking: [ sqlite, sqlite-unbundled ] needs: check steps: - uses: actions/checkout@v4 - run: mkdir /tmp/sqlite3-lib && wget -O /tmp/sqlite3-lib/ipaddr.so https://github.com/nalgeon/sqlean/releases/download/0.15.2/ipaddr.so + # https://blog.rust-lang.org/2025/03/02/Rustup-1.28.0.html + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-${{ matrix.linking }}-${{ matrix.runtime }}-${{ matrix.tls }}" - name: Install system sqlite library if: ${{ matrix.linking == 'sqlite-unbundled' }} @@ -140,7 +144,23 @@ jobs: - run: > cargo test --no-default-features - --features any,macros,${{ matrix.linking }},_unstable-all-types,runtime-${{ matrix.runtime }} + --features any,macros,migrate,${{ matrix.linking }},_unstable-all-types,runtime-${{ matrix.runtime }},${{ matrix.linking == 'sqlite' && 'sqlite-preupdate-hook' || ''}} + -- + --test-threads=1 + env: + DATABASE_URL: sqlite:tests/sqlite/sqlite.db + SQLX_OFFLINE_DIR: .sqlx + RUSTFLAGS: --cfg sqlite_ipaddr --cfg sqlite_test_sqlcipher + LD_LIBRARY_PATH: /tmp/sqlite3-lib + + # Run the `test-attr` test again to cover cleanup. + # The `sqlite-test-attr` test requires the `sqlite` feature. + - if: ${{ matrix.linking == 'sqlite' }} + run: > + cargo test + --test sqlite-test-attr + --no-default-features + --features any,macros,migrate,${{ matrix.linking }},_unstable-all-types,runtime-${{ matrix.runtime }},${{ matrix.linking == 'sqlite' && 'sqlite-preupdate-hook' || ''}} -- --test-threads=1 env: @@ -179,25 +199,29 @@ jobs: postgres: name: Postgres - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - postgres: [17, 13] - runtime: [async-std, tokio] - tls: [native-tls, rustls-aws-lc-rs, rustls-ring, none] + postgres: [ 17, 13 ] + runtime: [ async-std, tokio ] + tls: [ native-tls, rustls-aws-lc-rs, rustls-ring, none ] needs: check steps: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-postgres-${{ matrix.runtime }}-${{ matrix.tls }}" - env: # FIXME: needed to disable `ltree` tests in Postgres 9.6 # but `PgLTree` should just fall back to text format RUSTFLAGS: -D warnings --cfg postgres_${{ matrix.postgres }} - run: cargo build --features postgres,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + run: > + cargo build + --no-default-features + --features postgres,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }},macros,migrate - run: | docker compose -f tests/docker-compose.yml run -d -p 5432:5432 --name postgres_${{ matrix.postgres }} postgres_${{ matrix.postgres }} @@ -217,6 +241,19 @@ jobs: # but `PgLTree` should just fall back to text format RUSTFLAGS: --cfg postgres_${{ matrix.postgres }} + # Run the `test-attr` test again to cover cleanup. + - run: > + cargo test + --test postgres-test-attr + --no-default-features + --features any,postgres,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx + SQLX_OFFLINE_DIR: .sqlx + # FIXME: needed to disable `ltree` tests in Postgres 9.6 + # but `PgLTree` should just fall back to text format + RUSTFLAGS: --cfg postgres_${{ matrix.postgres }} + - if: matrix.tls != 'none' run: > cargo test @@ -279,19 +316,20 @@ jobs: mysql: name: MySQL - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - mysql: [8] - runtime: [async-std, tokio] - tls: [native-tls, rustls-aws-lc-rs, rustls-ring, none] + mysql: [ 8 ] + runtime: [ async-std, tokio ] + tls: [ native-tls, rustls-aws-lc-rs, rustls-ring, none ] needs: check steps: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-mysql-${{ matrix.runtime }}-${{ matrix.tls }}" - run: cargo build --features mysql,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} @@ -304,7 +342,18 @@ jobs: - run: > cargo test --no-default-features - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + env: + DATABASE_URL: mysql://root:password@localhost:3306/sqlx?ssl-mode=disabled + SQLX_OFFLINE_DIR: .sqlx + RUSTFLAGS: --cfg mysql_${{ matrix.mysql }} + + # Run the `test-attr` test again to cover cleanup. + - run: > + cargo test + --test mysql-test-attr + --no-default-features + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx?ssl-mode=disabled SQLX_OFFLINE_DIR: .sqlx @@ -315,7 +364,7 @@ jobs: run: > cargo test --no-default-features - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx SQLX_OFFLINE_DIR: .sqlx @@ -329,7 +378,7 @@ jobs: cargo build --no-default-features --test mysql-macros - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: SQLX_OFFLINE: true SQLX_OFFLINE_DIR: .sqlx @@ -341,7 +390,7 @@ jobs: cargo test --no-default-features --test mysql-macros - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx SQLX_OFFLINE: true @@ -360,26 +409,27 @@ jobs: run: > cargo test --no-default-features - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root@localhost:3306/sqlx?sslmode=verify_ca&ssl-ca=.%2Ftests%2Fcerts%2Fca.crt&ssl-key=.%2Ftests%2Fkeys%2Fclient.key&ssl-cert=.%2Ftests%2Fcerts%2Fclient.crt RUSTFLAGS: --cfg mysql_${{ matrix.mysql }} mariadb: name: MariaDB - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - mariadb: [verylatest, 11_4, 10_11, 10_4] - runtime: [async-std, tokio] - tls: [native-tls, rustls-aws-lc-rs, rustls-ring, none] + mariadb: [ verylatest, 11_4, 10_11, 10_4 ] + runtime: [ async-std, tokio ] + tls: [ native-tls, rustls-aws-lc-rs, rustls-ring, none ] needs: check steps: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-mysql-${{ matrix.runtime }}-${{ matrix.tls }}" - run: cargo build --features mysql,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} @@ -392,7 +442,18 @@ jobs: - run: > cargo test --no-default-features - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + env: + DATABASE_URL: mysql://root:password@localhost:3306/sqlx + SQLX_OFFLINE_DIR: .sqlx + RUSTFLAGS: --cfg mariadb_${{ matrix.mariadb }} + + # Run the `test-attr` test again to cover cleanup. + - run: > + cargo test + --test mysql-test-attr + --no-default-features + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx SQLX_OFFLINE_DIR: .sqlx @@ -406,7 +467,7 @@ jobs: cargo build --no-default-features --test mysql-macros - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: SQLX_OFFLINE: true SQLX_OFFLINE_DIR: .sqlx @@ -417,7 +478,7 @@ jobs: cargo test --no-default-features --test mysql-macros - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx SQLX_OFFLINE: true @@ -436,7 +497,7 @@ jobs: run: > cargo test --no-default-features - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root@localhost:3306/sqlx?sslmode=verify_ca&ssl-ca=.%2Ftests%2Fcerts%2Fca.crt&ssl-key=.%2Ftests%2Fkeys%2Fclient.key&ssl-cert=.%2Ftests%2Fcerts%2Fclient.crt RUSTFLAGS: --cfg mariadb_${{ matrix.mariadb }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c190be4af2..3fadc3d093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,182 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.8.6 - 2025-05-19 + +9 pull requests were merged this release cycle. + +### Added +* [[#3849]]: Add color and wrapping to cli help text [[@joshka]] + +### Changed +* [[#3830]]: build: drop unused `tempfile` dependency [[@paolobarbolini]] +* [[#3845]]: chore: clean up no longer used imports [[@tisonkun]] +* [[#3863]]: Use unnamed statement in pg when not persistent [[@ThomWright]] +* [[#3866]]: chore(doc): clarify compile-time verification and case conversion behavior [[@duhby]] + +### Fixed +* [[#3840]]: Fix docs.rs build of sqlx-sqlite [[@gferon]] +* [[#3848]]: fix(macros): don't mutate environment variables [[@joeydewaal]] +* [[#3855]]: fix `attrubute` typo in doc [[@kujeger]] +* [[#3856]]: fix(macros): slightly improve unsupported type error message [[@dyc3]] + +[#3830]: https://github.com/launchbadge/sqlx/pull/3830 +[#3840]: https://github.com/launchbadge/sqlx/pull/3840 +[#3845]: https://github.com/launchbadge/sqlx/pull/3845 +[#3848]: https://github.com/launchbadge/sqlx/pull/3848 +[#3849]: https://github.com/launchbadge/sqlx/pull/3849 +[#3855]: https://github.com/launchbadge/sqlx/pull/3855 +[#3856]: https://github.com/launchbadge/sqlx/pull/3856 +[#3863]: https://github.com/launchbadge/sqlx/pull/3863 +[#3866]: https://github.com/launchbadge/sqlx/pull/3866 + +## 0.8.5 - 2025-04-14 + +Hotfix release to address two new issues: +* [[#3823]]: `sqlx-cli@0.8.4` broke `.env` default resolution mechanism +* [[#3825]]: `sqlx@0.8.4` broke test fixture setup + +The `0.8.4` release will be yanked as of publishing this one. + +### Added +* In release PR: `sqlx-cli` now accepts `--no-dotenv` in subcommand arguments. +* In release PR: added functionality tests for `sqlx-cli` to CI. +* In release PR: test `#[sqlx::test]` twice in CI to cover cleanup. + +### Fixed +* In release PR: `sqlx-cli` correctly reads `.env` files by default again. + * Addresses [[#3823]]. +* In release PR: fix bugs in MySQL implementation of `#[sqlx::test]`. + * Addresses [[#3825]]. + +[#3823]: https://github.com/launchbadge/sqlx/issues/3823 +[#3825]: https://github.com/launchbadge/sqlx/issues/3825 + +## 0.8.4 - 2025-04-13 + +50 pull requests were merged this release cycle. + +### Added +* [[#3603]]: Added missing special casing for encoding embedded arrays of custom types [[@nico-incubiq]] +* [[#3625]]: feat(sqlite): add preupdate hook [[@aschey]] +* [[#3655]]: docs: add example for postgres enums with type TEXT [[@tisonkun]] +* [[#3677]]: Add json(nullable) macro attribute [[@seanaye]] +* [[#3687]]: Derive clone and debug for postgresql arguments [[@remysaissy]] +* [[#3690]]: feat: add postres geometry line segment [[@jayy-lmao]] +* [[#3707]]: feat(Sqlite): add LockedSqliteHandle::last_error [[@joeydewaal]] +* [[#3710]]: feat: add ipnet support [[@BeauGieskens]] +* [[#3711]]: feat(postgres): add geometry box [[@jayy-lmao]] +* [[#3714]]: chore: expose bstr feature [[@joeydewaal]] +* [[#3716]]: feat(postgres): add geometry path [[@jayy-lmao]] +* [[#3724]]: feat(sqlx-cli): Add flag to disable automatic loading of .env files [[@benwilber]] +* [[#3734]]: QueryBuilder: add debug_assert when `push_values` is passed an empty set of tuples [[@chanmaoganda]] +* [[#3745]]: feat: sqlx sqlite expose de/serialize [[@mattrighetti]] +* [[#3765]]: Merge of #3427 (by @mpyw) and #3614 (by @bonsairobo) [[@abonander]] + * [[#3427]] Expose `transaction_depth` through `get_transaction_depth()` method [[@mpyw]] + * Changed to `Connection::is_in_transaction` in [[#3765]] + * [[#3614]] Add `begin_with` methods to support database-specific transaction options [[@bonsairobo]] +* [[#3769]]: feat(postgres): add geometry polygon [[@jayy-lmao]] +* [[#3773]]: feat(postgres): add geometry circle [[@jayy-lmao]] + +### Changed +* [[#3665]]: build(deps): bump semver compatible dependencies [[@paolobarbolini]] +* [[#3669]]: refactor(cli): replace promptly with dialoguer [[@paolobarbolini]] +* [[#3672]]: add `#[track_caller]` to `Row::get()` [[@karambarakat]] +* [[#3708]]: chore(MySql): Remove unnecessary box [[@joeydewaal]] +* [[#3715]]: chore: add pg_copy regression tests [[@joeydewaal]] +* [[#3721]]: Replace some `futures-core` / `futures-util` APIs with `std` variants [[@paolobarbolini]] +* [[#3725]]: chore: replace rustls-pemfile with rustls-pki-types [[@tottoto]] +* [[#3754]]: chore(cli): remove unused async-trait crate from dependencies [[@tottoto]] +* [[#3762]]: docs(pool): recommend actix-web ThinData over Data to avoid two Arcs [[@jonasmalacofilho]] + +### Fixed +* [[#3289]]: Always set `SQLITE_OPEN_URI` on in-memory sqlite [[@LecrisUT]] +* [[#3334]]: Fix: nextest cleanup race condition [[@bonega]] +* [[#3666]]: fix(cli): running tests on 32bit platforms [[@paolobarbolini]] +* [[#3686]]: fix: handle nullable values by printing NULL instead of panicking [[@joeydewaal]] +* [[#3700]]: fix(Sqlite): stop sending rows after first error [[@joeydewaal]] +* [[#3701]]: fix(postgres) use signed int for length prefix in `PgCopyIn` [[@joeydewaal]] +* [[#3703]]: fix(Postgres) chunk pg_copy data [[@joeydewaal]] +* [[#3712]]: FromRow: Fix documentation order [[@Turbo87]] +* [[#3720]]: Fix readme: uuid feature is gating for all repos [[@jthacker]] +* [[#3728]]: postgres: Fix tracing span when dropping PgListener [[@chitoku-k]] +* [[#3741]]: Fix example calculation in docs [[@dns2utf8]] +* [[#3749]]: docs: add some missing backticks [[@soulwa]] +* [[#3753]]: Avoid privilege requirements by using an advisory lock in test setup (postgres). [[@kildrens]] +* [[#3755]]: Fix FromRow docs for tuples [[@xvapx]] +* [[#3768]]: chore(Sqlite): remove ci.db from repo [[@joeydewaal]] +* [[#3771]]: fix(ci): breakage from Rustup 1.28 [[@abonander]] +* [[#3786]]: Fix a copy-paste error on get_username docs [[@sulami]] +* [[#3801]]: Fix: Enable Json type when db feature isn't enabled [[@thriller08]] +* [[#3809]]: fix: PgConnectOptions docs [[@mbj]] +* [[#3811]]: Fix error message typo in PgPoint::from_str [[@TeCHiScy]] +* [[#3812]]: mysql: Fix panic on invalid text row length field [[@0xdeafbeef]] +* [[#3815]]: fix(macros): cache macro metadata based on `CARGO_MANIFEST_DIR` [[@joeydewaal]] +* Fixes in release PR [[#3819]] [[@abonander]]: + * fix(postgres): send `limit: 0` for all `Execute` messages + * Addresses [[#3673]]: Parallel workers not used on Postgres + * fix: let `CertificateInput::from` infer any PEM-encoded document + * Fixes `PGSSLKEY` not being parsed correctly when containing a PEM-encoded private key. + * doc: improve documentation of `PgConnectOptions` + * `PGHOSTADDR` now can be used to override `PGHOST`. + * Addresses [[#3740]]: Document the URL syntax for Unix-domain sockets when connecting to postgres + +[#3819]: https://github.com/launchbadge/sqlx/pull/3819 + +[#3673]: https://github.com/launchbadge/sqlx/issues/3673 +[#3740]: https://github.com/launchbadge/sqlx/issues/3740 + +[#3289]: https://github.com/launchbadge/sqlx/pull/3289 +[#3334]: https://github.com/launchbadge/sqlx/pull/3334 +[#3427]: https://github.com/launchbadge/sqlx/pull/3427 +[#3603]: https://github.com/launchbadge/sqlx/pull/3603 +[#3614]: https://github.com/launchbadge/sqlx/pull/3614 +[#3625]: https://github.com/launchbadge/sqlx/pull/3625 +[#3655]: https://github.com/launchbadge/sqlx/pull/3655 +[#3665]: https://github.com/launchbadge/sqlx/pull/3665 +[#3666]: https://github.com/launchbadge/sqlx/pull/3666 +[#3669]: https://github.com/launchbadge/sqlx/pull/3669 +[#3672]: https://github.com/launchbadge/sqlx/pull/3672 +[#3677]: https://github.com/launchbadge/sqlx/pull/3677 +[#3686]: https://github.com/launchbadge/sqlx/pull/3686 +[#3687]: https://github.com/launchbadge/sqlx/pull/3687 +[#3690]: https://github.com/launchbadge/sqlx/pull/3690 +[#3700]: https://github.com/launchbadge/sqlx/pull/3700 +[#3701]: https://github.com/launchbadge/sqlx/pull/3701 +[#3703]: https://github.com/launchbadge/sqlx/pull/3703 +[#3707]: https://github.com/launchbadge/sqlx/pull/3707 +[#3708]: https://github.com/launchbadge/sqlx/pull/3708 +[#3710]: https://github.com/launchbadge/sqlx/pull/3710 +[#3711]: https://github.com/launchbadge/sqlx/pull/3711 +[#3712]: https://github.com/launchbadge/sqlx/pull/3712 +[#3714]: https://github.com/launchbadge/sqlx/pull/3714 +[#3715]: https://github.com/launchbadge/sqlx/pull/3715 +[#3716]: https://github.com/launchbadge/sqlx/pull/3716 +[#3720]: https://github.com/launchbadge/sqlx/pull/3720 +[#3721]: https://github.com/launchbadge/sqlx/pull/3721 +[#3724]: https://github.com/launchbadge/sqlx/pull/3724 +[#3725]: https://github.com/launchbadge/sqlx/pull/3725 +[#3728]: https://github.com/launchbadge/sqlx/pull/3728 +[#3734]: https://github.com/launchbadge/sqlx/pull/3734 +[#3741]: https://github.com/launchbadge/sqlx/pull/3741 +[#3745]: https://github.com/launchbadge/sqlx/pull/3745 +[#3749]: https://github.com/launchbadge/sqlx/pull/3749 +[#3753]: https://github.com/launchbadge/sqlx/pull/3753 +[#3754]: https://github.com/launchbadge/sqlx/pull/3754 +[#3755]: https://github.com/launchbadge/sqlx/pull/3755 +[#3762]: https://github.com/launchbadge/sqlx/pull/3762 +[#3765]: https://github.com/launchbadge/sqlx/pull/3765 +[#3768]: https://github.com/launchbadge/sqlx/pull/3768 +[#3769]: https://github.com/launchbadge/sqlx/pull/3769 +[#3771]: https://github.com/launchbadge/sqlx/pull/3771 +[#3773]: https://github.com/launchbadge/sqlx/pull/3773 +[#3786]: https://github.com/launchbadge/sqlx/pull/3786 +[#3801]: https://github.com/launchbadge/sqlx/pull/3801 +[#3809]: https://github.com/launchbadge/sqlx/pull/3809 +[#3811]: https://github.com/launchbadge/sqlx/pull/3811 +[#3812]: https://github.com/launchbadge/sqlx/pull/3812 +[#3815]: https://github.com/launchbadge/sqlx/pull/3815 + ## 0.8.3 - 2025-01-03 41 pull requests were merged this release cycle. @@ -2700,3 +2876,33 @@ Fix docs.rs build by enabling a runtime feature in the docs.rs metadata in `Carg [@hsivonen]: https://github.com/hsivonen [@andreweggleston]: https://github.com/andreweggleston [@Suficio]: https://github.com/Suficio +[@bonega]: https://github.com/bonega +[@nico-incubiq]: https://github.com/nico-incubiq +[@tisonkun]: https://github.com/tisonkun +[@karambarakat]: https://github.com/karambarakat +[@seanaye]: https://github.com/seanaye +[@remysaissy]: https://github.com/remysaissy +[@BeauGieskens]: https://github.com/BeauGieskens +[@Turbo87]: https://github.com/Turbo87 +[@jthacker]: https://github.com/jthacker +[@benwilber]: https://github.com/benwilber +[@chitoku-k]: https://github.com/chitoku-k +[@chanmaoganda]: https://github.com/chanmaoganda +[@dns2utf8]: https://github.com/dns2utf8 +[@mattrighetti]: https://github.com/mattrighetti +[@soulwa]: https://github.com/soulwa +[@kildrens]: https://github.com/kildrens +[@xvapx]: https://github.com/xvapx +[@jonasmalacofilho]: https://github.com/jonasmalacofilho +[@sulami]: https://github.com/sulami +[@thriller08]: https://github.com/thriller08 +[@mbj]: https://github.com/mbj +[@TeCHiScy]: https://github.com/TeCHiScy +[@mpyw]: https://github.com/mpyw +[@bonsairobo]: https://github.com/bonsairobo +[@gferon]: https://github.com/gferon +[@joshka]: https://github.com/joshka +[@kujeger]: https://github.com/kujeger +[@dyc3]: https://github.com/dyc3 +[@ThomWright]: https://github.com/ThomWright +[@duhby]: https://github.com/duhby diff --git a/Cargo.lock b/Cargo.lock index 68f3c84cd8..64634c69da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" @@ -28,32 +28,20 @@ dependencies = [ "version_check", ] -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -78,57 +66,58 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "argon2" @@ -143,20 +132,21 @@ dependencies = [ [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "assert_cmd" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" dependencies = [ "anstyle", "bstr", "doc-comment", - "predicates 3.1.0", + "libc", + "predicates 3.1.3", "predicates-core", "predicates-tree", "wait-timeout", @@ -185,28 +175,26 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", - "event-listener 5.2.0", - "event-listener-strategy 0.5.0", + "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" -version = "1.8.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ - "async-lock 3.3.0", "async-task", "concurrent-queue", - "fastrand 2.0.1", - "futures-lite 2.2.0", + "fastrand 2.3.0", + "futures-lite 2.5.0", "slab", ] @@ -216,12 +204,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.2.0", + "async-channel 2.3.1", "async-executor", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.4.0", + "async-lock 3.4.0", "blocking", - "futures-lite 2.2.0", + "futures-lite 2.5.0", "once_cell", ] @@ -239,7 +227,7 @@ dependencies = [ "log", "parking", "polling 2.8.0", - "rustix 0.37.27", + "rustix 0.37.28", "slab", "socket2 0.4.10", "waker-fn", @@ -247,21 +235,21 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ - "async-lock 3.3.0", + "async-lock 3.4.0", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.2.0", + "futures-lite 2.5.0", "parking", - "polling 3.5.0", - "rustix 0.38.31", + "polling 3.7.4", + "rustix 0.38.43", "slab", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -275,12 +263,12 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 4.0.3", - "event-listener-strategy 0.4.0", + "event-listener 5.4.0", + "event-listener-strategy", "pin-project-lite", ] @@ -293,13 +281,13 @@ dependencies = [ "async-attributes", "async-channel 1.9.0", "async-global-executor", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.4.0", + "async-lock 3.4.0", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite 2.2.0", + "futures-lite 2.5.0", "gloo-timers", "kv-log-macro", "log", @@ -313,19 +301,19 @@ dependencies = [ [[package]] name = "async-task" -version = "4.7.0" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -345,34 +333,32 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" -version = "1.8.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a47f2fb521b70c11ce7369a6c5fa4bd6af7e5d62ec06303875bafe7c6ba245" +checksum = "f409eb70b561706bf8abba8ca9c112729c481595893fd06a2dd9af8ed8441148" dependencies = [ "aws-lc-sys", - "mirai-annotations", "paste", "zeroize", ] [[package]] name = "aws-lc-sys" -version = "0.19.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2927c7af777b460b7ccd95f8b67acd7b4c04ec8896bf0c8e80ba30523cffc057" +checksum = "923ded50f602b3007e5e63e3f094c479d9c8a9b42d7f4034e4afe456aa48bfd2" dependencies = [ "bindgen", "cc", "cmake", "dunce", "fs_extra", - "libc", "paste", ] @@ -452,17 +438,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -473,9 +459,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -483,20 +469,11 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "basic-toml" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" -dependencies = [ - "serde", -] - [[package]] name = "bigdecimal" -version = "0.4.3" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9324c8014cd04590682b34f1e9448d38f0674d0f7b2dc553331016ef0e4e9ebc" +checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" dependencies = [ "autocfg", "libm", @@ -507,11 +484,11 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.4" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.7.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -524,7 +501,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.87", + "syn 2.0.96", "which", ] @@ -542,9 +519,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" dependencies = [ "serde", ] @@ -581,49 +558,45 @@ dependencies = [ [[package]] name = "blocking" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ - "async-channel 2.2.0", - "async-lock 3.3.0", + "async-channel 2.3.1", "async-task", - "fastrand 2.0.1", "futures-io", - "futures-lite 2.2.0", + "futures-lite 2.5.0", "piper", - "tracing", ] [[package]] name = "borsh" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" dependencies = [ "borsh-derive", - "cfg_aliases", + "cfg_aliases 0.2.1", ] [[package]] name = "borsh-derive" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", - "syn_derive", + "syn 2.0.96", ] [[package]] name = "bstr" -version = "1.9.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "regex-automata", @@ -632,9 +605,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecheck" @@ -666,24 +639,24 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "camino" -version = "1.1.6" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" dependencies = [ "serde", ] [[package]] name = "cargo-platform" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" dependencies = [ "serde", ] @@ -699,7 +672,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 1.0.58", + "thiserror 1.0.69", ] [[package]] @@ -725,12 +698,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.6" +version = "1.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +checksum = "ad0cf6e91fde44c773c6ee7ec6bba798504641a8bc2eb7e37a04ffbf4dfaa55a" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -748,6 +722,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cfg_aliases" version = "0.2.1" @@ -756,15 +736,15 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.4", + "windows-targets 0.52.6", ] [[package]] @@ -807,9 +787,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.2" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" +checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" dependencies = [ "clap_builder", "clap_derive", @@ -817,68 +797,58 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.0", + "strsim", + "terminal_size", ] [[package]] name = "clap_complete" -version = "4.5.1" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c" +checksum = "33a7e468e750fa4b6be660e8b5651ad47372e8fb114030b594c2d75d48c5ffd0" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "clap_lex" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" - -[[package]] -name = "clipboard-win" -version = "4.5.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" -dependencies = [ - "error-code", - "str-buf", - "winapi", -] +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cmake" -version = "0.1.50" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compact_str" @@ -895,24 +865,24 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" -version = "0.15.8" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "unicode-width", - "windows-sys 0.52.0", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", ] [[package]] @@ -931,26 +901,36 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] [[package]] name = "crc" -version = "3.0.1" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" dependencies = [ "crc-catalog", ] @@ -1001,9 +981,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1020,18 +1000,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" @@ -1039,10 +1019,10 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.7.0", "crossterm_winapi", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", @@ -1076,9 +1056,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -1086,34 +1066,34 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.87", + "strsim", + "syn 2.0.96", ] [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "pem-rfc7468", @@ -1130,6 +1110,17 @@ dependencies = [ "serde", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "thiserror 1.0.69", +] + [[package]] name = "difflib" version = "0.4.0" @@ -1148,27 +1139,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -1177,7 +1147,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -1200,36 +1170,30 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "dunce" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.10.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" dependencies = [ "serde", ] [[package]] name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - -[[package]] -name = "endian-type" -version = "0.1.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "env_filter" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -1237,9 +1201,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "anstream", "anstyle", @@ -1255,23 +1219,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "errno" -version = "0.3.8" +name = "erased-serde" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" dependencies = [ - "libc", - "windows-sys 0.52.0", + "serde", + "typeid", ] [[package]] -name = "error-code" -version = "2.3.1" +name = "errno" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "str-buf", + "windows-sys 0.59.0", ] [[package]] @@ -1293,20 +1257,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -1315,21 +1268,11 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.3", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ - "event-listener 5.2.0", + "event-listener 5.4.0", "pin-project-lite", ] @@ -1344,39 +1287,22 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" - -[[package]] -name = "fd-lock" -version = "3.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" -dependencies = [ - "cfg-if", - "rustix 0.38.31", - "windows-sys 0.48.0", -] +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" -version = "0.2.23" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", - "redox_syscall", - "windows-sys 0.52.0", + "libredox", + "windows-sys 0.59.0", ] -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "float-cmp" version = "0.9.0" @@ -1388,13 +1314,13 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin", ] [[package]] @@ -1405,9 +1331,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" [[package]] name = "foreign-types" @@ -1453,9 +1379,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -1468,9 +1394,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1478,15 +1404,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1506,9 +1432,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -1527,11 +1453,11 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.2.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ - "fastrand 2.0.1", + "fastrand 2.3.0", "futures-core", "futures-io", "parking", @@ -1540,32 +1466,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1591,9 +1517,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -1602,15 +1528,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "gloo-timers" @@ -1626,9 +1552,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", @@ -1640,17 +1566,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.8", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.11", - "allocator-api2", + "ahash", ] [[package]] @@ -1691,6 +1607,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -1754,9 +1676,9 @@ checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -1772,9 +1694,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -1786,7 +1708,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.6", + "socket2 0.5.8", "tokio", "tower-service", "tracing", @@ -1795,9 +1717,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1931,7 +1853,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -1990,19 +1912,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -2013,11 +1935,17 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + [[package]] name = "ipnetwork" version = "0.20.0" @@ -2029,15 +1957,21 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -2067,25 +2001,26 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -2100,11 +2035,11 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin", ] [[package]] @@ -2115,33 +2050,33 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.4", + "windows-targets 0.52.6", ] [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.7.0", "libc", "redox_syscall", ] @@ -2166,21 +2101,27 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2188,27 +2129,27 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d" dependencies = [ "value-bag", ] [[package]] name = "lru" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] name = "mac_address" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4863ee94f19ed315bf3bc00299338d857d4b5bc856af375cc97d237382ad3856" +checksum = "8836fae9d0d4be2c8b4efcdd79e828a2faa058a90d005abf42f91cac5493a08e" dependencies = [ "nix", "winapi", @@ -2232,15 +2173,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" -version = "0.6.5" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] @@ -2259,11 +2200,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ - "adler", + "adler2", ] [[package]] @@ -2279,10 +2220,15 @@ dependencies = [ ] [[package]] -name = "mirai-annotations" -version = "1.12.0" +name = "mio" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] [[package]] name = "mockall" @@ -2313,40 +2259,30 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - [[package]] name = "nix" -version = "0.23.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 1.3.2", - "cc", + "bitflags 2.7.0", "cfg-if", + "cfg_aliases 0.1.1", "libc", "memoffset", ] @@ -2369,11 +2305,10 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2412,9 +2347,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -2423,52 +2358,42 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" -version = "0.32.2" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oorandom" -version = "11.1.3" +version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.7.0", "cfg-if", "foreign-types", "libc", @@ -2485,7 +2410,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -2496,18 +2421,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.2.3+3.2.1" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -2518,15 +2443,15 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -2534,15 +2459,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -2558,9 +2483,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem-rfc7468" @@ -2579,29 +2504,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2611,12 +2536,12 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.1" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.0.1", + "fastrand 2.3.0", "futures-io", ] @@ -2643,15 +2568,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -2662,15 +2587,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] @@ -2693,16 +2618,17 @@ dependencies = [ [[package]] name = "polling" -version = "3.5.0" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", + "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.31", + "rustix 0.38.43", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2713,9 +2639,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "predicates" @@ -2733,9 +2662,9 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.0" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", @@ -2744,15 +2673,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", @@ -2760,19 +2689,19 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.17" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" +checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705" dependencies = [ "proc-macro2", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] @@ -2803,22 +2732,13 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] -[[package]] -name = "promptly" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9acbc6c5a5b029fe58342f58445acb00ccfe24624e538894bc2f04ce112980ba" -dependencies = [ - "rustyline", -] - [[package]] name = "ptr_meta" version = "0.1.4" @@ -2841,9 +2761,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2854,16 +2774,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", -] - [[package]] name = "rand" version = "0.8.5" @@ -2909,7 +2819,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.7.0", "cassowary", "compact_str", "crossterm", @@ -2921,14 +2831,14 @@ dependencies = [ "strum_macros", "unicode-segmentation", "unicode-truncate", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] name = "rayon" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -2946,29 +2856,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" -dependencies = [ - "getrandom", - "libredox", - "thiserror 1.0.58", + "bitflags 2.7.0", ] [[package]] name = "regex" -version = "1.10.3" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2978,9 +2877,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2989,9 +2888,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rend" @@ -3012,16 +2911,16 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", + "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rkyv" -version = "0.7.44" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ "bitvec", "bytecheck", @@ -3037,9 +2936,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.44" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" dependencies = [ "proc-macro2", "quote", @@ -3048,9 +2947,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" dependencies = [ "const-oid", "digest", @@ -3068,9 +2967,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.34.3" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39449a79f45e8da28c57c341891b69a183044b29518bb8f86dbac9df60bb7df" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" dependencies = [ "arrayvec", "borsh", @@ -3084,9 +2983,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -3096,9 +2995,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.27" +version = "0.37.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" dependencies = [ "bitflags 1.3.2", "errno", @@ -3110,22 +3009,35 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +dependencies = [ + "bitflags 2.7.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.7.0", "errno", "libc", - "linux-raw-sys 0.4.13", + "linux-raw-sys 0.9.4", "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.23.11" +version = "0.23.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ "aws-lc-rs", "once_cell", @@ -3138,38 +3050,27 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile", "rustls-pki-types", "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" -dependencies = [ - "base64 0.22.0", - "rustls-pki-types", + "security-framework 3.2.0", ] [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" [[package]] name = "rustls-webpki" -version = "0.102.5" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "aws-lc-rs", "ring", @@ -3179,39 +3080,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" - -[[package]] -name = "rustyline" -version = "9.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7826789c0e25614b03e5a54a0717a86f9ff6e6e5247f92b369472869320039" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "clipboard-win", - "dirs-next", - "fd-lock", - "libc", - "log", - "memchr", - "nix", - "radix_trie", - "scopeguard", - "smallvec", - "unicode-segmentation", - "unicode-width", - "utf8parse", - "winapi", -] +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -3224,11 +3101,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3245,12 +3122,25 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 1.3.2", - "core-foundation", + "bitflags 2.7.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.7.0", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -3258,9 +3148,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -3268,44 +3158,63 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.197" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", +] + +[[package]] +name = "serde_fmt" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +dependencies = [ + "serde", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3343,7 +3252,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -3368,6 +3277,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -3386,20 +3301,20 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", "signal-hook", ] [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3416,9 +3331,9 @@ dependencies = [ [[package]] name = "simdutf8" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "slab" @@ -3431,9 +3346,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" dependencies = [ "serde", ] @@ -3450,20 +3365,14 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -3485,7 +3394,7 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.3" +version = "0.8.6" dependencies = [ "anyhow", "async-std", @@ -3515,23 +3424,22 @@ dependencies = [ [[package]] name = "sqlx-cli" -version = "0.8.3" +version = "0.8.6" dependencies = [ "anyhow", "assert_cmd", - "async-trait", "backoff", "cargo_metadata", "chrono", "clap", "clap_complete", "console", + "dialoguer", "dotenvy", "filetime", "futures", "glob", "openssl", - "promptly", "serde_json", "sqlx", "tempfile", @@ -3540,10 +3448,11 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.3" +version = "0.8.6" dependencies = [ "async-io 1.13.0", "async-std", + "base64 0.22.1", "bigdecimal", "bit-vec", "bstr", @@ -3552,14 +3461,15 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener 5.2.0", + "event-listener 5.4.0", "futures-core", "futures-intrusive", "futures-io", "futures-util", "hashbrown 0.15.2", "hashlink", - "indexmap 2.2.5", + "indexmap 2.7.0", + "ipnet", "ipnetwork", "log", "mac_address", @@ -3571,13 +3481,12 @@ dependencies = [ "rust_decimal", "rustls", "rustls-native-certs", - "rustls-pemfile", "serde", "serde_json", "sha2", "smallvec", "sqlx", - "thiserror 2.0.0", + "thiserror 2.0.11", "time", "tokio", "tokio-stream", @@ -3613,7 +3522,7 @@ dependencies = [ "serde_json", "serde_with", "sqlx", - "thiserror 2.0.0", + "thiserror 2.0.11", "time", "tokio", "tower", @@ -3631,7 +3540,7 @@ dependencies = [ "ratatui", "sqlx", "tokio", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -3715,18 +3624,18 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.3" +version = "0.8.6" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "sqlx-macros-core" -version = "0.8.3" +version = "0.8.6" dependencies = [ "async-std", "dotenvy", @@ -3743,20 +3652,19 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.87", - "tempfile", + "syn 2.0.96", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.3" +version = "0.8.6" dependencies = [ "atoi", - "base64 0.22.0", + "base64 0.22.1", "bigdecimal", - "bitflags 2.4.2", + "bitflags 2.7.0", "byteorder", "bytes", "chrono", @@ -3788,7 +3696,7 @@ dependencies = [ "sqlx", "sqlx-core", "stringprep", - "thiserror 2.0.0", + "thiserror 2.0.11", "time", "tracing", "uuid", @@ -3797,13 +3705,13 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.3" +version = "0.8.6" dependencies = [ "atoi", - "base64 0.22.0", + "base64 0.22.1", "bigdecimal", "bit-vec", - "bitflags 2.4.2", + "bitflags 2.7.0", "byteorder", "chrono", "crc", @@ -3816,6 +3724,7 @@ dependencies = [ "hkdf", "hmac", "home", + "ipnet", "ipnetwork", "itoa", "log", @@ -3833,7 +3742,7 @@ dependencies = [ "sqlx", "sqlx-core", "stringprep", - "thiserror 2.0.0", + "thiserror 2.0.11", "time", "tracing", "uuid", @@ -3842,7 +3751,7 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.3" +version = "0.8.6" dependencies = [ "atoi", "chrono", @@ -3860,6 +3769,7 @@ dependencies = [ "serde_urlencoded", "sqlx", "sqlx-core", + "thiserror 2.0.11", "time", "tracing", "url", @@ -3883,7 +3793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" dependencies = [ "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -3898,34 +3808,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "str-buf" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" - [[package]] name = "stringprep" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "finl_unicode", "unicode-bidi", "unicode-normalization", + "unicode-properties", ] [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "strsim" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" @@ -3946,31 +3844,98 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "syn" -version = "1.0.109" +name = "sval" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "f6dc0f9830c49db20e73273ffae9b5240f63c42e515af1da1fceefb69fceafd8" + +[[package]] +name = "sval_buffer" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "429922f7ad43c0ef8fd7309e14d750e38899e32eb7e8da656ea169dd28ee212f" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f16ff5d839396c11a30019b659b0976348f3803db0626f736764c473b50ff4" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c27a80b6151b0557f9ccbe89c11db571dc5f68113690c1e028d7e974bae94" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0deef63c70da622b2a8069d8600cf4b05396459e665862e7bdb290fd6cf3f155" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a39ce5976ae1feb814c35d290cf7cf8cd4f045782fe1548d6bc32e21f6156e9f" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7c6ee3751795a728bc9316a092023529ffea1783499afbc5c66f5fabebb1fa" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a5572d0321b68109a343634e3a5d576bf131b82180c6c442dee06349dfc652a" +dependencies = [ + "serde", + "sval", + "sval_nested", ] [[package]] name = "syn" -version = "2.0.87" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -3978,15 +3943,14 @@ dependencies = [ ] [[package]] -name = "syn_derive" -version = "0.1.8" +name = "syn" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ - "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.87", + "unicode-ident", ] [[package]] @@ -4003,7 +3967,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -4012,16 +3976,24 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-triple" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" + [[package]] name = "tempfile" -version = "3.10.1" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", - "fastrand 2.0.1", - "rustix 0.38.31", - "windows-sys 0.52.0", + "fastrand 2.3.0", + "getrandom", + "once_cell", + "rustix 0.38.43", + "windows-sys 0.59.0", ] [[package]] @@ -4033,57 +4005,67 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix 1.0.5", + "windows-sys 0.59.0", +] + [[package]] name = "termtree" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.58", + "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" -version = "2.0.0" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15291287e9bff1bc6f9ff3409ed9af665bec7a5fc8ac079ea96be07bca0e2668" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.0", + "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "thiserror-impl" -version = "2.0.0" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22efd00f33f93fa62848a7cab956c3d38c8d43095efda1decfc2b3a5dc0b8972" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -4102,9 +4084,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -4132,9 +4114,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -4147,58 +4129,74 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.3", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.6", + "socket2 0.5.8", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" -version = "0.21.1" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.7.0", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -4240,21 +4238,21 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -4264,20 +4262,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -4290,19 +4288,25 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" -version = "1.0.89" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9d3ba662913483d6722303f619e75ea10b7855b0f8e0d72799cf8621bb488f" +checksum = "8dcd332a5496c026f1e14b7f3d2b7bd98e509660c04239c58b0ba38a12daded4" dependencies = [ - "basic-toml", "glob", - "once_cell", "serde", "serde_derive", "serde_json", + "target-triple", "termcolor", + "toml", ] +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + [[package]] name = "typenum" version = "1.17.0" @@ -4311,30 +4315,36 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-truncate" @@ -4344,14 +4354,20 @@ checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools 0.13.0", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "untrusted" @@ -4361,9 +4377,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna 1.0.3", @@ -4384,15 +4400,15 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.7.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" dependencies = [ "serde", ] @@ -4441,9 +4457,39 @@ dependencies = [ [[package]] name = "value-bag" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec26a25bd6fca441cdd0f769fd7f891bae119f996de31f86a5eddccef54c1d" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b" +dependencies = [ + "erased-serde", + "serde", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] [[package]] name = "vcpkg" @@ -4453,9 +4499,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" @@ -4468,9 +4514,9 @@ dependencies = [ [[package]] name = "waker-fn" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" [[package]] name = "walkdir" @@ -4505,46 +4551,47 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4552,28 +4599,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", @@ -4581,9 +4628,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" dependencies = [ "rustls-pki-types", ] @@ -4597,14 +4644,14 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.31", + "rustix 0.38.43", ] [[package]] name = "whoami" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ "redox_syscall", "wasite", @@ -4628,11 +4675,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -4647,7 +4694,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.6", ] [[package]] @@ -4665,7 +4712,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -4685,17 +4741,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -4706,9 +4763,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -4718,9 +4775,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -4730,9 +4787,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -4742,9 +4805,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -4754,9 +4817,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -4766,9 +4829,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -4778,15 +4841,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] @@ -4814,9 +4877,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -4826,76 +4889,63 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", "synstructure", ] [[package]] name = "zeroize" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", -] +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zerovec" @@ -4916,5 +4966,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] diff --git a/Cargo.toml b/Cargo.toml index 316dc471e1..2419b50d97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.8.3" +version = "0.8.6" license = "MIT OR Apache-2.0" edition = "2021" repository = "https://github.com/launchbadge/sqlx" @@ -50,7 +50,7 @@ authors.workspace = true repository.workspace = true [package.metadata.docs.rs] -features = ["all-databases", "_unstable-all-types"] +features = ["all-databases", "_unstable-all-types", "sqlite-preupdate-hook"] rustdoc-args = ["--cfg", "docsrs"] [features] @@ -68,10 +68,12 @@ _unstable-all-types = [ "json", "time", "chrono", + "ipnet", "ipnetwork", "mac_address", "uuid", "bit-vec", + "bstr" ] # Base runtime features without TLS @@ -108,39 +110,43 @@ postgres = ["sqlx-postgres", "sqlx-macros?/postgres"] mysql = ["sqlx-mysql", "sqlx-macros?/mysql"] sqlite = ["_sqlite", "sqlx-sqlite/bundled", "sqlx-macros?/sqlite"] sqlite-unbundled = ["_sqlite", "sqlx-sqlite/unbundled", "sqlx-macros?/sqlite-unbundled"] +sqlite-preupdate-hook = ["sqlx-sqlite/preupdate-hook"] # types -json = ["sqlx-macros?/json", "sqlx-mysql?/json", "sqlx-postgres?/json", "sqlx-sqlite?/json"] +json = ["sqlx-core/json", "sqlx-macros?/json", "sqlx-mysql?/json", "sqlx-postgres?/json", "sqlx-sqlite?/json"] bigdecimal = ["sqlx-core/bigdecimal", "sqlx-macros?/bigdecimal", "sqlx-mysql?/bigdecimal", "sqlx-postgres?/bigdecimal"] bit-vec = ["sqlx-core/bit-vec", "sqlx-macros?/bit-vec", "sqlx-postgres?/bit-vec"] chrono = ["sqlx-core/chrono", "sqlx-macros?/chrono", "sqlx-mysql?/chrono", "sqlx-postgres?/chrono", "sqlx-sqlite?/chrono"] +ipnet = ["sqlx-core/ipnet", "sqlx-macros?/ipnet", "sqlx-postgres?/ipnet"] ipnetwork = ["sqlx-core/ipnetwork", "sqlx-macros?/ipnetwork", "sqlx-postgres?/ipnetwork"] mac_address = ["sqlx-core/mac_address", "sqlx-macros?/mac_address", "sqlx-postgres?/mac_address"] rust_decimal = ["sqlx-core/rust_decimal", "sqlx-macros?/rust_decimal", "sqlx-mysql?/rust_decimal", "sqlx-postgres?/rust_decimal"] time = ["sqlx-core/time", "sqlx-macros?/time", "sqlx-mysql?/time", "sqlx-postgres?/time", "sqlx-sqlite?/time"] uuid = ["sqlx-core/uuid", "sqlx-macros?/uuid", "sqlx-mysql?/uuid", "sqlx-postgres?/uuid", "sqlx-sqlite?/uuid"] regexp = ["sqlx-sqlite?/regexp"] +bstr = ["sqlx-core/bstr"] [workspace.dependencies] # Core Crates -sqlx-core = { version = "=0.8.3", path = "sqlx-core" } -sqlx-macros-core = { version = "=0.8.3", path = "sqlx-macros-core" } -sqlx-macros = { version = "=0.8.3", path = "sqlx-macros" } +sqlx-core = { version = "=0.8.6", path = "sqlx-core" } +sqlx-macros-core = { version = "=0.8.6", path = "sqlx-macros-core" } +sqlx-macros = { version = "=0.8.6", path = "sqlx-macros" } # Driver crates -sqlx-mysql = { version = "=0.8.3", path = "sqlx-mysql" } -sqlx-postgres = { version = "=0.8.3", path = "sqlx-postgres" } -sqlx-sqlite = { version = "=0.8.3", path = "sqlx-sqlite" } +sqlx-mysql = { version = "=0.8.6", path = "sqlx-mysql" } +sqlx-postgres = { version = "=0.8.6", path = "sqlx-postgres" } +sqlx-sqlite = { version = "=0.8.6", path = "sqlx-sqlite" } # Facade crate (for reference from sqlx-cli) -sqlx = { version = "=0.8.3", path = ".", default-features = false } +sqlx = { version = "=0.8.6", path = ".", default-features = false } # Common type integrations shared by multiple driver crates. # These are optional unless enabled in a workspace crate. bigdecimal = "0.4.0" bit-vec = "0.6.3" chrono = { version = "0.4.34", default-features = false, features = ["std", "clock"] } +ipnet = "2.3.0" ipnetwork = "0.20.0" mac_address = "1.1.5" rust_decimal = { version = "1.26.1", default-features = false, features = ["std"] } @@ -148,7 +154,7 @@ time = { version = "0.3.36", features = ["formatting", "parsing", "macros"] } uuid = "1.1.2" # Common utility crates -dotenvy = { version = "0.15.0", default-features = false } +dotenvy = { version = "0.15.7", default-features = false } # Runtimes [workspace.dependencies.async-std] @@ -186,6 +192,7 @@ rand_xoshiro = "0.6.0" hex = "0.4.3" tempfile = "3.10.1" criterion = { version = "0.5.1", features = ["async_tokio"] } +libsqlite3-sys = { version = "0.30.1" } # If this is an unconditional dev-dependency then Cargo will *always* try to build `libsqlite3-sys`, # even when SQLite isn't the intended test target, and fail if the build environment is not set up for compiling C code. diff --git a/FAQ.md b/FAQ.md index f0bccd3c41..cf13cf73ee 100644 --- a/FAQ.md +++ b/FAQ.md @@ -36,6 +36,62 @@ as they can often be a whole year or more out-of-date. [`rust-version`]: https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-rust-version-field +---------------------------------------------------------------- + +### Can SQLx Add Support for New Databases? + +We are always open to discuss adding support for new databases, but as of writing, have no plans to in the short term. + +Implementing support for a new database in SQLx is a _huge_ lift. Expecting this work to be done for free is highly unrealistic. +In all likelihood, the implementation would need to be written from scratch. +Even if Rust bindings exist, they may not support `async`. +Even if they support `async`, they may only support either Tokio or `async-std`, and not both. +Even if they support Tokio and `async-std`, the API may not be flexible enough or provide sufficient information (e.g. for implementing the macros). + +If we have to write the implementation from scratch, is the protocol publicly documented, and stable? + +Even if everything is supported on the client side, how will we run tests against the database? Is it open-source, or proprietary? Will it require a paid license? + +For example, Oracle Database's protocol is proprietary and only supported through their own libraries, which do not support Rust, and only have blocking APIs (see: [Oracle Call Interface for C](https://docs.oracle.com/en/database/oracle/oracle-database/23/lnoci/index.html)). +This makes it a poor candidate for an async-native crate like SQLx--though we support SQLite, which also only has a blocking API, that's the exception and not the rule. Wrapping blocking APIs is not very scalable. + +We still have plans to bring back the MSSQL driver, but this is not feasible as of writing with the current maintenance workload. Should this change, an announcement will be made on Github as well as our [Discord server](https://discord.gg/uuruzJ7). + +### What If I'm Willing to Contribute the Implementation? + +Being willing to contribute an implementation for a new database is one thing, but there's also the ongoing maintenance burden to consider. + +Are you willing to provide support long-term? +Will there be enough users that we can rely on outside contributions? +Or is support going to fall to the current maintainer(s)? + +This is the kind of thing that will need to be supported in SQLx _long_ after the initial implementation, or else later need to be removed. +If you don't have plans for how to support a new driver long-term, then it doesn't belong as part of SQLx itself. + +However, drivers don't necessarily need to live _in_ SQLx anymore. Since 0.7.0, drivers don't need to be compiled-in to be functional. +Support for third-party drivers in `sqlx-cli` and the `query!()` macros is pending, as well as documenting the process of writing a driver, but contributions are welcome in this regard. + +For example, see [sqlx-exasol](https://crates.io/crates/sqlx-exasol). + +---------------------------------------------------------------- +### Can SQLx Add Support for New Data-Type Crates (e.g. Jiff in addition to `chrono` and `time`)? + +This has a lot of the same considerations as adding support for new databases (see above), but with one big additional problem: Semantic Versioning. + +When we add trait implementations for types from an external crate, that crate then becomes part of our public API. We become beholden to its release cycle. + +If the crate's API is still evolving, meaning they are making breaking changes frequently, and thus releasing new major versions frequently, that then becomes a burden on us to upgrade and release a new major version as well so everyone _else_ can upgrade. + +We don't have the maintainer bandwidth to support multiple major versions simultaneously (we have no Long-Term Support policy), so this means that users who want to keep up-to-date are forced to make frequent manual upgrades as well. + +Thus, it is best that we stick to only supporting crates which have a stable API, and which are not making new major releases frequently. + +Conversely, adding support for SQLx _in_ these crates may not be desirable either, since SQLx is a large dependency and a higher-level crate. In this case, the SemVer problem gets pushed onto the other crate. + +There isn't a satisfying answer to this problem, but one option is to have an intermediate wrapper crate. +For example, [`jiff-sqlx`](https://crates.io/crates/jiff-sqlx), which is maintained by the author of Jiff. +API changes to SQLx are pending to make this pattern easier to use. + ---------------------------------------------------------------- ### I'm getting `HandshakeFailure` or `CorruptMessage` when trying to connect to a server over TLS using RusTLS. What gives? diff --git a/README.md b/README.md index 4d4a2338ec..cc0ecf2e66 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,10 @@ be removed in the future. * May result in link errors if the SQLite version is too old. Version `3.20.0` or newer is recommended. * Can increase build time due to the use of bindgen. +- `sqlite-preupdate-hook`: enables SQLite's [preupdate hook](https://sqlite.org/c3ref/preupdate_count.html) API. + * Exposed as a separate feature because it's generally not enabled by default. + * Using this feature with `sqlite-unbundled` may cause linker failures if the system SQLite version does not support it. + - `any`: Add support for the `Any` database driver, which can proxy to a database driver at runtime. - `derive`: Add support for the derive family macros, those are `FromRow`, `Type`, `Encode`, `Decode`. @@ -204,7 +208,7 @@ be removed in the future. - `migrate`: Add support for the migration management and `migrate!` macro, which allow compile-time embedded migrations. -- `uuid`: Add support for UUID (in Postgres). +- `uuid`: Add support for UUID. - `chrono`: Add support for date and time types from `chrono`. @@ -216,6 +220,8 @@ be removed in the future. - `rust_decimal`: Add support for `NUMERIC` using the `rust_decimal` crate. +- `ipnet`: Add support for `INET` and `CIDR` (in postgres) using the `ipnet` crate. + - `ipnetwork`: Add support for `INET` and `CIDR` (in postgres) using the `ipnetwork` crate. - `json`: Add support for `JSON` and `JSONB` (in postgres) using the `serde_json` crate. diff --git a/sqlx-cli/Cargo.toml b/sqlx-cli/Cargo.toml index 0b047ab136..de87b38ba0 100644 --- a/sqlx-cli/Cargo.toml +++ b/sqlx-cli/Cargo.toml @@ -26,20 +26,19 @@ path = "src/bin/cargo-sqlx.rs" [dependencies] dotenvy = "0.15.0" -tokio = { version = "1.15.0", features = ["macros", "rt", "rt-multi-thread"] } +tokio = { version = "1.15.0", features = ["macros", "rt", "rt-multi-thread", "signal"] } sqlx = { workspace = true, default-features = false, features = [ "runtime-tokio", "migrate", "any", ] } futures = "0.3.19" -clap = { version = "4.3.10", features = ["derive", "env"] } +clap = { version = "4.3.10", features = ["derive", "env", "wrap_help"] } clap_complete = { version = "4.3.1", optional = true } chrono = { version = "0.4.19", default-features = false, features = ["clock"] } anyhow = "1.0.52" -async-trait = "0.1.52" console = "0.15.0" -promptly = "0.3.0" +dialoguer = { version = "0.11", default-features = false } serde_json = "1.0.73" glob = "0.3.0" openssl = { version = "0.10.38", optional = true } diff --git a/sqlx-cli/src/bin/cargo-sqlx.rs b/sqlx-cli/src/bin/cargo-sqlx.rs index 58f7b345f4..a5b5db8ea9 100644 --- a/sqlx-cli/src/bin/cargo-sqlx.rs +++ b/sqlx-cli/src/bin/cargo-sqlx.rs @@ -13,7 +13,8 @@ enum Cli { #[tokio::main] async fn main() { - dotenvy::dotenv().ok(); + sqlx_cli::maybe_apply_dotenv(); + let Cli::Sqlx(opt) = Cli::parse(); if let Err(error) = sqlx_cli::run(opt).await { diff --git a/sqlx-cli/src/bin/sqlx.rs b/sqlx-cli/src/bin/sqlx.rs index 59025cd7da..c015907990 100644 --- a/sqlx-cli/src/bin/sqlx.rs +++ b/sqlx-cli/src/bin/sqlx.rs @@ -4,9 +4,13 @@ use sqlx_cli::Opt; #[tokio::main] async fn main() { - dotenvy::dotenv().ok(); + // Checks for `--no-dotenv` before parsing. + sqlx_cli::maybe_apply_dotenv(); + + let opt = Opt::parse(); + // no special handling here - if let Err(error) = sqlx_cli::run(Opt::parse()).await { + if let Err(error) = sqlx_cli::run(opt).await { println!("{} {}", style("error:").bold().red(), error); std::process::exit(1); } diff --git a/sqlx-cli/src/database.rs b/sqlx-cli/src/database.rs index 7a2056ab35..7a9bc6bf2f 100644 --- a/sqlx-cli/src/database.rs +++ b/sqlx-cli/src/database.rs @@ -1,9 +1,11 @@ use crate::migrate; use crate::opt::ConnectOpts; -use console::style; -use promptly::{prompt, ReadlineError}; +use console::{style, Term}; +use dialoguer::Confirm; use sqlx::any::Any; use sqlx::migrate::MigrateDatabase; +use std::{io, mem}; +use tokio::task; pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> { // NOTE: only retry the idempotent action. @@ -24,7 +26,7 @@ pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> { } pub async fn drop(connect_opts: &ConnectOpts, confirm: bool, force: bool) -> anyhow::Result<()> { - if confirm && !ask_to_continue_drop(connect_opts.required_db_url()?) { + if confirm && !ask_to_continue_drop(connect_opts.required_db_url()?.to_owned()).await { return Ok(()); } @@ -58,27 +60,46 @@ pub async fn setup(migration_source: &str, connect_opts: &ConnectOpts) -> anyhow migrate::run(migration_source, connect_opts, false, false, None).await } -fn ask_to_continue_drop(db_url: &str) -> bool { - loop { - let r: Result = - prompt(format!("Drop database at {}? (y/n)", style(db_url).cyan())); - match r { - Ok(response) => { - if response == "n" || response == "N" { - return false; - } else if response == "y" || response == "Y" { - return true; - } else { - println!( - "Response not recognized: {}\nPlease type 'y' or 'n' and press enter.", - response - ); - } - } - Err(e) => { - println!("{e}"); - return false; +async fn ask_to_continue_drop(db_url: String) -> bool { + // If the setup operation is cancelled while we are waiting for the user to decide whether + // or not to drop the database, this will restore the terminal's cursor to its normal state. + struct RestoreCursorGuard { + disarmed: bool, + } + + impl Drop for RestoreCursorGuard { + fn drop(&mut self) { + if !self.disarmed { + Term::stderr().show_cursor().unwrap() } } } + + let mut guard = RestoreCursorGuard { disarmed: false }; + + let decision_result = task::spawn_blocking(move || { + Confirm::new() + .with_prompt(format!("Drop database at {}?", style(&db_url).cyan())) + .wait_for_newline(true) + .default(false) + .show_default(true) + .interact() + }) + .await + .expect("Confirm thread panicked"); + match decision_result { + Ok(decision) => { + guard.disarmed = true; + decision + } + Err(dialoguer::Error::IO(err)) if err.kind() == io::ErrorKind::Interrupted => { + // Sometimes CTRL + C causes this error to be returned + mem::drop(guard); + false + } + Err(err) => { + mem::drop(guard); + panic!("Confirm dialog failed with {err}") + } + } } diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index bfd71e4bc1..cb31205b4f 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -5,6 +5,7 @@ use anyhow::Result; use futures::{Future, TryFutureExt}; use sqlx::{AnyConnection, Connection}; +use tokio::{select, signal}; use crate::opt::{Command, ConnectOpts, DatabaseCommand, MigrateCommand}; @@ -20,7 +21,36 @@ mod prepare; pub use crate::opt::Opt; +/// Check arguments for `--no-dotenv` _before_ Clap parsing, and apply `.env` if not set. +pub fn maybe_apply_dotenv() { + if std::env::args().any(|arg| arg == "--no-dotenv") { + return; + } + + dotenvy::dotenv().ok(); +} + pub async fn run(opt: Opt) -> Result<()> { + // This `select!` is here so that when the process receives a `SIGINT` (CTRL + C), + // the futures currently running on this task get dropped before the program exits. + // This is currently necessary for the consumers of the `dialoguer` crate to restore + // the user's terminal if the process is interrupted while a dialog is being displayed. + + let ctrlc_fut = signal::ctrl_c(); + let do_run_fut = do_run(opt); + + select! { + biased; + _ = ctrlc_fut => { + Ok(()) + }, + do_run_outcome = do_run_fut => { + do_run_outcome + } + } +} + +async fn do_run(opt: Opt) -> Result<()> { match opt.command { Command::Migrate(migrate) => match migrate.command { MigrateCommand::Add { diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index d5fe315234..133ba084f2 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -1,12 +1,26 @@ use std::ops::{Deref, Not}; -use clap::{Args, Parser}; +use clap::{ + builder::{styling::AnsiColor, Styles}, + Args, Parser, +}; #[cfg(feature = "completions")] use clap_complete::Shell; +const HELP_STYLES: Styles = Styles::styled() + .header(AnsiColor::Blue.on_default().bold()) + .usage(AnsiColor::Blue.on_default().bold()) + .literal(AnsiColor::White.on_default()) + .placeholder(AnsiColor::Green.on_default()); + #[derive(Parser, Debug)] -#[clap(version, about, author)] +#[clap(version, about, author, styles = HELP_STYLES)] pub struct Opt { + // https://github.com/launchbadge/sqlx/pull/3724 placed this here, + // but the intuitive place would be in the arguments for each subcommand. + #[clap(flatten)] + pub no_dotenv: NoDotenvOpt, + #[clap(subcommand)] pub command: Command, } @@ -241,6 +255,9 @@ impl Deref for Source { /// Argument for the database URL. #[derive(Args, Debug)] pub struct ConnectOpts { + #[clap(flatten)] + pub no_dotenv: NoDotenvOpt, + /// Location of the DB, by default will be read from the DATABASE_URL env var or `.env` files. #[clap(long, short = 'D', env)] pub database_url: Option, @@ -263,6 +280,16 @@ pub struct ConnectOpts { pub sqlite_create_db_wal: bool, } +#[derive(Args, Debug)] +pub struct NoDotenvOpt { + /// Do not automatically load `.env` files. + #[clap(long)] + // Parsing of this flag is actually handled _before_ calling Clap, + // by `crate::maybe_apply_dotenv()`. + #[allow(unused)] // TODO: switch to `#[expect]` + pub no_dotenv: bool, +} + impl ConnectOpts { /// Require a database URL to be provided, otherwise /// return an error. diff --git a/sqlx-cli/tests/add.rs b/sqlx-cli/tests/add.rs index 523ce4767e..1d5ed7c7dd 100644 --- a/sqlx-cli/tests/add.rs +++ b/sqlx-cli/tests/add.rs @@ -17,7 +17,7 @@ fn add_migration_ambiguous() -> anyhow::Result<()> { #[derive(Debug, PartialEq, Eq)] struct FileName { - id: usize, + id: u64, description: String, suffix: String, } @@ -50,7 +50,7 @@ impl From for FileName { fn from(path: PathBuf) -> Self { let filename = path.file_name().unwrap().to_string_lossy(); let (id, rest) = filename.split_once("_").unwrap(); - let id: usize = id.parse().unwrap(); + let id: u64 = id.parse().unwrap(); let (description, suffix) = rest.split_once(".").unwrap(); Self { id, diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index d662861470..f6017a9fee 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -25,7 +25,7 @@ _tls-native-tls = ["native-tls"] _tls-rustls-aws-lc-rs = ["_tls-rustls", "rustls/aws-lc-rs", "webpki-roots"] _tls-rustls-ring-webpki = ["_tls-rustls", "rustls/ring", "webpki-roots"] _tls-rustls-ring-native-roots = ["_tls-rustls", "rustls/ring", "rustls-native-certs"] -_tls-rustls = ["rustls", "rustls-pemfile"] +_tls-rustls = ["rustls"] _tls-none = [] # support offline/decoupled building (enables serialization of `Describe`) @@ -39,8 +39,7 @@ tokio = { workspace = true, optional = true } # TLS native-tls = { version = "0.2.10", optional = true } -rustls = { version = "0.23.11", default-features = false, features = ["std", "tls12"], optional = true } -rustls-pemfile = { version = "2", optional = true } +rustls = { version = "0.23.15", default-features = false, features = ["std", "tls12"], optional = true } webpki-roots = { version = "0.26", optional = true } rustls-native-certs = { version = "0.8.0", optional = true } @@ -49,11 +48,13 @@ bit-vec = { workspace = true, optional = true } bigdecimal = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } time = { workspace = true, optional = true } +ipnet = { workspace = true, optional = true } ipnetwork = { workspace = true, optional = true } mac_address = { workspace = true, optional = true } uuid = { workspace = true, optional = true } async-io = { version = "1.9.0", optional = true } +base64 = { version = "0.22.0", default-features = false, features = ["std"] } bytes = "1.1.0" chrono = { version = "0.4.34", default-features = false, features = ["clock"], optional = true } crc = { version = "3", optional = true } diff --git a/sqlx-core/src/acquire.rs b/sqlx-core/src/acquire.rs index c9d7fb215c..59bac9fa59 100644 --- a/sqlx-core/src/acquire.rs +++ b/sqlx-core/src/acquire.rs @@ -93,7 +93,7 @@ impl<'a, DB: Database> Acquire<'a> for &'_ Pool { let conn = self.acquire(); Box::pin(async move { - Transaction::begin(MaybePoolConnection::PoolConnection(conn.await?)).await + Transaction::begin(MaybePoolConnection::PoolConnection(conn.await?), None).await }) } } @@ -121,7 +121,7 @@ macro_rules! impl_acquire { 'c, Result<$crate::transaction::Transaction<'c, $DB>, $crate::error::Error>, > { - $crate::transaction::Transaction::begin(self) + $crate::transaction::Transaction::begin(self, None) } } }; diff --git a/sqlx-core/src/any/connection/backend.rs b/sqlx-core/src/any/connection/backend.rs index b30cbe83f3..6c84c1d8ce 100644 --- a/sqlx-core/src/any/connection/backend.rs +++ b/sqlx-core/src/any/connection/backend.rs @@ -3,6 +3,7 @@ use crate::describe::Describe; use either::Either; use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; +use std::borrow::Cow; use std::fmt::Debug; pub trait AnyConnectionBackend: std::any::Any + Debug + Send + 'static { @@ -26,7 +27,13 @@ pub trait AnyConnectionBackend: std::any::Any + Debug + Send + 'static { fn ping(&mut self) -> BoxFuture<'_, crate::Result<()>>; /// Begin a new transaction or establish a savepoint within the active transaction. - fn begin(&mut self) -> BoxFuture<'_, crate::Result<()>>; + /// + /// If this is a new transaction, `statement` may be used instead of the + /// default "BEGIN" statement. + /// + /// If we are already inside a transaction and `statement.is_some()`, then + /// `Error::InvalidSavePoint` is returned without running any statements. + fn begin(&mut self, statement: Option>) -> BoxFuture<'_, crate::Result<()>>; fn commit(&mut self) -> BoxFuture<'_, crate::Result<()>>; @@ -34,6 +41,26 @@ pub trait AnyConnectionBackend: std::any::Any + Debug + Send + 'static { fn start_rollback(&mut self); + /// Returns the current transaction depth. + /// + /// Transaction depth indicates the level of nested transactions: + /// - Level 0: No active transaction. + /// - Level 1: A transaction is active. + /// - Level 2 or higher: A transaction is active and one or more SAVEPOINTs have been created within it. + fn get_transaction_depth(&self) -> usize { + unimplemented!("get_transaction_depth() is not implemented for this backend. This is a provided method to avoid a breaking change, but it will become a required method in version 0.9 and later."); + } + + /// Checks if the connection is currently in a transaction. + /// + /// This method returns `true` if the current transaction depth is greater than 0, + /// indicating that a transaction is active. It returns `false` if the transaction depth is 0, + /// meaning no transaction is active. + #[inline] + fn is_in_transaction(&self) -> bool { + self.get_transaction_depth() != 0 + } + /// The number of statements currently cached in the connection. fn cached_statements_size(&self) -> usize { 0 diff --git a/sqlx-core/src/any/connection/mod.rs b/sqlx-core/src/any/connection/mod.rs index b6f795848a..8cf8fc510c 100644 --- a/sqlx-core/src/any/connection/mod.rs +++ b/sqlx-core/src/any/connection/mod.rs @@ -1,4 +1,5 @@ use futures_core::future::BoxFuture; +use std::borrow::Cow; use crate::any::{Any, AnyConnectOptions}; use crate::connection::{ConnectOptions, Connection}; @@ -87,7 +88,17 @@ impl Connection for AnyConnection { where Self: Sized, { - Transaction::begin(self) + Transaction::begin(self, None) + } + + fn begin_with( + &mut self, + statement: impl Into>, + ) -> BoxFuture<'_, Result, Error>> + where + Self: Sized, + { + Transaction::begin(self, Some(statement.into())) } fn cached_statements_size(&self) -> usize { diff --git a/sqlx-core/src/any/transaction.rs b/sqlx-core/src/any/transaction.rs index fce4175626..a553cda927 100644 --- a/sqlx-core/src/any/transaction.rs +++ b/sqlx-core/src/any/transaction.rs @@ -1,6 +1,8 @@ use futures_util::future::BoxFuture; +use std::borrow::Cow; use crate::any::{Any, AnyConnection}; +use crate::database::Database; use crate::error::Error; use crate::transaction::TransactionManager; @@ -9,8 +11,11 @@ pub struct AnyTransactionManager; impl TransactionManager for AnyTransactionManager { type Database = Any; - fn begin(conn: &mut AnyConnection) -> BoxFuture<'_, Result<(), Error>> { - conn.backend.begin() + fn begin<'conn>( + conn: &'conn mut AnyConnection, + statement: Option>, + ) -> BoxFuture<'conn, Result<(), Error>> { + conn.backend.begin(statement) } fn commit(conn: &mut AnyConnection) -> BoxFuture<'_, Result<(), Error>> { @@ -24,4 +29,8 @@ impl TransactionManager for AnyTransactionManager { fn start_rollback(conn: &mut AnyConnection) { conn.backend.start_rollback() } + + fn get_transaction_depth(conn: &::Connection) -> usize { + conn.backend.get_transaction_depth() + } } diff --git a/sqlx-core/src/connection.rs b/sqlx-core/src/connection.rs index ce2aa6c629..74e8cd3e8b 100644 --- a/sqlx-core/src/connection.rs +++ b/sqlx-core/src/connection.rs @@ -1,9 +1,10 @@ use crate::database::{Database, HasStatementCache}; use crate::error::Error; -use crate::transaction::Transaction; +use crate::transaction::{Transaction, TransactionManager}; use futures_core::future::BoxFuture; use log::LevelFilter; +use std::borrow::Cow; use std::fmt::Debug; use std::str::FromStr; use std::time::Duration; @@ -49,6 +50,33 @@ pub trait Connection: Send { where Self: Sized; + /// Begin a new transaction with a custom statement. + /// + /// Returns a [`Transaction`] for controlling and tracking the new transaction. + /// + /// Returns an error if the connection is already in a transaction or if + /// `statement` does not put the connection into a transaction. + fn begin_with( + &mut self, + statement: impl Into>, + ) -> BoxFuture<'_, Result, Error>> + where + Self: Sized, + { + Transaction::begin(self, Some(statement.into())) + } + + /// Returns `true` if the connection is currently in a transaction. + /// + /// # Note: Automatic Rollbacks May Not Be Counted + /// Certain database errors (such as a serializable isolation failure) + /// can cause automatic rollbacks of a transaction + /// which may not be indicated in the return value of this method. + #[inline] + fn is_in_transaction(&self) -> bool { + ::TransactionManager::get_transaction_depth(self) != 0 + } + /// Execute the function inside a transaction. /// /// If the function returns an error, the transaction will be rolled back. If it does not diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index 17774addd2..9ad5eff464 100644 --- a/sqlx-core/src/error.rs +++ b/sqlx-core/src/error.rs @@ -34,6 +34,12 @@ pub enum Error { #[error("error with configuration: {0}")] Configuration(#[source] BoxDynError), + /// One or more of the arguments to the called function was invalid. + /// + /// The string contains more information. + #[error("{0}")] + InvalidArgument(String), + /// Error returned from the database. #[error("error returned from database: {0}")] Database(#[source] Box), @@ -79,7 +85,7 @@ pub enum Error { }, /// Error occured while encoding a value. - #[error("error occured while encoding a value: {0}")] + #[error("error occurred while encoding a value: {0}")] Encode(#[source] BoxDynError), /// Error occurred while decoding a value. @@ -111,6 +117,12 @@ pub enum Error { #[cfg(feature = "migrate")] #[error("{0}")] Migrate(#[source] Box), + + #[error("attempted to call begin_with at non-zero transaction depth")] + InvalidSavePointStatement, + + #[error("got unexpected connection status after attempting to begin transaction")] + BeginFailed, } impl StdError for Box {} @@ -136,6 +148,12 @@ impl Error { Error::Protocol(err.to_string()) } + #[doc(hidden)] + #[inline] + pub fn database(err: impl DatabaseError) -> Self { + Error::Database(Box::new(err)) + } + #[doc(hidden)] #[inline] pub fn config(err: impl StdError + Send + Sync + 'static) -> Self { diff --git a/sqlx-core/src/ext/async_stream.rs b/sqlx-core/src/ext/async_stream.rs index a83aabed14..56777ca4db 100644 --- a/sqlx-core/src/ext/async_stream.rs +++ b/sqlx-core/src/ext/async_stream.rs @@ -121,7 +121,7 @@ impl<'a, T> Stream for TryAsyncStream<'a, T> { #[macro_export] macro_rules! try_stream { ($($block:tt)*) => { - $crate::ext::async_stream::TryAsyncStream::new(move |yielder| async move { + $crate::ext::async_stream::TryAsyncStream::new(move |yielder| ::tracing::Instrument::in_current_span(async move { // Anti-footgun: effectively pins `yielder` to this future to prevent any accidental // move to another task, which could deadlock. let yielder = &yielder; @@ -133,6 +133,6 @@ macro_rules! try_stream { } $($block)* - }) + })) } } diff --git a/sqlx-core/src/from_row.rs b/sqlx-core/src/from_row.rs index 9c647d370a..b96801cb65 100644 --- a/sqlx-core/src/from_row.rs +++ b/sqlx-core/src/from_row.rs @@ -65,6 +65,14 @@ use crate::{error::Error, row::Row}; /// reason), `lowercase`, `UPPERCASE`, `camelCase`, `PascalCase`, `SCREAMING_SNAKE_CASE` and `kebab-case`. /// The styling of each option is intended to be an example of its behavior. /// +/// Case conversion is handled by the `heck` crate. +/// See [its documentation](https://docs.rs/heck/0.5.0/heck/#definition-of-a-word-boundary) +/// for details. +/// +/// Note that numbers are *not* considered separate words. +/// For example, `Foo1` to snake case would be `foo1`, *not* `foo_1`. +/// See [this issue](https://github.com/launchbadge/sqlx/issues/3864) for discussion. +/// /// #### `default` /// /// When your struct contains a field that is not present in your query, @@ -111,7 +119,8 @@ use crate::{error::Error, row::Row}; /// different placeholder values, if applicable. /// /// This is similar to how `#[serde(default)]` behaves. -/// ### `flatten` +/// +/// #### `flatten` /// /// If you want to handle a field that implements [`FromRow`], /// you can use the `flatten` attribute to specify that you want @@ -177,33 +186,6 @@ use crate::{error::Error, row::Row}; /// assert!(user.addresses.is_empty()); /// ``` /// -/// ## Manual implementation -/// -/// You can also implement the [`FromRow`] trait by hand. This can be useful if you -/// have a struct with a field that needs manual decoding: -/// -/// -/// ```rust,ignore -/// use sqlx::{FromRow, sqlite::SqliteRow, sqlx::Row}; -/// struct MyCustomType { -/// custom: String, -/// } -/// -/// struct Foo { -/// bar: MyCustomType, -/// } -/// -/// impl FromRow<'_, SqliteRow> for Foo { -/// fn from_row(row: &SqliteRow) -> sqlx::Result { -/// Ok(Self { -/// bar: MyCustomType { -/// custom: row.try_get("custom")? -/// } -/// }) -/// } -/// } -/// ``` -/// /// #### `try_from` /// /// When your struct contains a field whose type is not matched with the database type, @@ -271,6 +253,59 @@ use crate::{error::Error, row::Row}; /// } /// } /// ``` +/// +/// By default the `#[sqlx(json)]` attribute will assume that the underlying database row is +/// _not_ NULL. This can cause issues when your field type is an `Option` because this would be +/// represented as the _not_ NULL (in terms of DB) JSON value of `null`. +/// +/// If you wish to describe a database row which _is_ NULLable but _cannot_ contain the JSON value `null`, +/// use the `#[sqlx(json(nullable))]` attribute. +/// +/// For example +/// ```rust,ignore +/// #[derive(serde::Deserialize)] +/// struct Data { +/// field1: String, +/// field2: u64 +/// } +/// +/// #[derive(sqlx::FromRow)] +/// struct User { +/// id: i32, +/// name: String, +/// #[sqlx(json(nullable))] +/// metadata: Option +/// } +/// ``` +/// Would describe a database field which _is_ NULLable but if it exists it must be the JSON representation of `Data` +/// and cannot be the JSON value `null` +/// +/// ## Manual implementation +/// +/// You can also implement the [`FromRow`] trait by hand. This can be useful if you +/// have a struct with a field that needs manual decoding: +/// +/// +/// ```rust,ignore +/// use sqlx::{FromRow, sqlite::SqliteRow, sqlx::Row}; +/// struct MyCustomType { +/// custom: String, +/// } +/// +/// struct Foo { +/// bar: MyCustomType, +/// } +/// +/// impl FromRow<'_, SqliteRow> for Foo { +/// fn from_row(row: &SqliteRow) -> sqlx::Result { +/// Ok(Self { +/// bar: MyCustomType { +/// custom: row.try_get("custom")? +/// } +/// }) +/// } +/// } +/// ``` pub trait FromRow<'r, R: Row>: Sized { fn from_row(row: &'r R) -> Result; } @@ -286,7 +321,7 @@ where } // implement FromRow for tuples of types that implement Decode -// up to tuples of 9 values +// up to tuples of 16 values macro_rules! impl_from_row_for_tuple { ($( ($idx:tt) -> $T:ident );+;) => { diff --git a/sqlx-core/src/io/write_and_flush.rs b/sqlx-core/src/io/write_and_flush.rs index 9e7824af81..8a0db31293 100644 --- a/sqlx-core/src/io/write_and_flush.rs +++ b/sqlx-core/src/io/write_and_flush.rs @@ -1,10 +1,9 @@ use crate::error::Error; -use futures_core::Future; -use futures_util::ready; use sqlx_rt::AsyncWrite; +use std::future::Future; use std::io::{BufRead, Cursor}; use std::pin::Pin; -use std::task::{Context, Poll}; +use std::task::{ready, Context, Poll}; // Atomic operation that writes the full buffer to the stream, flushes the stream, and then // clears the buffer (even if either of the two previous operations failed). diff --git a/sqlx-core/src/net/socket/mod.rs b/sqlx-core/src/net/socket/mod.rs index 6b09d318f7..d11f15884e 100644 --- a/sqlx-core/src/net/socket/mod.rs +++ b/sqlx-core/src/net/socket/mod.rs @@ -2,10 +2,9 @@ use std::future::Future; use std::io; use std::path::Path; use std::pin::Pin; -use std::task::{Context, Poll}; +use std::task::{ready, Context, Poll}; use bytes::BufMut; -use futures_core::ready; pub use buffered::{BufferedSocket, WriteBuffer}; diff --git a/sqlx-core/src/net/tls/mod.rs b/sqlx-core/src/net/tls/mod.rs index 3e9fd9b9a0..7bb1744189 100644 --- a/sqlx-core/src/net/tls/mod.rs +++ b/sqlx-core/src/net/tls/mod.rs @@ -25,11 +25,12 @@ pub enum CertificateInput { impl From for CertificateInput { fn from(value: String) -> Self { + // Leading and trailing whitespace/newlines let trimmed = value.trim(); - // Some heuristics according to https://tools.ietf.org/html/rfc7468 - if trimmed.starts_with("-----BEGIN CERTIFICATE-----") - && trimmed.contains("-----END CERTIFICATE-----") - { + + // Heuristic for PEM encoded inputs: + // https://tools.ietf.org/html/rfc7468 + if trimmed.starts_with("-----BEGIN") && trimmed.ends_with("-----") { CertificateInput::Inline(value.as_bytes().to_vec()) } else { CertificateInput::File(PathBuf::from(value)) diff --git a/sqlx-core/src/net/tls/tls_rustls.rs b/sqlx-core/src/net/tls/tls_rustls.rs index d56859808c..1a85cf0ff9 100644 --- a/sqlx-core/src/net/tls/tls_rustls.rs +++ b/sqlx-core/src/net/tls/tls_rustls.rs @@ -1,5 +1,5 @@ use futures_util::future; -use std::io::{self, BufReader, Cursor, Read, Write}; +use std::io::{self, Read, Write}; use std::sync::Arc; use std::task::{Context, Poll}; @@ -9,7 +9,10 @@ use rustls::{ WebPkiServerVerifier, }, crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider}, - pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime}, + pki_types::{ + pem::{self, PemObject}, + CertificateDer, PrivateKeyDer, ServerName, UnixTime, + }, CertificateError, ClientConfig, ClientConnection, Error as TlsError, RootCertStore, }; @@ -141,9 +144,8 @@ where if let Some(ca) = tls_config.root_cert_path { let data = ca.data().await?; - let mut cursor = Cursor::new(data); - for result in rustls_pemfile::certs(&mut cursor) { + for result in CertificateDer::pem_slice_iter(&data) { let Ok(cert) = result else { return Err(Error::Tls(format!("Invalid certificate {ca}").into())); }; @@ -196,19 +198,15 @@ where } fn certs_from_pem(pem: Vec) -> Result>, Error> { - let cur = Cursor::new(pem); - let mut reader = BufReader::new(cur); - rustls_pemfile::certs(&mut reader) + CertificateDer::pem_slice_iter(&pem) .map(|result| result.map_err(|err| Error::Tls(err.into()))) .collect() } fn private_key_from_pem(pem: Vec) -> Result, Error> { - let cur = Cursor::new(pem); - let mut reader = BufReader::new(cur); - match rustls_pemfile::private_key(&mut reader) { - Ok(Some(key)) => Ok(key), - Ok(None) => Err(Error::Configuration("no keys found pem file".into())), + match PrivateKeyDer::from_pem_slice(&pem) { + Ok(key) => Ok(key), + Err(pem::Error::NoItemsFound) => Err(Error::Configuration("no keys found pem file".into())), Err(e) => Err(Error::Configuration(e.to_string().into())), } } diff --git a/sqlx-core/src/net/tls/util.rs b/sqlx-core/src/net/tls/util.rs index 02a16ef5e1..ddbc7a58f2 100644 --- a/sqlx-core/src/net/tls/util.rs +++ b/sqlx-core/src/net/tls/util.rs @@ -1,9 +1,8 @@ use crate::net::Socket; use std::io::{self, Read, Write}; -use std::task::{Context, Poll}; +use std::task::{ready, Context, Poll}; -use futures_core::ready; use futures_util::future; pub struct StdSocket { diff --git a/sqlx-core/src/pool/connection.rs b/sqlx-core/src/pool/connection.rs index bf3a6d4b1c..c029fec6eb 100644 --- a/sqlx-core/src/pool/connection.rs +++ b/sqlx-core/src/pool/connection.rs @@ -191,7 +191,7 @@ impl<'c, DB: Database> crate::acquire::Acquire<'c> for &'c mut PoolConnection futures_core::future::BoxFuture<'c, Result, Error>> { - crate::transaction::Transaction::begin(&mut **self) + crate::transaction::Transaction::begin(&mut **self, None) } } diff --git a/sqlx-core/src/pool/inner.rs b/sqlx-core/src/pool/inner.rs index bbcc43134e..2066364a8e 100644 --- a/sqlx-core/src/pool/inner.rs +++ b/sqlx-core/src/pool/inner.rs @@ -10,6 +10,7 @@ use crate::sync::{AsyncSemaphore, AsyncSemaphoreReleaser}; use std::cmp; use std::future::Future; +use std::pin::pin; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use std::sync::{Arc, RwLock}; use std::task::Poll; @@ -130,19 +131,12 @@ impl PoolInner { // This is just going to cause unnecessary churn in `acquire()`. .filter(|_| self.size() < self.options.max_connections); - let acquire_self = self.semaphore.acquire(1).fuse(); - let mut close_event = self.close_event(); + let mut acquire_self = pin!(self.semaphore.acquire(1).fuse()); + let mut close_event = pin!(self.close_event()); if let Some(parent) = parent { - let acquire_parent = parent.0.semaphore.acquire(1); - let parent_close_event = parent.0.close_event(); - - futures_util::pin_mut!( - acquire_parent, - acquire_self, - close_event, - parent_close_event - ); + let mut acquire_parent = pin!(parent.0.semaphore.acquire(1)); + let mut parent_close_event = pin!(parent.0.close_event()); let mut poll_parent = false; diff --git a/sqlx-core/src/pool/mod.rs b/sqlx-core/src/pool/mod.rs index e998618413..d85bce246d 100644 --- a/sqlx-core/src/pool/mod.rs +++ b/sqlx-core/src/pool/mod.rs @@ -54,9 +54,10 @@ //! [`Pool::acquire`] or //! [`Pool::begin`]. +use std::borrow::Cow; use std::fmt; use std::future::Future; -use std::pin::Pin; +use std::pin::{pin, Pin}; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::{Duration, Instant}; @@ -109,7 +110,8 @@ mod options; /// application/daemon/web server/etc. and then shared with all tasks throughout the process' /// lifetime. How best to accomplish this depends on your program architecture. /// -/// In Actix-Web, for example, you can share a single pool with all request handlers using [web::Data]. +/// In Actix-Web, for example, you can efficiently share a single pool with all request handlers +/// using [web::ThinData]. /// /// Cloning `Pool` is cheap as it is simply a reference-counted handle to the inner pool state. /// When the last remaining handle to the pool is dropped, the connections owned by the pool are @@ -131,7 +133,7 @@ mod options; /// * [PgPool][crate::postgres::PgPool] (PostgreSQL) /// * [SqlitePool][crate::sqlite::SqlitePool] (SQLite) /// -/// [web::Data]: https://docs.rs/actix-web/3/actix_web/web/struct.Data.html +/// [web::ThinData]: https://docs.rs/actix-web/4.9.0/actix_web/web/struct.ThinData.html /// /// ### Note: Drop Behavior /// Due to a lack of async `Drop`, dropping the last `Pool` handle may not immediately clean @@ -367,13 +369,17 @@ impl Pool { /// Retrieves a connection and immediately begins a new transaction. pub async fn begin(&self) -> Result, Error> { - Transaction::begin(MaybePoolConnection::PoolConnection(self.acquire().await?)).await + Transaction::begin( + MaybePoolConnection::PoolConnection(self.acquire().await?), + None, + ) + .await } /// Attempts to retrieve a connection and immediately begins a new transaction if successful. pub async fn try_begin(&self) -> Result>, Error> { match self.try_acquire() { - Some(conn) => Transaction::begin(MaybePoolConnection::PoolConnection(conn)) + Some(conn) => Transaction::begin(MaybePoolConnection::PoolConnection(conn), None) .await .map(Some), @@ -381,6 +387,36 @@ impl Pool { } } + /// Retrieves a connection and immediately begins a new transaction using `statement`. + pub async fn begin_with( + &self, + statement: impl Into>, + ) -> Result, Error> { + Transaction::begin( + MaybePoolConnection::PoolConnection(self.acquire().await?), + Some(statement.into()), + ) + .await + } + + /// Attempts to retrieve a connection and, if successful, immediately begins a new + /// transaction using `statement`. + pub async fn try_begin_with( + &self, + statement: impl Into>, + ) -> Result>, Error> { + match self.try_acquire() { + Some(conn) => Transaction::begin( + MaybePoolConnection::PoolConnection(conn), + Some(statement.into()), + ) + .await + .map(Some), + + None => Ok(None), + } + } + /// Shut down the connection pool, immediately waking all tasks waiting for a connection. /// /// Upon calling this method, any currently waiting or subsequent calls to [`Pool::acquire`] and @@ -565,11 +601,11 @@ impl CloseEvent { .await .map_or(Ok(()), |_| Err(Error::PoolClosed))?; - futures_util::pin_mut!(fut); + let mut fut = pin!(fut); // I find that this is clearer in intent than `futures_util::future::select()` // or `futures_util::select_biased!{}` (which isn't enabled anyway). - futures_util::future::poll_fn(|cx| { + std::future::poll_fn(|cx| { // Poll `fut` first as the wakeup event is more likely for it than `self`. if let Poll::Ready(ret) = fut.as_mut().poll(cx) { return Poll::Ready(Ok(ret)); diff --git a/sqlx-core/src/pool/options.rs b/sqlx-core/src/pool/options.rs index 96dbf8ee3d..3d048f1795 100644 --- a/sqlx-core/src/pool/options.rs +++ b/sqlx-core/src/pool/options.rs @@ -484,7 +484,7 @@ impl PoolOptions { /// .await?; /// /// // Close the connection if the backend memory usage exceeds 256 MiB. - /// Ok(total_memory_usage <= (2 << 28)) + /// Ok(total_memory_usage <= (1 << 28)) /// })) /// .connect("postgres:// …").await?; /// # Ok(()) diff --git a/sqlx-core/src/query_builder.rs b/sqlx-core/src/query_builder.rs index 0d02048dcf..b242bf7b2a 100644 --- a/sqlx-core/src/query_builder.rs +++ b/sqlx-core/src/query_builder.rs @@ -323,6 +323,11 @@ where separated.push_unseparated(")"); } + debug_assert!( + separated.push_separator, + "No value being pushed. QueryBuilder may not build correct sql query!" + ); + separated.query_builder } diff --git a/sqlx-core/src/row.rs b/sqlx-core/src/row.rs index 4c96ced048..fe0b75ebb0 100644 --- a/sqlx-core/src/row.rs +++ b/sqlx-core/src/row.rs @@ -64,6 +64,7 @@ pub trait Row: Unpin + Send + Sync + 'static { /// See [`try_get`](Self::try_get) for a non-panicking version. /// #[inline] + #[track_caller] fn get<'r, T, I>(&'r self, index: I) -> T where I: ColumnIndex, diff --git a/sqlx-core/src/sync.rs b/sqlx-core/src/sync.rs index 27ad29c33e..14525611a0 100644 --- a/sqlx-core/src/sync.rs +++ b/sqlx-core/src/sync.rs @@ -4,12 +4,6 @@ // We'll generally lean towards Tokio's types as those are more featureful // (including `tokio-console` support) and more widely deployed. -#[cfg(all(feature = "_rt-async-std", not(feature = "_rt-tokio")))] -pub use async_std::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; - -#[cfg(feature = "_rt-tokio")] -pub use tokio::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; - pub struct AsyncSemaphore { // We use the semaphore from futures-intrusive as the one from async-std // is missing the ability to add arbitrary permits, and is not guaranteed to be fair: diff --git a/sqlx-core/src/testing/mod.rs b/sqlx-core/src/testing/mod.rs index d82d1a3616..051353383b 100644 --- a/sqlx-core/src/testing/mod.rs +++ b/sqlx-core/src/testing/mod.rs @@ -3,7 +3,9 @@ use std::time::Duration; use futures_core::future::BoxFuture; +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; pub use fixtures::FixtureSnapshot; +use sha2::{Digest, Sha512}; use crate::connection::{ConnectOptions, Connection}; use crate::database::Database; @@ -41,6 +43,17 @@ pub trait TestSupport: Database { /// This snapshot can then be used to generate test fixtures. fn snapshot(conn: &mut Self::Connection) -> BoxFuture<'_, Result, Error>>; + + /// Generate a unique database name for the given test path. + fn db_name(args: &TestArgs) -> String { + let mut hasher = Sha512::new(); + hasher.update(args.test_path.as_bytes()); + let hash = hasher.finalize(); + let hash = URL_SAFE.encode(&hash[..39]); + let db_name = format!("_sqlx_test_{}", hash).replace('-', "_"); + debug_assert!(db_name.len() == 63); + db_name + } } pub struct TestFixture { @@ -217,7 +230,7 @@ where let res = test_fn(test_context.pool_opts, test_context.connect_opts).await; if res.is_success() { - if let Err(e) = DB::cleanup_test(&test_context.db_name).await { + if let Err(e) = DB::cleanup_test(&DB::db_name(&args)).await { eprintln!( "failed to delete database {:?}: {}", test_context.db_name, e diff --git a/sqlx-core/src/transaction.rs b/sqlx-core/src/transaction.rs index 9cd38aab3a..39fe0eecff 100644 --- a/sqlx-core/src/transaction.rs +++ b/sqlx-core/src/transaction.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; use std::ops::{Deref, DerefMut}; +use std::backtrace::Backtrace; use futures_core::future::BoxFuture; @@ -16,9 +17,16 @@ pub trait TransactionManager { type Database: Database; /// Begin a new transaction or establish a savepoint within the active transaction. - fn begin( - conn: &mut ::Connection, - ) -> BoxFuture<'_, Result<(), Error>>; + /// + /// If this is a new transaction, `statement` may be used instead of the + /// default "BEGIN" statement. + /// + /// If we are already inside a transaction and `statement.is_some()`, then + /// `Error::InvalidSavePoint` is returned without running any statements. + fn begin<'conn>( + conn: &'conn mut ::Connection, + statement: Option>, + ) -> BoxFuture<'conn, Result<(), Error>>; /// Commit the active transaction or release the most recent savepoint. fn commit( @@ -32,6 +40,14 @@ pub trait TransactionManager { /// Starts to abort the active transaction or restore from the most recent snapshot. fn start_rollback(conn: &mut ::Connection); + + /// Returns the current transaction depth. + /// + /// Transaction depth indicates the level of nested transactions: + /// - Level 0: No active transaction. + /// - Level 1: A transaction is active. + /// - Level 2 or higher: A transaction is active and one or more SAVEPOINTs have been created within it. + fn get_transaction_depth(conn: &::Connection) -> usize; } /// An in-progress database transaction or savepoint. @@ -83,11 +99,12 @@ where #[doc(hidden)] pub fn begin( conn: impl Into>, + statement: Option>, ) -> BoxFuture<'c, Result> { let mut conn = conn.into(); Box::pin(async move { - DB::TransactionManager::begin(&mut conn).await?; + DB::TransactionManager::begin(&mut conn, statement).await?; Ok(Self { connection: conn, @@ -237,7 +254,7 @@ impl<'c, 't, DB: Database> crate::acquire::Acquire<'t> for &'t mut Transaction<' #[inline] fn begin(self) -> BoxFuture<'t, Result, Error>> { - Transaction::begin(&mut **self) + Transaction::begin(&mut **self, None) } } @@ -252,7 +269,11 @@ where // what this does depends on the database but generally this means we queue a rollback // operation that will happen on the next asynchronous invocation of the underlying // connection (including if the connection is returned to a pool) - + let bt = Backtrace::capture(); + tracing::warn!( + backtrace = ?bt, + "Detected incorrect transaction rollback" + ); DB::TransactionManager::start_rollback(&mut self.connection); } } diff --git a/sqlx-core/src/type_checking.rs b/sqlx-core/src/type_checking.rs index 384d15f42c..5766124530 100644 --- a/sqlx-core/src/type_checking.rs +++ b/sqlx-core/src/type_checking.rs @@ -72,11 +72,17 @@ where match T::decode(value.as_ref()) { Ok(value) => Debug::fmt(&value, f), - Err(e) => f.write_fmt(format_args!( - "(error decoding SQL type {} as {}: {e:?})", - info.name(), - std::any::type_name::() - )), + Err(e) => { + if e.is::() { + f.write_str("NULL") + } else { + f.write_fmt(format_args!( + "(error decoding SQL type {} as {}: {e:?})", + info.name(), + std::any::type_name::() + )) + } + } } }, } diff --git a/sqlx-core/src/types/mod.rs b/sqlx-core/src/types/mod.rs index 25837b1e77..b45fa2fa85 100644 --- a/sqlx-core/src/types/mod.rs +++ b/sqlx-core/src/types/mod.rs @@ -67,6 +67,13 @@ pub use bigdecimal::BigDecimal; #[doc(no_inline)] pub use rust_decimal::Decimal; +#[cfg(feature = "ipnet")] +#[cfg_attr(docsrs, doc(cfg(feature = "ipnet")))] +pub mod ipnet { + #[doc(no_inline)] + pub use ipnet::{IpNet, Ipv4Net, Ipv6Net}; +} + #[cfg(feature = "ipnetwork")] #[cfg_attr(docsrs, doc(cfg(feature = "ipnetwork")))] pub mod ipnetwork { @@ -85,12 +92,18 @@ pub mod mac_address { pub use json::{Json, JsonRawValue, JsonValue}; pub use text::Text; +#[cfg(feature = "bstr")] +pub use bstr::{BStr, BString}; + /// Indicates that a SQL type is supported for a database. /// /// ## Compile-time verification /// -/// With compile-time verification, the use of type overrides is currently required to make -/// use of any user-defined types. +/// Type definitions are *not* verified against the database at compile-time. +/// The [`query!()`](macro.query.html) macros have no implicit knowledge of user-defined types. +/// +/// When using custom types in query parameters or output columns with `query!()`, +/// the use of [type overrides](macro.query.html#type-overrides-bind-parameters-postgres-only) is required. /// /// ```rust,ignore /// struct MyUser { id: UserId, name: String } diff --git a/sqlx-macros-core/Cargo.toml b/sqlx-macros-core/Cargo.toml index 46786b7d8d..07d9d78862 100644 --- a/sqlx-macros-core/Cargo.toml +++ b/sqlx-macros-core/Cargo.toml @@ -38,6 +38,7 @@ json = ["sqlx-core/json", "sqlx-mysql?/json", "sqlx-postgres?/json", "sqlx-sqlit bigdecimal = ["sqlx-core/bigdecimal", "sqlx-mysql?/bigdecimal", "sqlx-postgres?/bigdecimal"] bit-vec = ["sqlx-core/bit-vec", "sqlx-postgres?/bit-vec"] chrono = ["sqlx-core/chrono", "sqlx-mysql?/chrono", "sqlx-postgres?/chrono", "sqlx-sqlite?/chrono"] +ipnet = ["sqlx-core/ipnet", "sqlx-postgres?/ipnet"] ipnetwork = ["sqlx-core/ipnetwork", "sqlx-postgres?/ipnetwork"] mac_address = ["sqlx-core/mac_address", "sqlx-postgres?/mac_address"] rust_decimal = ["sqlx-core/rust_decimal", "sqlx-mysql?/rust_decimal", "sqlx-postgres?/rust_decimal"] @@ -64,7 +65,6 @@ serde = { version = "1.0.132", features = ["derive"] } serde_json = { version = "1.0.73" } sha2 = { version = "0.10.0" } syn = { version = "2.0.52", default-features = false, features = ["full", "derive", "parsing", "printing", "clone-impls"] } -tempfile = { version = "3.10.1" } quote = { version = "1.0.26", default-features = false } url = { version = "2.2.2" } diff --git a/sqlx-macros-core/src/derives/attributes.rs b/sqlx-macros-core/src/derives/attributes.rs index cf18cffca4..c69687908b 100644 --- a/sqlx-macros-core/src/derives/attributes.rs +++ b/sqlx-macros-core/src/derives/attributes.rs @@ -1,8 +1,8 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote_spanned; use syn::{ - punctuated::Punctuated, token::Comma, Attribute, DeriveInput, Field, LitStr, Meta, Token, Type, - Variant, + parenthesized, punctuated::Punctuated, token::Comma, Attribute, DeriveInput, Field, LitStr, + Meta, Token, Type, Variant, }; macro_rules! assert_attribute { @@ -61,13 +61,18 @@ pub struct SqlxContainerAttributes { pub default: bool, } +pub enum JsonAttribute { + NonNullable, + Nullable, +} + pub struct SqlxChildAttributes { pub rename: Option, pub default: bool, pub flatten: bool, pub try_from: Option, pub skip: bool, - pub json: bool, + pub json: Option, } pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result { @@ -144,7 +149,7 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result syn::Result - (false, None, false) => { + (false, None, None) => { predicates .push(parse_quote!(#ty: ::sqlx::decode::Decode<#lifetime, R::Database>)); predicates.push(parse_quote!(#ty: ::sqlx::types::Type)); @@ -107,12 +107,12 @@ fn expand_derive_from_row_struct( parse_quote!(__row.try_get(#id_s)) } // Flatten - (true, None, false) => { + (true, None, None) => { predicates.push(parse_quote!(#ty: ::sqlx::FromRow<#lifetime, R>)); parse_quote!(<#ty as ::sqlx::FromRow<#lifetime, R>>::from_row(__row)) } // Flatten + Try from - (true, Some(try_from), false) => { + (true, Some(try_from), None) => { predicates.push(parse_quote!(#try_from: ::sqlx::FromRow<#lifetime, R>)); parse_quote!( <#try_from as ::sqlx::FromRow<#lifetime, R>>::from_row(__row) @@ -130,11 +130,11 @@ fn expand_derive_from_row_struct( ) } // Flatten + Json - (true, _, true) => { + (true, _, Some(_)) => { panic!("Cannot use both flatten and json") } // Try from - (false, Some(try_from), false) => { + (false, Some(try_from), None) => { predicates .push(parse_quote!(#try_from: ::sqlx::decode::Decode<#lifetime, R::Database>)); predicates.push(parse_quote!(#try_from: ::sqlx::types::Type)); @@ -154,8 +154,8 @@ fn expand_derive_from_row_struct( }) ) } - // Try from + Json - (false, Some(try_from), true) => { + // Try from + Json mandatory + (false, Some(try_from), Some(JsonAttribute::NonNullable)) => { predicates .push(parse_quote!(::sqlx::types::Json<#try_from>: ::sqlx::decode::Decode<#lifetime, R::Database>)); predicates.push(parse_quote!(::sqlx::types::Json<#try_from>: ::sqlx::types::Type)); @@ -175,14 +175,25 @@ fn expand_derive_from_row_struct( }) ) }, + // Try from + Json nullable + (false, Some(_), Some(JsonAttribute::Nullable)) => { + panic!("Cannot use both try from and json nullable") + }, // Json - (false, None, true) => { + (false, None, Some(JsonAttribute::NonNullable)) => { predicates .push(parse_quote!(::sqlx::types::Json<#ty>: ::sqlx::decode::Decode<#lifetime, R::Database>)); predicates.push(parse_quote!(::sqlx::types::Json<#ty>: ::sqlx::types::Type)); parse_quote!(__row.try_get::<::sqlx::types::Json<_>, _>(#id_s).map(|x| x.0)) }, + (false, None, Some(JsonAttribute::Nullable)) => { + predicates + .push(parse_quote!(::core::option::Option<::sqlx::types::Json<#ty>>: ::sqlx::decode::Decode<#lifetime, R::Database>)); + predicates.push(parse_quote!(::core::option::Option<::sqlx::types::Json<#ty>>: ::sqlx::types::Type)); + + parse_quote!(__row.try_get::<::core::option::Option<::sqlx::types::Json<_>>, _>(#id_s).map(|x| x.and_then(|y| y.0))) + }, }; if attributes.default { diff --git a/sqlx-macros-core/src/query/args.rs b/sqlx-macros-core/src/query/args.rs index ec17aeff65..788a9aadc5 100644 --- a/sqlx-macros-core/src/query/args.rs +++ b/sqlx-macros-core/src/query/args.rs @@ -66,7 +66,12 @@ pub fn quote_args( i + 1, ) } else { - format!("unsupported type {} for param #{}", param_ty, i + 1) + format!( + "no built in mapping found for type {} for param #{}; \ + a type override may be required, see documentation for details", + param_ty, + i + 1 + ) } })? .parse::() diff --git a/sqlx-macros-core/src/query/data.rs b/sqlx-macros-core/src/query/data.rs index 39f62bafdc..ddf55c8bb2 100644 --- a/sqlx-macros-core/src/query/data.rs +++ b/sqlx-macros-core/src/query/data.rs @@ -86,8 +86,8 @@ impl DynQueryData { let mut cache = OFFLINE_DATA_CACHE .lock() // Just reset the cache on error - .unwrap_or_else(|posion_err| { - let mut guard = posion_err.into_inner(); + .unwrap_or_else(|poison_err| { + let mut guard = poison_err.into_inner(); *guard = Default::default(); guard }); diff --git a/sqlx-macros-core/src/query/mod.rs b/sqlx-macros-core/src/query/mod.rs index 09acff9bd2..a51137413e 100644 --- a/sqlx-macros-core/src/query/mod.rs +++ b/sqlx-macros-core/src/query/mod.rs @@ -1,4 +1,5 @@ -use std::path::PathBuf; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::{fs, io}; @@ -73,6 +74,7 @@ struct Metadata { manifest_dir: PathBuf, offline: bool, database_url: Option, + offline_dir: Option, workspace_root: Arc>>, } @@ -106,73 +108,70 @@ impl Metadata { } } +static METADATA: Lazy>> = Lazy::new(Default::default); + // If we are in a workspace, lookup `workspace_root` since `CARGO_MANIFEST_DIR` won't // reflect the workspace dir: https://github.com/rust-lang/cargo/issues/3946 -static METADATA: Lazy = Lazy::new(|| { - let manifest_dir: PathBuf = env("CARGO_MANIFEST_DIR") - .expect("`CARGO_MANIFEST_DIR` must be set") - .into(); - - // If a .env file exists at CARGO_MANIFEST_DIR, load environment variables from this, - // otherwise fallback to default dotenv behaviour. - let env_path = manifest_dir.join(".env"); +fn init_metadata(manifest_dir: &String) -> Metadata { + let manifest_dir: PathBuf = manifest_dir.into(); - #[cfg_attr(not(procmacro2_semver_exempt), allow(unused_variables))] - let env_path = if env_path.exists() { - let res = dotenvy::from_path(&env_path); - if let Err(e) = res { - panic!("failed to load environment from {env_path:?}, {e}"); - } - - Some(env_path) - } else { - dotenvy::dotenv().ok() - }; - - // tell the compiler to watch the `.env` for changes, if applicable - #[cfg(procmacro2_semver_exempt)] - if let Some(env_path) = env_path.as_ref().and_then(|path| path.to_str()) { - proc_macro::tracked_path::path(env_path); - } + let (database_url, offline, offline_dir) = load_dot_env(&manifest_dir); let offline = env("SQLX_OFFLINE") + .ok() + .or(offline) .map(|s| s.eq_ignore_ascii_case("true") || s == "1") .unwrap_or(false); - let database_url = env("DATABASE_URL").ok(); + let database_url = env("DATABASE_URL").ok().or(database_url); Metadata { manifest_dir, offline, database_url, + offline_dir, workspace_root: Arc::new(Mutex::new(None)), } -}); +} pub fn expand_input<'a>( input: QueryMacroInput, drivers: impl IntoIterator, ) -> crate::Result { - let data_source = match &*METADATA { + let manifest_dir = env("CARGO_MANIFEST_DIR").expect("`CARGO_MANIFEST_DIR` must be set"); + + let mut metadata_lock = METADATA + .lock() + // Just reset the metadata on error + .unwrap_or_else(|poison_err| { + let mut guard = poison_err.into_inner(); + *guard = Default::default(); + guard + }); + + let metadata = metadata_lock + .entry(manifest_dir) + .or_insert_with_key(init_metadata); + + let data_source = match &metadata { Metadata { offline: false, database_url: Some(db_url), .. } => QueryDataSource::live(db_url)?, - Metadata { offline, .. } => { // Try load the cached query metadata file. let filename = format!("query-{}.json", hash_string(&input.sql)); // Check SQLX_OFFLINE_DIR, then local .sqlx, then workspace .sqlx. let dirs = [ - || env("SQLX_OFFLINE_DIR").ok().map(PathBuf::from), - || Some(METADATA.manifest_dir.join(".sqlx")), - || Some(METADATA.workspace_root().join(".sqlx")), + |meta: &Metadata| meta.offline_dir.as_deref().map(PathBuf::from), + |meta: &Metadata| Some(meta.manifest_dir.join(".sqlx")), + |meta: &Metadata| Some(meta.workspace_root().join(".sqlx")), ]; let Some(data_file_path) = dirs .iter() - .filter_map(|path| path()) + .filter_map(|path| path(metadata)) .map(|path| path.join(&filename)) .find(|path| path.exists()) else { @@ -184,7 +183,6 @@ pub fn expand_input<'a>( }.into() ); }; - QueryDataSource::Cached(DynQueryData::from_data_file(&data_file_path, &input.sql)?) } }; @@ -387,3 +385,52 @@ fn env(name: &str) -> Result { std::env::var(name) } } + +/// Get `DATABASE_URL`, `SQLX_OFFLINE` and `SQLX_OFFLINE_DIR` from the `.env`. +fn load_dot_env(manifest_dir: &Path) -> (Option, Option, Option) { + let mut env_path = manifest_dir.join(".env"); + + // If a .env file exists at CARGO_MANIFEST_DIR, load environment variables from this, + // otherwise fallback to default dotenv file. + #[cfg_attr(not(procmacro2_semver_exempt), allow(unused_variables))] + let env_file = if env_path.exists() { + let res = dotenvy::from_path_iter(&env_path); + match res { + Ok(iter) => Some(iter), + Err(e) => panic!("failed to load environment from {env_path:?}, {e}"), + } + } else { + #[allow(unused_assignments)] + { + env_path = PathBuf::from(".env"); + } + dotenvy::dotenv_iter().ok() + }; + + let mut offline = None; + let mut database_url = None; + let mut offline_dir = None; + + if let Some(env_file) = env_file { + // tell the compiler to watch the `.env` for changes. + #[cfg(procmacro2_semver_exempt)] + if let Some(env_path) = env_path.to_str() { + proc_macro::tracked_path::path(env_path); + } + + for item in env_file { + let Ok((key, value)) = item else { + continue; + }; + + match key.as_str() { + "DATABASE_URL" => database_url = Some(value), + "SQLX_OFFLINE" => offline = Some(value), + "SQLX_OFFLINE_DIR" => offline_dir = Some(value), + _ => {} + }; + } + } + + (database_url, offline, offline_dir) +} diff --git a/sqlx-macros-core/src/query/output.rs b/sqlx-macros-core/src/query/output.rs index 5e7cc5058d..3641e55db5 100644 --- a/sqlx-macros-core/src/query/output.rs +++ b/sqlx-macros-core/src/query/output.rs @@ -236,7 +236,7 @@ fn get_column_type(i: usize, column: &DB::Column) -> TokenStrea let message = if let Some(feature_gate) = ::get_feature_gate(type_info) { format!( - "optional sqlx feature `{feat}` required for type {ty} of {col}", + "SQLx feature `{feat}` required for type {ty} of {col}", ty = &type_info, feat = feature_gate, col = DisplayColumn { @@ -246,7 +246,8 @@ fn get_column_type(i: usize, column: &DB::Column) -> TokenStrea ) } else { format!( - "unsupported type {ty} of {col}", + "no built in mapping found for type {ty} of {col}; \ + a type override may be required, see documentation for details", ty = type_info, col = DisplayColumn { idx: i, diff --git a/sqlx-macros/Cargo.toml b/sqlx-macros/Cargo.toml index 5617d3f251..b513c3e808 100644 --- a/sqlx-macros/Cargo.toml +++ b/sqlx-macros/Cargo.toml @@ -37,6 +37,7 @@ sqlite-unbundled = ["sqlx-macros-core/sqlite-unbundled"] bigdecimal = ["sqlx-macros-core/bigdecimal"] bit-vec = ["sqlx-macros-core/bit-vec"] chrono = ["sqlx-macros-core/chrono"] +ipnet = ["sqlx-macros-core/ipnet"] ipnetwork = ["sqlx-macros-core/ipnetwork"] mac_address = ["sqlx-macros-core/mac_address"] rust_decimal = ["sqlx-macros-core/rust_decimal"] diff --git a/sqlx-mysql/src/any.rs b/sqlx-mysql/src/any.rs index 0466bfc0a4..19b3a6f27c 100644 --- a/sqlx-mysql/src/any.rs +++ b/sqlx-mysql/src/any.rs @@ -16,7 +16,8 @@ use sqlx_core::database::Database; use sqlx_core::describe::Describe; use sqlx_core::executor::Executor; use sqlx_core::transaction::TransactionManager; -use std::future; +use std::borrow::Cow; +use std::{future, pin::pin}; sqlx_core::declare_driver_with_optional_migrate!(DRIVER = MySql); @@ -37,8 +38,11 @@ impl AnyConnectionBackend for MySqlConnection { Connection::ping(self) } - fn begin(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> { - MySqlTransactionManager::begin(self) + fn begin( + &mut self, + statement: Option>, + ) -> BoxFuture<'_, sqlx_core::Result<()>> { + MySqlTransactionManager::begin(self, statement) } fn commit(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> { @@ -53,6 +57,10 @@ impl AnyConnectionBackend for MySqlConnection { MySqlTransactionManager::start_rollback(self) } + fn get_transaction_depth(&self) -> usize { + MySqlTransactionManager::get_transaction_depth(self) + } + fn shrink_buffers(&mut self) { Connection::shrink_buffers(self); } @@ -113,8 +121,7 @@ impl AnyConnectionBackend for MySqlConnection { Box::pin(async move { let arguments = arguments?; - let stream = self.run(query, arguments, persistent).await?; - futures_util::pin_mut!(stream); + let mut stream = pin!(self.run(query, arguments, persistent).await?); while let Some(result) = stream.try_next().await? { if let Either::Right(row) = result { diff --git a/sqlx-mysql/src/connection/establish.rs b/sqlx-mysql/src/connection/establish.rs index 0623a0556c..85a9d84f96 100644 --- a/sqlx-mysql/src/connection/establish.rs +++ b/sqlx-mysql/src/connection/establish.rs @@ -27,6 +27,7 @@ impl MySqlConnection { inner: Box::new(MySqlConnectionInner { stream, transaction_depth: 0, + status_flags: Default::default(), cache_statement: StatementCache::new(options.statement_cache_capacity), log_settings: options.log_settings.clone(), }), diff --git a/sqlx-mysql/src/connection/executor.rs b/sqlx-mysql/src/connection/executor.rs index 07c7979b08..4f5af4bf6d 100644 --- a/sqlx-mysql/src/connection/executor.rs +++ b/sqlx-mysql/src/connection/executor.rs @@ -21,8 +21,8 @@ use either::Either; use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; use futures_core::Stream; -use futures_util::{pin_mut, TryStreamExt}; -use std::{borrow::Cow, sync::Arc}; +use futures_util::TryStreamExt; +use std::{borrow::Cow, pin::pin, sync::Arc}; impl MySqlConnection { async fn prepare_statement<'c>( @@ -111,7 +111,7 @@ impl MySqlConnection { self.inner.stream.wait_until_ready().await?; self.inner.stream.waiting.push_back(Waiting::Result); - Ok(Box::pin(try_stream! { + Ok(try_stream! { // make a slot for the shared column data // as long as a reference to a row is not held past one iteration, this enables us // to re-use this memory freely between result sets @@ -166,6 +166,8 @@ impl MySqlConnection { // this indicates either a successful query with no rows at all or a failed query let ok = packet.ok()?; + self.inner.status_flags = ok.status; + let rows_affected = ok.affected_rows; logger.increase_rows_affected(rows_affected); let done = MySqlQueryResult { @@ -208,6 +210,8 @@ impl MySqlConnection { if packet[0] == 0xfe && packet.len() < 9 { let eof = packet.eof(self.inner.stream.capabilities)?; + self.inner.status_flags = eof.status; + r#yield!(Either::Left(MySqlQueryResult { rows_affected: 0, last_insert_id: 0, @@ -240,7 +244,7 @@ impl MySqlConnection { r#yield!(v); } } - })) + }) } } @@ -263,8 +267,7 @@ impl<'c> Executor<'c> for &'c mut MySqlConnection { Box::pin(try_stream! { let arguments = arguments?; - let s = self.run(sql, arguments, persistent).await?; - pin_mut!(s); + let mut s = pin!(self.run(sql, arguments, persistent).await?); while let Some(v) = s.try_next().await? { r#yield!(v); diff --git a/sqlx-mysql/src/connection/mod.rs b/sqlx-mysql/src/connection/mod.rs index c4978a7701..0a2f5fb839 100644 --- a/sqlx-mysql/src/connection/mod.rs +++ b/sqlx-mysql/src/connection/mod.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; use futures_core::future::BoxFuture; @@ -7,6 +8,7 @@ pub(crate) use stream::{MySqlStream, Waiting}; use crate::common::StatementCache; use crate::error::Error; +use crate::protocol::response::Status; use crate::protocol::statement::StmtClose; use crate::protocol::text::{Ping, Quit}; use crate::statement::MySqlStatementMetadata; @@ -34,6 +36,7 @@ pub(crate) struct MySqlConnectionInner { // transaction status pub(crate) transaction_depth: usize, + status_flags: Status, // cache by query string to the statement id and metadata cache_statement: StatementCache<(u32, MySqlStatementMetadata)>, @@ -41,6 +44,14 @@ pub(crate) struct MySqlConnectionInner { log_settings: LogSettings, } +impl MySqlConnection { + pub(crate) fn in_transaction(&self) -> bool { + self.inner + .status_flags + .intersects(Status::SERVER_STATUS_IN_TRANS) + } +} + impl Debug for MySqlConnection { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("MySqlConnection").finish() @@ -111,7 +122,17 @@ impl Connection for MySqlConnection { where Self: Sized, { - Transaction::begin(self) + Transaction::begin(self, None) + } + + fn begin_with( + &mut self, + statement: impl Into>, + ) -> BoxFuture<'_, Result, Error>> + where + Self: Sized, + { + Transaction::begin(self, Some(statement.into())) } fn shrink_buffers(&mut self) { diff --git a/sqlx-mysql/src/options/mod.rs b/sqlx-mysql/src/options/mod.rs index db2b20c19d..87732cb40c 100644 --- a/sqlx-mysql/src/options/mod.rs +++ b/sqlx-mysql/src/options/mod.rs @@ -448,7 +448,7 @@ impl MySqlConnectOptions { self.socket.as_ref() } - /// Get the server's port. + /// Get the current username. /// /// # Example /// diff --git a/sqlx-mysql/src/protocol/response/status.rs b/sqlx-mysql/src/protocol/response/status.rs index bf5013deed..4a8bb0375a 100644 --- a/sqlx-mysql/src/protocol/response/status.rs +++ b/sqlx-mysql/src/protocol/response/status.rs @@ -1,7 +1,7 @@ // https://dev.mysql.com/doc/dev/mysql-server/8.0.12/mysql__com_8h.html#a1d854e841086925be1883e4d7b4e8cad // https://mariadb.com/kb/en/library/mariadb-connectorc-types-and-definitions/#server-status bitflags::bitflags! { - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] pub struct Status: u16 { // Is raised when a multi-statement transaction has been started, either explicitly, // by means of BEGIN or COMMIT AND CHAIN, or implicitly, by the first diff --git a/sqlx-mysql/src/protocol/text/row.rs b/sqlx-mysql/src/protocol/text/row.rs index 0b81cd7f4f..e5f820c653 100644 --- a/sqlx-mysql/src/protocol/text/row.rs +++ b/sqlx-mysql/src/protocol/text/row.rs @@ -16,13 +16,20 @@ impl<'de> ProtocolDecode<'de, &'de [MySqlColumn]> for TextRow { let mut values = Vec::with_capacity(columns.len()); - for _ in columns { + for c in columns { if buf[0] == 0xfb { // NULL is sent as 0xfb values.push(None); buf.advance(1); } else { let size = buf.get_uint_lenenc(); + if (buf.remaining() as u64) < size { + return Err(err_protocol!( + "buffer exhausted when reading data for column {:?}; decoded length is {}, but only {} bytes remain in buffer. Malformed packet or protocol error?", + c, + size, + buf.remaining())); + } let size = usize::try_from(size) .map_err(|_| err_protocol!("TextRow length out of range: {size}"))?; diff --git a/sqlx-mysql/src/testing/mod.rs b/sqlx-mysql/src/testing/mod.rs index 2a9216d1b8..2b6d46718c 100644 --- a/sqlx-mysql/src/testing/mod.rs +++ b/sqlx-mysql/src/testing/mod.rs @@ -1,29 +1,24 @@ -use std::fmt::Write; use std::ops::Deref; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{Duration, SystemTime}; +use std::time::Duration; use futures_core::future::BoxFuture; -use once_cell::sync::OnceCell; - -use crate::connection::Connection; - use crate::error::Error; use crate::executor::Executor; use crate::pool::{Pool, PoolOptions}; use crate::query::query; -use crate::query_builder::QueryBuilder; -use crate::query_scalar::query_scalar; -use crate::{MySql, MySqlConnectOptions, MySqlConnection}; +use crate::{MySql, MySqlConnectOptions, MySqlConnection, MySqlDatabaseError}; +use once_cell::sync::OnceCell; +use sqlx_core::connection::Connection; +use sqlx_core::query_builder::QueryBuilder; +use sqlx_core::query_scalar::query_scalar; +use std::fmt::Write; pub(crate) use sqlx_core::testing::*; // Using a blocking `OnceCell` here because the critical sections are short. static MASTER_POOL: OnceCell> = OnceCell::new(); -// Automatically delete any databases created before the start of the test binary. -static DO_CLEANUP: AtomicBool = AtomicBool::new(true); impl TestSupport for MySql { fn test_context(args: &TestArgs) -> BoxFuture<'_, Result, Error>> { @@ -34,21 +29,11 @@ impl TestSupport for MySql { Box::pin(async move { let mut conn = MASTER_POOL .get() - .expect("cleanup_test() invoked outside `#[sqlx::test]") + .expect("cleanup_test() invoked outside `#[sqlx::test]`") .acquire() .await?; - let db_id = db_id(db_name); - - conn.execute(&format!("drop database if exists {db_name};")[..]) - .await?; - - query("delete from _sqlx_test_databases where db_id = ?") - .bind(db_id) - .execute(&mut *conn) - .await?; - - Ok(()) + do_cleanup(&mut conn, db_name).await }) } @@ -58,13 +43,55 @@ impl TestSupport for MySql { let mut conn = MySqlConnection::connect(&url).await?; - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); + let delete_db_names: Vec = + query_scalar("select db_name from _sqlx_test_databases") + .fetch_all(&mut conn) + .await?; + + if delete_db_names.is_empty() { + return Ok(None); + } + + let mut deleted_db_names = Vec::with_capacity(delete_db_names.len()); + + let mut command = String::new(); + + for db_name in &delete_db_names { + command.clear(); + + let db_name = format!("_sqlx_test_database_{db_name}"); + + writeln!(command, "drop database if exists {db_name};").ok(); + match conn.execute(&*command).await { + Ok(_deleted) => { + deleted_db_names.push(db_name); + } + // Assume a database error just means the DB is still in use. + Err(Error::Database(dbe)) => { + eprintln!("could not clean test database {db_name:?}: {dbe}") + } + // Bubble up other errors + Err(e) => return Err(e), + } + } + + if deleted_db_names.is_empty() { + return Ok(None); + } + + let mut query = + QueryBuilder::new("delete from _sqlx_test_databases where db_name in ("); + + let mut separated = query.separated(","); + + for db_name in &deleted_db_names { + separated.push_bind(db_name); + } + + query.push(")").build().execute(&mut conn).await?; - let num_deleted = do_cleanup(&mut conn, now).await?; let _ = conn.close().await; - Ok(Some(num_deleted)) + Ok(Some(delete_db_names.len())) }) } @@ -113,46 +140,36 @@ async fn test_context(args: &TestArgs) -> Result, Error> { let mut conn = master_pool.acquire().await?; + cleanup_old_dbs(&mut conn).await?; + // language=MySQL conn.execute( r#" create table if not exists _sqlx_test_databases ( - db_id bigint unsigned primary key auto_increment, + db_name text not null, test_path text not null, - created_at timestamp not null default current_timestamp - ); + created_at timestamp not null default current_timestamp, + -- BLOB/TEXT columns can only be used as index keys with a prefix length: + -- https://dev.mysql.com/doc/refman/8.4/en/column-indexes.html#column-indexes-prefix + primary key(db_name(63)) + ); "#, ) .await?; - // Record the current time _before_ we acquire the `DO_CLEANUP` permit. This - // prevents the first test thread from accidentally deleting new test dbs - // created by other test threads if we're a bit slow. - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); + let db_name = MySql::db_name(args); + do_cleanup(&mut conn, &db_name).await?; - // Only run cleanup if the test binary just started. - if DO_CLEANUP.swap(false, Ordering::SeqCst) { - do_cleanup(&mut conn, now).await?; - } - - query("insert into _sqlx_test_databases(test_path) values (?)") + query("insert into _sqlx_test_databases(db_name, test_path) values (?, ?)") + .bind(&db_name) .bind(args.test_path) .execute(&mut *conn) .await?; - // MySQL doesn't have `INSERT ... RETURNING` - let new_db_id: u64 = query_scalar("select last_insert_id()") - .fetch_one(&mut *conn) - .await?; - - let new_db_name = db_name(new_db_id); - - conn.execute(&format!("create database {new_db_name}")[..]) + conn.execute(&format!("create database {db_name}")[..]) .await?; - eprintln!("created database {new_db_name}"); + eprintln!("created database {db_name}"); Ok(TestContext { pool_opts: PoolOptions::new() @@ -167,74 +184,70 @@ async fn test_context(args: &TestArgs) -> Result, Error> { .connect_options() .deref() .clone() - .database(&new_db_name), - db_name: new_db_name, + .database(&db_name), + db_name, }) } -async fn do_cleanup(conn: &mut MySqlConnection, created_before: Duration) -> Result { - // since SystemTime is not monotonic we added a little margin here to avoid race conditions with other threads - let created_before_as_secs = created_before.as_secs() - 2; - let delete_db_ids: Vec = query_scalar( - "select db_id from _sqlx_test_databases \ - where created_at < from_unixtime(?)", - ) - .bind(created_before_as_secs) - .fetch_all(&mut *conn) - .await?; - - if delete_db_ids.is_empty() { - return Ok(0); - } - - let mut deleted_db_ids = Vec::with_capacity(delete_db_ids.len()); +async fn do_cleanup(conn: &mut MySqlConnection, db_name: &str) -> Result<(), Error> { + let delete_db_command = format!("drop database if exists {db_name};"); + conn.execute(&*delete_db_command).await?; + query("delete from _sqlx_test_databases where db_name = ?") + .bind(db_name) + .execute(&mut *conn) + .await?; - let mut command = String::new(); + Ok(()) +} - for db_id in delete_db_ids { - command.clear(); +/// Pre <0.8.4, test databases were stored by integer ID. +async fn cleanup_old_dbs(conn: &mut MySqlConnection) -> Result<(), Error> { + let res: Result, Error> = query_scalar("select db_id from _sqlx_test_databases") + .fetch_all(&mut *conn) + .await; + + let db_ids = match res { + Ok(db_ids) => db_ids, + Err(e) => { + if let Some(dbe) = e.as_database_error() { + match dbe.downcast_ref::().number() { + // Column `db_id` does not exist: + // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_bad_field_error + // + // The table has already been migrated. + 1054 => return Ok(()), + // Table `_sqlx_test_databases` does not exist. + // No cleanup needed. + // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_no_such_table + 1146 => return Ok(()), + _ => (), + } + } - let db_name = db_name(db_id); + return Err(e); + } + }; - writeln!(command, "drop database if exists {db_name}").ok(); - match conn.execute(&*command).await { - Ok(_deleted) => { - deleted_db_ids.push(db_id); - } + // Drop old-style test databases. + for id in db_ids { + match conn + .execute(&*format!( + "drop database if exists _sqlx_test_database_{id}" + )) + .await + { + Ok(_deleted) => (), // Assume a database error just means the DB is still in use. Err(Error::Database(dbe)) => { - eprintln!("could not clean test database {db_id:?}: {dbe}") + eprintln!("could not clean old test database _sqlx_test_database_{id}: {dbe}"); } // Bubble up other errors Err(e) => return Err(e), } } - let mut query = QueryBuilder::new("delete from _sqlx_test_databases where db_id in ("); - - let mut separated = query.separated(","); - - for db_id in &deleted_db_ids { - separated.push_bind(db_id); - } - - query.push(")").build().execute(&mut *conn).await?; - - Ok(deleted_db_ids.len()) -} - -fn db_name(id: u64) -> String { - format!("_sqlx_test_database_{id}") -} - -fn db_id(name: &str) -> u64 { - name.trim_start_matches("_sqlx_test_database_") - .parse() - .unwrap_or_else(|_1| panic!("failed to parse ID from database name {name:?}")) -} + conn.execute("drop table if exists _sqlx_test_databases") + .await?; -#[test] -fn test_db_name_id() { - assert_eq!(db_name(12345), "_sqlx_test_database_12345"); - assert_eq!(db_id("_sqlx_test_database_12345"), 12345); + Ok(()) } diff --git a/sqlx-mysql/src/transaction.rs b/sqlx-mysql/src/transaction.rs index d8538cc2b3..545cb5f4f2 100644 --- a/sqlx-mysql/src/transaction.rs +++ b/sqlx-mysql/src/transaction.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use futures_core::future::BoxFuture; use crate::connection::Waiting; @@ -14,12 +16,24 @@ pub struct MySqlTransactionManager; impl TransactionManager for MySqlTransactionManager { type Database = MySql; - fn begin(conn: &mut MySqlConnection) -> BoxFuture<'_, Result<(), Error>> { + fn begin<'conn>( + conn: &'conn mut MySqlConnection, + statement: Option>, + ) -> BoxFuture<'conn, Result<(), Error>> { Box::pin(async move { let depth = conn.inner.transaction_depth; - - conn.execute(&*begin_ansi_transaction_sql(depth)).await?; - conn.inner.transaction_depth = depth + 1; + let statement = match statement { + // custom `BEGIN` statements are not allowed if we're already in a transaction + // (we need to issue a `SAVEPOINT` instead) + Some(_) if depth > 0 => return Err(Error::InvalidSavePointStatement), + Some(statement) => statement, + None => begin_ansi_transaction_sql(depth), + }; + conn.execute(&*statement).await?; + if !conn.in_transaction() { + return Err(Error::BeginFailed); + } + conn.inner.transaction_depth += 1; Ok(()) }) @@ -65,4 +79,8 @@ impl TransactionManager for MySqlTransactionManager { conn.inner.transaction_depth = depth - 1; } } + + fn get_transaction_depth(conn: &MySqlConnection) -> usize { + conn.inner.transaction_depth + } } diff --git a/sqlx-postgres/Cargo.toml b/sqlx-postgres/Cargo.toml index 174a73b3fa..818aadbab7 100644 --- a/sqlx-postgres/Cargo.toml +++ b/sqlx-postgres/Cargo.toml @@ -19,6 +19,7 @@ offline = ["sqlx-core/offline"] bigdecimal = ["dep:bigdecimal", "dep:num-bigint", "sqlx-core/bigdecimal"] bit-vec = ["dep:bit-vec", "sqlx-core/bit-vec"] chrono = ["dep:chrono", "sqlx-core/chrono"] +ipnet = ["dep:ipnet", "sqlx-core/ipnet"] ipnetwork = ["dep:ipnetwork", "sqlx-core/ipnetwork"] mac_address = ["dep:mac_address", "sqlx-core/mac_address"] rust_decimal = ["dep:rust_decimal", "rust_decimal/maths", "sqlx-core/rust_decimal"] @@ -43,6 +44,7 @@ sha2 = { version = "0.10.0", default-features = false } bigdecimal = { workspace = true, optional = true } bit-vec = { workspace = true, optional = true } chrono = { workspace = true, optional = true } +ipnet = { workspace = true, optional = true } ipnetwork = { workspace = true, optional = true } mac_address = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } diff --git a/sqlx-postgres/src/any.rs b/sqlx-postgres/src/any.rs index efa9a044bc..e5b8a36627 100644 --- a/sqlx-postgres/src/any.rs +++ b/sqlx-postgres/src/any.rs @@ -5,7 +5,8 @@ use crate::{ use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; use futures_util::{stream, StreamExt, TryFutureExt, TryStreamExt}; -use std::future; +use std::borrow::Cow; +use std::{future, pin::pin}; use sqlx_core::any::{ Any, AnyArguments, AnyColumn, AnyConnectOptions, AnyConnectionBackend, AnyQueryResult, AnyRow, @@ -39,8 +40,11 @@ impl AnyConnectionBackend for PgConnection { Connection::ping(self) } - fn begin(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> { - PgTransactionManager::begin(self) + fn begin( + &mut self, + statement: Option>, + ) -> BoxFuture<'_, sqlx_core::Result<()>> { + PgTransactionManager::begin(self, statement) } fn commit(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> { @@ -55,6 +59,10 @@ impl AnyConnectionBackend for PgConnection { PgTransactionManager::start_rollback(self) } + fn get_transaction_depth(&self) -> usize { + PgTransactionManager::get_transaction_depth(self) + } + fn shrink_buffers(&mut self) { Connection::shrink_buffers(self); } @@ -89,7 +97,7 @@ impl AnyConnectionBackend for PgConnection { }; Box::pin( - self.run(query, arguments, 0, persistent, None) + self.run(query, arguments, persistent, None) .try_flatten_stream() .map( move |res: sqlx_core::Result>| match res? { @@ -115,8 +123,7 @@ impl AnyConnectionBackend for PgConnection { Box::pin(async move { let arguments = arguments?; - let stream = self.run(query, arguments, 1, persistent, None).await?; - futures_util::pin_mut!(stream); + let mut stream = pin!(self.run(query, arguments, persistent, None).await?); if let Some(Either::Right(row)) = stream.try_next().await? { return Ok(Some(AnyRow::try_from(&row)?)); diff --git a/sqlx-postgres/src/arguments.rs b/sqlx-postgres/src/arguments.rs index bc7e861c52..62a227e52d 100644 --- a/sqlx-postgres/src/arguments.rs +++ b/sqlx-postgres/src/arguments.rs @@ -22,7 +22,7 @@ use sqlx_core::error::BoxDynError; // that has a patch, we then apply the patch which should write to &mut Vec, // backtrack and update the prefixed-len, then write until the next patch offset -#[derive(Default)] +#[derive(Default, Debug, Clone)] pub struct PgArgumentBuffer { buffer: Vec, @@ -46,20 +46,32 @@ pub struct PgArgumentBuffer { type_holes: Vec<(usize, HoleKind)>, // Vec<{ offset, type_name }> } +#[derive(Debug, Clone)] enum HoleKind { Type { name: UStr }, Array(Arc), } +#[derive(Clone)] struct Patch { buf_offset: usize, arg_index: usize, #[allow(clippy::type_complexity)] - callback: Box, + callback: Arc, +} + +impl fmt::Debug for Patch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Patch") + .field("buf_offset", &self.buf_offset) + .field("arg_index", &self.arg_index) + .field("callback", &"") + .finish() + } } /// Implementation of [`Arguments`] for PostgreSQL. -#[derive(Default)] +#[derive(Default, Debug, Clone)] pub struct PgArguments { // Types of each bind parameter pub(crate) types: Vec, @@ -194,7 +206,7 @@ impl PgArgumentBuffer { self.patches.push(Patch { buf_offset: offset, arg_index, - callback: Box::new(callback), + callback: Arc::new(callback), }); } diff --git a/sqlx-postgres/src/connection/executor.rs b/sqlx-postgres/src/connection/executor.rs index 97503a5004..3fe4f402d8 100644 --- a/sqlx-postgres/src/connection/executor.rs +++ b/sqlx-postgres/src/connection/executor.rs @@ -15,19 +15,25 @@ use crate::{ use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; use futures_core::Stream; -use futures_util::{pin_mut, TryStreamExt}; +use futures_util::TryStreamExt; use sqlx_core::arguments::Arguments; use sqlx_core::Either; -use std::{borrow::Cow, sync::Arc}; +use std::{borrow::Cow, pin::pin, sync::Arc}; async fn prepare( conn: &mut PgConnection, sql: &str, parameters: &[PgTypeInfo], metadata: Option>, + persistent: bool, ) -> Result<(StatementId, Arc), Error> { - let id = conn.inner.next_statement_id; - conn.inner.next_statement_id = id.next(); + let id = if persistent { + let id = conn.inner.next_statement_id; + conn.inner.next_statement_id = id.next(); + id + } else { + StatementId::UNNAMED + }; // build a list of type OIDs to send to the database in the PARSE command // we have not yet started the query sequence, so we are *safe* to cleanly make @@ -163,8 +169,7 @@ impl PgConnection { &mut self, sql: &str, parameters: &[PgTypeInfo], - // should we store the result of this prepare to the cache - store_to_cache: bool, + persistent: bool, // optional metadata that was provided by the user, this means they are reusing // a statement object metadata: Option>, @@ -173,9 +178,9 @@ impl PgConnection { return Ok((*statement).clone()); } - let statement = prepare(self, sql, parameters, metadata).await?; + let statement = prepare(self, sql, parameters, metadata, persistent).await?; - if store_to_cache && self.inner.cache_statement.is_enabled() { + if persistent && self.inner.cache_statement.is_enabled() { if let Some((id, _)) = self.inner.cache_statement.insert(sql, statement.clone()) { self.inner.stream.write_msg(Close::Statement(id))?; self.write_sync(); @@ -194,7 +199,6 @@ impl PgConnection { &'c mut self, query: &'q str, arguments: Option, - limit: u8, persistent: bool, metadata_opt: Option>, ) -> Result, Error>> + 'e, Error> { @@ -247,7 +251,9 @@ impl PgConnection { // the protocol-level limit acts nearly identically to the `LIMIT` in SQL self.inner.stream.write_msg(message::Execute { portal: PortalId::UNNAMED, - limit: limit.into(), + // Non-zero limits cause query plan pessimization by disabling parallel workers: + // https://github.com/launchbadge/sqlx/issues/3673 + limit: 0, })?; // From https://www.postgresql.org/docs/current/protocol-flow.html: // @@ -393,8 +399,7 @@ impl<'c> Executor<'c> for &'c mut PgConnection { Box::pin(try_stream! { let arguments = arguments?; - let s = self.run(sql, arguments, 0, persistent, metadata).await?; - pin_mut!(s); + let mut s = pin!(self.run(sql, arguments, persistent, metadata).await?); while let Some(v) = s.try_next().await? { r#yield!(v); @@ -420,8 +425,7 @@ impl<'c> Executor<'c> for &'c mut PgConnection { Box::pin(async move { let arguments = arguments?; - let s = self.run(sql, arguments, 1, persistent, metadata).await?; - pin_mut!(s); + let mut s = pin!(self.run(sql, arguments, persistent, metadata).await?); // With deferred constraints we need to check all responses as we // could get a OK response (with uncommitted data), only to get an diff --git a/sqlx-postgres/src/connection/mod.rs b/sqlx-postgres/src/connection/mod.rs index c139f8e53d..ce499ed744 100644 --- a/sqlx-postgres/src/connection/mod.rs +++ b/sqlx-postgres/src/connection/mod.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; @@ -30,6 +31,8 @@ mod stream; mod tls; /// A connection to a PostgreSQL database. +/// +/// See [`PgConnectOptions`] for connection URL reference. pub struct PgConnection { pub(crate) inner: Box, } @@ -127,6 +130,13 @@ impl PgConnection { Ok(()) } + + pub(crate) fn in_transaction(&self) -> bool { + match self.inner.transaction_status { + TransactionStatus::Transaction => true, + TransactionStatus::Error | TransactionStatus::Idle => false, + } + } } impl Debug for PgConnection { @@ -179,7 +189,17 @@ impl Connection for PgConnection { where Self: Sized, { - Transaction::begin(self) + Transaction::begin(self, None) + } + + fn begin_with( + &mut self, + statement: impl Into>, + ) -> BoxFuture<'_, Result, Error>> + where + Self: Sized, + { + Transaction::begin(self, Some(statement.into())) } fn cached_statements_size(&self) -> usize { diff --git a/sqlx-postgres/src/copy.rs b/sqlx-postgres/src/copy.rs index ddc187e993..1315ea0e20 100644 --- a/sqlx-postgres/src/copy.rs +++ b/sqlx-postgres/src/copy.rs @@ -129,6 +129,9 @@ impl PgPoolCopyExt for Pool { } } +// (1 GiB - 1) - 1 - length prefix (4 bytes) +pub const PG_COPY_MAX_DATA_LEN: usize = 0x3fffffff - 1 - 4; + /// A connection in streaming `COPY FROM STDIN` mode. /// /// Created by [PgConnection::copy_in_raw] or [Pool::copy_out_raw]. @@ -186,15 +189,20 @@ impl> PgCopyIn { /// Send a chunk of `COPY` data. /// + /// The data is sent in chunks if it exceeds the maximum length of a `CopyData` message (1 GiB - 6 + /// bytes) and may be partially sent if this call is cancelled. + /// /// If you're copying data from an `AsyncRead`, maybe consider [Self::read_from] instead. pub async fn send(&mut self, data: impl Deref) -> Result<&mut Self> { - self.conn - .as_deref_mut() - .expect("send_data: conn taken") - .inner - .stream - .send(CopyData(data)) - .await?; + for chunk in data.deref().chunks(PG_COPY_MAX_DATA_LEN) { + self.conn + .as_deref_mut() + .expect("send_data: conn taken") + .inner + .stream + .send(CopyData(chunk)) + .await?; + } Ok(self) } @@ -230,10 +238,10 @@ impl> PgCopyIn { } // Write the length - let read32 = u32::try_from(read) - .map_err(|_| err_protocol!("number of bytes read exceeds 2^32: {}", read))?; + let read32 = i32::try_from(read) + .map_err(|_| err_protocol!("number of bytes read exceeds 2^31 - 1: {}", read))?; - (&mut buf.get_mut()[1..]).put_u32(read32 + 4); + (&mut buf.get_mut()[1..]).put_i32(read32 + 4); conn.inner.stream.flush().await?; } diff --git a/sqlx-postgres/src/lib.rs b/sqlx-postgres/src/lib.rs index 792f8bbdc0..bded75491c 100644 --- a/sqlx-postgres/src/lib.rs +++ b/sqlx-postgres/src/lib.rs @@ -34,6 +34,9 @@ mod value; #[doc(hidden)] pub mod any; +#[doc(hidden)] +pub use copy::PG_COPY_MAX_DATA_LEN; + #[cfg(feature = "migrate")] mod migrate; diff --git a/sqlx-postgres/src/listener.rs b/sqlx-postgres/src/listener.rs index b96f8d829f..17a46a916f 100644 --- a/sqlx-postgres/src/listener.rs +++ b/sqlx-postgres/src/listener.rs @@ -9,6 +9,7 @@ use futures_util::{FutureExt, StreamExt, TryFutureExt, TryStreamExt}; use sqlx_core::acquire::Acquire; use sqlx_core::transaction::Transaction; use sqlx_core::Either; +use tracing::Instrument; use crate::describe::Describe; use crate::error::Error; @@ -366,7 +367,7 @@ impl Drop for PgListener { }; // Unregister any listeners before returning the connection to the pool. - crate::rt::spawn(fut); + crate::rt::spawn(fut.in_current_span()); } } } diff --git a/sqlx-postgres/src/message/parse.rs b/sqlx-postgres/src/message/parse.rs index 75300c4815..62f57a1cc4 100644 --- a/sqlx-postgres/src/message/parse.rs +++ b/sqlx-postgres/src/message/parse.rs @@ -77,3 +77,19 @@ fn test_encode_parse() { assert_eq!(buf, EXPECTED); } + +#[test] +fn test_encode_parse_unnamed_statement() { + const EXPECTED: &[u8] = b"P\0\0\0\x15\0SELECT $1\0\0\x01\0\0\0\x19"; + + let mut buf = Vec::new(); + let m = Parse { + statement: StatementId::UNNAMED, + query: "SELECT $1", + param_types: &[Oid(25)], + }; + + m.encode_msg(&mut buf).unwrap(); + + assert_eq!(buf, EXPECTED); +} diff --git a/sqlx-postgres/src/options/doc.md b/sqlx-postgres/src/options/doc.md new file mode 100644 index 0000000000..15c2459c81 --- /dev/null +++ b/sqlx-postgres/src/options/doc.md @@ -0,0 +1,185 @@ +Options and flags which can be used to configure a PostgreSQL connection. + +A value of `PgConnectOptions` can be parsed from a connection URL, +as described by [libpq][libpq-connstring]. + +The general form for a connection URL is: + +```text +postgresql://[user[:password]@][host][:port][/dbname][?param1=value1&...] +``` + +The URL scheme designator can be either `postgresql://` or `postgres://`. +Each of the URL parts is optional. For defaults, see the next section. + +This type also implements [`FromStr`][std::str::FromStr] so you can parse it from a string +containing a connection URL and then further adjust options if necessary (see example below). + +Note that characters not allowed in URLs must be [percent-encoded]. + +# Parameters + +This API accepts many of the same parameters as [libpq][libpq-params]; +if a parameter is not passed in via URL, it is populated by reading +[environment variables][libpq-envars] or choosing customary defaults. + +| Parameter | Environment Variable | Default / Remarks | +|--------------------|----------------------|-------------------------------------------------------------| +| `user` | `PGUSER` | The `whoami` of the currently running process. | +| `password` | `PGPASSWORD` | Read from [`passfile`], if it exists. | +| [`passfile`] | `PGPASSFILE` | `~/.pgpass` or `%APPDATA%\postgresql\pgpass.conf` (Windows) | +| `host` | `PGHOST` | See [Note: Default Host](#note-default-host). | +| `hostaddr` | `PGHOSTADDR` | See [Note: Default Host](#note-default-host). | +| `port` | `PGPORT` | `5432` | +| `dbname` | `PGDATABASE` | Unset; defaults to the username server-side. | +| `sslmode` | `PGSSLMODE` | `prefer`. See [`PgSslMode`] for details. | +| `sslrootcert` | `PGSSLROOTCERT` | Unset. See [Note: SSL](#note-ssl). | +| `sslcert` | `PGSSLCERT` | Unset. See [Note: SSL](#note-ssl). | +| `sslkey` | `PGSSLKEY` | Unset. See [Note: SSL](#note-ssl). | +| `options` | `PGOPTIONS` | Unset. | +| `application_name` | `PGAPPNAME` | Unset. | + +[`passfile`] handling may be bypassed using [`PgConnectOptions::new_without_pgpass()`]. + +## SQLx-Specific +SQLx also parses some bespoke parameters. These are _not_ configurable by environment variable. +Instead, the name is linked to the method to set the value. + +| Parameter | Default | +|--------------------------------------------------------------|-------------------------------| +| [`statement-cache-capacity`][Self::statement_cache_capacity] | `100` | + +# Example URLs +```text +postgresql:// +postgresql://:5433 +postgresql://localhost +postgresql://localhost:5433 +postgresql://localhost/mydb +postgresql://user@localhost +postgresql://user:secret@localhost +postgresql://user:correct%20horse%20battery%20staple@localhost +postgresql://localhost?dbname=mydb&user=postgres&password=postgres +``` + +See also [Note: Unix Domain Sockets](#note-unix-domain-sockets) below. + +# Note: Default Host +If the connection URL does not contain a hostname and `PGHOST` is not set, +this constructor looks for an open Unix domain socket in one of a few standard locations +(configured when Postgres is built): + +* `/var/run/postgresql/.s.PGSQL.{port}` (Debian) +* `/private/tmp/.s.PGSQL.{port}` (macOS when installed through Homebrew) +* `/tmp/.s.PGSQL.{port}` (default otherwise) + +This depends on the value of `port` being correct. +If Postgres is using a port other than the default (`5432`), `port` must be set. + +If no Unix domain socket is found, `localhost` is assumed. + +Note: this description is updated on a best-effort basis. +See `default_host()` in the same source file as this method for the current behavior. + +# Note: SSL +## Root Certs +If `sslrootcert` is not set, the default root certificates used depends on Cargo features: + +* If `tls-native-tls` is enabled, the system root certificates are used. +* If `tls-rustls-native-roots` is enabled, the system root certificates are used. +* Otherwise, TLS roots are populated using the [`webpki-roots`] crate. + +## Environment Variables +Unlike with `libpq`, the following environment variables may be _either_ +a path to a file _or_ a string value containing a [PEM-encoded value][rfc7468]: + +* `PGSSLROOTCERT` +* `PGSSLCERT` +* `PGSSLKEY` + +If the string begins with the standard `-----BEGIN -----` header +and ends with the standard `-----END -----` footer, +it is parsed directly. + +This behavior is _only_ implemented for the environment variables, not the URL parameters. + +Note: passing the SSL private key via environment variable may be a security risk. + +# Note: Unix Domain Sockets +If you want to connect to Postgres over a Unix domain socket, you can pass the path +to the _directory_ containing the socket as the `host` parameter. + +The final path to the socket will be `{host}/.s.PGSQL.{port}` as is standard for Postgres. + +If you're passing the domain socket path as the host segment of the URL, forward slashes +in the path must be [percent-encoded] (replacing `/` with `%2F`), e.g.: + +```text +postgres://%2Fvar%2Frun%2Fpostgresql/dbname + +Different port: +postgres://%2Fvar%2Frun%2Fpostgresql:5433/dbname + +With username and password: +postgres://user:password@%2Fvar%2Frun%2Fpostgresql/dbname + +With username and password, and different port: +postgres://user:password@%2Fvar%2Frun%2Fpostgresql:5432/dbname +``` + +Instead, the hostname can be passed in the query segment of the URL, +which does not require forward-slashes to be percent-encoded +(however, [other characters are][percent-encoded]): + +```text +postgres:dbname?host=/var/run/postgresql + +Different port: +postgres://:5433/dbname?host=/var/run/postgresql + +With username and password: +postgres://user:password@/dbname?host=/var/run/postgresql + +With username and password, and different port: +postgres://user:password@:5433/dbname?host=/var/run/postgresql +``` + +# Example + +```rust,no_run +use sqlx::{Connection, ConnectOptions}; +use sqlx::postgres::{PgConnectOptions, PgConnection, PgPool, PgSslMode}; + +# async fn example() -> sqlx::Result<()> { +// URL connection string +let conn = PgConnection::connect("postgres://localhost/mydb").await?; + +// Manually-constructed options +let conn = PgConnectOptions::new() + .host("secret-host") + .port(2525) + .username("secret-user") + .password("secret-password") + .ssl_mode(PgSslMode::Require) + .connect() + .await?; + +// Modifying options parsed from a string +let mut opts: PgConnectOptions = "postgres://localhost/mydb".parse()?; + +// Change the log verbosity level for queries. +// Information about SQL queries is logged at `DEBUG` level by default. +opts = opts.log_statements(log::LevelFilter::Trace); + +let pool = PgPool::connect_with(opts).await?; +# Ok(()) +# } +``` + +[percent-encoded]: https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding +[`passfile`]: https://www.postgresql.org/docs/current/libpq-pgpass.html +[libpq-connstring]: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING +[libpq-params]: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS +[libpq-envars]: https://www.postgresql.org/docs/current/libpq-envars.html +[rfc7468]: https://datatracker.ietf.org/doc/html/rfc7468 +[`webpki-roots`]: https://docs.rs/webpki-roots \ No newline at end of file diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index a0b222606a..723721a97c 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -12,80 +12,7 @@ mod parse; mod pgpass; mod ssl_mode; -/// Options and flags which can be used to configure a PostgreSQL connection. -/// -/// A value of `PgConnectOptions` can be parsed from a connection URL, -/// as described by [libpq](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING). -/// -/// The general form for a connection URL is: -/// -/// ```text -/// postgresql://[user[:password]@][host][:port][/dbname][?param1=value1&...] -/// ``` -/// -/// This type also implements [`FromStr`][std::str::FromStr] so you can parse it from a string -/// containing a connection URL and then further adjust options if necessary (see example below). -/// -/// ## Parameters -/// -/// |Parameter|Default|Description| -/// |---------|-------|-----------| -/// | `sslmode` | `prefer` | Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated. See [`PgSslMode`]. | -/// | `sslrootcert` | `None` | Sets the name of a file containing a list of trusted SSL Certificate Authorities. | -/// | `statement-cache-capacity` | `100` | The maximum number of prepared statements stored in the cache. Set to `0` to disable. | -/// | `host` | `None` | Path to the directory containing a PostgreSQL unix domain socket, which will be used instead of TCP if set. | -/// | `hostaddr` | `None` | Same as `host`, but only accepts IP addresses. | -/// | `application-name` | `None` | The name will be displayed in the pg_stat_activity view and included in CSV log entries. | -/// | `user` | result of `whoami` | PostgreSQL user name to connect as. | -/// | `password` | `None` | Password to be used if the server demands password authentication. | -/// | `port` | `5432` | Port number to connect to at the server host, or socket file name extension for Unix-domain connections. | -/// | `dbname` | `None` | The database name. | -/// | `options` | `None` | The runtime parameters to send to the server at connection start. | -/// -/// The URL scheme designator can be either `postgresql://` or `postgres://`. -/// Each of the URL parts is optional. -/// -/// ```text -/// postgresql:// -/// postgresql://localhost -/// postgresql://localhost:5433 -/// postgresql://localhost/mydb -/// postgresql://user@localhost -/// postgresql://user:secret@localhost -/// postgresql://localhost?dbname=mydb&user=postgres&password=postgres -/// ``` -/// -/// # Example -/// -/// ```rust,no_run -/// use sqlx::{Connection, ConnectOptions}; -/// use sqlx::postgres::{PgConnectOptions, PgConnection, PgPool, PgSslMode}; -/// -/// # async fn example() -> sqlx::Result<()> { -/// // URL connection string -/// let conn = PgConnection::connect("postgres://localhost/mydb").await?; -/// -/// // Manually-constructed options -/// let conn = PgConnectOptions::new() -/// .host("secret-host") -/// .port(2525) -/// .username("secret-user") -/// .password("secret-password") -/// .ssl_mode(PgSslMode::Require) -/// .connect() -/// .await?; -/// -/// // Modifying options parsed from a string -/// let mut opts: PgConnectOptions = "postgres://localhost/mydb".parse()?; -/// -/// // Change the log verbosity level for queries. -/// // Information about SQL queries is logged at `DEBUG` level by default. -/// opts = opts.log_statements(log::LevelFilter::Trace); -/// -/// let pool = PgPool::connect_with(opts).await?; -/// # Ok(()) -/// # } -/// ``` +#[doc = include_str!("doc.md")] #[derive(Debug, Clone)] pub struct PgConnectOptions { pub(crate) host: String, @@ -112,39 +39,30 @@ impl Default for PgConnectOptions { } impl PgConnectOptions { - /// Creates a new, default set of options ready for configuration. - /// - /// By default, this reads the following environment variables and sets their - /// equivalent options. - /// - /// * `PGHOST` - /// * `PGPORT` - /// * `PGUSER` - /// * `PGPASSWORD` - /// * `PGDATABASE` - /// * `PGSSLROOTCERT` - /// * `PGSSLCERT` - /// * `PGSSLKEY` - /// * `PGSSLMODE` - /// * `PGAPPNAME` + /// Create a default set of connection options populated from the current environment. /// - /// # Example + /// This behaves as if parsed from the connection string `postgres://` /// - /// ```rust - /// # use sqlx_postgres::PgConnectOptions; - /// let options = PgConnectOptions::new(); - /// ``` + /// See the type-level documentation for details. pub fn new() -> Self { Self::new_without_pgpass().apply_pgpass() } + /// Create a default set of connection options _without_ reading from `passfile`. + /// + /// Equivalent to [`PgConnectOptions::new()`] but `passfile` is ignored. + /// + /// See the type-level documentation for details. pub fn new_without_pgpass() -> Self { let port = var("PGPORT") .ok() .and_then(|v| v.parse().ok()) .unwrap_or(5432); - let host = var("PGHOST").ok().unwrap_or_else(|| default_host(port)); + let host = var("PGHOSTADDR") + .ok() + .or_else(|| var("PGHOST").ok()) + .unwrap_or_else(|| default_host(port)); let username = var("PGUSER").ok().unwrap_or_else(whoami::username); @@ -159,6 +77,9 @@ impl PgConnectOptions { database, ssl_root_cert: var("PGSSLROOTCERT").ok().map(CertificateInput::from), ssl_client_cert: var("PGSSLCERT").ok().map(CertificateInput::from), + // As of writing, the implementation of `From` only looks for + // `-----BEGIN CERTIFICATE-----` and so will not attempt to parse + // a PEM-encoded private key. ssl_client_key: var("PGSSLKEY").ok().map(CertificateInput::from), ssl_mode: var("PGSSLMODE") .ok() diff --git a/sqlx-postgres/src/testing/mod.rs b/sqlx-postgres/src/testing/mod.rs index fb36ab4136..af20fe87ea 100644 --- a/sqlx-postgres/src/testing/mod.rs +++ b/sqlx-postgres/src/testing/mod.rs @@ -1,20 +1,18 @@ use std::fmt::Write; use std::ops::Deref; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{Duration, SystemTime}; +use std::time::Duration; use futures_core::future::BoxFuture; use once_cell::sync::OnceCell; - -use crate::connection::Connection; +use sqlx_core::connection::Connection; +use sqlx_core::query_scalar::query_scalar; use crate::error::Error; use crate::executor::Executor; use crate::pool::{Pool, PoolOptions}; use crate::query::query; -use crate::query_scalar::query_scalar; use crate::{PgConnectOptions, PgConnection, Postgres}; pub(crate) use sqlx_core::testing::*; @@ -22,7 +20,6 @@ pub(crate) use sqlx_core::testing::*; // Using a blocking `OnceCell` here because the critical sections are short. static MASTER_POOL: OnceCell> = OnceCell::new(); // Automatically delete any databases created before the start of the test binary. -static DO_CLEANUP: AtomicBool = AtomicBool::new(true); impl TestSupport for Postgres { fn test_context(args: &TestArgs) -> BoxFuture<'_, Result, Error>> { @@ -33,19 +30,11 @@ impl TestSupport for Postgres { Box::pin(async move { let mut conn = MASTER_POOL .get() - .expect("cleanup_test() invoked outside `#[sqlx::test]") + .expect("cleanup_test() invoked outside `#[sqlx::test]`") .acquire() .await?; - conn.execute(&format!("drop database if exists {db_name:?};")[..]) - .await?; - - query("delete from _sqlx_test.databases where db_name = $1") - .bind(db_name) - .execute(&mut *conn) - .await?; - - Ok(()) + do_cleanup(&mut conn, db_name).await }) } @@ -55,13 +44,42 @@ impl TestSupport for Postgres { let mut conn = PgConnection::connect(&url).await?; - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); + let delete_db_names: Vec = + query_scalar("select db_name from _sqlx_test.databases") + .fetch_all(&mut conn) + .await?; + + if delete_db_names.is_empty() { + return Ok(None); + } + + let mut deleted_db_names = Vec::with_capacity(delete_db_names.len()); + + let mut command = String::new(); + + for db_name in &delete_db_names { + command.clear(); + writeln!(command, "drop database if exists {db_name:?};").ok(); + match conn.execute(&*command).await { + Ok(_deleted) => { + deleted_db_names.push(db_name); + } + // Assume a database error just means the DB is still in use. + Err(Error::Database(dbe)) => { + eprintln!("could not clean test database {db_name:?}: {dbe}") + } + // Bubble up other errors + Err(e) => return Err(e), + } + } + + query("delete from _sqlx_test.databases where db_name = any($1::text[])") + .bind(&deleted_db_names) + .execute(&mut conn) + .await?; - let num_deleted = do_cleanup(&mut conn, now).await?; let _ = conn.close().await; - Ok(Some(num_deleted)) + Ok(Some(delete_db_names.len())) }) } @@ -116,8 +134,9 @@ async fn test_context(args: &TestArgs) -> Result, Error> { // I couldn't find a bug on the mailing list for `CREATE SCHEMA` specifically, // but a clearly related bug with `CREATE TABLE` has been known since 2007: // https://www.postgresql.org/message-id/200710222037.l9MKbCJZ098744%40wwwmaster.postgresql.org + // magic constant 8318549251334697844 is just 8 ascii bytes 'sqlxtest'. r#" - lock table pg_catalog.pg_namespace in share row exclusive mode; + select pg_advisory_xact_lock(8318549251334697844); create schema if not exists _sqlx_test; @@ -135,31 +154,22 @@ async fn test_context(args: &TestArgs) -> Result, Error> { ) .await?; - // Record the current time _before_ we acquire the `DO_CLEANUP` permit. This - // prevents the first test thread from accidentally deleting new test dbs - // created by other test threads if we're a bit slow. - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); + let db_name = Postgres::db_name(args); + do_cleanup(&mut conn, &db_name).await?; - // Only run cleanup if the test binary just started. - if DO_CLEANUP.swap(false, Ordering::SeqCst) { - do_cleanup(&mut conn, now).await?; - } - - let new_db_name: String = query_scalar( + query( r#" - insert into _sqlx_test.databases(db_name, test_path) - select '_sqlx_test_' || nextval('_sqlx_test.database_ids'), $1 - returning db_name + insert into _sqlx_test.databases(db_name, test_path) values ($1, $2) "#, ) + .bind(&db_name) .bind(args.test_path) - .fetch_one(&mut *conn) + .execute(&mut *conn) .await?; - conn.execute(&format!("create database {new_db_name:?}")[..]) - .await?; + let create_command = format!("create database {db_name:?}"); + debug_assert!(create_command.starts_with("create database \"")); + conn.execute(&(create_command)[..]).await?; Ok(TestContext { pool_opts: PoolOptions::new() @@ -174,52 +184,18 @@ async fn test_context(args: &TestArgs) -> Result, Error> { .connect_options() .deref() .clone() - .database(&new_db_name), - db_name: new_db_name, + .database(&db_name), + db_name, }) } -async fn do_cleanup(conn: &mut PgConnection, created_before: Duration) -> Result { - // since SystemTime is not monotonic we added a little margin here to avoid race conditions with other threads - let created_before = i64::try_from(created_before.as_secs()).unwrap() - 2; - - let delete_db_names: Vec = query_scalar( - "select db_name from _sqlx_test.databases \ - where created_at < (to_timestamp($1) at time zone 'UTC')", - ) - .bind(created_before) - .fetch_all(&mut *conn) - .await?; - - if delete_db_names.is_empty() { - return Ok(0); - } - - let mut deleted_db_names = Vec::with_capacity(delete_db_names.len()); - let delete_db_names = delete_db_names.into_iter(); - - let mut command = String::new(); - - for db_name in delete_db_names { - command.clear(); - writeln!(command, "drop database if exists {db_name:?};").ok(); - match conn.execute(&*command).await { - Ok(_deleted) => { - deleted_db_names.push(db_name); - } - // Assume a database error just means the DB is still in use. - Err(Error::Database(dbe)) => { - eprintln!("could not clean test database {db_name:?}: {dbe}") - } - // Bubble up other errors - Err(e) => return Err(e), - } - } - - query("delete from _sqlx_test.databases where db_name = any($1::text[])") - .bind(&deleted_db_names) +async fn do_cleanup(conn: &mut PgConnection, db_name: &str) -> Result<(), Error> { + let delete_db_command = format!("drop database if exists {db_name:?};"); + conn.execute(&*delete_db_command).await?; + query("delete from _sqlx_test.databases where db_name = $1::text") + .bind(db_name) .execute(&mut *conn) .await?; - Ok(deleted_db_names.len()) + Ok(()) } diff --git a/sqlx-postgres/src/transaction.rs b/sqlx-postgres/src/transaction.rs index e7c78488eb..23352a8dcf 100644 --- a/sqlx-postgres/src/transaction.rs +++ b/sqlx-postgres/src/transaction.rs @@ -1,4 +1,6 @@ use futures_core::future::BoxFuture; +use sqlx_core::database::Database; +use std::borrow::Cow; use crate::error::Error; use crate::executor::Executor; @@ -13,13 +15,27 @@ pub struct PgTransactionManager; impl TransactionManager for PgTransactionManager { type Database = Postgres; - fn begin(conn: &mut PgConnection) -> BoxFuture<'_, Result<(), Error>> { + fn begin<'conn>( + conn: &'conn mut PgConnection, + statement: Option>, + ) -> BoxFuture<'conn, Result<(), Error>> { Box::pin(async move { + let depth = conn.inner.transaction_depth; + let statement = match statement { + // custom `BEGIN` statements are not allowed if we're already in + // a transaction (we need to issue a `SAVEPOINT` instead) + Some(_) if depth > 0 => return Err(Error::InvalidSavePointStatement), + Some(statement) => statement, + None => begin_ansi_transaction_sql(depth), + }; + let rollback = Rollback::new(conn); - let query = begin_ansi_transaction_sql(rollback.conn.inner.transaction_depth); - rollback.conn.queue_simple_query(&query)?; - rollback.conn.inner.transaction_depth += 1; + rollback.conn.queue_simple_query(&statement)?; rollback.conn.wait_until_ready().await?; + if !rollback.conn.in_transaction() { + return Err(Error::BeginFailed); + } + rollback.conn.inner.transaction_depth += 1; rollback.defuse(); Ok(()) @@ -62,6 +78,10 @@ impl TransactionManager for PgTransactionManager { conn.inner.transaction_depth -= 1; } } + + fn get_transaction_depth(conn: &::Connection) -> usize { + conn.inner.transaction_depth + } } struct Rollback<'c> { diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index eb18c5a999..672d9f73e6 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -36,6 +36,16 @@ impl_type_checking!( sqlx::postgres::types::PgLine, + sqlx::postgres::types::PgLSeg, + + sqlx::postgres::types::PgBox, + + sqlx::postgres::types::PgPath, + + sqlx::postgres::types::PgPolygon, + + sqlx::postgres::types::PgCircle, + #[cfg(feature = "uuid")] sqlx::types::Uuid, @@ -78,6 +88,9 @@ impl_type_checking!( #[cfg(feature = "ipnetwork")] sqlx::types::ipnetwork::IpNetwork, + #[cfg(feature = "ipnet")] + sqlx::types::ipnet::IpNet, + #[cfg(feature = "mac_address")] sqlx::types::mac_address::MacAddress, @@ -139,6 +152,9 @@ impl_type_checking!( #[cfg(feature = "ipnetwork")] Vec | &[sqlx::types::ipnetwork::IpNetwork], + #[cfg(feature = "ipnet")] + Vec | &[sqlx::types::ipnet::IpNet], + #[cfg(feature = "mac_address")] Vec | &[sqlx::types::mac_address::MacAddress], diff --git a/sqlx-postgres/src/type_info.rs b/sqlx-postgres/src/type_info.rs index 3d948f73d4..28c56758e9 100644 --- a/sqlx-postgres/src/type_info.rs +++ b/sqlx-postgres/src/type_info.rs @@ -185,7 +185,7 @@ pub enum PgTypeKind { Range(PgTypeInfo), } -#[derive(Debug)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))] pub struct PgArrayOf { pub(crate) elem_name: UStr, diff --git a/sqlx-postgres/src/types/cube.rs b/sqlx-postgres/src/types/cube.rs index f39d82651c..cc2a016090 100644 --- a/sqlx-postgres/src/types/cube.rs +++ b/sqlx-postgres/src/types/cube.rs @@ -20,7 +20,7 @@ const IS_POINT_FLAG: u32 = 1 << 31; #[derive(Debug, Clone, PartialEq)] pub enum PgCube { /// A one-dimensional point. - // FIXME: `Point1D(f64) + // FIXME: `Point1D(f64)` Point(f64), /// An N-dimensional point ("represented internally as a zero-volume cube"). // FIXME: `PointND(f64)` @@ -32,7 +32,7 @@ pub enum PgCube { // FIXME: add `Cube3D { lower_left: [f64; 3], upper_right: [f64; 3] }`? /// An N-dimensional cube with points representing lower-left and upper-right corners, respectively. - // FIXME: CubeND { lower_left: Vec, upper_right: Vec }` + // FIXME: `CubeND { lower_left: Vec, upper_right: Vec }` MultiDimension(Vec>), } diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs new file mode 100644 index 0000000000..28016b2786 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -0,0 +1,324 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use std::str::FromStr; + +const ERROR: &str = "error decoding BOX"; + +/// ## Postgres Geometric Box type +/// +/// Description: Rectangular box +/// Representation: `((upper_right_x,upper_right_y),(lower_left_x,lower_left_y))` +/// +/// Boxes are represented by pairs of points that are opposite corners of the box. Values of type box are specified using any of the following syntaxes: +/// +/// ```text +/// ( ( upper_right_x , upper_right_y ) , ( lower_left_x , lower_left_y ) ) +/// ( upper_right_x , upper_right_y ) , ( lower_left_x , lower_left_y ) +/// upper_right_x , upper_right_y , lower_left_x , lower_left_y +/// ``` +/// where `(upper_right_x,upper_right_y) and (lower_left_x,lower_left_y)` are any two opposite corners of the box. +/// Any two opposite corners can be supplied on input, but the values will be reordered as needed to store the upper right and lower left corners, in that order. +/// +/// See [Postgres Manual, Section 8.8.4: Geometric Types - Boxes][PG.S.8.8.4] for details. +/// +/// [PG.S.8.8.4]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES +/// +#[derive(Debug, Clone, PartialEq)] +pub struct PgBox { + pub upper_right_x: f64, + pub upper_right_y: f64, + pub lower_left_x: f64, + pub lower_left_y: f64, +} + +impl Type for PgBox { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("box") + } +} + +impl PgHasArrayType for PgBox { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_box") + } +} + +impl<'r> Decode<'r, Postgres> for PgBox { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgBox::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgBox::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgBox { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("box")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgBox { + type Err = BoxDynError; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); + let mut parts = sanitised.split(','); + + let upper_right_x = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get upper_right_x from {}", ERROR, s))?; + + let upper_right_y = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get upper_right_y from {}", ERROR, s))?; + + let lower_left_x = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get lower_left_x from {}", ERROR, s))?; + + let lower_left_y = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get lower_left_y from {}", ERROR, s))?; + + if parts.next().is_some() { + return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); + } + + Ok(PgBox { + upper_right_x, + upper_right_y, + lower_left_x, + lower_left_y, + }) + } +} + +impl PgBox { + fn from_bytes(mut bytes: &[u8]) -> Result { + let upper_right_x = bytes.get_f64(); + let upper_right_y = bytes.get_f64(); + let lower_left_x = bytes.get_f64(); + let lower_left_y = bytes.get_f64(); + + Ok(PgBox { + upper_right_x, + upper_right_y, + lower_left_x, + lower_left_y, + }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { + let min_x = &self.upper_right_x.min(self.lower_left_x); + let min_y = &self.upper_right_y.min(self.lower_left_y); + let max_x = &self.upper_right_x.max(self.lower_left_x); + let max_y = &self.upper_right_y.max(self.lower_left_y); + + buff.extend_from_slice(&max_x.to_be_bytes()); + buff.extend_from_slice(&max_y.to_be_bytes()); + buff.extend_from_slice(&min_x.to_be_bytes()); + buff.extend_from_slice(&min_y.to_be_bytes()); + + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +#[cfg(test)] +mod box_tests { + + use std::str::FromStr; + + use super::PgBox; + + const BOX_BYTES: &[u8] = &[ + 64, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, + 0, 0, 0, 0, + ]; + + #[test] + fn can_deserialise_box_type_bytes_in_order() { + let pg_box = PgBox::from_bytes(BOX_BYTES).unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 2., + upper_right_y: 2., + lower_left_x: -2., + lower_left_y: -2. + } + ) + } + + #[test] + fn can_deserialise_box_type_str_first_syntax() { + let pg_box = PgBox::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 1., + upper_right_y: 2., + lower_left_x: 3., + lower_left_y: 4. + } + ); + } + #[test] + fn can_deserialise_box_type_str_second_syntax() { + let pg_box = PgBox::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 1., + upper_right_y: 2., + lower_left_x: 3., + lower_left_y: 4. + } + ); + } + + #[test] + fn can_deserialise_box_type_str_third_syntax() { + let pg_box = PgBox::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 1., + upper_right_y: 2., + lower_left_x: 3., + lower_left_y: 4. + } + ); + } + + #[test] + fn can_deserialise_box_type_str_fourth_syntax() { + let pg_box = PgBox::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 1., + upper_right_y: 2., + lower_left_x: 3., + lower_left_y: 4. + } + ); + } + + #[test] + fn cannot_deserialise_too_many_numbers() { + let input_str = "1, 2, 3, 4, 5"; + let pg_box = PgBox::from_str(input_str); + assert!(pg_box.is_err()); + if let Err(err) = pg_box { + assert_eq!( + err.to_string(), + format!("error decoding BOX: too many numbers inputted in {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_too_few_numbers() { + let input_str = "1, 2, 3 "; + let pg_box = PgBox::from_str(input_str); + assert!(pg_box.is_err()); + if let Err(err) = pg_box { + assert_eq!( + err.to_string(), + format!("error decoding BOX: could not get lower_left_y from {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_invalid_numbers() { + let input_str = "1, 2, 3, FOUR"; + let pg_box = PgBox::from_str(input_str); + assert!(pg_box.is_err()); + if let Err(err) = pg_box { + assert_eq!( + err.to_string(), + format!("error decoding BOX: could not get lower_left_y from {input_str}") + ) + } + } + + #[test] + fn can_deserialise_box_type_str_float() { + let pg_box = PgBox::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 1.1, + upper_right_y: 2.2, + lower_left_x: 3.3, + lower_left_y: 4.4 + } + ); + } + + #[test] + fn can_serialise_box_type_in_order() { + let pg_box = PgBox { + upper_right_x: 2., + lower_left_x: -2., + upper_right_y: -2., + lower_left_y: 2., + }; + assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) + } + + #[test] + fn can_serialise_box_type_out_of_order() { + let pg_box = PgBox { + upper_right_x: -2., + lower_left_x: 2., + upper_right_y: 2., + lower_left_y: -2., + }; + assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) + } + + #[test] + fn can_order_box() { + let pg_box = PgBox { + upper_right_x: -2., + lower_left_x: 2., + upper_right_y: 2., + lower_left_y: -2., + }; + let bytes = pg_box.serialize_to_vec(); + + let pg_box = PgBox::from_bytes(&bytes).unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 2., + upper_right_y: 2., + lower_left_x: -2., + lower_left_y: -2. + } + ) + } +} diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs new file mode 100644 index 0000000000..dde54dd276 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -0,0 +1,250 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use sqlx_core::Error; +use std::str::FromStr; + +const ERROR: &str = "error decoding CIRCLE"; + +/// ## Postgres Geometric Circle type +/// +/// Description: Circle +/// Representation: `< (x, y), radius >` (center point and radius) +/// +/// ```text +/// < ( x , y ) , radius > +/// ( ( x , y ) , radius ) +/// ( x , y ) , radius +/// x , y , radius +/// ``` +/// where `(x,y)` is the center point. +/// +/// See [Postgres Manual, Section 8.8.7, Geometric Types - Circles][PG.S.8.8.7] for details. +/// +/// [PG.S.8.8.7]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-CIRCLE +/// +#[derive(Debug, Clone, PartialEq)] +pub struct PgCircle { + pub x: f64, + pub y: f64, + pub radius: f64, +} + +impl Type for PgCircle { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("circle") + } +} + +impl PgHasArrayType for PgCircle { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_circle") + } +} + +impl<'r> Decode<'r, Postgres> for PgCircle { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgCircle::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgCircle::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgCircle { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("circle")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgCircle { + type Err = BoxDynError; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(['<', '>', '(', ')', ' '], ""); + let mut parts = sanitised.split(','); + + let x = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or_else(|| format!("{}: could not get x from {}", ERROR, s))?; + + let y = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or_else(|| format!("{}: could not get y from {}", ERROR, s))?; + + let radius = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or_else(|| format!("{}: could not get radius from {}", ERROR, s))?; + + if parts.next().is_some() { + return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); + } + + if radius < 0. { + return Err(format!("{}: cannot have negative radius: {}", ERROR, s).into()); + } + + Ok(PgCircle { x, y, radius }) + } +} + +impl PgCircle { + fn from_bytes(mut bytes: &[u8]) -> Result { + let x = bytes.get_f64(); + let y = bytes.get_f64(); + let r = bytes.get_f64(); + Ok(PgCircle { x, y, radius: r }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + buff.extend_from_slice(&self.x.to_be_bytes()); + buff.extend_from_slice(&self.y.to_be_bytes()); + buff.extend_from_slice(&self.radius.to_be_bytes()); + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +#[cfg(test)] +mod circle_tests { + + use std::str::FromStr; + + use super::PgCircle; + + const CIRCLE_BYTES: &[u8] = &[ + 63, 241, 153, 153, 153, 153, 153, 154, 64, 1, 153, 153, 153, 153, 153, 154, 64, 10, 102, + 102, 102, 102, 102, 102, + ]; + + #[test] + fn can_deserialise_circle_type_bytes() { + let circle = PgCircle::from_bytes(CIRCLE_BYTES).unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.1, + y: 2.2, + radius: 3.3 + } + ) + } + + #[test] + fn can_deserialise_circle_type_str() { + let circle = PgCircle::from_str("<(1, 2), 3 >").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + radius: 3.0 + } + ); + } + + #[test] + fn can_deserialise_circle_type_str_second_syntax() { + let circle = PgCircle::from_str("((1, 2), 3 )").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + radius: 3.0 + } + ); + } + + #[test] + fn can_deserialise_circle_type_str_third_syntax() { + let circle = PgCircle::from_str("(1, 2), 3 ").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + radius: 3.0 + } + ); + } + + #[test] + fn can_deserialise_circle_type_str_fourth_syntax() { + let circle = PgCircle::from_str("1, 2, 3 ").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + radius: 3.0 + } + ); + } + + #[test] + fn cannot_deserialise_circle_invalid_numbers() { + let input_str = "1, 2, Three"; + let circle = PgCircle::from_str(input_str); + assert!(circle.is_err()); + if let Err(err) = circle { + assert_eq!( + err.to_string(), + format!("error decoding CIRCLE: could not get radius from {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_circle_negative_radius() { + let input_str = "1, 2, -3"; + let circle = PgCircle::from_str(input_str); + assert!(circle.is_err()); + if let Err(err) = circle { + assert_eq!( + err.to_string(), + format!("error decoding CIRCLE: cannot have negative radius: {input_str}") + ) + } + } + + #[test] + fn can_deserialise_circle_type_str_float() { + let circle = PgCircle::from_str("<(1.1, 2.2), 3.3>").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.1, + y: 2.2, + radius: 3.3 + } + ); + } + + #[test] + fn can_serialise_circle_type() { + let circle = PgCircle { + x: 1.1, + y: 2.2, + radius: 3.3, + }; + assert_eq!(circle.serialize_to_vec(), CIRCLE_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index 43f93c1c33..8f08c949ef 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -15,7 +15,10 @@ const ERROR: &str = "error decoding LINE"; /// /// Lines are represented by the linear equation Ax + By + C = 0, where A and B are not both zero. /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LINE +/// See [Postgres Manual, Section 8.8.2, Geometric Types - Lines][PG.S.8.8.2] for details. +/// +/// [PG.S.8.8.2]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-LINE +/// #[derive(Debug, Clone, PartialEq)] pub struct PgLine { pub a: f64, diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs new file mode 100644 index 0000000000..cd08e4da4a --- /dev/null +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -0,0 +1,286 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use std::str::FromStr; + +const ERROR: &str = "error decoding LSEG"; + +/// ## Postgres Geometric Line Segment type +/// +/// Description: Finite line segment +/// Representation: `((start_x,start_y),(end_x,end_y))` +/// +/// +/// Line segments are represented by pairs of points that are the endpoints of the segment. Values of type lseg are specified using any of the following syntaxes: +/// ```text +/// [ ( start_x , start_y ) , ( end_x , end_y ) ] +/// ( ( start_x , start_y ) , ( end_x , end_y ) ) +/// ( start_x , start_y ) , ( end_x , end_y ) +/// start_x , start_y , end_x , end_y +/// ``` +/// where `(start_x,start_y) and (end_x,end_y)` are the end points of the line segment. +/// +/// See [Postgres Manual, Section 8.8.3, Geometric Types - Line Segments][PG.S.8.8.3] for details. +/// +/// [PG.S.8.8.3]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-LSEG +/// +#[doc(alias = "line segment")] +#[derive(Debug, Clone, PartialEq)] +pub struct PgLSeg { + pub start_x: f64, + pub start_y: f64, + pub end_x: f64, + pub end_y: f64, +} + +impl Type for PgLSeg { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("lseg") + } +} + +impl PgHasArrayType for PgLSeg { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_lseg") + } +} + +impl<'r> Decode<'r, Postgres> for PgLSeg { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgLSeg::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgLSeg::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgLSeg { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("lseg")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgLSeg { + type Err = BoxDynError; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); + let mut parts = sanitised.split(','); + + let start_x = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get start_x from {}", ERROR, s))?; + + let start_y = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get start_y from {}", ERROR, s))?; + + let end_x = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get end_x from {}", ERROR, s))?; + + let end_y = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get end_y from {}", ERROR, s))?; + + if parts.next().is_some() { + return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); + } + + Ok(PgLSeg { + start_x, + start_y, + end_x, + end_y, + }) + } +} + +impl PgLSeg { + fn from_bytes(mut bytes: &[u8]) -> Result { + let start_x = bytes.get_f64(); + let start_y = bytes.get_f64(); + let end_x = bytes.get_f64(); + let end_y = bytes.get_f64(); + + Ok(PgLSeg { + start_x, + start_y, + end_x, + end_y, + }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> { + buff.extend_from_slice(&self.start_x.to_be_bytes()); + buff.extend_from_slice(&self.start_y.to_be_bytes()); + buff.extend_from_slice(&self.end_x.to_be_bytes()); + buff.extend_from_slice(&self.end_y.to_be_bytes()); + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +#[cfg(test)] +mod lseg_tests { + + use std::str::FromStr; + + use super::PgLSeg; + + const LINE_SEGMENT_BYTES: &[u8] = &[ + 63, 241, 153, 153, 153, 153, 153, 154, 64, 1, 153, 153, 153, 153, 153, 154, 64, 10, 102, + 102, 102, 102, 102, 102, 64, 17, 153, 153, 153, 153, 153, 154, + ]; + + #[test] + fn can_deserialise_lseg_type_bytes() { + let lseg = PgLSeg::from_bytes(LINE_SEGMENT_BYTES).unwrap(); + assert_eq!( + lseg, + PgLSeg { + start_x: 1.1, + start_y: 2.2, + end_x: 3.3, + end_y: 4.4 + } + ) + } + + #[test] + fn can_deserialise_lseg_type_str_first_syntax() { + let lseg = PgLSeg::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + lseg, + PgLSeg { + start_x: 1., + start_y: 2., + end_x: 3., + end_y: 4. + } + ); + } + #[test] + fn can_deserialise_lseg_type_str_second_syntax() { + let lseg = PgLSeg::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + lseg, + PgLSeg { + start_x: 1., + start_y: 2., + end_x: 3., + end_y: 4. + } + ); + } + + #[test] + fn can_deserialise_lseg_type_str_third_syntax() { + let lseg = PgLSeg::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + lseg, + PgLSeg { + start_x: 1., + start_y: 2., + end_x: 3., + end_y: 4. + } + ); + } + + #[test] + fn can_deserialise_lseg_type_str_fourth_syntax() { + let lseg = PgLSeg::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + lseg, + PgLSeg { + start_x: 1., + start_y: 2., + end_x: 3., + end_y: 4. + } + ); + } + + #[test] + fn can_deserialise_too_many_numbers() { + let input_str = "1, 2, 3, 4, 5"; + let lseg = PgLSeg::from_str(input_str); + assert!(lseg.is_err()); + if let Err(err) = lseg { + assert_eq!( + err.to_string(), + format!("error decoding LSEG: too many numbers inputted in {input_str}") + ) + } + } + + #[test] + fn can_deserialise_too_few_numbers() { + let input_str = "1, 2, 3"; + let lseg = PgLSeg::from_str(input_str); + assert!(lseg.is_err()); + if let Err(err) = lseg { + assert_eq!( + err.to_string(), + format!("error decoding LSEG: could not get end_y from {input_str}") + ) + } + } + + #[test] + fn can_deserialise_invalid_numbers() { + let input_str = "1, 2, 3, FOUR"; + let lseg = PgLSeg::from_str(input_str); + assert!(lseg.is_err()); + if let Err(err) = lseg { + assert_eq!( + err.to_string(), + format!("error decoding LSEG: could not get end_y from {input_str}") + ) + } + } + + #[test] + fn can_deserialise_lseg_type_str_float() { + let lseg = PgLSeg::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + lseg, + PgLSeg { + start_x: 1.1, + start_y: 2.2, + end_x: 3.3, + end_y: 4.4 + } + ); + } + + #[test] + fn can_serialise_lseg_type() { + let lseg = PgLSeg { + start_x: 1.1, + start_y: 2.2, + end_x: 3.3, + end_y: 4.4, + }; + assert_eq!(lseg.serialize_to_vec(), LINE_SEGMENT_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/geometry/mod.rs b/sqlx-postgres/src/types/geometry/mod.rs index daf9f1deb9..c3142145ee 100644 --- a/sqlx-postgres/src/types/geometry/mod.rs +++ b/sqlx-postgres/src/types/geometry/mod.rs @@ -1,2 +1,7 @@ +pub mod r#box; +pub mod circle; pub mod line; +pub mod line_segment; +pub mod path; pub mod point; +pub mod polygon; diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs new file mode 100644 index 0000000000..6799289fac --- /dev/null +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -0,0 +1,375 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::{PgPoint, Type}; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use sqlx_core::Error; +use std::mem; +use std::str::FromStr; + +const BYTE_WIDTH: usize = mem::size_of::(); + +/// ## Postgres Geometric Path type +/// +/// Description: Open path or Closed path (similar to polygon) +/// Representation: Open `[(x1,y1),...]`, Closed `((x1,y1),...)` +/// +/// Paths are represented by lists of connected points. Paths can be open, where the first and last points in the list are considered not connected, or closed, where the first and last points are considered connected. +/// Values of type path are specified using any of the following syntaxes: +/// ```text +/// [ ( x1 , y1 ) , ... , ( xn , yn ) ] +/// ( ( x1 , y1 ) , ... , ( xn , yn ) ) +/// ( x1 , y1 ) , ... , ( xn , yn ) +/// ( x1 , y1 , ... , xn , yn ) +/// x1 , y1 , ... , xn , yn +/// ``` +/// where the points are the end points of the line segments comprising the path. Square brackets `([])` indicate an open path, while parentheses `(())` indicate a closed path. +/// When the outermost parentheses are omitted, as in the third through fifth syntaxes, a closed path is assumed. +/// +/// See [Postgres Manual, Section 8.8.5, Geometric Types - Paths][PG.S.8.8.5] for details. +/// +/// [PG.S.8.8.5]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS +/// +#[derive(Debug, Clone, PartialEq)] +pub struct PgPath { + pub closed: bool, + pub points: Vec, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct Header { + is_closed: bool, + length: usize, +} + +impl Type for PgPath { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("path") + } +} + +impl PgHasArrayType for PgPath { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_path") + } +} + +impl<'r> Decode<'r, Postgres> for PgPath { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgPath::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgPath::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgPath { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("path")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgPath { + type Err = Error; + + fn from_str(s: &str) -> Result { + let closed = !s.contains('['); + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); + let parts = sanitised.split(',').collect::>(); + + let mut points = vec![]; + + if parts.len() % 2 != 0 { + return Err(Error::Decode( + format!("Unmatched pair in PATH: {}", s).into(), + )); + } + + for chunk in parts.chunks_exact(2) { + if let [x_str, y_str] = chunk { + let x = parse_float_from_str(x_str, "could not get x")?; + let y = parse_float_from_str(y_str, "could not get y")?; + + let point = PgPoint { x, y }; + points.push(point); + } + } + + if !points.is_empty() { + return Ok(PgPath { points, closed }); + } + + Err(Error::Decode( + format!("could not get path from {}", s).into(), + )) + } +} + +impl PgPath { + fn header(&self) -> Header { + Header { + is_closed: self.closed, + length: self.points.len(), + } + } + + fn from_bytes(mut bytes: &[u8]) -> Result { + let header = Header::try_read(&mut bytes)?; + + if bytes.len() != header.data_size() { + return Err(format!( + "expected {} bytes after header, got {}", + header.data_size(), + bytes.len() + ) + .into()); + } + + if bytes.len() % BYTE_WIDTH * 2 != 0 { + return Err(format!( + "data length not divisible by pairs of {BYTE_WIDTH}: {}", + bytes.len() + ) + .into()); + } + + let mut out_points = Vec::with_capacity(bytes.len() / (BYTE_WIDTH * 2)); + + while bytes.has_remaining() { + let point = PgPoint { + x: bytes.get_f64(), + y: bytes.get_f64(), + }; + out_points.push(point) + } + Ok(PgPath { + closed: header.is_closed, + points: out_points, + }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> { + let header = self.header(); + buff.reserve(header.data_size()); + header.try_write(buff)?; + + for point in &self.points { + buff.extend_from_slice(&point.x.to_be_bytes()); + buff.extend_from_slice(&point.y.to_be_bytes()); + } + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +impl Header { + const HEADER_WIDTH: usize = mem::size_of::() + mem::size_of::(); + + fn data_size(&self) -> usize { + self.length * BYTE_WIDTH * 2 + } + + fn try_read(buf: &mut &[u8]) -> Result { + if buf.len() < Self::HEADER_WIDTH { + return Err(format!( + "expected PATH data to contain at least {} bytes, got {}", + Self::HEADER_WIDTH, + buf.len() + )); + } + + let is_closed = buf.get_i8(); + let length = buf.get_i32(); + + let length = usize::try_from(length).ok().ok_or_else(|| { + format!( + "received PATH data length: {length}. Expected length between 0 and {}", + usize::MAX + ) + })?; + + Ok(Self { + is_closed: is_closed != 0, + length, + }) + } + + fn try_write(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { + let is_closed = self.is_closed as i8; + + let length = i32::try_from(self.length).map_err(|_| { + format!( + "PATH length exceeds allowed maximum ({} > {})", + self.length, + i32::MAX + ) + })?; + + buff.extend(is_closed.to_be_bytes()); + buff.extend(length.to_be_bytes()); + + Ok(()) + } +} + +fn parse_float_from_str(s: &str, error_msg: &str) -> Result { + s.parse().map_err(|_| Error::Decode(error_msg.into())) +} + +#[cfg(test)] +mod path_tests { + + use std::str::FromStr; + + use crate::types::PgPoint; + + use super::PgPath; + + const PATH_CLOSED_BYTES: &[u8] = &[ + 1, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, + 64, 16, 0, 0, 0, 0, 0, 0, + ]; + + const PATH_OPEN_BYTES: &[u8] = &[ + 0, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, + 64, 16, 0, 0, 0, 0, 0, 0, + ]; + + const PATH_UNEVEN_POINTS: &[u8] = &[ + 0, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, + 64, 16, 0, 0, + ]; + + #[test] + fn can_deserialise_path_type_bytes_closed() { + let path = PgPath::from_bytes(PATH_CLOSED_BYTES).unwrap(); + assert_eq!( + path, + PgPath { + closed: true, + points: vec![PgPoint { x: 1.0, y: 2.0 }, PgPoint { x: 3.0, y: 4.0 }] + } + ) + } + + #[test] + fn cannot_deserialise_path_type_uneven_point_bytes() { + let path = PgPath::from_bytes(PATH_UNEVEN_POINTS); + assert!(path.is_err()); + + if let Err(err) = path { + assert_eq!( + err.to_string(), + format!("expected 32 bytes after header, got 28") + ) + } + } + + #[test] + fn can_deserialise_path_type_bytes_open() { + let path = PgPath::from_bytes(PATH_OPEN_BYTES).unwrap(); + assert_eq!( + path, + PgPath { + closed: false, + points: vec![PgPoint { x: 1.0, y: 2.0 }, PgPoint { x: 3.0, y: 4.0 }] + } + ) + } + + #[test] + fn can_deserialise_path_type_str_first_syntax() { + let path = PgPath::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + path, + PgPath { + closed: false, + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn cannot_deserialise_path_type_str_uneven_points_first_syntax() { + let input_str = "[( 1, 2), (3)]"; + let path = PgPath::from_str(input_str); + + assert!(path.is_err()); + + if let Err(err) = path { + assert_eq!( + err.to_string(), + format!("error occurred while decoding: Unmatched pair in PATH: {input_str}") + ) + } + } + + #[test] + fn can_deserialise_path_type_str_second_syntax() { + let path = PgPath::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + path, + PgPath { + closed: true, + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_path_type_str_third_syntax() { + let path = PgPath::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + path, + PgPath { + closed: true, + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_path_type_str_fourth_syntax() { + let path = PgPath::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + path, + PgPath { + closed: true, + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_path_type_str_float() { + let path = PgPath::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + path, + PgPath { + closed: true, + points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }] + } + ); + } + + #[test] + fn can_serialise_path_type() { + let path = PgPath { + closed: true, + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }], + }; + assert_eq!(path.serialize_to_vec(), PATH_CLOSED_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs index cc10672950..5078ce1ee4 100644 --- a/sqlx-postgres/src/types/geometry/point.rs +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -19,7 +19,10 @@ use std::str::FromStr; /// ```` /// where x and y are the respective coordinates, as floating-point numbers. /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-POINTS +/// See [Postgres Manual, Section 8.8.1, Geometric Types - Points][PG.S.8.8.1] for details. +/// +/// [PG.S.8.8.1]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-GEOMETRIC-POINTS +/// #[derive(Debug, Clone, PartialEq)] pub struct PgPoint { pub x: f64, @@ -74,7 +77,7 @@ impl FromStr for PgPoint { .ok_or_else(|| format!("error decoding POINT: could not get x and y from {}", s))?; let x = parse_float_from_str(x_str, "error decoding POINT: could not get x")?; - let y = parse_float_from_str(y_str, "error decoding POINT: could not get x")?; + let y = parse_float_from_str(y_str, "error decoding POINT: could not get y")?; Ok(PgPoint { x, y }) } diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs new file mode 100644 index 0000000000..a5a203c680 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -0,0 +1,366 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::{PgPoint, Type}; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use sqlx_core::Error; +use std::mem; +use std::str::FromStr; + +const BYTE_WIDTH: usize = mem::size_of::(); + +/// ## Postgres Geometric Polygon type +/// +/// Description: Polygon (similar to closed polygon) +/// Representation: `((x1,y1),...)` +/// +/// Polygons are represented by lists of points (the vertexes of the polygon). Polygons are very similar to closed paths; the essential semantic difference is that a polygon is considered to include the area within it, while a path is not. +/// An important implementation difference between polygons and paths is that the stored representation of a polygon includes its smallest bounding box. This speeds up certain search operations, although computing the bounding box adds overhead while constructing new polygons. +/// Values of type polygon are specified using any of the following syntaxes: +/// +/// ```text +/// ( ( x1 , y1 ) , ... , ( xn , yn ) ) +/// ( x1 , y1 ) , ... , ( xn , yn ) +/// ( x1 , y1 , ... , xn , yn ) +/// x1 , y1 , ... , xn , yn +/// ``` +/// +/// where the points are the end points of the line segments comprising the boundary of the polygon. +/// +/// See [Postgres Manual, Section 8.8.6, Geometric Types - Polygons][PG.S.8.8.6] for details. +/// +/// [PG.S.8.8.6]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-POLYGON +/// +#[derive(Debug, Clone, PartialEq)] +pub struct PgPolygon { + pub points: Vec, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct Header { + length: usize, +} + +impl Type for PgPolygon { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("polygon") + } +} + +impl PgHasArrayType for PgPolygon { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_polygon") + } +} + +impl<'r> Decode<'r, Postgres> for PgPolygon { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgPolygon::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgPolygon::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgPolygon { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("polygon")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgPolygon { + type Err = Error; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); + let parts = sanitised.split(',').collect::>(); + + let mut points = vec![]; + + if parts.len() % 2 != 0 { + return Err(Error::Decode( + format!("Unmatched pair in POLYGON: {}", s).into(), + )); + } + + for chunk in parts.chunks_exact(2) { + if let [x_str, y_str] = chunk { + let x = parse_float_from_str(x_str, "could not get x")?; + let y = parse_float_from_str(y_str, "could not get y")?; + + let point = PgPoint { x, y }; + points.push(point); + } + } + + if !points.is_empty() { + return Ok(PgPolygon { points }); + } + + Err(Error::Decode( + format!("could not get polygon from {}", s).into(), + )) + } +} + +impl PgPolygon { + fn header(&self) -> Header { + Header { + length: self.points.len(), + } + } + + fn from_bytes(mut bytes: &[u8]) -> Result { + let header = Header::try_read(&mut bytes)?; + + if bytes.len() != header.data_size() { + return Err(format!( + "expected {} bytes after header, got {}", + header.data_size(), + bytes.len() + ) + .into()); + } + + if bytes.len() % BYTE_WIDTH * 2 != 0 { + return Err(format!( + "data length not divisible by pairs of {BYTE_WIDTH}: {}", + bytes.len() + ) + .into()); + } + + let mut out_points = Vec::with_capacity(bytes.len() / (BYTE_WIDTH * 2)); + while bytes.has_remaining() { + let point = PgPoint { + x: bytes.get_f64(), + y: bytes.get_f64(), + }; + out_points.push(point) + } + Ok(PgPolygon { points: out_points }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> { + let header = self.header(); + buff.reserve(header.data_size()); + header.try_write(buff)?; + + for point in &self.points { + buff.extend_from_slice(&point.x.to_be_bytes()); + buff.extend_from_slice(&point.y.to_be_bytes()); + } + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +impl Header { + const HEADER_WIDTH: usize = mem::size_of::() + mem::size_of::(); + + fn data_size(&self) -> usize { + self.length * BYTE_WIDTH * 2 + } + + fn try_read(buf: &mut &[u8]) -> Result { + if buf.len() < Self::HEADER_WIDTH { + return Err(format!( + "expected polygon data to contain at least {} bytes, got {}", + Self::HEADER_WIDTH, + buf.len() + )); + } + + let length = buf.get_i32(); + + let length = usize::try_from(length).ok().ok_or_else(|| { + format!( + "received polygon with length: {length}. Expected length between 0 and {}", + usize::MAX + ) + })?; + + Ok(Self { length }) + } + + fn try_write(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { + let length = i32::try_from(self.length).map_err(|_| { + format!( + "polygon length exceeds allowed maximum ({} > {})", + self.length, + i32::MAX + ) + })?; + + buff.extend(length.to_be_bytes()); + + Ok(()) + } +} + +fn parse_float_from_str(s: &str, error_msg: &str) -> Result { + s.parse().map_err(|_| Error::Decode(error_msg.into())) +} + +#[cfg(test)] +mod polygon_tests { + + use std::str::FromStr; + + use crate::types::PgPoint; + + use super::PgPolygon; + + const POLYGON_BYTES: &[u8] = &[ + 0, 0, 0, 12, 192, 0, 0, 0, 0, 0, 0, 0, 192, 8, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, + 0, 192, 8, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, 0, 63, + 240, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, + 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 192, + 8, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, 0, 0, 0, 192, 8, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, + 240, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, + 0, 0, 0, + ]; + + #[test] + fn can_deserialise_polygon_type_bytes() { + let polygon = PgPolygon::from_bytes(POLYGON_BYTES).unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![ + PgPoint { x: -2., y: -3. }, + PgPoint { x: -1., y: -3. }, + PgPoint { x: -1., y: -1. }, + PgPoint { x: 1., y: 1. }, + PgPoint { x: 1., y: 3. }, + PgPoint { x: 2., y: 3. }, + PgPoint { x: 2., y: -3. }, + PgPoint { x: 1., y: -3. }, + PgPoint { x: 1., y: 0. }, + PgPoint { x: -1., y: 0. }, + PgPoint { x: -1., y: -2. }, + PgPoint { x: -2., y: -2. } + ] + } + ) + } + + #[test] + fn can_deserialise_polygon_type_str_first_syntax() { + let polygon = PgPolygon::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_polygon_type_str_second_syntax() { + let polygon = PgPolygon::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn cannot_deserialise_polygon_type_str_uneven_points_first_syntax() { + let input_str = "[( 1, 2), (3)]"; + let polygon = PgPolygon::from_str(input_str); + + assert!(polygon.is_err()); + + if let Err(err) = polygon { + assert_eq!( + err.to_string(), + format!("error occurred while decoding: Unmatched pair in POLYGON: {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_polygon_type_str_invalid_numbers() { + let input_str = "[( 1, 2), (2, three)]"; + let polygon = PgPolygon::from_str(input_str); + + assert!(polygon.is_err()); + + if let Err(err) = polygon { + assert_eq!( + err.to_string(), + format!("error occurred while decoding: could not get y") + ) + } + } + + #[test] + fn can_deserialise_polygon_type_str_third_syntax() { + let polygon = PgPolygon::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_polygon_type_str_fourth_syntax() { + let polygon = PgPolygon::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_polygon_type_str_float() { + let polygon = PgPolygon::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }] + } + ); + } + + #[test] + fn can_serialise_polygon_type() { + let polygon = PgPolygon { + points: vec![ + PgPoint { x: -2., y: -3. }, + PgPoint { x: -1., y: -3. }, + PgPoint { x: -1., y: -1. }, + PgPoint { x: 1., y: 1. }, + PgPoint { x: 1., y: 3. }, + PgPoint { x: 2., y: 3. }, + PgPoint { x: 2., y: -3. }, + PgPoint { x: 1., y: -3. }, + PgPoint { x: 1., y: 0. }, + PgPoint { x: -1., y: 0. }, + PgPoint { x: -1., y: -2. }, + PgPoint { x: -2., y: -2. }, + ], + }; + assert_eq!(polygon.serialize_to_vec(), POLYGON_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/ipnet/ipaddr.rs b/sqlx-postgres/src/types/ipnet/ipaddr.rs new file mode 100644 index 0000000000..b157eff3c6 --- /dev/null +++ b/sqlx-postgres/src/types/ipnet/ipaddr.rs @@ -0,0 +1,62 @@ +use std::net::IpAddr; + +use ipnet::IpNet; + +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef, Postgres}; + +impl Type for IpAddr +where + IpNet: Type, +{ + fn type_info() -> PgTypeInfo { + IpNet::type_info() + } + + fn compatible(ty: &PgTypeInfo) -> bool { + IpNet::compatible(ty) + } +} + +impl PgHasArrayType for IpAddr { + fn array_type_info() -> PgTypeInfo { + ::array_type_info() + } + + fn array_compatible(ty: &PgTypeInfo) -> bool { + ::array_compatible(ty) + } +} + +impl<'db> Encode<'db, Postgres> for IpAddr +where + IpNet: Encode<'db, Postgres>, +{ + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + IpNet::from(*self).encode_by_ref(buf) + } + + fn size_hint(&self) -> usize { + IpNet::from(*self).size_hint() + } +} + +impl<'db> Decode<'db, Postgres> for IpAddr +where + IpNet: Decode<'db, Postgres>, +{ + fn decode(value: PgValueRef<'db>) -> Result { + let ipnet = IpNet::decode(value)?; + + if matches!(ipnet, IpNet::V4(net) if net.prefix_len() != 32) + || matches!(ipnet, IpNet::V6(net) if net.prefix_len() != 128) + { + Err("lossy decode from inet/cidr")? + } + + Ok(ipnet.addr()) + } +} diff --git a/sqlx-postgres/src/types/ipnet/ipnet.rs b/sqlx-postgres/src/types/ipnet/ipnet.rs new file mode 100644 index 0000000000..1f986174b8 --- /dev/null +++ b/sqlx-postgres/src/types/ipnet/ipnet.rs @@ -0,0 +1,130 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[cfg(feature = "ipnet")] +use ipnet::{IpNet, Ipv4Net, Ipv6Net}; + +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; + +// https://github.com/postgres/postgres/blob/574925bfd0a8175f6e161936ea11d9695677ba09/src/include/utils/inet.h#L39 + +// Technically this is a magic number here but it doesn't make sense to drag in the whole of `libc` +// just for one constant. +const PGSQL_AF_INET: u8 = 2; // AF_INET +const PGSQL_AF_INET6: u8 = PGSQL_AF_INET + 1; + +impl Type for IpNet { + fn type_info() -> PgTypeInfo { + PgTypeInfo::INET + } + + fn compatible(ty: &PgTypeInfo) -> bool { + *ty == PgTypeInfo::CIDR || *ty == PgTypeInfo::INET + } +} + +impl PgHasArrayType for IpNet { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::INET_ARRAY + } + + fn array_compatible(ty: &PgTypeInfo) -> bool { + *ty == PgTypeInfo::CIDR_ARRAY || *ty == PgTypeInfo::INET_ARRAY + } +} + +impl Encode<'_, Postgres> for IpNet { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + // https://github.com/postgres/postgres/blob/574925bfd0a8175f6e161936ea11d9695677ba09/src/backend/utils/adt/network.c#L293 + // https://github.com/postgres/postgres/blob/574925bfd0a8175f6e161936ea11d9695677ba09/src/backend/utils/adt/network.c#L271 + + match self { + IpNet::V4(net) => { + buf.push(PGSQL_AF_INET); // ip_family + buf.push(net.prefix_len()); // ip_bits + buf.push(0); // is_cidr + buf.push(4); // nb (number of bytes) + buf.extend_from_slice(&net.addr().octets()) // address + } + + IpNet::V6(net) => { + buf.push(PGSQL_AF_INET6); // ip_family + buf.push(net.prefix_len()); // ip_bits + buf.push(0); // is_cidr + buf.push(16); // nb (number of bytes) + buf.extend_from_slice(&net.addr().octets()); // address + } + } + + Ok(IsNull::No) + } + + fn size_hint(&self) -> usize { + match self { + IpNet::V4(_) => 8, + IpNet::V6(_) => 20, + } + } +} + +impl Decode<'_, Postgres> for IpNet { + fn decode(value: PgValueRef<'_>) -> Result { + let bytes = match value.format() { + PgValueFormat::Binary => value.as_bytes()?, + PgValueFormat::Text => { + let s = value.as_str()?; + println!("{s}"); + if s.contains('/') { + return Ok(s.parse()?); + } + // IpNet::from_str doesn't handle conversion from IpAddr to IpNet + let addr: IpAddr = s.parse()?; + return Ok(addr.into()); + } + }; + + if bytes.len() >= 8 { + let family = bytes[0]; + let prefix = bytes[1]; + let _is_cidr = bytes[2] != 0; + let len = bytes[3]; + + match family { + PGSQL_AF_INET => { + if bytes.len() == 8 && len == 4 { + let inet = Ipv4Net::new( + Ipv4Addr::new(bytes[4], bytes[5], bytes[6], bytes[7]), + prefix, + )?; + + return Ok(IpNet::V4(inet)); + } + } + + PGSQL_AF_INET6 => { + if bytes.len() == 20 && len == 16 { + let inet = Ipv6Net::new( + Ipv6Addr::from([ + bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], + bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], + bytes[16], bytes[17], bytes[18], bytes[19], + ]), + prefix, + )?; + + return Ok(IpNet::V6(inet)); + } + } + + _ => { + return Err(format!("unknown ip family {family}").into()); + } + } + } + + Err("invalid data received when expecting an INET".into()) + } +} diff --git a/sqlx-postgres/src/types/ipnet/mod.rs b/sqlx-postgres/src/types/ipnet/mod.rs new file mode 100644 index 0000000000..cd40cf30da --- /dev/null +++ b/sqlx-postgres/src/types/ipnet/mod.rs @@ -0,0 +1,7 @@ +// Prefer `ipnetwork` over `ipnet` because it was implemented first (want to avoid breaking change). +#[cfg(not(feature = "ipnetwork"))] +mod ipaddr; + +// Parent module is named after the `ipnet` crate, this is named after the `IpNet` type. +#[allow(clippy::module_inception)] +mod ipnet; diff --git a/sqlx-postgres/src/types/ipaddr.rs b/sqlx-postgres/src/types/ipnetwork/ipaddr.rs similarity index 100% rename from sqlx-postgres/src/types/ipaddr.rs rename to sqlx-postgres/src/types/ipnetwork/ipaddr.rs diff --git a/sqlx-postgres/src/types/ipnetwork.rs b/sqlx-postgres/src/types/ipnetwork/ipnetwork.rs similarity index 100% rename from sqlx-postgres/src/types/ipnetwork.rs rename to sqlx-postgres/src/types/ipnetwork/ipnetwork.rs diff --git a/sqlx-postgres/src/types/ipnetwork/mod.rs b/sqlx-postgres/src/types/ipnetwork/mod.rs new file mode 100644 index 0000000000..de40244c65 --- /dev/null +++ b/sqlx-postgres/src/types/ipnetwork/mod.rs @@ -0,0 +1,5 @@ +mod ipaddr; + +// Parent module is named after the `ipnetwork` crate, this is named after the `IpNetwork` type. +#[allow(clippy::module_inception)] +mod ipnetwork; diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 26feb05580..0faefbb482 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -21,8 +21,13 @@ //! | [`PgLQuery`] | LQUERY | //! | [`PgCiText`] | CITEXT1 | //! | [`PgCube`] | CUBE | -//! | [`PgPoint] | POINT | -//! | [`PgLine] | LINE | +//! | [`PgPoint`] | POINT | +//! | [`PgLine`] | LINE | +//! | [`PgLSeg`] | LSEG | +//! | [`PgBox`] | BOX | +//! | [`PgPath`] | PATH | +//! | [`PgPolygon`] | POLYGON | +//! | [`PgCircle`] | CIRCLE | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., @@ -82,7 +87,7 @@ //! //! ### [`ipnetwork`](https://crates.io/crates/ipnetwork) //! -//! Requires the `ipnetwork` Cargo feature flag. +//! Requires the `ipnetwork` Cargo feature flag (takes precedence over `ipnet` if both are used). //! //! | Rust type | Postgres type(s) | //! |---------------------------------------|------------------------------------------------------| @@ -95,6 +100,17 @@ //! //! `IpNetwork` does not have this limitation. //! +//! ### [`ipnet`](https://crates.io/crates/ipnet) +//! +//! Requires the `ipnet` Cargo feature flag. +//! +//! | Rust type | Postgres type(s) | +//! |---------------------------------------|------------------------------------------------------| +//! | `ipnet::IpNet` | INET, CIDR | +//! | `std::net::IpAddr` | INET, CIDR | +//! +//! The same `IpAddr` limitation for smaller network prefixes applies as with `ipnet`. +//! //! ### [`mac_address`](https://crates.io/crates/mac_address) //! //! Requires the `mac_address` Cargo feature flag. @@ -179,6 +195,18 @@ //! enum Mood { Sad = 0, Ok = 1, Happy = 2 } //! ``` //! +//! Rust enumerations may also be defined to be represented as a string using `type_name = "text"`. +//! The following type expects a SQL type of `TEXT` and will convert to/from the Rust enumeration. +//! +//! ```rust,ignore +//! #[derive(sqlx::Type)] +//! #[sqlx(type_name = "text")] +//! enum Mood { Sad, Ok, Happy } +//! ``` +//! +//! Note that an error can occur if you attempt to decode a value not contained within the enum +//! definition. +//! use crate::type_info::PgTypeKind; use crate::{PgTypeInfo, Postgres}; @@ -231,11 +259,11 @@ mod time; #[cfg(feature = "uuid")] mod uuid; -#[cfg(feature = "ipnetwork")] -mod ipnetwork; +#[cfg(feature = "ipnet")] +mod ipnet; #[cfg(feature = "ipnetwork")] -mod ipaddr; +mod ipnetwork; #[cfg(feature = "mac_address")] mod mac_address; @@ -246,8 +274,13 @@ mod bit_vec; pub use array::PgHasArrayType; pub use citext::PgCiText; pub use cube::PgCube; +pub use geometry::circle::PgCircle; pub use geometry::line::PgLine; +pub use geometry::line_segment::PgLSeg; +pub use geometry::path::PgPath; pub use geometry::point::PgPoint; +pub use geometry::polygon::PgPolygon; +pub use geometry::r#box::PgBox; pub use hstore::PgHstore; pub use interval::PgInterval; pub use lquery::PgLQuery; diff --git a/sqlx-postgres/src/types/record.rs b/sqlx-postgres/src/types/record.rs index c4eb639368..6e37182c40 100644 --- a/sqlx-postgres/src/types/record.rs +++ b/sqlx-postgres/src/types/record.rs @@ -41,13 +41,13 @@ impl<'a> PgRecordEncoder<'a> { { let ty = value.produces().unwrap_or_else(T::type_info); - if let PgType::DeclareWithName(name) = ty.0 { + match ty.0 { // push a hole for this type ID // to be filled in on query execution - self.buf.patch_type_by_name(&name); - } else { + PgType::DeclareWithName(name) => self.buf.patch_type_by_name(&name), + PgType::DeclareArrayOf(array) => self.buf.patch_array_type(array), // write type id - self.buf.extend(&ty.0.oid().0.to_be_bytes()); + pg_type => self.buf.extend(&pg_type.oid().0.to_be_bytes()), } self.buf.encode(value)?; diff --git a/sqlx-sqlite/Cargo.toml b/sqlx-sqlite/Cargo.toml index 391bf4523c..ca4c84c958 100644 --- a/sqlx-sqlite/Cargo.toml +++ b/sqlx-sqlite/Cargo.toml @@ -23,6 +23,8 @@ uuid = ["dep:uuid", "sqlx-core/uuid"] regexp = ["dep:regex"] +preupdate-hook = ["libsqlite3-sys/preupdate_hook"] + bundled = ["libsqlite3-sys/bundled"] unbundled = ["libsqlite3-sys/buildtime_bindgen"] @@ -48,6 +50,7 @@ atoi = "2.0" log = "0.4.18" tracing = { version = "0.1.37", features = ["log"] } +thiserror = "2.0.0" serde = { version = "1.0.145", features = ["derive"], optional = true } regex = { version = "1.5.5", optional = true } @@ -69,3 +72,6 @@ sqlx = { workspace = true, default-features = false, features = ["macros", "runt [lints] workspace = true + +[package.metadata.docs.rs] +features = ["bundled", "any", "json", "chrono", "time", "uuid"] \ No newline at end of file diff --git a/sqlx-sqlite/src/any.rs b/sqlx-sqlite/src/any.rs index 01600d9931..c72370d0ff 100644 --- a/sqlx-sqlite/src/any.rs +++ b/sqlx-sqlite/src/any.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use crate::{ Either, Sqlite, SqliteArgumentValue, SqliteArguments, SqliteColumn, SqliteConnectOptions, SqliteConnection, SqliteQueryResult, SqliteRow, SqliteTransactionManager, SqliteTypeInfo, @@ -17,6 +19,7 @@ use sqlx_core::database::Database; use sqlx_core::describe::Describe; use sqlx_core::executor::Executor; use sqlx_core::transaction::TransactionManager; +use std::pin::pin; sqlx_core::declare_driver_with_optional_migrate!(DRIVER = Sqlite); @@ -37,8 +40,11 @@ impl AnyConnectionBackend for SqliteConnection { Connection::ping(self) } - fn begin(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> { - SqliteTransactionManager::begin(self) + fn begin( + &mut self, + statement: Option>, + ) -> BoxFuture<'_, sqlx_core::Result<()>> { + SqliteTransactionManager::begin(self, statement) } fn commit(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> { @@ -53,6 +59,10 @@ impl AnyConnectionBackend for SqliteConnection { SqliteTransactionManager::start_rollback(self) } + fn get_transaction_depth(&self) -> usize { + SqliteTransactionManager::get_transaction_depth(self) + } + fn shrink_buffers(&mut self) { // NO-OP. } @@ -105,12 +115,12 @@ impl AnyConnectionBackend for SqliteConnection { let args = arguments.map(map_arguments); Box::pin(async move { - let stream = self - .worker - .execute(query, args, self.row_channel_size, persistent, Some(1)) - .map_ok(flume::Receiver::into_stream) - .await?; - futures_util::pin_mut!(stream); + let mut stream = pin!( + self.worker + .execute(query, args, self.row_channel_size, persistent, Some(1)) + .map_ok(flume::Receiver::into_stream) + .await? + ); if let Some(Either::Right(row)) = stream.try_next().await? { return Ok(Some(AnyRow::try_from(&row)?)); diff --git a/sqlx-sqlite/src/connection/collation.rs b/sqlx-sqlite/src/connection/collation.rs index 573a9af892..e7422138bc 100644 --- a/sqlx-sqlite/src/connection/collation.rs +++ b/sqlx-sqlite/src/connection/collation.rs @@ -10,7 +10,6 @@ use libsqlite3_sys::{sqlite3_create_collation_v2, SQLITE_OK, SQLITE_UTF8}; use crate::connection::handle::ConnectionHandle; use crate::error::Error; -use crate::SqliteError; #[derive(Clone)] pub struct Collation { @@ -67,7 +66,7 @@ impl Collation { } else { // The xDestroy callback is not called if the sqlite3_create_collation_v2() function fails. drop(unsafe { Arc::from_raw(raw_f) }); - Err(Error::Database(Box::new(SqliteError::new(handle.as_ptr())))) + Err(handle.expect_error().into()) } } } @@ -112,7 +111,7 @@ where } else { // The xDestroy callback is not called if the sqlite3_create_collation_v2() function fails. drop(unsafe { Box::from_raw(boxed_f) }); - Err(Error::Database(Box::new(SqliteError::new(handle.as_ptr())))) + Err(handle.expect_error().into()) } } diff --git a/sqlx-sqlite/src/connection/establish.rs b/sqlx-sqlite/src/connection/establish.rs index 40f9b4c302..545bad747c 100644 --- a/sqlx-sqlite/src/connection/establish.rs +++ b/sqlx-sqlite/src/connection/establish.rs @@ -8,6 +8,7 @@ use libsqlite3_sys::{ sqlite3_load_extension, sqlite3_open_v2, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, SQLITE_OK, SQLITE_OPEN_CREATE, SQLITE_OPEN_FULLMUTEX, SQLITE_OPEN_MEMORY, SQLITE_OPEN_NOMUTEX, SQLITE_OPEN_PRIVATECACHE, SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_SHAREDCACHE, + SQLITE_OPEN_URI, }; use percent_encoding::NON_ALPHANUMERIC; use sqlx_core::IndexMap; @@ -67,11 +68,14 @@ impl EstablishParams { })? .to_owned(); + // Set common flags we expect to have in sqlite + let mut flags = SQLITE_OPEN_URI; + // By default, we connect to an in-memory database. // [SQLITE_OPEN_NOMUTEX] will instruct [sqlite3_open_v2] to return an error if it // cannot satisfy our wish for a thread-safe, lock-free connection object - let mut flags = if options.serialized { + flags |= if options.serialized { SQLITE_OPEN_FULLMUTEX } else { SQLITE_OPEN_NOMUTEX @@ -111,7 +115,6 @@ impl EstablishParams { percent_encoding::percent_encode(filename.as_bytes(), NON_ALPHANUMERIC), serde_urlencoded::to_string(&query_params).unwrap() ); - flags |= libsqlite3_sys::SQLITE_OPEN_URI; } let filename = CString::new(filename).map_err(|_| { @@ -204,10 +207,10 @@ impl EstablishParams { // SAFE: tested for NULL just above // This allows any returns below to close this handle with RAII - let handle = unsafe { ConnectionHandle::new(handle) }; + let mut handle = unsafe { ConnectionHandle::new(handle) }; if status != SQLITE_OK { - return Err(Error::Database(Box::new(SqliteError::new(handle.as_ptr())))); + return Err(Error::Database(Box::new(handle.expect_error()))); } // Enable extended result codes @@ -226,33 +229,29 @@ impl EstablishParams { for ext in self.extensions.iter() { // `sqlite3_load_extension` is unusual as it returns its errors via an out-pointer // rather than by calling `sqlite3_errmsg` - let mut error = null_mut(); + let mut error_msg = null_mut(); status = unsafe { sqlite3_load_extension( handle.as_ptr(), ext.0.as_ptr(), ext.1.as_ref().map_or(null(), |e| e.as_ptr()), - addr_of_mut!(error), + addr_of_mut!(error_msg), ) }; if status != SQLITE_OK { + let mut e = handle.expect_error(); + // SAFETY: We become responsible for any memory allocation at `&error`, so test // for null and take an RAII version for returns - let err_msg = if !error.is_null() { - unsafe { - let e = CStr::from_ptr(error).into(); - sqlite3_free(error as *mut c_void); - e - } - } else { - CString::new("Unknown error when loading extension") - .expect("text should be representable as a CString") - }; - return Err(Error::Database(Box::new(SqliteError::extension( - handle.as_ptr(), - &err_msg, - )))); + if !error_msg.is_null() { + e = e.with_message(unsafe { + let msg = CStr::from_ptr(error_msg).to_string_lossy().into(); + sqlite3_free(error_msg as *mut c_void); + msg + }); + } + return Err(Error::Database(Box::new(e))); } } // Preempt any hypothetical security issues arising from leaving ENABLE_LOAD_EXTENSION // on by disabling the flag again once we've loaded all the requested modules. @@ -271,7 +270,7 @@ impl EstablishParams { // configure a `regexp` function for sqlite, it does not come with one by default let status = crate::regexp::register(handle.as_ptr()); if status != SQLITE_OK { - return Err(Error::Database(Box::new(SqliteError::new(handle.as_ptr())))); + return Err(Error::Database(Box::new(handle.expect_error()))); } } @@ -286,16 +285,17 @@ impl EstablishParams { status = unsafe { sqlite3_busy_timeout(handle.as_ptr(), ms) }; if status != SQLITE_OK { - return Err(Error::Database(Box::new(SqliteError::new(handle.as_ptr())))); + return Err(Error::Database(Box::new(handle.expect_error()))); } Ok(ConnectionState { handle, statements: Statements::new(self.statement_cache_capacity), - transaction_depth: 0, log_settings: self.log_settings.clone(), progress_handler_callback: None, update_hook_callback: None, + #[cfg(feature = "preupdate-hook")] + preupdate_hook_callback: None, commit_hook_callback: None, rollback_hook_callback: None, }) diff --git a/sqlx-sqlite/src/connection/executor.rs b/sqlx-sqlite/src/connection/executor.rs index 541a4f7d4d..1f6ce7726f 100644 --- a/sqlx-sqlite/src/connection/executor.rs +++ b/sqlx-sqlite/src/connection/executor.rs @@ -8,7 +8,7 @@ use sqlx_core::describe::Describe; use sqlx_core::error::Error; use sqlx_core::executor::{Execute, Executor}; use sqlx_core::Either; -use std::future; +use std::{future, pin::pin}; impl<'c> Executor<'c> for &'c mut SqliteConnection { type Database = Sqlite; @@ -56,13 +56,11 @@ impl<'c> Executor<'c> for &'c mut SqliteConnection { let persistent = query.persistent() && arguments.is_some(); Box::pin(async move { - let stream = self + let mut stream = pin!(self .worker .execute(sql, arguments, self.row_channel_size, persistent, Some(1)) .map_ok(flume::Receiver::into_stream) - .try_flatten_stream(); - - futures_util::pin_mut!(stream); + .try_flatten_stream()); while let Some(res) = stream.try_next().await? { if let Either::Right(row) = res { diff --git a/sqlx-sqlite/src/connection/handle.rs b/sqlx-sqlite/src/connection/handle.rs index aaf5b74eaa..60fbe17dc6 100644 --- a/sqlx-sqlite/src/connection/handle.rs +++ b/sqlx-sqlite/src/connection/handle.rs @@ -46,6 +46,17 @@ impl ConnectionHandle { unsafe { sqlite3_last_insert_rowid(self.as_ptr()) } } + pub(crate) fn last_error(&mut self) -> Option { + // SAFETY: we have exclusive access to the database handle + unsafe { SqliteError::try_new(self.as_ptr()) } + } + + #[track_caller] + pub(crate) fn expect_error(&mut self) -> SqliteError { + self.last_error() + .expect("expected error code to be set in current context") + } + pub(crate) fn exec(&mut self, query: impl Into) -> Result<(), Error> { let query = query.into(); let query = CString::new(query).map_err(|_| err_protocol!("query contains nul bytes"))?; diff --git a/sqlx-sqlite/src/connection/mod.rs b/sqlx-sqlite/src/connection/mod.rs index a579b8a605..b94ad91c4d 100644 --- a/sqlx-sqlite/src/connection/mod.rs +++ b/sqlx-sqlite/src/connection/mod.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::cmp::Ordering; use std::ffi::CStr; use std::fmt::Write; @@ -11,9 +12,11 @@ use futures_core::future::BoxFuture; use futures_intrusive::sync::MutexGuard; use futures_util::future; use libsqlite3_sys::{ - sqlite3, sqlite3_commit_hook, sqlite3_progress_handler, sqlite3_rollback_hook, - sqlite3_update_hook, SQLITE_DELETE, SQLITE_INSERT, SQLITE_UPDATE, + sqlite3, sqlite3_commit_hook, sqlite3_get_autocommit, sqlite3_progress_handler, + sqlite3_rollback_hook, sqlite3_update_hook, SQLITE_DELETE, SQLITE_INSERT, SQLITE_UPDATE, }; +#[cfg(feature = "preupdate-hook")] +pub use preupdate_hook::*; pub(crate) use handle::ConnectionHandle; use sqlx_core::common::StatementCache; @@ -26,7 +29,7 @@ use crate::connection::establish::EstablishParams; use crate::connection::worker::ConnectionWorker; use crate::options::OptimizeOnClose; use crate::statement::VirtualStatement; -use crate::{Sqlite, SqliteConnectOptions}; +use crate::{Sqlite, SqliteConnectOptions, SqliteError}; pub(crate) mod collation; pub(crate) mod describe; @@ -36,6 +39,9 @@ mod executor; mod explain; mod handle; pub(crate) mod intmap; +#[cfg(feature = "preupdate-hook")] +mod preupdate_hook; +pub(crate) mod serialize; mod worker; @@ -88,6 +94,7 @@ pub struct UpdateHookResult<'a> { pub table: &'a str, pub rowid: i64, } + pub(crate) struct UpdateHookHandler(NonNull); unsafe impl Send for UpdateHookHandler {} @@ -100,9 +107,6 @@ unsafe impl Send for RollbackHookHandler {} pub(crate) struct ConnectionState { pub(crate) handle: ConnectionHandle, - // transaction status - pub(crate) transaction_depth: usize, - pub(crate) statements: Statements, log_settings: LogSettings, @@ -112,6 +116,8 @@ pub(crate) struct ConnectionState { progress_handler_callback: Option, update_hook_callback: Option, + #[cfg(feature = "preupdate-hook")] + preupdate_hook_callback: Option, commit_hook_callback: Option, @@ -138,6 +144,16 @@ impl ConnectionState { } } + #[cfg(feature = "preupdate-hook")] + pub(crate) fn remove_preupdate_hook(&mut self) { + if let Some(mut handler) = self.preupdate_hook_callback.take() { + unsafe { + libsqlite3_sys::sqlite3_preupdate_hook(self.handle.as_ptr(), None, ptr::null_mut()); + let _ = { Box::from_raw(handler.0.as_mut()) }; + } + } + } + pub(crate) fn remove_commit_hook(&mut self) { if let Some(mut handler) = self.commit_hook_callback.take() { unsafe { @@ -235,14 +251,21 @@ impl Connection for SqliteConnection { where Self: Sized, { - Transaction::begin(self) + Transaction::begin(self, None) + } + + fn begin_with( + &mut self, + statement: impl Into>, + ) -> BoxFuture<'_, Result, Error>> + where + Self: Sized, + { + Transaction::begin(self, Some(statement.into())) } fn cached_statements_size(&self) -> usize { - self.worker - .shared - .cached_statements_size - .load(std::sync::atomic::Ordering::Acquire) + self.worker.shared.get_cached_statements_size() } fn clear_cached_statements(&mut self) -> BoxFuture<'_, Result<(), Error>> { @@ -421,6 +444,34 @@ impl LockedSqliteHandle<'_> { } } + /// Registers a hook that is invoked prior to each `INSERT`, `UPDATE`, and `DELETE` operation on a database table. + /// At most one preupdate hook may be registered at a time on a single database connection. + /// + /// The preupdate hook only fires for changes to real database tables; + /// it is not invoked for changes to virtual tables or to system tables like sqlite_sequence or sqlite_stat1. + /// + /// See https://sqlite.org/c3ref/preupdate_count.html + #[cfg(feature = "preupdate-hook")] + pub fn set_preupdate_hook(&mut self, callback: F) + where + F: FnMut(PreupdateHookResult) + Send + 'static, + { + unsafe { + let callback_boxed = Box::new(callback); + // SAFETY: `Box::into_raw()` always returns a non-null pointer. + let callback = NonNull::new_unchecked(Box::into_raw(callback_boxed)); + let handler = callback.as_ptr() as *mut _; + self.guard.remove_preupdate_hook(); + self.guard.preupdate_hook_callback = Some(PreupdateHookHandler(callback)); + + libsqlite3_sys::sqlite3_preupdate_hook( + self.as_raw_handle().as_mut(), + Some(preupdate_hook::), + handler, + ); + } + } + /// Sets a commit hook that is invoked whenever a transaction is committed. If the commit hook callback /// returns `false`, then the operation is turned into a ROLLBACK. /// @@ -485,6 +536,11 @@ impl LockedSqliteHandle<'_> { self.guard.remove_update_hook(); } + #[cfg(feature = "preupdate-hook")] + pub fn remove_preupdate_hook(&mut self) { + self.guard.remove_preupdate_hook(); + } + pub fn remove_commit_hook(&mut self) { self.guard.remove_commit_hook(); } @@ -492,6 +548,15 @@ impl LockedSqliteHandle<'_> { pub fn remove_rollback_hook(&mut self) { self.guard.remove_rollback_hook(); } + + pub fn last_error(&mut self) -> Option { + self.guard.handle.last_error() + } + + pub(crate) fn in_transaction(&mut self) -> bool { + let ret = unsafe { sqlite3_get_autocommit(self.as_raw_handle().as_ptr()) }; + ret == 0 + } } impl Drop for ConnectionState { diff --git a/sqlx-sqlite/src/connection/preupdate_hook.rs b/sqlx-sqlite/src/connection/preupdate_hook.rs new file mode 100644 index 0000000000..edcb078124 --- /dev/null +++ b/sqlx-sqlite/src/connection/preupdate_hook.rs @@ -0,0 +1,160 @@ +use super::SqliteOperation; +use crate::type_info::DataType; +use crate::{SqliteError, SqliteTypeInfo, SqliteValueRef}; + +use libsqlite3_sys::{ + sqlite3, sqlite3_preupdate_count, sqlite3_preupdate_depth, sqlite3_preupdate_new, + sqlite3_preupdate_old, sqlite3_value, sqlite3_value_type, SQLITE_OK, +}; +use std::ffi::CStr; +use std::marker::PhantomData; +use std::os::raw::{c_char, c_int, c_void}; +use std::panic::catch_unwind; +use std::ptr; +use std::ptr::NonNull; + +#[derive(Debug, thiserror::Error)] +pub enum PreupdateError { + /// Error returned from the database. + #[error("error returned from database: {0}")] + Database(#[source] SqliteError), + /// Index is not within the valid column range + #[error("{0} is not within the valid column range")] + ColumnIndexOutOfBounds(i32), + /// Column value accessor was invoked from an invalid operation + #[error("column value accessor was invoked from an invalid operation")] + InvalidOperation, +} + +pub(crate) struct PreupdateHookHandler( + pub(super) NonNull, +); +unsafe impl Send for PreupdateHookHandler {} + +#[derive(Debug)] +pub struct PreupdateHookResult<'a> { + pub operation: SqliteOperation, + pub database: &'a str, + pub table: &'a str, + db: *mut sqlite3, + // The database pointer should not be usable after the preupdate hook. + // The lifetime on this struct needs to ensure it cannot outlive the callback. + _db_lifetime: PhantomData<&'a ()>, + old_row_id: i64, + new_row_id: i64, +} + +impl<'a> PreupdateHookResult<'a> { + /// Gets the amount of columns in the row being inserted, deleted, or updated. + pub fn get_column_count(&self) -> i32 { + unsafe { sqlite3_preupdate_count(self.db) } + } + + /// Gets the depth of the query that triggered the preupdate hook. + /// Returns 0 if the preupdate callback was invoked as a result of + /// a direct insert, update, or delete operation; + /// 1 for inserts, updates, or deletes invoked by top-level triggers; + /// 2 for changes resulting from triggers called by top-level triggers; and so forth. + pub fn get_query_depth(&self) -> i32 { + unsafe { sqlite3_preupdate_depth(self.db) } + } + + /// Gets the row id of the row being updated/deleted. + /// Returns an error if called from an insert operation. + pub fn get_old_row_id(&self) -> Result { + if self.operation == SqliteOperation::Insert { + return Err(PreupdateError::InvalidOperation); + } + Ok(self.old_row_id) + } + + /// Gets the row id of the row being inserted/updated. + /// Returns an error if called from a delete operation. + pub fn get_new_row_id(&self) -> Result { + if self.operation == SqliteOperation::Delete { + return Err(PreupdateError::InvalidOperation); + } + Ok(self.new_row_id) + } + + /// Gets the value of the row being updated/deleted at the specified index. + /// Returns an error if called from an insert operation or the index is out of bounds. + pub fn get_old_column_value(&self, i: i32) -> Result, PreupdateError> { + if self.operation == SqliteOperation::Insert { + return Err(PreupdateError::InvalidOperation); + } + self.validate_column_index(i)?; + + let mut p_value: *mut sqlite3_value = ptr::null_mut(); + unsafe { + let ret = sqlite3_preupdate_old(self.db, i, &mut p_value); + self.get_value(ret, p_value) + } + } + + /// Gets the value of the row being inserted/updated at the specified index. + /// Returns an error if called from a delete operation or the index is out of bounds. + pub fn get_new_column_value(&self, i: i32) -> Result, PreupdateError> { + if self.operation == SqliteOperation::Delete { + return Err(PreupdateError::InvalidOperation); + } + self.validate_column_index(i)?; + + let mut p_value: *mut sqlite3_value = ptr::null_mut(); + unsafe { + let ret = sqlite3_preupdate_new(self.db, i, &mut p_value); + self.get_value(ret, p_value) + } + } + + fn validate_column_index(&self, i: i32) -> Result<(), PreupdateError> { + if i < 0 || i >= self.get_column_count() { + return Err(PreupdateError::ColumnIndexOutOfBounds(i)); + } + Ok(()) + } + + unsafe fn get_value( + &self, + ret: i32, + p_value: *mut sqlite3_value, + ) -> Result, PreupdateError> { + if ret != SQLITE_OK { + return Err(PreupdateError::Database(SqliteError::new(self.db))); + } + let data_type = DataType::from_code(sqlite3_value_type(p_value)); + // SAFETY: SQLite will free the sqlite3_value when the callback returns + Ok(SqliteValueRef::borrowed(p_value, SqliteTypeInfo(data_type))) + } +} + +pub(super) extern "C" fn preupdate_hook( + callback: *mut c_void, + db: *mut sqlite3, + op_code: c_int, + database: *const c_char, + table: *const c_char, + old_row_id: i64, + new_row_id: i64, +) where + F: FnMut(PreupdateHookResult) + Send + 'static, +{ + unsafe { + let _ = catch_unwind(|| { + let callback: *mut F = callback.cast::(); + let operation: SqliteOperation = op_code.into(); + let database = CStr::from_ptr(database).to_str().unwrap_or_default(); + let table = CStr::from_ptr(table).to_str().unwrap_or_default(); + + (*callback)(PreupdateHookResult { + operation, + database, + table, + old_row_id, + new_row_id, + db, + _db_lifetime: PhantomData, + }) + }); + } +} diff --git a/sqlx-sqlite/src/connection/serialize.rs b/sqlx-sqlite/src/connection/serialize.rs new file mode 100644 index 0000000000..c8835093da --- /dev/null +++ b/sqlx-sqlite/src/connection/serialize.rs @@ -0,0 +1,297 @@ +use super::ConnectionState; +use crate::{error::Error, SqliteConnection, SqliteError}; +use libsqlite3_sys::{ + sqlite3_deserialize, sqlite3_free, sqlite3_malloc64, sqlite3_serialize, + SQLITE_DESERIALIZE_FREEONCLOSE, SQLITE_DESERIALIZE_READONLY, SQLITE_DESERIALIZE_RESIZEABLE, + SQLITE_NOMEM, SQLITE_OK, +}; +use std::ffi::c_char; +use std::fmt::Debug; +use std::{ + ops::{Deref, DerefMut}, + ptr, + ptr::NonNull, +}; + +impl SqliteConnection { + /// Serialize the given SQLite database schema using [`sqlite3_serialize()`]. + /// + /// The returned buffer is a SQLite managed allocation containing the equivalent data + /// as writing the database to disk. It is freed on-drop. + /// + /// To serialize the primary, unqualified schema (`main`), pass `None` for the schema name. + /// + /// # Errors + /// * [`Error::InvalidArgument`] if the schema name contains a zero/NUL byte (`\0`). + /// * [`Error::Database`] if the schema does not exist or another error occurs. + /// + /// [`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html + pub async fn serialize(&mut self, schema: Option<&str>) -> Result { + let schema = schema.map(SchemaName::try_from).transpose()?; + + self.worker.serialize(schema).await + } + + /// Deserialize a SQLite database from a buffer into the specified schema using [`sqlite3_deserialize()`]. + /// + /// The given schema will be disconnected and re-connected as an in-memory database + /// backed by `data`, which should be the serialized form of a database previously returned + /// by a call to [`Self::serialize()`], documented as being equivalent to + /// the contents of the database file on disk. + /// + /// An error will be returned if a schema with the given name is not already attached. + /// You can use `ATTACH ':memory' as ""` to create an empty schema first. + /// + /// Pass `None` to deserialize to the primary, unqualified schema (`main`). + /// + /// The SQLite connection will take ownership of `data` and will free it when the connection + /// is closed or the schema is detached ([`SQLITE_DESERIALIZE_FREEONCLOSE`][deserialize-flags]). + /// + /// If `read_only` is `true`, the schema is opened as read-only ([`SQLITE_DESERIALIZE_READONLY`][deserialize-flags]). + /// If `false`, the schema is marked as resizable ([`SQLITE_DESERIALIZE_RESIZABLE`][deserialize-flags]). + /// + /// If the database is in WAL mode, an error is returned. + /// See [`sqlite3_deserialize()`] for details. + /// + /// # Errors + /// * [`Error::InvalidArgument`] if the schema name contains a zero/NUL byte (`\0`). + /// * [`Error::Database`] if an error occurs during deserialization. + /// + /// [`sqlite3_deserialize()`]: https://sqlite.org/c3ref/deserialize.html + /// [deserialize-flags]: https://sqlite.org/c3ref/c_deserialize_freeonclose.html + pub async fn deserialize( + &mut self, + schema: Option<&str>, + data: SqliteOwnedBuf, + read_only: bool, + ) -> Result<(), Error> { + let schema = schema.map(SchemaName::try_from).transpose()?; + + self.worker.deserialize(schema, data, read_only).await + } +} + +pub(crate) fn serialize( + conn: &mut ConnectionState, + schema: Option, +) -> Result { + let mut size = 0; + + let buf = unsafe { + let ptr = sqlite3_serialize( + conn.handle.as_ptr(), + schema.as_ref().map_or(ptr::null(), SchemaName::as_ptr), + &mut size, + 0, + ); + + // looking at the source, `sqlite3_serialize` actually sets `size = -1` on error: + // https://github.com/sqlite/sqlite/blob/da5f81387843f92652128087a8f8ecef0b79461d/src/memdb.c#L776 + usize::try_from(size) + .ok() + .and_then(|size| SqliteOwnedBuf::from_raw(ptr, size)) + }; + + if let Some(buf) = buf { + return Ok(buf); + } + + if let Some(error) = conn.handle.last_error() { + return Err(error.into()); + } + + if size > 0 { + // If `size` is positive but `sqlite3_serialize` still returned NULL, + // the most likely culprit is an out-of-memory condition. + return Err(SqliteError::from_code(SQLITE_NOMEM).into()); + } + + // Otherwise, the schema was probably not found. + // We return the equivalent error as when you try to execute `PRAGMA .page_count` + // against a non-existent schema. + Err(SqliteError::generic(format!( + "database {} does not exist", + schema.as_ref().map_or("main", SchemaName::as_str) + )) + .into()) +} + +pub(crate) fn deserialize( + conn: &mut ConnectionState, + schema: Option, + data: SqliteOwnedBuf, + read_only: bool, +) -> Result<(), Error> { + // SQLITE_DESERIALIZE_FREEONCLOSE causes SQLite to take ownership of the buffer + let mut flags = SQLITE_DESERIALIZE_FREEONCLOSE; + if read_only { + flags |= SQLITE_DESERIALIZE_READONLY; + } else { + flags |= SQLITE_DESERIALIZE_RESIZEABLE; + } + + let (buf, size) = data.into_raw(); + + let rc = unsafe { + sqlite3_deserialize( + conn.handle.as_ptr(), + schema.as_ref().map_or(ptr::null(), SchemaName::as_ptr), + buf, + i64::try_from(size).unwrap(), + i64::try_from(size).unwrap(), + flags, + ) + }; + + match rc { + SQLITE_OK => Ok(()), + SQLITE_NOMEM => Err(SqliteError::from_code(SQLITE_NOMEM).into()), + // SQLite unfortunately doesn't set any specific message for deserialization errors. + _ => Err(SqliteError::generic("an error occurred during deserialization").into()), + } +} + +/// Memory buffer owned and allocated by SQLite. Freed on drop. +/// +/// Intended primarily for use with [`SqliteConnection::serialize()`] and [`SqliteConnection::deserialize()`]. +/// +/// Can be created from `&[u8]` using the `TryFrom` impl. The slice must not be empty. +#[derive(Debug)] +pub struct SqliteOwnedBuf { + ptr: NonNull, + size: usize, +} + +unsafe impl Send for SqliteOwnedBuf {} +unsafe impl Sync for SqliteOwnedBuf {} + +impl Drop for SqliteOwnedBuf { + fn drop(&mut self) { + unsafe { + sqlite3_free(self.ptr.as_ptr().cast()); + } + } +} + +impl SqliteOwnedBuf { + /// Uses `sqlite3_malloc` to allocate a buffer and returns a pointer to it. + /// + /// # Safety + /// The allocated buffer is uninitialized. + unsafe fn with_capacity(size: usize) -> Option { + let ptr = sqlite3_malloc64(u64::try_from(size).unwrap()).cast::(); + Self::from_raw(ptr, size) + } + + /// Creates a new mem buffer from a pointer that has been created with sqlite_malloc + /// + /// # Safety: + /// * The pointer must point to a valid allocation created by `sqlite3_malloc()`, or `NULL`. + unsafe fn from_raw(ptr: *mut u8, size: usize) -> Option { + Some(Self { + ptr: NonNull::new(ptr)?, + size, + }) + } + + fn into_raw(self) -> (*mut u8, usize) { + let raw = (self.ptr.as_ptr(), self.size); + // this is used in sqlite_deserialize and + // underlying buffer must not be freed + std::mem::forget(self); + raw + } +} + +/// # Errors +/// Returns [`Error::InvalidArgument`] if the slice is empty. +impl TryFrom<&[u8]> for SqliteOwnedBuf { + type Error = Error; + + fn try_from(bytes: &[u8]) -> Result { + unsafe { + // SAFETY: `buf` is not initialized until `ptr::copy_nonoverlapping` completes. + let mut buf = Self::with_capacity(bytes.len()).ok_or_else(|| { + Error::InvalidArgument("SQLite owned buffer cannot be empty".to_string()) + })?; + ptr::copy_nonoverlapping(bytes.as_ptr(), buf.ptr.as_mut(), buf.size); + Ok(buf) + } + } +} + +impl Deref for SqliteOwnedBuf { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.size) } + } +} + +impl DerefMut for SqliteOwnedBuf { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { std::slice::from_raw_parts_mut(self.ptr.as_mut(), self.size) } + } +} + +impl AsRef<[u8]> for SqliteOwnedBuf { + fn as_ref(&self) -> &[u8] { + self.deref() + } +} + +impl AsMut<[u8]> for SqliteOwnedBuf { + fn as_mut(&mut self) -> &mut [u8] { + self.deref_mut() + } +} + +/// Checked schema name to pass to SQLite. +/// +/// # Safety: +/// * Valid UTF-8 (not guaranteed by `CString`) +/// * No internal zero bytes (`\0`) (not guaranteed by `String`) +/// * Terminated with a zero byte (`\0`) (not guaranteed by `String`) +#[derive(Debug)] +pub(crate) struct SchemaName(Box); + +impl SchemaName { + /// Get the schema name as a string without the zero byte terminator. + pub fn as_str(&self) -> &str { + &self.0[..self.0.len() - 1] + } + + /// Get a pointer to the string data, suitable for passing as C's `*const char`. + /// + /// # Safety + /// The string data is guaranteed to be terminated with a zero byte. + pub fn as_ptr(&self) -> *const c_char { + self.0.as_ptr() as *const c_char + } +} + +impl<'a> TryFrom<&'a str> for SchemaName { + type Error = Error; + + fn try_from(name: &'a str) -> Result { + // SAFETY: we must ensure that the string does not contain an internal NULL byte + if let Some(pos) = name.as_bytes().iter().position(|&b| b == 0) { + return Err(Error::InvalidArgument(format!( + "schema name {name:?} contains a zero byte at index {pos}" + ))); + } + + let capacity = name.len().checked_add(1).unwrap(); + + let mut s = String::new(); + // `String::with_capacity()` does not guarantee that it will not overallocate, + // which might mean an unnecessary reallocation to make `capacity == len` + // in the conversion to `Box`. + s.reserve_exact(capacity); + + s.push_str(name); + s.push('\0'); + + Ok(SchemaName(s.into())) + } +} diff --git a/sqlx-sqlite/src/connection/worker.rs b/sqlx-sqlite/src/connection/worker.rs index a01de2419c..00a4c2999c 100644 --- a/sqlx-sqlite/src/connection/worker.rs +++ b/sqlx-sqlite/src/connection/worker.rs @@ -21,6 +21,8 @@ use crate::connection::execute; use crate::connection::ConnectionState; use crate::{Sqlite, SqliteArguments, SqliteQueryResult, SqliteRow, SqliteStatement}; +use super::serialize::{deserialize, serialize, SchemaName, SqliteOwnedBuf}; + // Each SQLite connection has a dedicated thread. // TODO: Tweak this so that we can use a thread pool per pool of SQLite3 connections to reduce @@ -34,10 +36,21 @@ pub(crate) struct ConnectionWorker { } pub(crate) struct WorkerSharedState { - pub(crate) cached_statements_size: AtomicUsize, + transaction_depth: AtomicUsize, + cached_statements_size: AtomicUsize, pub(crate) conn: Mutex, } +impl WorkerSharedState { + pub(crate) fn get_transaction_depth(&self) -> usize { + self.transaction_depth.load(Ordering::Acquire) + } + + pub(crate) fn get_cached_statements_size(&self) -> usize { + self.cached_statements_size.load(Ordering::Acquire) + } +} + enum Command { Prepare { query: Box, @@ -54,8 +67,19 @@ enum Command { tx: flume::Sender, Error>>, limit: Option, }, + Serialize { + schema: Option, + tx: oneshot::Sender>, + }, + Deserialize { + schema: Option, + data: SqliteOwnedBuf, + read_only: bool, + tx: oneshot::Sender>, + }, Begin { tx: rendezvous_oneshot::Sender>, + statement: Option>, }, Commit { tx: rendezvous_oneshot::Sender>, @@ -93,6 +117,7 @@ impl ConnectionWorker { }; let shared = Arc::new(WorkerSharedState { + transaction_depth: AtomicUsize::new(0), cached_statements_size: AtomicUsize::new(0), // note: must be fair because in `Command::UnlockDb` we unlock the mutex // and then immediately try to relock it; an unfair mutex would immediately @@ -151,7 +176,8 @@ impl ConnectionWorker { match limit { None => { for res in iter { - if tx.send(res).is_err() { + let has_error = res.is_err(); + if tx.send(res).is_err() || has_error { break; } } @@ -171,7 +197,8 @@ impl ConnectionWorker { } } } - if tx.send(res).is_err() { + let has_error = res.is_err(); + if tx.send(res).is_err() || has_error { break; } } @@ -180,13 +207,27 @@ impl ConnectionWorker { update_cached_statements_size(&conn, &shared.cached_statements_size); } - Command::Begin { tx } => { - let depth = conn.transaction_depth; + Command::Begin { tx, statement } => { + let depth = shared.transaction_depth.load(Ordering::Acquire); + + let statement = match statement { + // custom `BEGIN` statements are not allowed if + // we're already in a transaction (we need to + // issue a `SAVEPOINT` instead) + Some(_) if depth > 0 => { + if tx.blocking_send(Err(Error::InvalidSavePointStatement)).is_err() { + break; + } + continue; + }, + Some(statement) => statement, + None => begin_ansi_transaction_sql(depth), + }; let res = conn.handle - .exec(begin_ansi_transaction_sql(depth)) + .exec(statement) .map(|_| { - conn.transaction_depth += 1; + shared.transaction_depth.fetch_add(1, Ordering::Release); }); let res_ok = res.is_ok(); @@ -199,7 +240,7 @@ impl ConnectionWorker { .handle .exec(rollback_ansi_transaction_sql(depth + 1)) .map(|_| { - conn.transaction_depth -= 1; + shared.transaction_depth.fetch_sub(1, Ordering::Release); }) { // The rollback failed. To prevent leaving the connection @@ -211,13 +252,13 @@ impl ConnectionWorker { } } Command::Commit { tx } => { - let depth = conn.transaction_depth; + let depth = shared.transaction_depth.load(Ordering::Acquire); let res = if depth > 0 { conn.handle .exec(commit_ansi_transaction_sql(depth)) .map(|_| { - conn.transaction_depth -= 1; + shared.transaction_depth.fetch_sub(1, Ordering::Release); }) } else { Ok(()) @@ -237,13 +278,13 @@ impl ConnectionWorker { continue; } - let depth = conn.transaction_depth; + let depth = shared.transaction_depth.load(Ordering::Acquire); let res = if depth > 0 { conn.handle .exec(rollback_ansi_transaction_sql(depth)) .map(|_| { - conn.transaction_depth -= 1; + shared.transaction_depth.fetch_sub(1, Ordering::Release); }) } else { Ok(()) @@ -261,6 +302,12 @@ impl ConnectionWorker { } } } + Command::Serialize { schema, tx } => { + tx.send(serialize(&mut conn, schema)).ok(); + } + Command::Deserialize { schema, data, read_only, tx } => { + tx.send(deserialize(&mut conn, schema, data, read_only)).ok(); + } Command::ClearCache { tx } => { conn.statements.clear(); update_cached_statements_size(&conn, &shared.cached_statements_size); @@ -331,8 +378,11 @@ impl ConnectionWorker { Ok(rx) } - pub(crate) async fn begin(&mut self) -> Result<(), Error> { - self.oneshot_cmd_with_ack(|tx| Command::Begin { tx }) + pub(crate) async fn begin( + &mut self, + statement: Option>, + ) -> Result<(), Error> { + self.oneshot_cmd_with_ack(|tx| Command::Begin { tx, statement }) .await? } @@ -356,6 +406,29 @@ impl ConnectionWorker { self.oneshot_cmd(|tx| Command::Ping { tx }).await } + pub(crate) async fn deserialize( + &mut self, + schema: Option, + data: SqliteOwnedBuf, + read_only: bool, + ) -> Result<(), Error> { + self.oneshot_cmd(|tx| Command::Deserialize { + schema, + data, + read_only, + tx, + }) + .await? + } + + pub(crate) async fn serialize( + &mut self, + schema: Option, + ) -> Result { + self.oneshot_cmd(|tx| Command::Serialize { schema, tx }) + .await? + } + async fn oneshot_cmd(&mut self, command: F) -> Result where F: FnOnce(oneshot::Sender) -> Command, diff --git a/sqlx-sqlite/src/error.rs b/sqlx-sqlite/src/error.rs index c00374fe60..eee2e8b1a2 100644 --- a/sqlx-sqlite/src/error.rs +++ b/sqlx-sqlite/src/error.rs @@ -2,12 +2,12 @@ use std::error::Error as StdError; use std::ffi::CStr; use std::fmt::{self, Display, Formatter}; use std::os::raw::c_int; -use std::{borrow::Cow, str::from_utf8_unchecked}; +use std::{borrow::Cow, str}; use libsqlite3_sys::{ - sqlite3, sqlite3_errmsg, sqlite3_extended_errcode, SQLITE_CONSTRAINT_CHECK, + sqlite3, sqlite3_errmsg, sqlite3_errstr, sqlite3_extended_errcode, SQLITE_CONSTRAINT_CHECK, SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_NOTNULL, SQLITE_CONSTRAINT_PRIMARYKEY, - SQLITE_CONSTRAINT_UNIQUE, + SQLITE_CONSTRAINT_UNIQUE, SQLITE_ERROR, }; pub(crate) use sqlx_core::error::*; @@ -18,33 +18,65 @@ pub(crate) use sqlx_core::error::*; #[derive(Debug)] pub struct SqliteError { code: c_int, - message: String, + message: Cow<'static, str>, } impl SqliteError { - pub(crate) fn new(handle: *mut sqlite3) -> Self { + pub(crate) unsafe fn new(handle: *mut sqlite3) -> Self { + Self::try_new(handle).expect("There should be an error") + } + + pub(crate) unsafe fn try_new(handle: *mut sqlite3) -> Option { // returns the extended result code even when extended result codes are disabled let code: c_int = unsafe { sqlite3_extended_errcode(handle) }; + if code == 0 { + return None; + } + // return English-language text that describes the error let message = unsafe { let msg = sqlite3_errmsg(handle); debug_assert!(!msg.is_null()); - from_utf8_unchecked(CStr::from_ptr(msg).to_bytes()) + str::from_utf8_unchecked(CStr::from_ptr(msg).to_bytes()).to_owned() }; - Self { + Some(Self { code, - message: message.to_owned(), - } + message: message.into(), + }) } /// For errors during extension load, the error message is supplied via a separate pointer - pub(crate) fn extension(handle: *mut sqlite3, error_msg: &CStr) -> Self { - let mut err = Self::new(handle); - err.message = unsafe { from_utf8_unchecked(error_msg.to_bytes()).to_owned() }; - err + pub(crate) fn with_message(mut self, error_msg: String) -> Self { + self.message = error_msg.into(); + self + } + + pub(crate) fn from_code(code: c_int) -> Self { + let message = unsafe { + let errstr = sqlite3_errstr(code); + + if !errstr.is_null() { + // SAFETY: `errstr` is guaranteed to be UTF-8 + // The lifetime of the string is "internally managed"; + // the implementation just selects from an array of static strings. + // We copy to an owned buffer in case `libsqlite3` is dynamically loaded somehow. + Cow::Owned(str::from_utf8_unchecked(CStr::from_ptr(errstr).to_bytes()).into()) + } else { + Cow::Borrowed("") + } + }; + + SqliteError { code, message } + } + + pub(crate) fn generic(message: impl Into>) -> Self { + Self { + code: SQLITE_ERROR, + message: message.into(), + } } } diff --git a/sqlx-sqlite/src/lib.rs b/sqlx-sqlite/src/lib.rs index 3bcb6d148d..e4a122b6bd 100644 --- a/sqlx-sqlite/src/lib.rs +++ b/sqlx-sqlite/src/lib.rs @@ -46,6 +46,9 @@ use std::sync::atomic::AtomicBool; pub use arguments::{SqliteArgumentValue, SqliteArguments}; pub use column::SqliteColumn; +pub use connection::serialize::SqliteOwnedBuf; +#[cfg(feature = "preupdate-hook")] +pub use connection::PreupdateHookResult; pub use connection::{LockedSqliteHandle, SqliteConnection, SqliteOperation, UpdateHookResult}; pub use database::Sqlite; pub use error::SqliteError; diff --git a/sqlx-sqlite/src/statement/handle.rs b/sqlx-sqlite/src/statement/handle.rs index 2925d1a199..ccc299fcd2 100644 --- a/sqlx-sqlite/src/statement/handle.rs +++ b/sqlx-sqlite/src/statement/handle.rs @@ -81,8 +81,8 @@ impl StatementHandle { } #[inline] - pub(crate) fn last_error(&self) -> SqliteError { - SqliteError::new(unsafe { self.db_handle() }) + pub(crate) fn last_error(&mut self) -> SqliteError { + unsafe { SqliteError::new(self.db_handle()) } } #[inline] diff --git a/sqlx-sqlite/src/statement/virtual.rs b/sqlx-sqlite/src/statement/virtual.rs index 6be980c36a..2817146bc3 100644 --- a/sqlx-sqlite/src/statement/virtual.rs +++ b/sqlx-sqlite/src/statement/virtual.rs @@ -184,7 +184,7 @@ fn prepare( }; if status != SQLITE_OK { - return Err(SqliteError::new(conn).into()); + return Err(unsafe { SqliteError::new(conn).into() }); } // tail should point to the first byte past the end of the first SQL diff --git a/sqlx-sqlite/src/testing/mod.rs b/sqlx-sqlite/src/testing/mod.rs index 3398c6b493..324b519119 100644 --- a/sqlx-sqlite/src/testing/mod.rs +++ b/sqlx-sqlite/src/testing/mod.rs @@ -30,6 +30,10 @@ impl TestSupport for Sqlite { ) -> BoxFuture<'_, Result, Error>> { todo!() } + + fn db_name(args: &TestArgs) -> String { + convert_path(args.test_path) + } } async fn test_context(args: &TestArgs) -> Result, Error> { diff --git a/sqlx-sqlite/src/transaction.rs b/sqlx-sqlite/src/transaction.rs index 24eaca51b1..55a80ab9f3 100644 --- a/sqlx-sqlite/src/transaction.rs +++ b/sqlx-sqlite/src/transaction.rs @@ -1,17 +1,33 @@ use futures_core::future::BoxFuture; +use std::borrow::Cow; -use crate::{Sqlite, SqliteConnection}; use sqlx_core::error::Error; use sqlx_core::transaction::TransactionManager; +use crate::{Sqlite, SqliteConnection}; + /// Implementation of [`TransactionManager`] for SQLite. pub struct SqliteTransactionManager; impl TransactionManager for SqliteTransactionManager { type Database = Sqlite; - fn begin(conn: &mut SqliteConnection) -> BoxFuture<'_, Result<(), Error>> { - Box::pin(conn.worker.begin()) + fn begin<'conn>( + conn: &'conn mut SqliteConnection, + statement: Option>, + ) -> BoxFuture<'conn, Result<(), Error>> { + Box::pin(async { + let is_custom_statement = statement.is_some(); + conn.worker.begin(statement).await?; + if is_custom_statement { + // Check that custom statement actually put the connection into a transaction. + let mut handle = conn.lock_handle().await?; + if !handle.in_transaction() { + return Err(Error::BeginFailed); + } + } + Ok(()) + }) } fn commit(conn: &mut SqliteConnection) -> BoxFuture<'_, Result<(), Error>> { @@ -25,4 +41,8 @@ impl TransactionManager for SqliteTransactionManager { fn start_rollback(conn: &mut SqliteConnection) { conn.worker.start_rollback().ok(); } + + fn get_transaction_depth(conn: &SqliteConnection) -> usize { + conn.worker.shared.get_transaction_depth() + } } diff --git a/sqlx-sqlite/src/value.rs b/sqlx-sqlite/src/value.rs index 967b3f7476..469c4e70d5 100644 --- a/sqlx-sqlite/src/value.rs +++ b/sqlx-sqlite/src/value.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::marker::PhantomData; use std::ptr::NonNull; use std::slice::from_raw_parts; use std::str::from_utf8; @@ -17,6 +18,7 @@ use crate::{Sqlite, SqliteTypeInfo}; enum SqliteValueData<'r> { Value(&'r SqliteValue), + BorrowedHandle(ValueHandle<'r>), } pub struct SqliteValueRef<'r>(SqliteValueData<'r>); @@ -26,31 +28,44 @@ impl<'r> SqliteValueRef<'r> { Self(SqliteValueData::Value(value)) } + // SAFETY: The supplied sqlite3_value must not be null and SQLite must free it. It will not be freed on drop. + // The lifetime on this struct should tie it to whatever scope it's valid for before SQLite frees it. + #[allow(unused)] + pub(crate) unsafe fn borrowed(value: *mut sqlite3_value, type_info: SqliteTypeInfo) -> Self { + debug_assert!(!value.is_null()); + let handle = ValueHandle::new_borrowed(NonNull::new_unchecked(value), type_info); + Self(SqliteValueData::BorrowedHandle(handle)) + } + // NOTE: `int()` is deliberately omitted because it will silently truncate a wider value, // which is likely to cause bugs: // https://github.com/launchbadge/sqlx/issues/3179 // (Similar bug in Postgres): https://github.com/launchbadge/sqlx/issues/3161 pub(super) fn int64(&self) -> i64 { - match self.0 { - SqliteValueData::Value(v) => v.int64(), + match &self.0 { + SqliteValueData::Value(v) => v.0.int64(), + SqliteValueData::BorrowedHandle(v) => v.int64(), } } pub(super) fn double(&self) -> f64 { - match self.0 { - SqliteValueData::Value(v) => v.double(), + match &self.0 { + SqliteValueData::Value(v) => v.0.double(), + SqliteValueData::BorrowedHandle(v) => v.double(), } } pub(super) fn blob(&self) -> &'r [u8] { - match self.0 { - SqliteValueData::Value(v) => v.blob(), + match &self.0 { + SqliteValueData::Value(v) => v.0.blob(), + SqliteValueData::BorrowedHandle(v) => v.blob(), } } pub(super) fn text(&self) -> Result<&'r str, BoxDynError> { - match self.0 { - SqliteValueData::Value(v) => v.text(), + match &self.0 { + SqliteValueData::Value(v) => v.0.text(), + SqliteValueData::BorrowedHandle(v) => v.text(), } } } @@ -59,50 +74,66 @@ impl<'r> ValueRef<'r> for SqliteValueRef<'r> { type Database = Sqlite; fn to_owned(&self) -> SqliteValue { - match self.0 { - SqliteValueData::Value(v) => v.clone(), + match &self.0 { + SqliteValueData::Value(v) => (*v).clone(), + SqliteValueData::BorrowedHandle(v) => unsafe { + SqliteValue::new(v.value.as_ptr(), v.type_info.clone()) + }, } } fn type_info(&self) -> Cow<'_, SqliteTypeInfo> { - match self.0 { + match &self.0 { SqliteValueData::Value(v) => v.type_info(), + SqliteValueData::BorrowedHandle(v) => v.type_info(), } } fn is_null(&self) -> bool { - match self.0 { + match &self.0 { SqliteValueData::Value(v) => v.is_null(), + SqliteValueData::BorrowedHandle(v) => v.is_null(), } } } #[derive(Clone)] -pub struct SqliteValue { - pub(crate) handle: Arc, - pub(crate) type_info: SqliteTypeInfo, -} +pub struct SqliteValue(Arc>); -pub(crate) struct ValueHandle(NonNull); +pub(crate) struct ValueHandle<'a> { + value: NonNull, + type_info: SqliteTypeInfo, + free_on_drop: bool, + _sqlite_value_lifetime: PhantomData<&'a ()>, +} // SAFE: only protected value objects are stored in SqliteValue -unsafe impl Send for ValueHandle {} -unsafe impl Sync for ValueHandle {} +unsafe impl<'a> Send for ValueHandle<'a> {} +unsafe impl<'a> Sync for ValueHandle<'a> {} -impl SqliteValue { - pub(crate) unsafe fn new(value: *mut sqlite3_value, type_info: SqliteTypeInfo) -> Self { - debug_assert!(!value.is_null()); +impl ValueHandle<'static> { + fn new_owned(value: NonNull, type_info: SqliteTypeInfo) -> Self { + Self { + value, + type_info, + free_on_drop: true, + _sqlite_value_lifetime: PhantomData, + } + } +} +impl<'a> ValueHandle<'a> { + fn new_borrowed(value: NonNull, type_info: SqliteTypeInfo) -> Self { Self { + value, type_info, - handle: Arc::new(ValueHandle(NonNull::new_unchecked(sqlite3_value_dup( - value, - )))), + free_on_drop: false, + _sqlite_value_lifetime: PhantomData, } } fn type_info_opt(&self) -> Option { - let dt = DataType::from_code(unsafe { sqlite3_value_type(self.handle.0.as_ptr()) }); + let dt = DataType::from_code(unsafe { sqlite3_value_type(self.value.as_ptr()) }); if let DataType::Null = dt { None @@ -112,15 +143,15 @@ impl SqliteValue { } fn int64(&self) -> i64 { - unsafe { sqlite3_value_int64(self.handle.0.as_ptr()) } + unsafe { sqlite3_value_int64(self.value.as_ptr()) } } fn double(&self) -> f64 { - unsafe { sqlite3_value_double(self.handle.0.as_ptr()) } + unsafe { sqlite3_value_double(self.value.as_ptr()) } } - fn blob(&self) -> &[u8] { - let len = unsafe { sqlite3_value_bytes(self.handle.0.as_ptr()) }; + fn blob<'b>(&self) -> &'b [u8] { + let len = unsafe { sqlite3_value_bytes(self.value.as_ptr()) }; // This likely means UB in SQLite itself or our usage of it; // signed integer overflow is UB in the C standard. @@ -133,23 +164,15 @@ impl SqliteValue { return &[]; } - let ptr = unsafe { sqlite3_value_blob(self.handle.0.as_ptr()) } as *const u8; + let ptr = unsafe { sqlite3_value_blob(self.value.as_ptr()) } as *const u8; debug_assert!(!ptr.is_null()); unsafe { from_raw_parts(ptr, len) } } - fn text(&self) -> Result<&str, BoxDynError> { + fn text<'b>(&self) -> Result<&'b str, BoxDynError> { Ok(from_utf8(self.blob())?) } -} - -impl Value for SqliteValue { - type Database = Sqlite; - - fn as_ref(&self) -> SqliteValueRef<'_> { - SqliteValueRef::value(self) - } fn type_info(&self) -> Cow<'_, SqliteTypeInfo> { self.type_info_opt() @@ -158,18 +181,46 @@ impl Value for SqliteValue { } fn is_null(&self) -> bool { - unsafe { sqlite3_value_type(self.handle.0.as_ptr()) == SQLITE_NULL } + unsafe { sqlite3_value_type(self.value.as_ptr()) == SQLITE_NULL } } } -impl Drop for ValueHandle { +impl<'a> Drop for ValueHandle<'a> { fn drop(&mut self) { - unsafe { - sqlite3_value_free(self.0.as_ptr()); + if self.free_on_drop { + unsafe { + sqlite3_value_free(self.value.as_ptr()); + } } } } +impl SqliteValue { + // SAFETY: The sqlite3_value must be non-null and SQLite must not free it. It will be freed on drop. + pub(crate) unsafe fn new(value: *mut sqlite3_value, type_info: SqliteTypeInfo) -> Self { + debug_assert!(!value.is_null()); + let handle = + ValueHandle::new_owned(NonNull::new_unchecked(sqlite3_value_dup(value)), type_info); + Self(Arc::new(handle)) + } +} + +impl Value for SqliteValue { + type Database = Sqlite; + + fn as_ref(&self) -> SqliteValueRef<'_> { + SqliteValueRef::value(self) + } + + fn type_info(&self) -> Cow<'_, SqliteTypeInfo> { + self.0.type_info() + } + + fn is_null(&self) -> bool { + self.0.is_null() + } +} + // #[cfg(feature = "any")] // impl<'r> From> for crate::any::AnyValueRef<'r> { // #[inline] diff --git a/src/lib.rs b/src/lib.rs index 870fa703c5..ed76c5f5ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,14 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("lib.md")] +#[cfg(all( + feature = "sqlite-preupdate-hook", + not(any(feature = "sqlite", feature = "sqlite-unbundled")) +))] +compile_error!( + "sqlite-preupdate-hook requires either 'sqlite' or 'sqlite-unbundled' to be enabled" +); + pub use sqlx_core::acquire::Acquire; pub use sqlx_core::arguments::{Arguments, IntoArguments}; pub use sqlx_core::column::Column; diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 7f8ff747f9..9e81935876 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -249,7 +249,7 @@ /// /// ##### Force a Different/Custom Type /// Selecting a column `foo as "foo: T"` (Postgres / SQLite) or `` foo as `foo: T` `` (MySQL) -/// overrides the inferred type which is useful when selecting user-defined custom types +/// overrides the inferred type which is useful when selecting user-defined [custom types][crate::Type#compile-time-verification] /// (dynamic type checking is still done so if the types are incompatible this will be an error /// at runtime instead of compile-time). Note that this syntax alone doesn't override inferred nullability, /// but it is compatible with the forced not-null and forced nullable annotations: diff --git a/tests/mysql/error.rs b/tests/mysql/error.rs index 7c84266c32..3ee1024fc8 100644 --- a/tests/mysql/error.rs +++ b/tests/mysql/error.rs @@ -1,4 +1,4 @@ -use sqlx::{error::ErrorKind, mysql::MySql, Connection}; +use sqlx::{error::ErrorKind, mysql::MySql, Connection, Error}; use sqlx_test::new; #[sqlx_macros::test] @@ -74,3 +74,29 @@ async fn it_fails_with_check_violation() -> anyhow::Result<()> { Ok(()) } + +#[sqlx_macros::test] +async fn it_fails_with_begin_failed() -> anyhow::Result<()> { + let mut conn = new::().await?; + let res = conn.begin_with("SELECT * FROM tweet").await; + + let err = res.unwrap_err(); + + assert!(matches!(err, Error::BeginFailed), "{err:?}"); + + Ok(()) +} + +#[sqlx_macros::test] +async fn it_fails_with_invalid_save_point_statement() -> anyhow::Result<()> { + let mut conn = new::().await?; + let mut txn = conn.begin().await?; + let txn_conn = sqlx::Acquire::acquire(&mut txn).await?; + let res = txn_conn.begin_with("BEGIN").await; + + let err = res.unwrap_err(); + + assert!(matches!(err, Error::InvalidSavePointStatement), "{err}"); + + Ok(()) +} diff --git a/tests/mysql/macros.rs b/tests/mysql/macros.rs index f6bc75955a..8187f6d8d8 100644 --- a/tests/mysql/macros.rs +++ b/tests/mysql/macros.rs @@ -494,6 +494,31 @@ async fn test_from_row_json_attr() -> anyhow::Result<()> { Ok(()) } +#[sqlx_macros::test] +async fn test_from_row_json_attr_nullable() -> anyhow::Result<()> { + #[derive(serde::Deserialize)] + #[allow(dead_code)] + struct J { + a: u32, + b: u32, + } + + #[derive(sqlx::FromRow)] + struct Record { + #[sqlx(json(nullable))] + j: Option, + } + + let mut conn = new::().await?; + + let record = sqlx::query_as::<_, Record>("select NULL as j") + .fetch_one(&mut conn) + .await?; + + assert!(record.j.is_none()); + Ok(()) +} + #[sqlx_macros::test] async fn test_from_row_json_try_from_attr() -> anyhow::Result<()> { #[derive(serde::Deserialize)] diff --git a/tests/mysql/test-attr.rs b/tests/mysql/test-attr.rs index 158be8f816..75ca668602 100644 --- a/tests/mysql/test-attr.rs +++ b/tests/mysql/test-attr.rs @@ -12,11 +12,7 @@ async fn it_gets_a_pool(pool: MySqlPool) -> sqlx::Result<()> { .fetch_one(&mut *conn) .await?; - assert!( - db_name.starts_with("_sqlx_test_database_"), - "db_name: {:?}", - db_name - ); + assert!(db_name.starts_with("_sqlx_test_"), "db_name: {:?}", db_name); Ok(()) } diff --git a/tests/postgres/derives.rs b/tests/postgres/derives.rs index dada74fe4d..13f9bf1d5d 100644 --- a/tests/postgres/derives.rs +++ b/tests/postgres/derives.rs @@ -810,3 +810,69 @@ async fn test_custom_pg_array() -> anyhow::Result<()> { } Ok(()) } + +#[sqlx_macros::test] +async fn test_record_array_type() -> anyhow::Result<()> { + let mut conn = new::().await?; + + conn.execute( + r#" +DROP TABLE IF EXISTS responses; + +DROP TYPE IF EXISTS http_response CASCADE; +DROP TYPE IF EXISTS header_pair CASCADE; + +CREATE TYPE header_pair AS ( + name TEXT, + value TEXT +); + +CREATE TYPE http_response AS ( + headers header_pair[] +); + +CREATE TABLE responses ( + response http_response NOT NULL +); + "#, + ) + .await?; + + #[derive(Debug, sqlx::Type)] + #[sqlx(type_name = "http_response")] + struct HttpResponseRecord { + headers: Vec, + } + + #[derive(Debug, sqlx::Type)] + #[sqlx(type_name = "header_pair")] + struct HeaderPairRecord { + name: String, + value: String, + } + + let value = HttpResponseRecord { + headers: vec![ + HeaderPairRecord { + name: "Content-Type".to_owned(), + value: "text/html; charset=utf-8".to_owned(), + }, + HeaderPairRecord { + name: "Cache-Control".to_owned(), + value: "max-age=0".to_owned(), + }, + ], + }; + + sqlx::query( + " +INSERT INTO responses (response) +VALUES ($1) + ", + ) + .bind(&value) + .execute(&mut conn) + .await?; + + Ok(()) +} diff --git a/tests/postgres/error.rs b/tests/postgres/error.rs index d6f78140da..32bf814770 100644 --- a/tests/postgres/error.rs +++ b/tests/postgres/error.rs @@ -1,4 +1,4 @@ -use sqlx::{error::ErrorKind, postgres::Postgres, Connection}; +use sqlx::{error::ErrorKind, postgres::Postgres, Connection, Error}; use sqlx_test::new; #[sqlx_macros::test] @@ -74,3 +74,29 @@ async fn it_fails_with_check_violation() -> anyhow::Result<()> { Ok(()) } + +#[sqlx_macros::test] +async fn it_fails_with_begin_failed() -> anyhow::Result<()> { + let mut conn = new::().await?; + let res = conn.begin_with("SELECT * FROM tweet").await; + + let err = res.unwrap_err(); + + assert!(matches!(err, Error::BeginFailed), "{err:?}"); + + Ok(()) +} + +#[sqlx_macros::test] +async fn it_fails_with_invalid_save_point_statement() -> anyhow::Result<()> { + let mut conn = new::().await?; + let mut txn = conn.begin().await?; + let txn_conn = sqlx::Acquire::acquire(&mut txn).await?; + let res = txn_conn.begin_with("BEGIN").await; + + let err = res.unwrap_err(); + + assert!(matches!(err, Error::InvalidSavePointStatement), "{err}"); + + Ok(()) +} diff --git a/tests/postgres/postgres.rs b/tests/postgres/postgres.rs index 87a18db510..f0d453a9a3 100644 --- a/tests/postgres/postgres.rs +++ b/tests/postgres/postgres.rs @@ -3,13 +3,13 @@ use futures::{Stream, StreamExt, TryStreamExt}; use sqlx::postgres::types::Oid; use sqlx::postgres::{ PgAdvisoryLock, PgConnectOptions, PgConnection, PgDatabaseError, PgErrorPosition, PgListener, - PgPoolOptions, PgRow, PgSeverity, Postgres, + PgPoolOptions, PgRow, PgSeverity, Postgres, PG_COPY_MAX_DATA_LEN, }; use sqlx::{Column, Connection, Executor, Row, Statement, TypeInfo}; use sqlx_core::{bytes::Bytes, error::BoxDynError}; use sqlx_test::{new, pool, setup_if_needed}; use std::env; -use std::pin::Pin; +use std::pin::{pin, Pin}; use std::sync::Arc; use std::time::Duration; @@ -515,6 +515,7 @@ async fn it_can_work_with_transactions() -> anyhow::Result<()> { #[sqlx_macros::test] async fn it_can_work_with_nested_transactions() -> anyhow::Result<()> { let mut conn = new::().await?; + assert!(!conn.is_in_transaction()); conn.execute("CREATE TABLE IF NOT EXISTS _sqlx_users_2523 (id INTEGER PRIMARY KEY)") .await?; @@ -523,6 +524,7 @@ async fn it_can_work_with_nested_transactions() -> anyhow::Result<()> { // begin let mut tx = conn.begin().await?; // transaction + assert!(tx.is_in_transaction()); // insert a user sqlx::query("INSERT INTO _sqlx_users_2523 (id) VALUES ($1)") @@ -532,6 +534,7 @@ async fn it_can_work_with_nested_transactions() -> anyhow::Result<()> { // begin once more let mut tx2 = tx.begin().await?; // savepoint + assert!(tx2.is_in_transaction()); // insert another user sqlx::query("INSERT INTO _sqlx_users_2523 (id) VALUES ($1)") @@ -541,6 +544,7 @@ async fn it_can_work_with_nested_transactions() -> anyhow::Result<()> { // never mind, rollback tx2.rollback().await?; // roll that one back + assert!(tx.is_in_transaction()); // did we really? let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_2523") @@ -551,6 +555,7 @@ async fn it_can_work_with_nested_transactions() -> anyhow::Result<()> { // actually, commit tx.commit().await?; + assert!(!conn.is_in_transaction()); // did we really? let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_2523") @@ -637,8 +642,7 @@ async fn pool_smoke_test() -> anyhow::Result<()> { let pool = pool.clone(); sqlx_core::rt::spawn(async move { while !pool.is_closed() { - let acquire = pool.acquire(); - futures::pin_mut!(acquire); + let mut acquire = pin!(pool.acquire()); // poll the acquire future once to put the waiter in the queue future::poll_fn(move |cx| { @@ -813,6 +817,27 @@ async fn it_closes_statement_from_cache_issue_470() -> anyhow::Result<()> { Ok(()) } +#[sqlx_macros::test] +async fn it_closes_statements_when_not_persistent_issue_3850() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let _row = sqlx::query("SELECT $1 AS val") + .bind(Oid(1)) + .persistent(false) + .fetch_one(&mut conn) + .await?; + + let row = sqlx::query("SELECT count(*) AS num_prepared_statements FROM pg_prepared_statements") + .persistent(false) + .fetch_one(&mut conn) + .await?; + + let n: i64 = row.get("num_prepared_statements"); + assert_eq!(0, n, "no prepared statements should be open"); + + Ok(()) +} + #[sqlx_macros::test] async fn it_sets_application_name() -> anyhow::Result<()> { sqlx_test::setup_if_needed(); @@ -2042,3 +2067,78 @@ async fn test_issue_3052() { "expected encode error, got {too_large_error:?}", ); } + +#[sqlx_macros::test] +async fn test_pg_copy_chunked() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let mut row = "1".repeat(PG_COPY_MAX_DATA_LEN / 10 - 1); + row.push_str("\n"); + + // creates a payload with COPY_MAX_DATA_LEN + 1 as size + let mut payload = row.repeat(10); + payload.push_str("12345678\n"); + + assert_eq!(payload.len(), PG_COPY_MAX_DATA_LEN + 1); + + let mut copy = conn.copy_in_raw("COPY products(name) FROM STDIN").await?; + + assert!(copy.send(payload.as_bytes()).await.is_ok()); + assert!(copy.finish().await.is_ok()); + Ok(()) +} + +async fn test_copy_in_error_case(query: &str, expected_error: &str) -> anyhow::Result<()> { + let mut conn = new::().await?; + conn.execute("CREATE TEMPORARY TABLE IF NOT EXISTS invalid_copy_target (id int4)") + .await?; + // Try the COPY operation + match conn.copy_in_raw(query).await { + Ok(_) => anyhow::bail!("expected error"), + Err(e) => assert!( + e.to_string().contains(expected_error), + "expected error to contain: {expected_error}, got: {e:?}" + ), + } + // Verify connection is still usable + let value = sqlx::query("select 1 + 1") + .try_map(|row: PgRow| row.try_get::(0)) + .fetch_one(&mut conn) + .await?; + assert_eq!(2i32, value); + Ok(()) +} +#[sqlx_macros::test] +async fn it_can_recover_from_copy_in_to_missing_table() -> anyhow::Result<()> { + test_copy_in_error_case( + r#" + COPY nonexistent_table (id) FROM STDIN WITH (FORMAT CSV, HEADER); + "#, + "does not exist", + ) + .await +} +#[sqlx_macros::test] +async fn it_can_recover_from_copy_in_empty_query() -> anyhow::Result<()> { + test_copy_in_error_case("", "EmptyQuery").await +} +#[sqlx_macros::test] +async fn it_can_recover_from_copy_in_syntax_error() -> anyhow::Result<()> { + test_copy_in_error_case( + r#" + COPY FROM STDIN WITH (FORMAT CSV); + "#, + "syntax error", + ) + .await +} +#[sqlx_macros::test] +async fn it_can_recover_from_copy_in_invalid_params() -> anyhow::Result<()> { + test_copy_in_error_case( + r#" + COPY invalid_copy_target FROM STDIN WITH (FORMAT CSV, INVALID_PARAM true); + "#, + "invalid_param", + ) + .await +} diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 3f6c362043..d5d34bc1b3 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -2,6 +2,7 @@ extern crate time_ as time; use std::net::SocketAddr; use std::ops::Bound; +use std::str::FromStr; use sqlx::postgres::types::{Oid, PgCiText, PgInterval, PgMoney, PgRange}; use sqlx::postgres::Postgres; @@ -9,7 +10,6 @@ use sqlx_test::{new, test_decode_type, test_prepared_type, test_type}; use sqlx_core::executor::Executor; use sqlx_core::types::Text; -use std::str::FromStr; test_type!(null>(Postgres, "NULL::int2" == None:: @@ -171,6 +171,38 @@ test_type!(uuid_vec>(Postgres, ] )); +#[cfg(feature = "ipnet")] +test_type!(ipnet(Postgres, + "'127.0.0.1'::inet" + == "127.0.0.1/32" + .parse::() + .unwrap(), + "'8.8.8.8/24'::inet" + == "8.8.8.8/24" + .parse::() + .unwrap(), + "'10.1.1/24'::inet" + == "10.1.1.0/24" + .parse::() + .unwrap(), + "'::ffff:1.2.3.0'::inet" + == "::ffff:1.2.3.0/128" + .parse::() + .unwrap(), + "'2001:4f8:3:ba::/64'::inet" + == "2001:4f8:3:ba::/64" + .parse::() + .unwrap(), + "'192.168'::cidr" + == "192.168.0.0/24" + .parse::() + .unwrap(), + "'::ffff:1.2.3.0/120'::cidr" + == "::ffff:1.2.3.0/120" + .parse::() + .unwrap(), +)); + #[cfg(feature = "ipnetwork")] test_type!(ipnetwork(Postgres, "'127.0.0.1'::inet" @@ -232,6 +264,15 @@ test_type!(bitvec( }, )); +#[cfg(feature = "ipnet")] +test_type!(ipnet_vec>(Postgres, + "'{127.0.0.1,8.8.8.8/24}'::inet[]" + == vec![ + "127.0.0.1/32".parse::().unwrap(), + "8.8.8.8/24".parse::().unwrap() + ] +)); + #[cfg(feature = "ipnetwork")] test_type!(ipnetwork_vec>(Postgres, "'{127.0.0.1,8.8.8.8/24}'::inet[]" @@ -509,6 +550,44 @@ test_type!(line(Postgres, "line('((0.0, 0.0), (1.0,1.0))')" == sqlx::postgres::types::PgLine { a: 1., b: -1., c: 0. }, )); +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(lseg(Postgres, + "lseg('((1.0, 2.0), (3.0,4.0))')" == sqlx::postgres::types::PgLSeg { start_x: 1., start_y: 2., end_x: 3. , end_y: 4.}, +)); + +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(box(Postgres, + "box('((1.0, 2.0), (3.0,4.0))')" == sqlx::postgres::types::PgBox { upper_right_x: 3., upper_right_y: 4., lower_left_x: 1. , lower_left_y: 2.}, +)); + +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(_box>(Postgres, + "array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4))')]" @= vec![sqlx::postgres::types::PgBox { upper_right_x: 3., upper_right_y: 4., lower_left_x: 1., lower_left_y: 2. }, sqlx::postgres::types::PgBox { upper_right_x: 3.3, upper_right_y: 4.4, lower_left_x: 1.1, lower_left_y: 2.2 }], +)); + +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(path(Postgres, + "path('((1.0, 2.0), (3.0,4.0))')" == sqlx::postgres::types::PgPath { closed: true, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, + "path('[(1.0, 2.0), (3.0,4.0)]')" == sqlx::postgres::types::PgPath { closed: false, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, +)); + +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(polygon(Postgres, + "polygon('((-2,-3),(-1,-3),(-1,-1),(1,1),(1,3),(2,3),(2,-3),(1,-3),(1,0),(-1,0),(-1,-2),(-2,-2))')" ~= sqlx::postgres::types::PgPolygon { points: vec![ + sqlx::postgres::types::PgPoint { x: -2., y: -3. }, sqlx::postgres::types::PgPoint { x: -1., y: -3. }, sqlx::postgres::types::PgPoint { x: -1., y: -1. }, sqlx::postgres::types::PgPoint { x: 1., y: 1. }, + sqlx::postgres::types::PgPoint { x: 1., y: 3. }, sqlx::postgres::types::PgPoint { x: 2., y: 3. }, sqlx::postgres::types::PgPoint { x: 2., y: -3. }, sqlx::postgres::types::PgPoint { x: 1., y: -3. }, + sqlx::postgres::types::PgPoint { x: 1., y: 0. }, sqlx::postgres::types::PgPoint { x: -1., y: 0. }, sqlx::postgres::types::PgPoint { x: -1., y: -2. }, sqlx::postgres::types::PgPoint { x: -2., y: -2. }, + ]}, +)); + +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(circle(Postgres, + "circle('<(1.1, -2.2), 3.3>')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, + "circle('((1.1, -2.2), 3.3)')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, + "circle('(1.1, -2.2), 3.3')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, + "circle('1.1, -2.2, 3.3')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, +)); + #[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(), diff --git a/tests/sqlite/error.rs b/tests/sqlite/error.rs index 1f6b797e69..8729842b70 100644 --- a/tests/sqlite/error.rs +++ b/tests/sqlite/error.rs @@ -1,4 +1,4 @@ -use sqlx::{error::ErrorKind, sqlite::Sqlite, Connection, Executor}; +use sqlx::{error::ErrorKind, sqlite::Sqlite, Connection, Error, Executor}; use sqlx_test::new; #[sqlx_macros::test] @@ -70,3 +70,29 @@ async fn it_fails_with_check_violation() -> anyhow::Result<()> { Ok(()) } + +#[sqlx_macros::test] +async fn it_fails_with_begin_failed() -> anyhow::Result<()> { + let mut conn = new::().await?; + let res = conn.begin_with("SELECT * FROM tweet").await; + + let err = res.unwrap_err(); + + assert!(matches!(err, Error::BeginFailed), "{err:?}"); + + Ok(()) +} + +#[sqlx_macros::test] +async fn it_fails_with_invalid_save_point_statement() -> anyhow::Result<()> { + let mut conn = new::().await?; + let mut txn = conn.begin().await?; + let txn_conn = sqlx::Acquire::acquire(&mut txn).await?; + let res = txn_conn.begin_with("BEGIN").await; + + let err = res.unwrap_err(); + + assert!(matches!(err, Error::InvalidSavePointStatement), "{err}"); + + Ok(()) +} diff --git a/tests/sqlite/sqlite.rs b/tests/sqlite/sqlite.rs index b733ccbb4c..c23c4fc9ef 100644 --- a/tests/sqlite/sqlite.rs +++ b/tests/sqlite/sqlite.rs @@ -6,7 +6,9 @@ use sqlx::{ query, sqlite::Sqlite, sqlite::SqliteRow, Column, ConnectOptions, Connection, Executor, Row, SqliteConnection, SqlitePool, Statement, TypeInfo, }; +use sqlx_sqlite::LockedSqliteHandle; use sqlx_test::new; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; #[sqlx_macros::test] @@ -268,7 +270,7 @@ async fn it_handles_empty_queries() -> anyhow::Result<()> { } #[sqlx_macros::test] -fn it_binds_parameters() -> anyhow::Result<()> { +async fn it_binds_parameters() -> anyhow::Result<()> { let mut conn = new::().await?; let v: i32 = sqlx::query_scalar("SELECT ?") @@ -290,7 +292,7 @@ fn it_binds_parameters() -> anyhow::Result<()> { } #[sqlx_macros::test] -fn it_binds_dollar_parameters() -> anyhow::Result<()> { +async fn it_binds_dollar_parameters() -> anyhow::Result<()> { let mut conn = new::().await?; let v: (i32, i32) = sqlx::query_as("SELECT $1, $2") @@ -798,7 +800,7 @@ async fn test_multiple_set_progress_handler_calls_drop_old_handler() -> anyhow:: #[sqlx_macros::test] async fn test_query_with_update_hook() -> anyhow::Result<()> { let mut conn = new::().await?; - + static CALLED: AtomicBool = AtomicBool::new(false); // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. let state = format!("test"); conn.lock_handle().await?.set_update_hook(move |result| { @@ -807,11 +809,13 @@ async fn test_query_with_update_hook() -> anyhow::Result<()> { assert_eq!(result.database, "main"); assert_eq!(result.table, "tweet"); assert_eq!(result.rowid, 2); + CALLED.store(true, Ordering::Relaxed); }); let _ = sqlx::query("INSERT INTO tweet ( id, text ) VALUES ( 3, 'Hello, World' )") .execute(&mut conn) .await?; + assert!(CALLED.load(Ordering::Relaxed)); Ok(()) } @@ -852,10 +856,11 @@ async fn test_multiple_set_update_hook_calls_drop_old_handler() -> anyhow::Resul #[sqlx_macros::test] async fn test_query_with_commit_hook() -> anyhow::Result<()> { let mut conn = new::().await?; - + static CALLED: AtomicBool = AtomicBool::new(false); // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. let state = format!("test"); conn.lock_handle().await?.set_commit_hook(move || { + CALLED.store(true, Ordering::Relaxed); assert_eq!(state, "test"); false }); @@ -870,7 +875,7 @@ async fn test_query_with_commit_hook() -> anyhow::Result<()> { } _ => panic!("expected an error"), } - + assert!(CALLED.load(Ordering::Relaxed)); Ok(()) } @@ -916,8 +921,10 @@ async fn test_query_with_rollback_hook() -> anyhow::Result<()> { // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. let state = format!("test"); + static CALLED: AtomicBool = AtomicBool::new(false); conn.lock_handle().await?.set_rollback_hook(move || { assert_eq!(state, "test"); + CALLED.store(true, Ordering::Relaxed); }); let mut tx = conn.begin().await?; @@ -925,6 +932,7 @@ async fn test_query_with_rollback_hook() -> anyhow::Result<()> { .execute(&mut *tx) .await?; tx.rollback().await?; + assert!(CALLED.load(Ordering::Relaxed)); Ok(()) } @@ -960,3 +968,402 @@ async fn test_multiple_set_rollback_hook_calls_drop_old_handler() -> anyhow::Res assert_eq!(1, Arc::strong_count(&ref_counted_object)); Ok(()) } + +#[cfg(feature = "sqlite-preupdate-hook")] +#[sqlx_macros::test] +async fn test_query_with_preupdate_hook_insert() -> anyhow::Result<()> { + use sqlx::Decode; + + let mut conn = new::().await?; + static CALLED: AtomicBool = AtomicBool::new(false); + // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. + let state = format!("test"); + conn.lock_handle().await?.set_preupdate_hook({ + move |result| { + assert_eq!(state, "test"); + assert_eq!(result.operation, SqliteOperation::Insert); + assert_eq!(result.database, "main"); + assert_eq!(result.table, "tweet"); + + assert_eq!(4, result.get_column_count()); + assert_eq!(2, result.get_new_row_id().unwrap()); + assert_eq!(0, result.get_query_depth()); + assert_eq!( + 4, + >::decode(result.get_new_column_value(0).unwrap()).unwrap() + ); + assert_eq!( + "Hello, World", + >::decode(result.get_new_column_value(1).unwrap()) + .unwrap() + ); + // out of bounds access should return an error + assert!(result.get_new_column_value(4).is_err()); + // old values aren't available for inserts + assert!(result.get_old_column_value(0).is_err()); + assert!(result.get_old_row_id().is_err()); + + CALLED.store(true, Ordering::Relaxed); + } + }); + + let _ = sqlx::query("INSERT INTO tweet ( id, text ) VALUES ( 4, 'Hello, World' )") + .execute(&mut conn) + .await?; + + assert!(CALLED.load(Ordering::Relaxed)); + conn.lock_handle().await?.remove_preupdate_hook(); + let _ = sqlx::query("DELETE FROM tweet where id = 4") + .execute(&mut conn) + .await?; + Ok(()) +} + +#[cfg(feature = "sqlite-preupdate-hook")] +#[sqlx_macros::test] +async fn test_query_with_preupdate_hook_delete() -> anyhow::Result<()> { + use sqlx::Decode; + + let mut conn = new::().await?; + let _ = sqlx::query("INSERT INTO tweet ( id, text ) VALUES ( 5, 'Hello, World' )") + .execute(&mut conn) + .await?; + static CALLED: AtomicBool = AtomicBool::new(false); + // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. + let state = format!("test"); + conn.lock_handle().await?.set_preupdate_hook(move |result| { + assert_eq!(state, "test"); + assert_eq!(result.operation, SqliteOperation::Delete); + assert_eq!(result.database, "main"); + assert_eq!(result.table, "tweet"); + + assert_eq!(4, result.get_column_count()); + assert_eq!(2, result.get_old_row_id().unwrap()); + assert_eq!(0, result.get_query_depth()); + assert_eq!( + 5, + >::decode(result.get_old_column_value(0).unwrap()).unwrap() + ); + assert_eq!( + "Hello, World", + >::decode(result.get_old_column_value(1).unwrap()).unwrap() + ); + // out of bounds access should return an error + assert!(result.get_old_column_value(4).is_err()); + // new values aren't available for deletes + assert!(result.get_new_column_value(0).is_err()); + assert!(result.get_new_row_id().is_err()); + + CALLED.store(true, Ordering::Relaxed); + }); + + let _ = sqlx::query("DELETE FROM tweet WHERE id = 5") + .execute(&mut conn) + .await?; + assert!(CALLED.load(Ordering::Relaxed)); + Ok(()) +} + +#[cfg(feature = "sqlite-preupdate-hook")] +#[sqlx_macros::test] +async fn test_query_with_preupdate_hook_update() -> anyhow::Result<()> { + use sqlx::Decode; + use sqlx::{Value, ValueRef}; + + let mut conn = new::().await?; + let _ = sqlx::query("INSERT INTO tweet ( id, text ) VALUES ( 6, 'Hello, World' )") + .execute(&mut conn) + .await?; + static CALLED: AtomicBool = AtomicBool::new(false); + let sqlite_value_stored: Arc>> = Default::default(); + // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. + let state = format!("test"); + conn.lock_handle().await?.set_preupdate_hook({ + let sqlite_value_stored = sqlite_value_stored.clone(); + move |result| { + assert_eq!(state, "test"); + assert_eq!(result.operation, SqliteOperation::Update); + assert_eq!(result.database, "main"); + assert_eq!(result.table, "tweet"); + + assert_eq!(4, result.get_column_count()); + assert_eq!(4, result.get_column_count()); + + assert_eq!(2, result.get_old_row_id().unwrap()); + assert_eq!(2, result.get_new_row_id().unwrap()); + + assert_eq!(0, result.get_query_depth()); + assert_eq!(0, result.get_query_depth()); + + assert_eq!( + 6, + >::decode(result.get_old_column_value(0).unwrap()).unwrap() + ); + assert_eq!( + 6, + >::decode(result.get_new_column_value(0).unwrap()).unwrap() + ); + + assert_eq!( + "Hello, World", + >::decode(result.get_old_column_value(1).unwrap()) + .unwrap() + ); + assert_eq!( + "Hello, World2", + >::decode(result.get_new_column_value(1).unwrap()) + .unwrap() + ); + *sqlite_value_stored.lock().unwrap() = + Some(result.get_old_column_value(0).unwrap().to_owned()); + + // out of bounds access should return an error + assert!(result.get_old_column_value(4).is_err()); + assert!(result.get_new_column_value(4).is_err()); + + CALLED.store(true, Ordering::Relaxed); + } + }); + + let _ = sqlx::query("UPDATE tweet SET text = 'Hello, World2' WHERE id = 6") + .execute(&mut conn) + .await?; + + assert!(CALLED.load(Ordering::Relaxed)); + conn.lock_handle().await?.remove_preupdate_hook(); + let _ = sqlx::query("DELETE FROM tweet where id = 6") + .execute(&mut conn) + .await?; + // Ensure that taking an owned SqliteValue maintains a valid reference after the callback returns + assert_eq!( + 6, + >::decode( + sqlite_value_stored.lock().unwrap().take().unwrap().as_ref() + ) + .unwrap() + ); + Ok(()) +} + +#[cfg(feature = "sqlite-preupdate-hook")] +#[sqlx_macros::test] +async fn test_multiple_set_preupdate_hook_calls_drop_old_handler() -> anyhow::Result<()> { + let ref_counted_object = Arc::new(0); + assert_eq!(1, Arc::strong_count(&ref_counted_object)); + + { + let mut conn = new::().await?; + + let o = ref_counted_object.clone(); + conn.lock_handle().await?.set_preupdate_hook(move |_| { + println!("{o:?}"); + }); + assert_eq!(2, Arc::strong_count(&ref_counted_object)); + + let o = ref_counted_object.clone(); + conn.lock_handle().await?.set_preupdate_hook(move |_| { + println!("{o:?}"); + }); + assert_eq!(2, Arc::strong_count(&ref_counted_object)); + + let o = ref_counted_object.clone(); + conn.lock_handle().await?.set_preupdate_hook(move |_| { + println!("{o:?}"); + }); + assert_eq!(2, Arc::strong_count(&ref_counted_object)); + + conn.lock_handle().await?.remove_preupdate_hook(); + } + + assert_eq!(1, Arc::strong_count(&ref_counted_object)); + Ok(()) +} + +#[sqlx_macros::test] +async fn test_get_last_error() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let _ = sqlx::query("select 1").fetch_one(&mut conn).await?; + + { + let mut handle = conn.lock_handle().await?; + assert!(handle.last_error().is_none()); + } + + let _ = sqlx::query("invalid statement").fetch_one(&mut conn).await; + + { + let mut handle = conn.lock_handle().await?; + assert!(handle.last_error().is_some()); + } + + Ok(()) +} + +#[sqlx_macros::test] +async fn test_serialize_deserialize() -> anyhow::Result<()> { + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + sqlx::raw_sql("create table foo(bar integer not null, baz text not null)") + .execute(&mut conn) + .await?; + + sqlx::query("insert into foo(bar, baz) values (1234, 'Lorem ipsum'), (5678, 'dolor sit amet')") + .execute(&mut conn) + .await?; + + let serialized = conn.serialize(None).await?; + + // Close and open a new connection to ensure cleanliness. + conn.close().await?; + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + conn.deserialize(None, serialized, false).await?; + + let rows = sqlx::query_as::<_, (i32, String)>("select bar, baz from foo") + .fetch_all(&mut conn) + .await?; + + assert_eq!(rows.len(), 2); + + assert_eq!(rows[0].0, 1234); + assert_eq!(rows[0].1, "Lorem ipsum"); + + assert_eq!(rows[1].0, 5678); + assert_eq!(rows[1].1, "dolor sit amet"); + + Ok(()) +} +#[sqlx_macros::test] +async fn test_serialize_deserialize_with_schema() -> anyhow::Result<()> { + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + sqlx::raw_sql( + "attach ':memory:' as foo; create table foo.foo(bar integer not null, baz text not null)", + ) + .execute(&mut conn) + .await?; + + sqlx::query( + "insert into foo.foo(bar, baz) values (1234, 'Lorem ipsum'), (5678, 'dolor sit amet')", + ) + .execute(&mut conn) + .await?; + + let serialized = conn.serialize(Some("foo")).await?; + + // Close and open a new connection to ensure cleanliness. + conn.close().await?; + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + // Unexpected quirk: the schema must exist before deserialization. + sqlx::raw_sql("attach ':memory:' as foo") + .execute(&mut conn) + .await?; + + conn.deserialize(Some("foo"), serialized, false).await?; + + let rows = sqlx::query_as::<_, (i32, String)>("select bar, baz from foo.foo") + .fetch_all(&mut conn) + .await?; + + assert_eq!(rows.len(), 2); + + assert_eq!(rows[0].0, 1234); + assert_eq!(rows[0].1, "Lorem ipsum"); + + assert_eq!(rows[1].0, 5678); + assert_eq!(rows[1].1, "dolor sit amet"); + + Ok(()) +} + +#[sqlx_macros::test] +async fn test_serialize_nonexistent_schema() -> anyhow::Result<()> { + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + let err = conn + .serialize(Some("foobar")) + .await + .expect_err("an error should have been returned"); + + let sqlx::Error::Database(dbe) = err else { + panic!("expected DatabaseError: {err:?}") + }; + + assert_eq!(dbe.code().as_deref(), Some("1")); + assert_eq!(dbe.message(), "database foobar does not exist"); + + Ok(()) +} + +#[sqlx_macros::test] +async fn test_serialize_invalid_schema() -> anyhow::Result<()> { + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + let err = conn + .serialize(Some("foo\0bar")) + .await + .expect_err("an error should have been returned"); + + let sqlx::Error::InvalidArgument(msg) = err else { + panic!("expected InvalidArgument: {err:?}") + }; + + assert_eq!( + msg, + "schema name \"foo\\0bar\" contains a zero byte at index 3" + ); + + Ok(()) +} + +#[sqlx_macros::test] +async fn it_can_use_transaction_options() -> anyhow::Result<()> { + async fn check_txn_state(conn: &mut SqliteConnection, expected: SqliteTransactionState) { + let state = transaction_state(&mut conn.lock_handle().await.unwrap()); + assert_eq!(state, expected); + } + + let mut conn = SqliteConnectOptions::new() + .in_memory(true) + .connect() + .await + .unwrap(); + + check_txn_state(&mut conn, SqliteTransactionState::None).await; + + let mut tx = conn.begin_with("BEGIN DEFERRED").await?; + check_txn_state(&mut tx, SqliteTransactionState::None).await; + drop(tx); + + let mut tx = conn.begin_with("BEGIN IMMEDIATE").await?; + check_txn_state(&mut tx, SqliteTransactionState::Write).await; + drop(tx); + + let mut tx = conn.begin_with("BEGIN EXCLUSIVE").await?; + check_txn_state(&mut tx, SqliteTransactionState::Write).await; + drop(tx); + + Ok(()) +} + +fn transaction_state(handle: &mut LockedSqliteHandle) -> SqliteTransactionState { + use libsqlite3_sys::{sqlite3_txn_state, SQLITE_TXN_NONE, SQLITE_TXN_READ, SQLITE_TXN_WRITE}; + + let unchecked_state = + unsafe { sqlite3_txn_state(handle.as_raw_handle().as_ptr(), std::ptr::null()) }; + match unchecked_state { + SQLITE_TXN_NONE => SqliteTransactionState::None, + SQLITE_TXN_READ => SqliteTransactionState::Read, + SQLITE_TXN_WRITE => SqliteTransactionState::Write, + _ => panic!("unknown txn state: {unchecked_state}"), + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SqliteTransactionState { + None, + Read, + Write, +} diff --git a/tests/ui-tests.rs b/tests/ui-tests.rs index f74694b870..4a5ca240e1 100644 --- a/tests/ui-tests.rs +++ b/tests/ui-tests.rs @@ -17,7 +17,7 @@ fn ui_tests() { t.compile_fail("tests/ui/postgres/gated/uuid.rs"); } - if cfg!(not(feature = "ipnetwork")) { + if cfg!(not(feature = "ipnet")) && cfg!(not(feature = "ipnetwork")) { t.compile_fail("tests/ui/postgres/gated/ipnetwork.rs"); } }