diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 4b405dfa02..970afa7b37 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -26,15 +26,16 @@ jobs: - run:> cargo build -p sqlx-cli - --bin sqlx --release --no-default-features - --features mysql,postgres,sqlite,sqlx-toml + --features mysql,postgres,sqlite,sqlx-toml,mysql-rsa - uses: actions/upload-artifact@v4 with: name: sqlx-cli - path: target/release/sqlx + path: | + target/release/sqlx + target/release/cargo-sqlx mysql: name: MySQL Examples @@ -42,6 +43,10 @@ jobs: needs: sqlx-cli timeout-minutes: 30 + strategy: + matrix: + offline: ['', 'offline'] + services: mysql: image: mysql:latest @@ -60,7 +65,7 @@ jobs: - run: | ls -R /home/runner/.local/bin - chmod +x /home/runner/.local/bin/sqlx + chmod +x /home/runner/.local/bin/sqlx /home/runner/.local/bin/cargo-sqlx echo /home/runner/.local/bin>> $GITHUB_PATH sleep 10 @@ -77,9 +82,32 @@ jobs: DATABASE_URL: mysql://root:password@localhost:3306/todos?ssl-mode=disabled run: sqlx db setup + - name: Todos (Prepare) + if: ${{ matrix.offline }} + working-directory: examples/mysql/todos + env: + DATABASE_URL: mysql://root:password@localhost:3306/todos?ssl-mode=disabled + run: cargo sqlx prepare + + - name: Todos (Check Offline) + if: ${{ matrix.offline }} + run: | + cargo clean -p sqlx-example-mysql-todos + cargo check -p sqlx-example-mysql-todos + + - name: Todos (Prepare from .env) + if: ${{ matrix.offline }} + working-directory: examples/mysql/todos + run: | + echo "DATABASE_URL=mysql://root:password@localhost:3306/todos?ssl-mode=disabled"> .env + cargo clean -p sqlx-example-mysql-todos + cargo sqlx prepare + rm .env + - name: Todos (Run) env: DATABASE_URL: mysql://root:password@localhost:3306/todos?ssl-mode=disabled + SQLX_OFFLINE: ${{ matrix.offline == 'offline' }} run: cargo run -p sqlx-example-mysql-todos postgres: @@ -88,6 +116,10 @@ jobs: needs: sqlx-cli timeout-minutes: 30 + strategy: + matrix: + offline: ['', 'offline'] + services: postgres: image: postgres:latest @@ -106,6 +138,7 @@ jobs: - run: | ls -R /home/runner/.local/bin chmod +x $HOME/.local/bin/sqlx + chmod +x $HOME/.local/bin/cargo-sqlx echo $HOME/.local/bin>> $GITHUB_PATH sleep 10 @@ -120,14 +153,32 @@ jobs: DATABASE_URL: postgres://postgres:password@localhost:5432/axum-social run: sqlx db setup - - name: Axum Social with Tests (Check) + # Test `cargo sqlx prepare` setting `DATABASE_URL` both directly and in `.env` + # This doesn't need to be done for every single example here, but should at least cover potential problem cases. + - name: Axum Social with Tests (Prepare) + if: ${{ matrix.offline }} env: DATABASE_URL: postgres://postgres:password@localhost:5432/axum-social - run: cargo check -p sqlx-example-postgres-axum-social + run: cargo sqlx prepare -- -p sqlx-example-postgres-axum-social + + - name: Axum Social with Tests (Check Offline) + if: ${{ matrix.offline }} + run: | + cargo clean -p sqlx-example-postgres-axum-social + cargo check -p sqlx-example-postgres-axum-social + + - name: Axum Social with Tests (Prepare from .env) + if: ${{ matrix.offline }} + run: | + echo "DATABASE_URL=postgres://postgres:password@localhost:5432/axum-social"> .env + cargo clean -p sqlx-example-postgres-axum-social + cargo sqlx prepare -- -p sqlx-example-postgres-axum-social + rm .env - name: Axum Social with Tests (Test) env: DATABASE_URL: postgres://postgres:password@localhost:5432/axum-social + SQLX_OFFLINE: ${{ matrix.offline == 'offline' }} run: cargo test -p sqlx-example-postgres-axum-social # The Chat example has an interactive TUI which is not trivial to test automatically, @@ -190,11 +241,47 @@ jobs: (cd payments && sqlx db setup) sqlx db setup + - name: Multi-Database (Prepare) + if: ${{ matrix.offline }} + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database + ACCOUNTS_DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database-accounts + PAYMENTS_DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database-payments + run: | + cargo clean -p sqlx-example-postgres-multi-database-accounts + cargo clean -p sqlx-example-postgres-multi-database-payments + cargo clean -p sqlx-example-postgres-multi-database + # should include -accounts and -payments + cargo sqlx prepare -- -p sqlx-example-postgres-multi-database + + - name: Multi-Database (Check Offline) + if: ${{ matrix.offline }} + run: | + cargo clean -p sqlx-example-postgres-multi-database + cargo check -p sqlx-example-postgres-multi-database + + - name: Multi-Database (Prepare from .env) + if: ${{ matrix.offline }} + run: | + # Tried to get this to work with heredocs but had trouble writing tabs in YAML + echo 'DATABASE_URL=postgres://postgres:password@localhost:5432/multi-database'>.env + # Important: append, don't truncate + echo 'ACCOUNTS_DATABASE_URL=postgres://postgres:password@localhost:5432/multi-database-accounts'>> .env + echo 'PAYMENTS_DATABASE_URL=postgres://postgres:password@localhost:5432/multi-database-payments'>> .env + + cargo clean -p sqlx-example-postgres-multi-database-accounts + cargo clean -p sqlx-example-postgres-multi-database-payments + cargo clean -p sqlx-example-postgres-multi-database + cargo sqlx prepare -- -p sqlx-example-postgres-multi-database + + rm .env + - name: Multi-Database (Run) env: DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database ACCOUNTS_DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database-accounts PAYMENTS_DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database-payments + SQLX_OFFLINE: ${{ matrix.offline == 'offline' }} run: cargo run -p sqlx-example-postgres-multi-database - name: Multi-Tenant (Setup) @@ -206,9 +293,38 @@ jobs: (cd payments && sqlx migrate run) sqlx migrate run + - name: Multi-Tenant (Prepare) + if: ${{ matrix.offline }} + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/multi-tenant + run: | + cargo clean -p sqlx-example-postgres-multi-tenant-accounts + cargo clean -p sqlx-example-postgres-multi-tenant-payments + cargo clean -p sqlx-example-postgres-multi-tenant + # should include -accounts and -payments + cargo sqlx prepare -- -p sqlx-example-postgres-multi-tenant + + - name: Multi-Tenant (Check Offline) + if: ${{ matrix.offline }} + run: cargo check -p sqlx-example-postgres-multi-tenant + + - name: Multi-Tenant (Prepare from .env) + if: ${{ matrix.offline }} + run: | + echo "DATABASE_URL=postgres://postgres:password@localhost:5432/multi-tenant"> .env + + cargo clean -p sqlx-example-postgres-multi-tenant-accounts + cargo clean -p sqlx-example-postgres-multi-tenant-payments + cargo clean -p sqlx-example-postgres-multi-tenant + # should include -accounts and -payments + cargo sqlx prepare -- -p sqlx-example-postgres-multi-tenant + + rm .env + - name: Multi-Tenant (Run) env: DATABASE_URL: postgres://postgres:password@localhost:5432/multi-tenant + SQLX_OFFLINE: ${{ matrix.offline == 'offline' }} run: cargo run -p sqlx-example-postgres-multi-tenant - name: Preferred-Crates (Setup) @@ -217,7 +333,7 @@ jobs: DATABASE_URL: postgres://postgres:password@localhost:5432/preferred-crates run: sqlx db setup - - name: Multi-Tenant (Run) + - name: Preferred-Crates (Run) env: DATABASE_URL: postgres://postgres:password@localhost:5432/preferred-crates run: cargo run -p sqlx-example-postgres-preferred-crates @@ -275,7 +391,28 @@ jobs: DATABASE_URL: sqlite://todos.sqlite run: sqlx db setup --source=examples/sqlite/todos/migrations + - name: Todos (Prepare) + if: ${{ matrix.offline }} + env: + DATABASE_URL: sqlite://todos.sqlite + run: cargo sqlx prepare -- -p sqlx-example-sqlite-todos + + - name: Todos (Check Offline) + if: ${{ matrix.offline }} + run: | + cargo clean -p sqlx-example-sqlite-todos + cargo check -p sqlx-example-sqlite-todos + + - name: Todos (Prepare from .env) + if: ${{ matrix.offline }} + run: | + echo "DATABASE_URL=sqlite://todos.sqlite"> .env + cargo clean -p sqlx-example-sqlite-todos + cargo sqlx prepare -- -p sqlx-example-sqlite-todos + rm .env + - name: TODOs (Run) env: DATABASE_URL: sqlite://todos.sqlite + SQLX_OFFLINE: ${{ matrix.offline == 'offline' }} run: cargo run -p sqlx-example-sqlite-todos diff --git a/.github/workflows/sqlx-cli.yml b/.github/workflows/sqlx-cli.yml index 927616c69b..95c7a3edfc 100644 --- a/.github/workflows/sqlx-cli.yml +++ b/.github/workflows/sqlx-cli.yml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Rust run: | @@ -45,13 +45,13 @@ jobs: - ubuntu-latest # FIXME: migrations tests fail on Windows for whatever reason # - windows-latest - - macOS-13 + - macOS-15-intel - macOS-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Rust run: rustup show active-toolchain || rustup toolchain install @@ -76,7 +76,7 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Rust run: rustup show active-toolchain || rustup toolchain install @@ -156,7 +156,7 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Rust run: rustup show active-toolchain || rustup toolchain install @@ -228,7 +228,7 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Rust run: rustup show active-toolchain || rustup toolchain install @@ -302,7 +302,7 @@ jobs: os: - ubuntu-latest - windows-latest - - macOS-13 + - macOS-15-intel - macOS-latest include: - os: ubuntu-latest @@ -312,7 +312,7 @@ jobs: - os: windows-latest target: x86_64-pc-windows-msvc bin: target/debug/cargo-sqlx.exe - - os: macOS-13 + - os: macOS-15-intel target: x86_64-apple-darwin bin: target/debug/cargo-sqlx - os: macOS-latest @@ -322,7 +322,7 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Rust run: | diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index b2f81b75ad..50ef29dd92 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -7,13 +7,23 @@ on: - main - "*-dev" +env: + MYSQL_ISOLATED_TESTS: | + it_can_handle_split_packets + rustsec_2024_0363 + PG_ISOLATED_TESTS: | + test_pg_copy_chunked + rustsec_2024_0363 + SQLITE_ISOLATED_TESTS: | + rustsec_2024_0363 + jobs: format: name: Format runs-on: ubuntu-24.04 timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - run: rustup component add rustfmt - run: cargo fmt --all -- --check @@ -23,11 +33,14 @@ jobs: strategy: matrix: # Note: because `async-std` is deprecated, we only check it in a single job to save CI time. - runtime: [ async-std, async-global-executor, smol, tokio ] + runtime: [ async-global-executor, smol, tokio ] tls: [ native-tls, rustls, none ] + include: + - runtime: async-std + tls: rustls timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # Swatinem/rust-cache recommends setting up the rust toolchain first because it's used in cache keys - name: Setup Rust @@ -55,11 +68,11 @@ jobs: --target-dir target/beta/ check-minimal-versions: - name: Check build using minimal versions + name: Check build using direct minimal versions runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Rust run: | rustup show active-toolchain || rustup toolchain install @@ -72,7 +85,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # https://blog.rust-lang.org/2025/03/02/Rustup-1.28.0.html - name: Setup Rust @@ -128,7 +141,7 @@ jobs: needs: check timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - run: mkdir /tmp/sqlite3-lib && wget -O /tmp/sqlite3-lib/ipaddr.so https://github.com/nalgeon/sqlean/releases/download/0.15.2/ipaddr.so @@ -147,12 +160,17 @@ jobs: # Create data dir for offline mode - run: mkdir .sqlx - - run:> - cargo test - --no-default-features - --features any,macros,migrate,${{ matrix.linking }},_unstable-all-types,runtime-${{ matrix.runtime }},${{ matrix.linking == 'sqlite' && 'sqlite-preupdate-hook' || ''}} - -- - --test-threads=1 + - run: | + SKIP_ARGS=() + for test in $SQLITE_ISOLATED_TESTS; do + SKIP_ARGS+=(--skip "$test") + done + cargo test \ + --no-default-features \ + --features any,macros,migrate,${{ matrix.linking }},_unstable-all-types,runtime-${{ matrix.runtime }},${{ matrix.linking == 'sqlite' && 'sqlite-preupdate-hook' || ''}} \ + -- \ + "${SKIP_ARGS[@]}" \ + --test-threads=1 env: DATABASE_URL: sqlite:tests/sqlite/sqlite.db SQLX_OFFLINE_DIR: .sqlx @@ -203,6 +221,37 @@ jobs: RUSTFLAGS: --cfg sqlite_ipaddr LD_LIBRARY_PATH: /tmp/sqlite3-lib + sqlite-isolated: + name: SQLite Isolated + runs-on: ubuntu-24.04 + needs: check + timeout-minutes: 20 + steps: + - uses: actions/checkout@v5 + + - 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 + + - run: | + for test in $SQLITE_ISOLATED_TESTS; do + cargo test \ + --no-default-features \ + --features sqlite,macros,runtime-tokio \ + --test sqlite-rustsec \ + -- \ + --exact "$test" \ + --test-threads=1 + done + env: + DATABASE_URL: sqlite:tests/sqlite/sqlite.db + RUSTFLAGS: --cfg sqlite_ipaddr --cfg sqlite_test_sqlcipher + LD_LIBRARY_PATH: /tmp/sqlite3-lib + postgres: name: Postgres runs-on: ubuntu-24.04 @@ -214,7 +263,7 @@ jobs: needs: check timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Rust run: rustup show active-toolchain || rustup toolchain install @@ -237,15 +286,37 @@ jobs: # Create data dir for offline mode - run: mkdir .sqlx - - run:> - cargo test - --no-default-features - --features any,postgres,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + - run: | + SKIP_ARGS=() + for test in $PG_ISOLATED_TESTS; do + SKIP_ARGS+=(--skip "$test") + done + cargo test \ + --no-default-features \ + --features any,postgres,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} \ + -- \ + "${SKIP_ARGS[@]}" env: DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx SQLX_OFFLINE_DIR: .sqlx RUSTFLAGS: -D warnings --cfg postgres="${{ matrix.postgres }}" + # Run tests again with implied linux user + - run: | + SKIP_ARGS=() + for test in $PG_ISOLATED_TESTS; do + SKIP_ARGS+=(--skip "$test") + done + cargo test \ + --no-default-features \ + --features any,postgres,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} \ + -- \ + "${SKIP_ARGS[@]}" + env: + DATABASE_URL: postgres:///sqlx?password=runner-password + SQLX_OFFLINE_DIR: .sqlx + RUSTFLAGS: -D warnings --cfg postgres="${{ matrix.postgres }}" + # Run the `test-attr` test again to cover cleanup. - run:> cargo test @@ -258,10 +329,16 @@ jobs: RUSTFLAGS: -D warnings --cfg postgres="${{ matrix.postgres }}" - if: matrix.tls != 'none' - run:> - cargo test - --no-default-features - --features any,postgres,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + run: | + SKIP_ARGS=() + for test in $PG_ISOLATED_TESTS; do + SKIP_ARGS+=(--skip "$test") + done + cargo test \ + --no-default-features \ + --features any,postgres,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} \ + -- \ + "${SKIP_ARGS[@]}" env: DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx?sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt SQLX_OFFLINE_DIR: .sqlx @@ -304,7 +381,7 @@ jobs: needs: check timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Rust run: rustup show active-toolchain || rustup toolchain install @@ -317,14 +394,52 @@ jobs: - run: | docker exec postgres_${{ matrix.postgres }}_client_ssl bash -c "until pg_isready; do sleep 1; done" - - run:> - cargo test - --no-default-features - --features any,postgres,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + - run: | + SKIP_ARGS=() + for test in $PG_ISOLATED_TESTS; do + SKIP_ARGS+=(--skip "$test") + done + cargo test \ + --no-default-features \ + --features any,postgres,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} \ + -- \ + "${SKIP_ARGS[@]}" env: DATABASE_URL: postgres://postgres@localhost:5432/sqlx?sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt&sslkey=.%2Ftests%2Fcerts%2Fkeys%2Fclient.key&sslcert=.%2Ftests%2Fcerts%2Fclient.crt RUSTFLAGS: -D warnings --cfg postgres="${{ matrix.postgres }}" + postgres-isolated: + name: Postgres Isolated + runs-on: ubuntu-24.04 + needs: check + timeout-minutes: 20 + steps: + - uses: actions/checkout@v5 + + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + + - uses: Swatinem/rust-cache@v2 + + - run: | + docker compose -f tests/docker-compose.yml run -d -p 5432:5432 --name postgres_17 postgres_17 + docker exec postgres_17 bash -c "until pg_isready; do sleep 1; done" + + # Run isolated Postgres tests to avoid stalling the main job. + - run: | + for test in $PG_ISOLATED_TESTS; do + cargo test \ + --no-default-features \ + --features any,postgres,macros,_unstable-all-types,runtime-tokio,tls-none \ + --test postgres \ + -- \ + --exact "$test" \ + --test-threads=1 + done + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx + RUSTFLAGS: -D warnings --cfg postgres="17" + mysql: name: MySQL runs-on: ubuntu-24.04 @@ -336,7 +451,7 @@ jobs: needs: check timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Rust run: rustup show active-toolchain || rustup toolchain install @@ -346,15 +461,28 @@ jobs: - run: cargo build --features mysql,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} - run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 --name mysql_${{ matrix.mysql }} mysql_${{ matrix.mysql }} - - run: sleep 60 + - name: Wait for MySQL + run: | + docker exec mysql_${{ matrix.mysql }} bash -c ' + until (command -v mysqladmin>/dev/null && mysqladmin ping -uroot -ppassword --silent) || \ + (command -v mariadb-admin>/dev/null && mariadb-admin ping -uroot -ppassword --silent); do + sleep 2 + done + ' # Create data dir for offline mode - run: mkdir .sqlx - - run:> - cargo test - --no-default-features - --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + - run: | + SKIP_ARGS=() + for test in $MYSQL_ISOLATED_TESTS; do + SKIP_ARGS+=(--skip "$test") + done + cargo test \ + --no-default-features \ + --features any,mysql,mysql-rsa,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} \ + -- \ + "${SKIP_ARGS[@]}" env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx?ssl-mode=disabled SQLX_OFFLINE_DIR: .sqlx @@ -365,7 +493,7 @@ jobs: cargo test --test mysql-test-attr --no-default-features - --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,mysql-rsa,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 @@ -373,10 +501,27 @@ jobs: # MySQL 5.7 supports TLS but not TLSv1.3 as required by RusTLS. - if: ${{ !(matrix.mysql == '5_7' && matrix.tls == 'rustls') }} + run: | + SKIP_ARGS=() + for test in $MYSQL_ISOLATED_TESTS; do + SKIP_ARGS+=(--skip "$test") + done + cargo test \ + --no-default-features \ + --features any,mysql,${{ matrix.tls == 'none' && 'mysql-rsa,' || '' }}macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} \ + -- \ + "${SKIP_ARGS[@]}" + env: + DATABASE_URL: mysql://root:password@localhost:3306/sqlx + SQLX_OFFLINE_DIR: .sqlx + RUSTFLAGS: --cfg mysql_${{ matrix.mysql }} + + # Minimal coverage for mysql-rsa with TLS enabled; RSA should be inert when TLS is used. + - if: ${{ matrix.mysql == '8' && matrix.runtime == 'tokio' && matrix.tls == 'native-tls' }} run:> cargo test --no-default-features - --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,mysql-rsa,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx SQLX_OFFLINE_DIR: .sqlx @@ -402,7 +547,7 @@ jobs: cargo test --no-default-features --test mysql-macros - --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,${{ matrix.tls == 'none' && 'mysql-rsa,' || '' }}macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx SQLX_OFFLINE: true @@ -415,17 +560,66 @@ jobs: run: | docker stop mysql_${{ matrix.mysql }} docker compose -f tests/docker-compose.yml run -d -p 3306:3306 --name mysql_${{ matrix.mysql }}_client_ssl mysql_${{ matrix.mysql }}_client_ssl - sleep 60 + docker exec mysql_${{ matrix.mysql }}_client_ssl bash -c ' + until (command -v mysqladmin>/dev/null && mysqladmin ping -uroot --silent) || \ + (command -v mariadb-admin>/dev/null && mariadb-admin ping -uroot --silent); do + sleep 2 + done + ' - if: ${{ matrix.tls != 'none' }} - run:> - cargo test - --no-default-features - --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + run: | + SKIP_ARGS=() + for test in $MYSQL_ISOLATED_TESTS; do + SKIP_ARGS+=(--skip "$test") + done + cargo test \ + --no-default-features \ + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} \ + -- \ + "${SKIP_ARGS[@]}" env: DATABASE_URL: mysql://root@localhost:3306/sqlx?sslmode=verify_ca&ssl-ca=.%2Ftests%2Fcerts%2Fca.crt&ssl-key=.%2Ftests%2Fcerts%2Fkeys%2Fclient.key&ssl-cert=.%2Ftests%2Fcerts%2Fclient.crt RUSTFLAGS: --cfg mysql_${{ matrix.mysql }} + mysql-isolated: + name: MySQL Isolated + runs-on: ubuntu-24.04 + needs: check + timeout-minutes: 20 + steps: + - uses: actions/checkout@v5 + + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + + - uses: Swatinem/rust-cache@v2 + + - run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 --name mysql_8 mysql_8 + - name: Wait for MySQL + run: | + docker exec mysql_8 bash -c ' + until (command -v mysqladmin>/dev/null && mysqladmin ping -uroot -ppassword --silent) || \ + (command -v mariadb-admin>/dev/null && mariadb-admin ping -uroot -ppassword --silent); do + sleep 2 + done + ' + + # Run isolated MySQL tests to avoid stalling the main job. + - run: | + for test in $MYSQL_ISOLATED_TESTS; do + cargo test \ + --no-default-features \ + --features any,mysql,mysql-rsa,macros,migrate,_unstable-all-types,runtime-tokio,tls-none \ + --test mysql \ + -- \ + --exact "$test" \ + --test-threads=1 + done + env: + DATABASE_URL: mysql://root:password@localhost:3306/sqlx?ssl-mode=disabled + RUSTFLAGS: --cfg mysql_8 + mariadb: name: MariaDB runs-on: ubuntu-24.04 @@ -437,7 +631,7 @@ jobs: needs: check timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Rust run: rustup show active-toolchain || rustup toolchain install @@ -447,15 +641,28 @@ jobs: - run: cargo build --features mysql,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} - run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 --name mariadb_${{ matrix.mariadb }} mariadb_${{ matrix.mariadb }} - - run: sleep 30 + - name: Wait for MariaDB + run: | + docker exec mariadb_${{ matrix.mariadb }} bash -c ' + until (command -v mysqladmin>/dev/null && mysqladmin ping -uroot -ppassword --silent) || \ + (command -v mariadb-admin>/dev/null && mariadb-admin ping -uroot -ppassword --silent); do + sleep 2 + done + ' # Create data dir for offline mode - run: mkdir .sqlx - - run:> - cargo test - --no-default-features - --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + - run: | + SKIP_ARGS=() + for test in $MYSQL_ISOLATED_TESTS; do + SKIP_ARGS+=(--skip "$test") + done + cargo test \ + --no-default-features \ + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} \ + -- \ + "${SKIP_ARGS[@]}" env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx SQLX_OFFLINE_DIR: .sqlx @@ -504,13 +711,24 @@ jobs: run: | docker stop mariadb_${{ matrix.mariadb }} docker compose -f tests/docker-compose.yml run -d -p 3306:3306 --name mariadb_${{ matrix.mariadb }}_client_ssl mariadb_${{ matrix.mariadb }}_client_ssl - sleep 60 + docker exec mariadb_${{ matrix.mariadb }}_client_ssl bash -c ' + until (command -v mysqladmin>/dev/null && mysqladmin ping -uroot --silent) || \ + (command -v mariadb-admin>/dev/null && mariadb-admin ping -uroot --silent); do + sleep 2 + done + ' - if: ${{ matrix.tls != 'none' }} - run:> - cargo test - --no-default-features - --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + run: | + SKIP_ARGS=() + for test in $MYSQL_ISOLATED_TESTS; do + SKIP_ARGS+=(--skip "$test") + done + cargo test \ + --no-default-features \ + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} \ + -- \ + "${SKIP_ARGS[@]}" env: DATABASE_URL: mysql://root@localhost:3306/sqlx?sslmode=verify_ca&ssl-ca=.%2Ftests%2Fcerts%2Fca.crt&ssl-key=.%2Ftests%2Fcerts%2Fkeys%2Fclient.key&ssl-cert=.%2Ftests%2Fcerts%2Fclient.crt RUSTFLAGS: --cfg mariadb="${{ matrix.mariadb }}" diff --git a/.gitignore b/.gitignore index df28142027..f83ef5b774 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,11 @@ ipaddr.so # Temporary files from running the tests locally like they would be run from CI .sqlx + +# Checking-in a `Cargo.lock` is not that helpful for a published package: +# * Requires manual `cargo update` frequently to test with new versions +# * `cargo install sqlx-cli` uses latest deps anyways unless `--locked` is passed +# * Libraries don't really need reproducible builds +# * Should also check with `-Z direct-minimal-versions` (which our CI does) +# * PRs which add/remove/update deps cause churn which results in annyoing merge conflicts +Cargo.lock \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9036a38d09..237c1d6a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,20 +5,36 @@ 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.9.0-alpha.1 - 2025年05月19日 +## 0.9.0 - 2026年05月06日 -Accumulated changes since the beginning of the alpha cycle. Effectively a draft CHANGELOG for the 0.9.0 release. +### Important Announcements -This section will be replaced in subsequent alpha releases. See the Git history of this file for previous alphas. +#### New Github Organization +Shortly after this release is published, the SQLx repository will be transferred to a new GitHub organization: +https://github.com/transact-rs/ + +This is because SQLx has not been owned or maintained by LaunchBadge, LLC. for a few years now, and has since been +informally transferred to the collective ownership of its principal authors. Moving the repository to a new +organization makes this change more clear, and also allows for potentially inviting outside collaborators. + +#### `Cargo.lock` Removed from Tracking +The `Cargo.lock` has been removed from tracking in Git. CI should now always test with the latest versions of +all dependencies by default, alongside our pass that checks with `cargo generate-lockfile -Z minimal-versions`. + +This should eliminate the need for any PRs that update dependencies to also update `Cargo.lock` or +contend with an endless stream of merge conflicts against it. + +**N.B.** `cargo install --locked sqlx-cli` will no longer work. However, `cargo install sqlx-cli` has _always_ +used the latest dependencies by default, ignoring the lockfile, so most users should not be affected. For users +requiring reproducible builds, consider maintaining your own lockfile instead; historically, we only ran `cargo update` +sporadically, so relying on SQLx's lockfile offered few guarantees anyway. + +See [the manual page for `cargo install`][man-cargo-install] for details. ### Breaking +As per our [MSRV policy](FAQ.md#MSRV), the supported Rust version for this release cycle is [`1.94.0`](https://doc.rust-lang.org/stable/releases.html#version-1940-2026年03月05日). + -* [[#3821]]: Groundwork for 0.9.0-alpha.1 [[@abonander]] - * Increased MSRV to 1.86 and set rust-version - * Deleted deprecated combination runtime+TLS features (e.g. `runtime-tokio-native-tls`) - * Deleted re-export of unstable `TransactionManager` trait in `sqlx`. - * Not technically a breaking change because it's `#[doc(hidden)]`, - but [it _will_ break SeaORM][seaorm-2600] if not proactively fixed. * [[#3383]]: feat: create `sqlx.toml` format [[@abonander]] * SQLx and `sqlx-cli` now support per-crate configuration files (`sqlx.toml`) * New functionality includes, but is not limited to: @@ -42,13 +58,324 @@ This section will be replaced in subsequent alpha releases. See the Git history * **Breaking changes**: * Significant changes to the `Migrate` trait * `sqlx::migrate::resolve_blocking()` is now `#[doc(hidden)]` and thus SemVer-exempt. +* [[#3486]]: fix(logs): Correct spelling of aquired_after_secs tracing field [[@iamjpotts]] + * Breaking behavior change: implementations parsing `tracing` logs from SQLx will need to update the spelling. +* [[#3495]]: feat(postgres): remove lifetime from `PgAdvisoryLockGuard` [[@bonsairobo]] +* [[#3526]]: Return &mut Self from the migrator set_ methods [[@nipunn1313]] + * Minor breaking change: `Migrator::set_ignore_missing` and `set_locking` now return `&mut Self` instead of `&Self` + which may break code in rare circumstances. +* [[#3541]]: Postgres: force generic plan for better nullability inference. [[@joeydewaal]] + * Breaking change: may alter the output of the `query!()` macros for certain queries in Postgres. +* [[#3613]]: fix: `RawSql` lifetime issues [[@abonander]] + * Breaking change: adds `DB` type parameter to all methods of `RawSql` +* [[#3670]]: Bump ipnetwork to v0.21.1 [[@BeauGieskens]] +* [[#3674]]: Implement `Decode`, `Encode` and `Type` for `Box`, `Arc`, `Cow` and `Rc` [[@joeydewaal]] + * Breaking change: `impl Decode for Cow` now always decodes `Cow::Owned`, lifetime is unlinked + * See this discussion for motivation: https://github.com/launchbadge/sqlx/pull/3674#discussion_r2008611502 +* [[#3723]]: Add SqlStr [[@joeydewaal]] + * Breaking change: all `query*()` functions now take `impl SqlSafeStr` + which is only implemented for `&'static str` and `AssertSqlSafe`. + For all others, wrap in `AssertSqlSafe()`. + * This, along with [[#3960]], finally allows returning owned queries as the type will be `Query<'static, DB>`. + * `SqlSafeStr` trait is deliberately similar to `std::panic::UnwindSafe`, + serving as a speedbump to warn users about naïvely building queries with `format!()` + while allowing a workaround for advanced usage that is easy to spot on code review. +* [[#3800]]: Escape PostgreSQL Options [[@V02460]] + * Breaking behavior change: options passed to `PgConnectOptions::options()` are now automatically escaped. + Manual escaping of options is no longer necessary and may cause incorrect behavior. +* [[#3821]]: Groundwork for 0.9.0-alpha.1 [[@abonander]] + * Increased MSRV to 1.86 and set rust-version + * Deleted deprecated combination runtime+TLS features (e.g. `runtime-tokio-native-tls`) + * Deleted re-export of unstable `TransactionManager` trait in `sqlx`. + * Not technically a breaking change because it's `#[doc(hidden)]`, + but [it _will_ break SeaORM][seaorm-2600] if not proactively fixed. +* [[#3924]]: breaking(mysql): assume all non-binary collations compatible with `str` [[@abonander]] + * Text (or text-like) columns which previously were inferred to be `Vec` will be inferred to be `String` + (this should ultimately fix more code than it breaks). + * `SET NAMES utf8mb4 COLLATE utf8_general_ci` is no longer sent by default; instead, `SET NAMES utf8mb4` is sent to + allow the server to select the appropriate default collation (since this is version- and configuration-dependent). + * `MySqlConnectOptions::charset()` and `::collation()` now imply `::set_names(true)` because they don't do anything otherwise. + * Setting `charset` doesn't change what's sent in the `Protocol::HandshakeResponse41` packet as that normally only + matters for error messages before `SET NAMES` is sent. + The default collation if `set_names = false` is `utf8mb4_general_ci`. + * See [this comment](https://github.com/launchbadge/sqlx/blob/388c424f486bf20542a8a37d296dbcf86bb6dffd/sqlx-mysql/src/collation.rs#L1-L37) for details. + * Incidental breaking change: `RawSql::fetch_optional()` now returns `sqlx::Result>` + instead of `sqlx::Result`. Whoops. +* [[#3928]]: breaking(sqlite): `libsqlite3-sys` versioning, feature flags, safety changes [[@abonander]] + * SemVer policy changes: `libsqlite3-sys` version is now specified using a range. + The maximum of the range may now be increased in any backwards-compatible release. + The minimum of the range may only be increased in major releases. + If you have `libsqlite3-sys` in your dependencies, Cargo should choose a compatible version automatically. + If otherwise unconstrained, Cargo should choose the latest version supported. + * SQLite extension loading (including through the new `sqlx-toml` feature) is now `unsafe`. + * Added new **non-default** features corresponding to conditionally compiled SQLite APIs: + * `sqlite-deserialize` enabling `SqliteConnection::serialize()` and `SqliteConnection::deserialize()` + * `sqlite-load-extension` enabling `SqliteConnectOptions::extension()` and `::extension_with_entrypoint()` + * `sqlite-unlock-notify` enables internal use of `sqlite3_unlock_notify()` + * `SqliteValue` and `SqliteValueRef` changes: + * The [`sqlite3_value*` interface](https://www.sqlite.org/c3ref/value_blob.html) reserves the right to be stateful. + Without protection, any call could theoretically invalidate values previously returned, leading to dangling pointers. + * `SqliteValue` is now `!Sync` and `SqliteValueRef` is `!Send` to prevent data races from concurrent accesses. + * Instead, clone or wrap the `SqliteValue` in `Mutex`, or convert the `SqliteValueRef` to an owned value. + * `SqliteValue` and any derived `SqliteValueRef`s now internally track if that value has been used to decode a + borrowed `&[u8]` or `&str` and errors if it's used to decode any other type. + * This is not expected to affect the vast majority of usages, which should only decode a single type + per `SqliteValue`/`SqliteValueRef`. + * See new docs on `SqliteValue` for details. +* [[#3949]]: Postgres: move `PgLTree::from` to `From>` implementation [[@JerryQ17]] +* [[#3957]]: refactor(sqlite): do not borrow bound values, delete lifetime on `SqliteArguments` [[@iamjpotts]] +* [[#3958]]: refactor(any): Remove lifetime parameter from AnyArguments [[@iamjpotts]] +* [[#3960]]: refactor(core): Remove lifetime parameter from Arguments trait [[@iamjpotts]] +* [[#3993]]: Unescape PostgreSQL passfile password [[@V02460]] + * Previously, `.pgpass` file handling did not process backslash-escapes in the password part. + Now it does, which may change what password is sent to the server. +* [[#4008]]: make `#[derive(sqlx::Type)]` automatically generate `impl PgHasArrayType` by default for newtype structs [[@papaj-na-wrotkach]] + * Manual implementations of PgHasArrayType for newtypes will conflict with the generated one. + Delete the manual impl or add `#[sqlx(no_pg_array)]` where conflicts occur. +* [[#4077]]: breaking: make `offline` optional to allow building without `serde` [[@CathalMullan]] +* [[#4094]]: Bump bit-vec to v0.8 [[@zennozenith]] +* [[#4142]]: feat(mysql): add mysql-rsa feature for non-TLS RSA auth [[@dertin]] + * Connections requiring RSA password encryption now need to enable the `mysql-rsa` feature + or an error will be generated at runtime. RSA encryption is only used for plaintext (non-TLS) connections. +* [[#4255]]: breaking(any+mysql): correctly convert text and blob types to `AnyTypeInfo` [[@abonander]] + +### Added +* [[#3641]]: feat(Postgres): support nested domain types [[@joeydewaal]] +* [[#3651]]: Add PgBindIter for encoding and use it as the implementation encoding &[T] [[@tylerhawkes]] +* [[#3675]]: feat: implement Encode, Decode, Type for `Arc` and `Arc<[u8]>` (and `Rc` equivalents) [[@joeydewaal]] +* [[#3791]]: Smol+async global executor 1.80 dev [[@martin-kolarik]] + * Adds `runtime-smol` and `runtime-async-global-executor` features to replace usages of the deprecated `async-std` crate. +* [[#3859]]: Add more JsonRawValue encode/decode impls. [[@Dirbaio]] +* [[#3881]]: CLi: made cli-lib modules publicly available for other crates [[@silvestrpredko]] +* [[#3889]]: Compile-time support for external drivers [[@bobozaur]] +* [[#3917]]: feat(sqlx.toml): support SQLite extensions in macros and sqlx-cli [[@djarb]] +* [[#3918]]: Feature: Add exclusion violation error kind [[@barskern]] +* [[#3971]]: Allow single-field named structs to be transparent [[@Xiretza]] +* [[#4015]]: feat(sqlite): `no_tx` migration support [[@AlexTMjugador]] +* [[#4020]]: Add `Migrator::with_migrations()` constructor [[@xb284524239]] +* [[#3846]]: Add the possibility to skip migrations [[@Dosenpfand]] +* [[#4107]]: Add SQLite extension entrypoint config to `sqlx.toml`, update SQLite extension example [[@supleed2]] +* [[#4118]]: [postgres] Display line number in error message [[@mousetail]] +* [[#4123]]: feat: add `Json::into_inner()` [[@chrxn1c]] +* [[#4153]]: Add on unimplemented diagnostic to `SqlStr` [[@joeydewaal]] +* [[#4167]]: add sqlite serialize/deserialize example [[@mattrighetti]] +* [[#4228]]: sqlx-postgres: Make `PgNotification` struct clone [[@michaelvanstraten]] + + +### Changed +* [[#3525]]: Remove unnecessary boxfutures [[@joeydewaal]] +* [[#3867]]: sqlx-postgres: Bump etcetera to 0.10.0 [[@miniduikboot]] +* [[#3709]]: chore: replace once_cell `OnceCell`/`Lazy` with std `OnceLock`/`LazyLock` [[@paolobarbolini]] +* [[#3890]]: feat: Unify `Debug` implementations across `PgRow`, `MySqlRow` and `SqliteRow` [[@davidcornu]] +* [[#3911]]: chore: upgrade async-io to v2.4.1 [[@zebrapurring]] +* [[#3938]]: Move `QueryLogger` back [[@joeydewaal]] +* [[#3956]]: chore(sqlite): Remove unused test of removed git2 feature [[@iamjpotts]] +* [[#3962]]: Give SQLX_OFFLINE_DIR from environment precedence in macros [[@psionic-k]] +* [[#3968]]: chore(ci): Add timeouts to ci jobs [[@iamjpotts]] +* [[#4002]]: sqlx-postgres(tests): cleanup 2 unit tests. [[@joeydewaal]] +* [[#4022]]: refactor: tweaks after #3791 [[@abonander]] +* [[#4257]]: Prefer to give real data to `.bind()` in `README.md` [[@sobolevn]] +* [[#4042]]: Update to webpki-roots 1 [[@tottoto]] +* [[#4072]]: chore: update hashlink to v0.11.0 [[@anmolitor]] +* [[#4143]]: Bump whoami to v2 [[@tisonkun]] +* [[#4161]]: sqlx-sqlite: relax libsqlite3-sys constraint to allow 0.36.x [[@darioAnongba]] +* [[#4173]]: ci: check direct minimal versions [[@ricochet]] + * Note: reverted in 0.9.0 release but still listed for contributor credit. See end of PR thread for details. +* [[#4189]]: Bump flume to 0.12.0 [[@opoplawski]] +* [[#4223]]: test(sqlite): add regression test for ORDER BY + LIMIT nullability (#4147) [[@barry3406]] +* [[#4230]]: chore: Update to cargo_metadata 0.23 [[@tottoto]] +* [[#4233]]: Change reference to dotenvy [[@graemer957]] +* [[#4235]]: chore: Update to validator 0.20 [[@tottoto]] +* [[#4253]]: chore: update example to axum 0.8 [[@tottoto]] +* Release PR: + * Upgraded all Rust-Crypto crates, `rand` + * Upgraded `etcetera` to `0.11.0` + * Increased max of `libsqlite3-sys` version range to `<0.38.0` + + +### Fixed +* [[#3840]]: Fix docs.rs build of sqlx-sqlite [[@gferon]] +* [[#3848]]: fix(macros): don't mutate environment variables [[@joeydewaal]] +* [[#3856]]: fix(macros): slightly improve unsupported type error message [[@dyc3]] +* [[#3857]]: fix(mysql): validate parameter count for prepared statements [[@cvzx]] +* [[#3861]]: Fix NoHostnameTlsVerifier for rustls 0.23.24 and above [[@elichai]] +* [[#3863]]: Use unnamed statement in pg when not persistent [[@ThomWright]] +* [[#3874]]: Further reduce dependency on `futures` and `futures-util` [[@paolobarbolini]] +* [[#3886]]: fix: use Executor::fetch in QueryAs::fetch [[@bobozaur]] +* [[#3910]]: feat(ok): add correct handling of ok packets in MYSQL implementation [[@0xfourzerofour]] +* [[#3914]]: fix: regenerate test certificates [[@abonander]] +* [[#3915]]: fix: spec_error is used by try_from derive [[@saiintbrisson]] +* [[#3919]]: fix[sqlx-postgres]: do a checked_mul to prevent panic'ing [[@nhatcher-frequenz]] +* [[#3923]]: sqlx-mysql: Fix bug in cleanup test db's. [[@joeydewaal]] +* [[#3950]]: chore: Fix warnings for custom postgres_## cfg flags [[@iamjpotts]] +* [[#3952]]: `Pool.close`: close all connections before returning [[@jpmelos]] +* [[#3975]]: fix documentation for rustls native root certificates [[@2ndDerivative]] +* [[#3977]]: refactor(ci): Use separate job for postgres ssl auth tests [[@iamjpotts]] +* [[#3980]]: Correctly `ROLLBACK` transaction when dropped during `BEGIN`. [[@kevincox]] +* [[#3981]]: SQLite: fix transaction level accounting with bad custom command. [[@kevincox]] +* [[#3986]]: chore(core): Fix docstring for Query::try_bind [[@iamjpotts]] +* [[#3987]]: chore(deps): Resolve deprecation warning for chrono Date and ymd methods [[@iamjpotts]] +* [[#3988]]: refactor(sqlite): Resolve duplicate test target warning for macros.rs [[@iamjpotts]] +* [[#3989]]: chore(deps): Set default-features=false on sqlx in workspace.dependencies [[@iamjpotts]] +* [[#3991]]: fix(sqlite): regression when decoding nulls [[@abonander]] +* [[#4006]]: PostgreSQL SASL – run SHA256 in a blocking executor [[@ThomWright]] +* [[#4007]]: fix(compose): use OS-assigned ports for all conatiners [[@papaj-na-wrotkach]] +* [[#4009]]: Drop cached db connections in macros upon hitting an error [[@swlynch99]] +* [[#4024]]: fix(sqlite) Migrate revert with no-transaction [[@Dosenpfand]] +* [[#4027]]: native tls handshake: build TlsConnector in blocking threadpool [[@daviduebler]] +* [[#4053]]: fix(macros): smarter `.env` loading, caching, and invalidation [[@abonander]] + * Additional credit to [[@AlexTMjugador]] ([[#4018]]) and [[@Diggsey]] ([[#4039]]) for their proposed solutions + which served as a useful comparison. +* [[#4068]]: Fix typo in migration example from 'uesrs' to 'users' [[@squidpickles]] +* [[#4069]]: fix some spelling issues [[@joeydewaal]] +* [[#4086]]: fix(mysql): Work around for Issue #2206 (ColumnNotFound error when querying) [[@duelafn]] +* [[#4088]]: (Fix) Handle nullability of SQLite rowid alias columns [[@Lege19]] +* [[#4100]]: postgres: update pgpass path on windows [[@joeydewaal]] +* [[#4134]]: fix CI: replace removed macOS runner, deprecated use of `Command::cargo_bin()` [[@abonander]] +* [[#4136]]: Ensure Deterministic Migration Order [[@aoengin]] +* [[#4158]]: Fix panic in JSONB decoder on invalid version byte [[@jrey8343]] +* [[#4165]]: sqlx-postgres: fix correct operator precedence in byte length check [[@cuiweixie]] +* [[#4171]]: fix(postgres): remove home crate in favor of std::env::home_dir [[@ricochet]] +* [[#4172]]: fix(sqlx-cli): bump openssl minimum to 0.10.46 [[@ricochet]] +* [[#4176]]: fix(mysql): return error instead of panic on truncated OK packet [[@cvzx]] +* [[#4199]]: fix(postgres): make advisory lock cancel safe [[@joeydewaal]] +* [[#4201]]: Fix SCRAM password `SASLprep` [[@var4yn]] +* [[#4202]]: fix: replace from_utf8_unchecked with from_utf8_lossy in SqliteError [[@joaquinhuigomez]] +* [[#4203]]: fix: use sqlite3_value_text for REGEXP to match SQLite coercion [[@joaquinhuigomez]] +* [[#4219]]: sqlite: lossily coerce invalid UTF-8 in custom collation callback [[@joaquinhuigomez]] +* [[#4221]]: fix: replace `from_utf8_unchecked` with `from_utf8` in SQLite column name handling [[@barry3406]] +* [[#4226]]: fix(postgres): use non-prepared statements for metadata queries [[@abonander]] +* [[#4227]]: fix(macros-core): update unstable proc_macro APIs for recent nightly [[@barry3406]] +* [[#4234]]: fix: Use correct path in error when failing to create tmp dir in prepare [[@Miesvanderlippe]] +* [[#4245]]: fix(mysql): repair caching_sha2_password fast-auth path [[@altmannmarcelo]] +* [[#4251]]: fix(tls): potential deadlock in `StdSocket::poll_ready()` [[@abonander]] [seaorm-2600]: https://github.com/SeaQL/sea-orm/issues/2600 [feature unification]: https://doc.rust-lang.org/cargo/reference/features.html#feature-unification [preferred-crates]: examples/postgres/preferred-crates +[man-cargo-install]: https://doc.rust-lang.org/cargo/commands/cargo-install.html#dealing-with-the-lockfile [#3821]: https://github.com/launchbadge/sqlx/pull/3821 [#3383]: https://github.com/launchbadge/sqlx/pull/3383 +[#3486]: https://github.com/launchbadge/sqlx/pull/3486 +[#3495]: https://github.com/launchbadge/sqlx/pull/3495 +[#3525]: https://github.com/launchbadge/sqlx/pull/3525 +[#3526]: https://github.com/launchbadge/sqlx/pull/3526 +[#3541]: https://github.com/launchbadge/sqlx/pull/3541 +[#3613]: https://github.com/launchbadge/sqlx/pull/3613 +[#3641]: https://github.com/launchbadge/sqlx/pull/3641 +[#3651]: https://github.com/launchbadge/sqlx/pull/3651 +[#3670]: https://github.com/launchbadge/sqlx/pull/3670 +[#3674]: https://github.com/launchbadge/sqlx/pull/3674 +[#3675]: https://github.com/launchbadge/sqlx/pull/3675 +[#3709]: https://github.com/launchbadge/sqlx/pull/3709 +[#3723]: https://github.com/launchbadge/sqlx/pull/3723 +[#3791]: https://github.com/launchbadge/sqlx/pull/3791 +[#3800]: https://github.com/launchbadge/sqlx/pull/3800 +[#3821]: https://github.com/launchbadge/sqlx/pull/3821 +[#3840]: https://github.com/launchbadge/sqlx/pull/3840 +[#3848]: https://github.com/launchbadge/sqlx/pull/3848 +[#3856]: https://github.com/launchbadge/sqlx/pull/3856 +[#3857]: https://github.com/launchbadge/sqlx/pull/3857 +[#3859]: https://github.com/launchbadge/sqlx/pull/3859 +[#3861]: https://github.com/launchbadge/sqlx/pull/3861 +[#3863]: https://github.com/launchbadge/sqlx/pull/3863 +[#3867]: https://github.com/launchbadge/sqlx/pull/3867 +[#3874]: https://github.com/launchbadge/sqlx/pull/3874 +[#3881]: https://github.com/launchbadge/sqlx/pull/3881 +[#3886]: https://github.com/launchbadge/sqlx/pull/3886 +[#3889]: https://github.com/launchbadge/sqlx/pull/3889 +[#3890]: https://github.com/launchbadge/sqlx/pull/3890 +[#3910]: https://github.com/launchbadge/sqlx/pull/3910 +[#3911]: https://github.com/launchbadge/sqlx/pull/3911 +[#3914]: https://github.com/launchbadge/sqlx/pull/3914 +[#3915]: https://github.com/launchbadge/sqlx/pull/3915 +[#3917]: https://github.com/launchbadge/sqlx/pull/3917 +[#3918]: https://github.com/launchbadge/sqlx/pull/3918 +[#3919]: https://github.com/launchbadge/sqlx/pull/3919 +[#3923]: https://github.com/launchbadge/sqlx/pull/3923 +[#3924]: https://github.com/launchbadge/sqlx/pull/3924 +[#3928]: https://github.com/launchbadge/sqlx/pull/3928 +[#3938]: https://github.com/launchbadge/sqlx/pull/3938 +[#3949]: https://github.com/launchbadge/sqlx/pull/3949 +[#3950]: https://github.com/launchbadge/sqlx/pull/3950 +[#3952]: https://github.com/launchbadge/sqlx/pull/3952 +[#3956]: https://github.com/launchbadge/sqlx/pull/3956 +[#3957]: https://github.com/launchbadge/sqlx/pull/3957 +[#3958]: https://github.com/launchbadge/sqlx/pull/3958 +[#3960]: https://github.com/launchbadge/sqlx/pull/3960 +[#3962]: https://github.com/launchbadge/sqlx/pull/3962 +[#3968]: https://github.com/launchbadge/sqlx/pull/3968 +[#3971]: https://github.com/launchbadge/sqlx/pull/3971 +[#3975]: https://github.com/launchbadge/sqlx/pull/3975 +[#3977]: https://github.com/launchbadge/sqlx/pull/3977 +[#3980]: https://github.com/launchbadge/sqlx/pull/3980 +[#3981]: https://github.com/launchbadge/sqlx/pull/3981 +[#3986]: https://github.com/launchbadge/sqlx/pull/3986 +[#3987]: https://github.com/launchbadge/sqlx/pull/3987 +[#3988]: https://github.com/launchbadge/sqlx/pull/3988 +[#3989]: https://github.com/launchbadge/sqlx/pull/3989 +[#3991]: https://github.com/launchbadge/sqlx/pull/3991 +[#4002]: https://github.com/launchbadge/sqlx/pull/4002 +[#4006]: https://github.com/launchbadge/sqlx/pull/4006 +[#4007]: https://github.com/launchbadge/sqlx/pull/4007 +[#4008]: https://github.com/launchbadge/sqlx/pull/4008 +[#4009]: https://github.com/launchbadge/sqlx/pull/4009 +[#4015]: https://github.com/launchbadge/sqlx/pull/4015 +[#4018]: https://github.com/launchbadge/sqlx/pull/4018 +[#4020]: https://github.com/launchbadge/sqlx/pull/4020 +[#4022]: https://github.com/launchbadge/sqlx/pull/4022 +[#4024]: https://github.com/launchbadge/sqlx/pull/4024 +[#4027]: https://github.com/launchbadge/sqlx/pull/4027 +[#4039]: https://github.com/launchbadge/sqlx/pull/4039 +[#4053]: https://github.com/launchbadge/sqlx/pull/4053 +[#3846]: https://github.com/launchbadge/sqlx/pull/3846 +[#3993]: https://github.com/launchbadge/sqlx/pull/3993 +[#4042]: https://github.com/launchbadge/sqlx/pull/4042 +[#4068]: https://github.com/launchbadge/sqlx/pull/4068 +[#4069]: https://github.com/launchbadge/sqlx/pull/4069 +[#4072]: https://github.com/launchbadge/sqlx/pull/4072 +[#4077]: https://github.com/launchbadge/sqlx/pull/4077 +[#4086]: https://github.com/launchbadge/sqlx/pull/4086 +[#4088]: https://github.com/launchbadge/sqlx/pull/4088 +[#4094]: https://github.com/launchbadge/sqlx/pull/4094 +[#4100]: https://github.com/launchbadge/sqlx/pull/4100 +[#4107]: https://github.com/launchbadge/sqlx/pull/4107 +[#4118]: https://github.com/launchbadge/sqlx/pull/4118 +[#4123]: https://github.com/launchbadge/sqlx/pull/4123 +[#4134]: https://github.com/launchbadge/sqlx/pull/4134 +[#4136]: https://github.com/launchbadge/sqlx/pull/4136 +[#4142]: https://github.com/launchbadge/sqlx/pull/4142 +[#4143]: https://github.com/launchbadge/sqlx/pull/4143 +[#4153]: https://github.com/launchbadge/sqlx/pull/4153 +[#4158]: https://github.com/launchbadge/sqlx/pull/4158 +[#4161]: https://github.com/launchbadge/sqlx/pull/4161 +[#4165]: https://github.com/launchbadge/sqlx/pull/4165 +[#4167]: https://github.com/launchbadge/sqlx/pull/4167 +[#4171]: https://github.com/launchbadge/sqlx/pull/4171 +[#4172]: https://github.com/launchbadge/sqlx/pull/4172 +[#4173]: https://github.com/launchbadge/sqlx/pull/4173 +[#4176]: https://github.com/launchbadge/sqlx/pull/4176 +[#4189]: https://github.com/launchbadge/sqlx/pull/4189 +[#4199]: https://github.com/launchbadge/sqlx/pull/4199 +[#4201]: https://github.com/launchbadge/sqlx/pull/4201 +[#4202]: https://github.com/launchbadge/sqlx/pull/4202 +[#4203]: https://github.com/launchbadge/sqlx/pull/4203 +[#4219]: https://github.com/launchbadge/sqlx/pull/4219 +[#4221]: https://github.com/launchbadge/sqlx/pull/4221 +[#4223]: https://github.com/launchbadge/sqlx/pull/4223 +[#4226]: https://github.com/launchbadge/sqlx/pull/4226 +[#4227]: https://github.com/launchbadge/sqlx/pull/4227 +[#4228]: https://github.com/launchbadge/sqlx/pull/4228 +[#4230]: https://github.com/launchbadge/sqlx/pull/4230 +[#4233]: https://github.com/launchbadge/sqlx/pull/4233 +[#4234]: https://github.com/launchbadge/sqlx/pull/4234 +[#4235]: https://github.com/launchbadge/sqlx/pull/4235 +[#4245]: https://github.com/launchbadge/sqlx/pull/4245 +[#4251]: https://github.com/launchbadge/sqlx/pull/4251 +[#4253]: https://github.com/launchbadge/sqlx/pull/4253 +[#4255]: https://github.com/launchbadge/sqlx/pull/4255 +[#4257]: https://github.com/launchbadge/sqlx/pull/4257 ## 0.8.6 - 2025-05-19 @@ -2951,3 +3278,52 @@ Fix docs.rs build by enabling a runtime feature in the docs.rs metadata in `Carg [@dyc3]: https://github.com/dyc3 [@ThomWright]: https://github.com/ThomWright [@duhby]: https://github.com/duhby +[@V02460]: https://github.com/V02460 +[@nipunn1313]: https://github.com/nipunn1313 +[@miniduikboot]: https://github.com/miniduikboot +[@0xfourzerofour]: https://github.com/0xfourzerofour +[@AlexTMjugador]: https://github.com/AlexTMjugador +[@martin-kolarik]: https://github.com/martin-kolarik +[@cvzx]: https://github.com/cvzx +[@Dirbaio]: https://github.com/Dirbaio +[@elichai]: https://github.com/elichai +[@silvestrpredko]: https://github.com/silvestrpredko +[@davidcornu]: https://github.com/davidcornu +[@zebrapurring]: https://github.com/zebrapurring +[@djarb]: https://github.com/djarb +[@barskern]: https://github.com/barskern +[@nhatcher-frequenz]: https://github.com/nhatcher-frequenz +[@JerryQ17]: https://github.com/JerryQ17 +[@jpmelos]: https://github.com/jpmelos +[@psionic-k]: https://github.com/psionic-k +[@Xiretza]: https://github.com/Xiretza +[@2ndDerivative]: https://github.com/2ndDerivative +[@kevincox]: https://github.com/kevincox +[@papaj-na-wrotkach]: https://github.com/papaj-na-wrotkach +[@xb284524239]: https://github.com/xb284524239 +[@Dosenpfand]: https://github.com/Dosenpfand +[@daviduebler]: https://github.com/daviduebler +[@squidpickles]: https://github.com/squidpickles +[@anmolitor]: https://github.com/anmolitor +[@CathalMullan]: https://github.com/CathalMullan +[@duelafn]: https://github.com/duelafn +[@Lege19]: https://github.com/Lege19 +[@zennozenith]: https://github.com/zennozenith +[@supleed2]: https://github.com/supleed2 +[@mousetail]: https://github.com/mousetail +[@chrxn1c]: https://github.com/chrxn1c +[@aoengin]: https://github.com/aoengin +[@dertin]: https://github.com/dertin +[@jrey8343]: https://github.com/jrey8343 +[@darioAnongba]: https://github.com/darioAnongba +[@cuiweixie]: https://github.com/cuiweixie +[@ricochet]: https://github.com/ricochet +[@opoplawski]: https://github.com/opoplawski +[@var4yn]: https://github.com/var4yn +[@joaquinhuigomez]: https://github.com/joaquinhuigomez +[@barry3406]: https://github.com/barry3406 +[@michaelvanstraten]: https://github.com/michaelvanstraten +[@graemer957]: https://github.com/graemer957 +[@Miesvanderlippe]: https://github.com/Miesvanderlippe +[@altmannmarcelo]: https://github.com/altmannmarcelo +[@sobolevn]: https://github.com/sobolevn diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 61d2e7d7b6..0000000000 --- a/Cargo.lock +++ /dev/null @@ -1,5274 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - -[[package]] -name = "anstream" -version = "0.6.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.59.0", -] - -[[package]] -name = "anyhow" -version = "1.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" - -[[package]] -name = "argon2" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" -dependencies = [ - "base64ct", - "blake2", - "password-hash 0.4.2", -] - -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures", - "password-hash 0.5.0", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "assert_cmd" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" -dependencies = [ - "anstyle", - "bstr", - "doc-comment", - "libc", - "predicates 3.1.3", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - -[[package]] -name = "async-attributes" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.5.0", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite", - "once_cell", -] - -[[package]] -name = "async-global-executor" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13f937e26114b93193065fd44f507aa2e9169ad0cdabbb996920b1fe1ddea7ba" -dependencies = [ - "async-channel 2.5.0", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3" -dependencies = [ - "async-lock", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 1.0.7", - "slab", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-lock" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" -dependencies = [ - "event-listener 5.4.0", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-process" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde3f4e40e6021d7acffc90095cbd6dc54cb593903d1de5832f435eb274b85dc" -dependencies = [ - "async-channel 2.5.0", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener 5.4.0", - "futures-lite", - "rustix 1.0.7", - "tracing", -] - -[[package]] -name = "async-signal" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7605a4e50d4b06df3898d5a70bf5fde51ed9059b0434b73105193bc27acce0d" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 1.0.7", - "signal-hook-registry", - "slab", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-std" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" -dependencies = [ - "async-attributes", - "async-channel 1.9.0", - "async-global-executor 2.4.1", - "async-io", - "async-lock", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "aws-lc-rs" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" -dependencies = [ - "bindgen", - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "axum" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43" -dependencies = [ - "async-trait", - "axum-core", - "axum-macros", - "bitflags 1.3.2", - "bytes", - "futures-util", - "http", - "http-body", - "hyper", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-http", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "mime", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6293dae2ec708e679da6736e857cf8532886ef258e92930f38279c12641628b8" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "backoff" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" -dependencies = [ - "futures-core", - "getrandom 0.2.16", - "instant", - "pin-project-lite", - "rand", - "tokio", -] - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" - -[[package]] -name = "bigdecimal" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" -dependencies = [ - "autocfg", - "libm", - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.9.1", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.104", - "which", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" -dependencies = [ - "serde", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel 2.5.0", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - -[[package]] -name = "borsh" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" -dependencies = [ - "borsh-derive", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "bstr" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "camino" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "castaway" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cc" -version = "1.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" -dependencies = [ - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", - "terminal_size", -] - -[[package]] -name = "clap_complete" -version = "4.5.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aad5b1b4de04fead402672b48897030eec1f3bfe1550776322f59f6d6e6a5677" -dependencies = [ - "clap", -] - -[[package]] -name = "clap_derive" -version = "4.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "clap_lex" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" - -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - -[[package]] -name = "color-eyre" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" -dependencies = [ - "backtrace", - "color-spantrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", - "tracing-error", -] - -[[package]] -name = "color-spantrace" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" -dependencies = [ - "once_cell", - "owo-colors", - "tracing-core", - "tracing-error", -] - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "compact_str" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "ryu", - "static_assertions", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width 0.2.1", - "windows-sys 0.59.0", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "futures", - "is-terminal", - "itertools 0.10.5", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "tokio", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crossterm" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" -dependencies = [ - "bitflags 2.9.1", - "crossterm_winapi", - "libc", - "mio 0.8.11", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.104", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "downcast" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -dependencies = [ - "serde", -] - -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - -[[package]] -name = "env_filter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "etcetera" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.59.0", -] - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener 5.4.0", - "pin-project-lite", -] - -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "filetime" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" -dependencies = [ - "cfg-if", - "libc", - "libredox", - "windows-sys 0.59.0", -] - -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" -dependencies = [ - "num-traits", -] - -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "futures-core", - "futures-sink", - "spin", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fragile" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "glob" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" - -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "half" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" -dependencies = [ - "cfg-if", - "crunchy", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.4", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "http-range-header" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "if_chain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" - -[[package]] -name = "indenter" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" -dependencies = [ - "equivalent", - "hashbrown 0.15.4", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-uring" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "ipnetwork" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" - -[[package]] -name = "is-terminal" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.59.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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jiff" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde", -] - -[[package]] -name = "jiff-static" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "jobserver" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "libc" -version = "0.2.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" - -[[package]] -name = "libloading" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" -dependencies = [ - "cfg-if", - "windows-targets 0.53.2", -] - -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "libredox" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" -dependencies = [ - "bitflags 2.9.1", - "libc", - "redox_syscall", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "bindgen", - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -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 = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -dependencies = [ - "value-bag", -] - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.4", -] - -[[package]] -name = "mac_address" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" -dependencies = [ - "nix", - "winapi", -] - -[[package]] -name = "matchit" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.48.0", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "mockall" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" -dependencies = [ - "cfg-if", - "downcast", - "fragile", - "lazy_static", - "mockall_derive", - "predicates 2.1.5", - "predicates-tree", -] - -[[package]] -name = "mockall_derive" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - -[[package]] -name = "openssl" -version = "0.10.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-src" -version = "300.5.1+3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "735230c832b28c000e3bc117119e6466a663ec73506bc0a9907ea4187508e42a" -dependencies = [ - "cc", -] - -[[package]] -name = "openssl-sys" -version = "0.9.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" -dependencies = [ - "cc", - "libc", - "openssl-src", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "owo-colors" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - -[[package]] -name = "polling" -version = "3.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.0.7", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "predicates" -version = "2.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" -dependencies = [ - "difflib", - "float-cmp", - "itertools 0.10.5", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates" -version = "3.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" -dependencies = [ - "anstyle", - "difflib", - "predicates-core", -] - -[[package]] -name = "predicates-core" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" - -[[package]] -name = "predicates-tree" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" -dependencies = [ - "predicates-core", - "termtree", -] - -[[package]] -name = "prettyplease" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" -dependencies = [ - "proc-macro2", - "syn 2.0.104", -] - -[[package]] -name = "proc-macro-crate" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_xoshiro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" -dependencies = [ - "rand_core", -] - -[[package]] -name = "ratatui" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" -dependencies = [ - "bitflags 2.9.1", - "cassowary", - "compact_str", - "crossterm", - "itertools 0.13.0", - "lru", - "paste", - "stability", - "strum", - "strum_macros", - "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.1.14", -] - -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" -dependencies = [ - "bitflags 2.9.1", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rkyv" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rsa" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "rust_decimal" -version = "1.37.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand", - "rkyv", - "serde", - "serde_json", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.9.1", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" -dependencies = [ - "bitflags 2.9.1", - "errno", - "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.23.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" -dependencies = [ - "aws-lc-rs", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework 3.2.0", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.1", - "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.9.1", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" -dependencies = [ - "serde", -] - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "serde_json" -version = "1.0.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" -dependencies = [ - "base64 0.13.1", - "chrono", - "hex", - "indexmap 1.9.3", - "serde", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" -dependencies = [ - "libc", - "mio 0.8.11", - "signal-hook", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "slab" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] - -[[package]] -name = "smol" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" -dependencies = [ - "async-channel 2.5.0", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-net", - "async-process", - "blocking", - "futures-lite", -] - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlx" -version = "0.9.0-alpha.1" -dependencies = [ - "anyhow", - "async-std", - "criterion", - "dotenvy", - "env_logger", - "futures-util", - "hex", - "libsqlite3-sys", - "paste", - "rand", - "rand_xoshiro", - "serde", - "serde_json", - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "sqlx-test", - "tempfile", - "time", - "tokio", - "trybuild", - "url", -] - -[[package]] -name = "sqlx-cli" -version = "0.9.0-alpha.1" -dependencies = [ - "anyhow", - "assert_cmd", - "backoff", - "cargo_metadata", - "chrono", - "clap", - "clap_complete", - "console", - "dialoguer", - "dotenvy", - "filetime", - "futures-util", - "glob", - "openssl", - "serde_json", - "sqlx", - "tempfile", - "tokio", -] - -[[package]] -name = "sqlx-core" -version = "0.9.0-alpha.1" -dependencies = [ - "async-fs", - "async-global-executor 3.1.0", - "async-io", - "async-std", - "async-task", - "base64 0.22.1", - "bigdecimal", - "bit-vec", - "bstr", - "bytes", - "cfg-if", - "chrono", - "crc", - "crossbeam-queue", - "either", - "event-listener 5.4.0", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.15.4", - "hashlink", - "indexmap 2.10.0", - "ipnet", - "ipnetwork", - "log", - "mac_address", - "memchr", - "native-tls", - "percent-encoding", - "rust_decimal", - "rustls", - "rustls-native-certs", - "serde", - "serde_json", - "sha2", - "smallvec", - "smol", - "sqlx", - "thiserror 2.0.12", - "time", - "tokio", - "tokio-stream", - "toml", - "tracing", - "url", - "uuid", - "webpki-roots 0.26.11", -] - -[[package]] -name = "sqlx-example-mysql-todos" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "sqlx", - "tokio", -] - -[[package]] -name = "sqlx-example-postgres-axum-social" -version = "0.1.0" -dependencies = [ - "anyhow", - "argon2 0.4.1", - "axum", - "dotenvy", - "rand", - "regex", - "serde", - "serde_json", - "serde_with", - "sqlx", - "thiserror 2.0.12", - "time", - "tokio", - "tower", - "tracing", - "uuid", - "validator", -] - -[[package]] -name = "sqlx-example-postgres-chat" -version = "0.1.0" -dependencies = [ - "crossterm", - "ratatui", - "sqlx", - "tokio", - "unicode-width 0.1.14", -] - -[[package]] -name = "sqlx-example-postgres-files" -version = "0.1.0" -dependencies = [ - "anyhow", - "dotenvy", - "sqlx", - "tokio", -] - -[[package]] -name = "sqlx-example-postgres-json" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "dotenvy", - "serde", - "serde_json", - "sqlx", - "tokio", -] - -[[package]] -name = "sqlx-example-postgres-listen" -version = "0.1.0" -dependencies = [ - "futures-util", - "sqlx", - "tokio", -] - -[[package]] -name = "sqlx-example-postgres-mockable-todos" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "clap", - "dotenvy", - "mockall", - "sqlx", - "tokio", -] - -[[package]] -name = "sqlx-example-postgres-multi-database" -version = "0.9.0-alpha.1" -dependencies = [ - "color-eyre", - "dotenvy", - "rand", - "rust_decimal", - "sqlx", - "sqlx-example-postgres-multi-database-accounts", - "sqlx-example-postgres-multi-database-payments", - "tokio", - "tracing-subscriber", -] - -[[package]] -name = "sqlx-example-postgres-multi-database-accounts" -version = "0.1.0" -dependencies = [ - "argon2 0.5.3", - "password-hash 0.5.0", - "rand", - "serde", - "sqlx", - "thiserror 1.0.69", - "time", - "tokio", - "uuid", -] - -[[package]] -name = "sqlx-example-postgres-multi-database-payments" -version = "0.1.0" -dependencies = [ - "rust_decimal", - "sqlx", - "sqlx-example-postgres-multi-database-accounts", - "time", - "uuid", -] - -[[package]] -name = "sqlx-example-postgres-multi-tenant" -version = "0.9.0-alpha.1" -dependencies = [ - "color-eyre", - "dotenvy", - "rand", - "rust_decimal", - "sqlx", - "sqlx-example-postgres-multi-tenant-accounts", - "sqlx-example-postgres-multi-tenant-payments", - "tokio", - "tracing-subscriber", -] - -[[package]] -name = "sqlx-example-postgres-multi-tenant-accounts" -version = "0.1.0" -dependencies = [ - "argon2 0.5.3", - "password-hash 0.5.0", - "rand", - "serde", - "sqlx", - "thiserror 1.0.69", - "time", - "tokio", - "uuid", -] - -[[package]] -name = "sqlx-example-postgres-multi-tenant-payments" -version = "0.1.0" -dependencies = [ - "rust_decimal", - "sqlx", - "sqlx-example-postgres-multi-tenant-accounts", - "time", - "uuid", -] - -[[package]] -name = "sqlx-example-postgres-preferred-crates" -version = "0.9.0-alpha.1" -dependencies = [ - "anyhow", - "chrono", - "dotenvy", - "serde", - "sqlx", - "sqlx-example-postgres-preferred-crates-uses-rust-decimal", - "sqlx-example-postgres-preferred-crates-uses-time", - "tokio", - "uuid", -] - -[[package]] -name = "sqlx-example-postgres-preferred-crates-uses-rust-decimal" -version = "0.9.0-alpha.1" -dependencies = [ - "chrono", - "rust_decimal", - "sqlx", - "uuid", -] - -[[package]] -name = "sqlx-example-postgres-preferred-crates-uses-time" -version = "0.9.0-alpha.1" -dependencies = [ - "serde", - "sqlx", - "time", - "uuid", -] - -[[package]] -name = "sqlx-example-postgres-todos" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "dotenvy", - "sqlx", - "tokio", -] - -[[package]] -name = "sqlx-example-postgres-transaction" -version = "0.1.0" -dependencies = [ - "sqlx", - "tokio", -] - -[[package]] -name = "sqlx-example-sqlite-extension" -version = "0.1.0" -dependencies = [ - "anyhow", - "sqlx", - "tokio", -] - -[[package]] -name = "sqlx-example-sqlite-todos" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "sqlx", - "tokio", -] - -[[package]] -name = "sqlx-macros" -version = "0.9.0-alpha.1" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 2.0.104", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.9.0-alpha.1" -dependencies = [ - "async-global-executor 3.1.0", - "async-std", - "cfg-if", - "dotenvy", - "either", - "heck 0.5.0", - "hex", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "smol", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn 2.0.104", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.9.0-alpha.1" -dependencies = [ - "atoi", - "base64 0.22.1", - "bigdecimal", - "bitflags 2.9.1", - "byteorder", - "bytes", - "chrono", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "percent-encoding", - "rand", - "rsa", - "rust_decimal", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlx", - "sqlx-core", - "stringprep", - "thiserror 2.0.12", - "time", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.9.0-alpha.1" -dependencies = [ - "atoi", - "base64 0.22.1", - "bigdecimal", - "bit-vec", - "bitflags 2.9.1", - "byteorder", - "chrono", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "ipnet", - "ipnetwork", - "itoa", - "log", - "mac_address", - "md-5", - "memchr", - "num-bigint", - "rand", - "rust_decimal", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx", - "sqlx-core", - "stringprep", - "thiserror 2.0.12", - "time", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.9.0-alpha.1" -dependencies = [ - "atoi", - "chrono", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "regex", - "serde", - "serde_urlencoded", - "sqlx", - "sqlx-core", - "thiserror 2.0.12", - "time", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "sqlx-test" -version = "0.1.0" -dependencies = [ - "anyhow", - "dotenvy", - "env_logger", - "sqlx", -] - -[[package]] -name = "stability" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" -dependencies = [ - "quote", - "syn 2.0.104", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.104", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "target-triple" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" - -[[package]] -name = "tempfile" -version = "3.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" -dependencies = [ - "fastrand", - "getrandom 0.3.3", - "once_cell", - "rustix 1.0.7", - "windows-sys 0.59.0", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -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.7", - "windows-sys 0.59.0", -] - -[[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "tinyvec" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.46.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" -dependencies = [ - "backtrace", - "bytes", - "io-uring", - "libc", - "mio 1.0.4", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "slab", - "socket2", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap 2.10.0", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" -dependencies = [ - "bitflags 1.3.2", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-range-header", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-error" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" -dependencies = [ - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "trybuild" -version = "1.0.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c9bf9513a2f4aeef5fdac8677d7d349c79fdbcc03b9c86da6e9d254f1e43be2" -dependencies = [ - "glob", - "serde", - "serde_derive", - "serde_json", - "target-triple", - "termcolor", - "toml", -] - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-truncate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" -dependencies = [ - "itertools 0.13.0", - "unicode-segmentation", - "unicode-width 0.1.14", -] - -[[package]] -name = "unicode-0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna 1.0.3", - "percent-encoding", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" -dependencies = [ - "js-sys", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "validator" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" -dependencies = [ - "idna 0.4.0", - "lazy_static", - "regex", - "serde", - "serde_derive", - "serde_json", - "url", - "validator_derive", -] - -[[package]] -name = "validator_derive" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" -dependencies = [ - "if_chain", - "lazy_static", - "proc-macro-error", - "proc-macro2", - "quote", - "regex", - "syn 1.0.109", - "validator_types", -] - -[[package]] -name = "validator_types" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" -dependencies = [ - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "value-bag" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.104", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.1", -] - -[[package]] -name = "webpki-roots" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - -[[package]] -name = "whoami" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" -dependencies = [ - "redox_syscall", - "wasite", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "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]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "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]] -name = "windows-targets" -version = "0.53.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[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_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "winnow" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] - -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] diff --git a/Cargo.toml b/Cargo.toml index b24b59cfa0..3738b814ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,10 +24,11 @@ members = [ "examples/postgres/transaction", "examples/sqlite/todos", "examples/sqlite/extension", + "examples/sqlite/serialize", ] [workspace.package] -version = "0.9.0-alpha.1" +version = "0.9.0" license = "MIT OR Apache-2.0" # TODO: upgrade to edition 2024 (after merging all pending PRs) edition = "2021" @@ -40,7 +41,7 @@ authors = [ "Chloe Ross ", "Daniel Akhterov ", ] -rust-version = "1.86.0" +rust-version = "1.94.0" [package] name = "sqlx" @@ -63,7 +64,7 @@ rustdoc-args = ["--cfg", "docsrs"] default = ["any", "macros", "migrate", "json"] derive = ["sqlx-macros/derive"] -macros = ["derive", "sqlx-macros/macros"] +macros = ["derive", "sqlx-macros/macros", "sqlx-core/offline", "sqlx-mysql?/offline", "sqlx-postgres?/offline", "sqlx-sqlite?/offline"] migrate = ["sqlx-core/migrate", "sqlx-macros?/migrate", "sqlx-mysql?/migrate", "sqlx-postgres?/migrate", "sqlx-sqlite?/migrate"] # Enable parsing of `sqlx.toml` for configuring macros and migrations. @@ -120,6 +121,7 @@ _sqlite = [] any = ["sqlx-core/any", "sqlx-mysql?/any", "sqlx-postgres?/any", "sqlx-sqlite?/any"] postgres = ["sqlx-postgres", "sqlx-macros?/postgres"] mysql = ["sqlx-mysql", "sqlx-macros?/mysql"] +mysql-rsa = ["mysql", "sqlx-mysql/rsa", "sqlx-macros?/mysql-rsa"] sqlite = ["sqlite-bundled", "sqlite-deserialize", "sqlite-load-extension", "sqlite-unlock-notify"] # SQLite base features @@ -163,33 +165,46 @@ bstr = ["sqlx-core/bstr"] [workspace.dependencies] # Core Crates -sqlx-core = { version = "=0.9.0-alpha.1", path = "sqlx-core" } -sqlx-macros-core = { version = "=0.9.0-alpha.1", path = "sqlx-macros-core" } -sqlx-macros = { version = "=0.9.0-alpha.1", path = "sqlx-macros" } +sqlx-core = { version = "=0.9.0", path = "sqlx-core" } +sqlx-macros-core = { version = "=0.9.0", path = "sqlx-macros-core" } +sqlx-macros = { version = "=0.9.0", path = "sqlx-macros" } # Driver crates -sqlx-mysql = { version = "=0.9.0-alpha.1", path = "sqlx-mysql" } -sqlx-postgres = { version = "=0.9.0-alpha.1", path = "sqlx-postgres" } -sqlx-sqlite = { version = "=0.9.0-alpha.1", path = "sqlx-sqlite" } +sqlx-mysql = { version = "=0.9.0", path = "sqlx-mysql", default-features = false } +sqlx-postgres = { version = "=0.9.0", path = "sqlx-postgres" } +sqlx-sqlite = { version = "=0.9.0", path = "sqlx-sqlite" } # Facade crate (for reference from sqlx-cli) -sqlx = { version = "=0.9.0-alpha.1", path = ".", default-features = false } +sqlx = { version = "=0.9.0", 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" +bit-vec = "0.8" chrono = { version = "0.4.34", default-features = false, features = ["std", "clock"] } ipnet = "2.3.0" ipnetwork = "0.21.1" mac_address = "1.1.5" -rust_decimal = { version = "1.26.1", default-features = false, features = ["std"] } -time = { version = "0.3.36", features = ["formatting", "parsing", "macros"] } -uuid = "1.1.2" +rust_decimal = { version = "1.36.0", default-features = false, features = ["std"] } +time = { version = "0.3.47", features = ["formatting", "parsing", "macros"] } +uuid = "1.12.1" # Common utility crates +base64 = { version = "0.22.1", default-features = false, features = ["alloc"] } cfg-if = "1.0.0" -dotenvy = { version = "0.15.0", default-features = false } +dotenvy = { version = "0.15.7", default-features = false } +thiserror = { version = "2.0.18", default-features = false, features = ["std"] } + +# Cryptography +crc = { version = "3.0.0" } +digest = { version = "0.11.2", default-features = false, features = ["alloc"] } +hkdf = { version = "0.13.0" } +hmac = { version = "0.13.0", default-features = false } +md-5 = { version = "0.11.0", default-features = false } +rsa = { version = "0.10.0-rc.18" } # FIXME: when `rsa 0.10` is released +rand = { version = "0.10.1", features = ["thread_rng"], default-features = false } +sha1 = { version = "0.11.0", default-features = false } +sha2 = { version = "0.11.0", default-features = false } # Runtimes [workspace.dependencies.async-global-executor] @@ -205,44 +220,44 @@ version = "2.0" default-features = false [workspace.dependencies.tokio] -version = "1" +version = "1.25.0" features = ["time", "net", "sync", "fs", "io-util", "rt"] default-features = false [dependencies] -sqlx-core = { workspace = true, features = ["offline", "migrate"] } +sqlx-core = { workspace = true, features = ["migrate"] } sqlx-macros = { workspace = true, optional = true } -sqlx-mysql = { workspace = true, optional = true } +sqlx-mysql = { workspace = true, optional = true, default-features = false } sqlx-postgres = { workspace = true, optional = true } sqlx-sqlite = { workspace = true, optional = true } [dev-dependencies] -anyhow = "1.0.52" -time_ = { version = "0.3.2", package = "time" } -futures-util = { version = "0.3.19", default-features = false, features = ["alloc"] } +anyhow = "1.0.58" +time_ = { version = "0.3.47", package = "time" } +futures-util = { version = "0.3.32", default-features = false, features = ["alloc"] } env_logger = "0.11" async-std = { workspace = true, features = ["attributes"] } -tokio = { version = "1.15.0", features = ["full"] } -dotenvy = "0.15.0" +tokio = { version = "1.25.0", features = ["full"] } +dotenvy = "0.15.7" trybuild = "1.0.53" sqlx-test = { path = "./sqlx-test" } paste = "1.0.6" -serde = { version = "1.0.132", features = ["derive"] } -serde_json = "1.0.73" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.142" url = "2.2.2" -rand = "0.8.4" -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" } +libsqlite3-sys = { version = "0.37.0" } + +rand.workspace = true # 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. [target.'cfg(sqlite_test_sqlcipher)'.dev-dependencies] # Enable testing with SQLCipher if specifically requested. -libsqlite3-sys = { version = "0.30.1", features = ["bundled-sqlcipher"] } +libsqlite3-sys = { version = "0.37.0", features = ["bundled-sqlcipher"] } # Common lint settings for the workspace [workspace.lints.clippy] diff --git a/LICENSE-APACHE b/LICENSE-APACHE index c79147e873..e14699fb14 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -186,7 +186,8 @@ file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2020 LaunchBadge, LLC +Copyright (C) SQLx Contributors +Portions of this work Copyright (C) LaunchBadge, LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -198,4 +199,4 @@ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT index 861bf60855..6fe828e8ea 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,5 @@ -Copyright (c) 2020 LaunchBadge, LLC +Copyright (C) SQLx Contributors +Portions of this work Copyright (C) LaunchBadge, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated diff --git a/README.md b/README.md index f1e53cdced..ff98f45350 100644 --- a/README.md +++ b/README.md @@ -172,10 +172,13 @@ be removed in the future. - `tls-native-tls`: Use the `native-tls` TLS backend (OpenSSL on *nix, SChannel on Windows, Secure Transport on macOS). - `tls-rustls`: Use the `rustls` TLS backend (cross-platform backend, only supports TLS 1.2 and 1.3). +- `tls-rustls-aws-lc-rs`: Use the `rustls` TLS backend with `aws-lc-rs`. - `postgres`: Add support for the Postgres database server. - `mysql`: Add support for the MySQL/MariaDB database server. +- Note: RSA auth without TLS requires `mysql-rsa` (not enabled by `mysql`). +- `mysql-rsa`: Enable RSA password encryption for `caching_sha2_password`/`sha256_password` when TLS is off. Only enable it if you must connect without TLS to servers that require RSA auth. Prefer using TLS. - `mssql`: Add support for the MSSQL database server. @@ -331,7 +334,7 @@ use futures_util::TryStreamExt; use sqlx::Row; let mut rows = sqlx::query("SELECT * FROM users WHERE email = ?") - .bind(email) + .bind("user@example.com") .fetch(&mut conn); while let Some(row) = rows.try_next().await? { @@ -355,8 +358,8 @@ let mut stream = sqlx::query("SELECT * FROM users") struct User { name: String, id: i64 } let mut stream = sqlx::query_as::<_, User>("SELECT * FROM users WHERE email = ? OR name = ?") - .bind(user_email) - .bind(user_name) + .bind("user@example.com") + .bind("example_username") .fetch(&mut conn); ``` @@ -400,13 +403,13 @@ Differences from `query()`: queries against; the database does not have to contain any data but must be the same kind (MySQL, Postgres, etc.) and have the same schema as the database you will be connecting to at runtime. - For convenience, you can use [a `.env` file][dotenv]1 to set DATABASE_URL so that you don't have to pass it every time: + For convenience, you can use [a `.env` file][dotenvy]1 to set DATABASE_URL so that you don't have to pass it every time: ``` DATABASE_URL=mysql://localhost/my_database ``` -[dotenv]: https://github.com/dotenv-rs/dotenv#examples +[dotenvy]: https://github.com/allan2/dotenvy?tab=readme-ov-file#what-is-an-environment-file The biggest downside to `query!()` is that the output type cannot be named (due to Rust not officially supporting anonymous records). To address that, there is a `query_as!()` macro that is diff --git a/clippy.toml b/clippy.toml index 0158870aca..c24a03b6f7 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,3 +1,5 @@ +doc-valid-idents = ["SQLite"] + [[disallowed-methods]] path = "core::cmp::Ord::min" reason = ''' diff --git a/examples/mysql/todos/Cargo.toml b/examples/mysql/todos/Cargo.toml index db8c677980..b82657ed49 100644 --- a/examples/mysql/todos/Cargo.toml +++ b/examples/mysql/todos/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" workspace = "../../../" [dependencies] -anyhow = "1.0" -sqlx = { path = "../../../", features = [ "mysql", "runtime-tokio", "tls-native-tls" ] } -clap = { version = "4", features = ["derive"] } -tokio = { version = "1.20.0", features = ["rt", "macros"]} +anyhow = "1.0.58" +sqlx = { path = "../../../", features = [ "mysql", "mysql-rsa", "runtime-tokio", "tls-native-tls" ] } +clap = { version = "4.4.7", features = ["derive"] } +tokio = { version = "1.25.0", features = ["rt", "macros"]} diff --git a/examples/postgres/axum-social-with-tests/Cargo.toml b/examples/postgres/axum-social-with-tests/Cargo.toml index 05257a617c..2603105979 100644 --- a/examples/postgres/axum-social-with-tests/Cargo.toml +++ b/examples/postgres/axum-social-with-tests/Cargo.toml @@ -7,26 +7,27 @@ edition = "2021" [dependencies] # Primary crates -axum = { version = "0.5.13", features = ["macros"] } -sqlx = { path = "../../../", features = [ "runtime-tokio", "tls-rustls-ring", "postgres", "time", "uuid" ] } -tokio = { version = "1.20.1", features = ["rt-multi-thread", "macros"] } +axum = { version = "0.8", features = ["macros"] } +sqlx = { path = "../../..", features = [ "runtime-tokio", "tls-rustls-ring", "postgres", "time", "uuid" ] } +tokio = { version = "1.25.0", features = ["rt-multi-thread", "macros"] } # Important secondary crates -argon2 = "0.4.1" -rand = "0.8.5" +argon2 = "0.6.0-rc.8" # FIXME: update to `0.6.0` when released +rand = "0.10.1" regex = "1.6.0" -serde = "1.0.140" -serde_with = { version = "2.0.0", features = ["time_0_3"] } -time = "0.3.11" -uuid = { version = "1.1.2", features = ["serde"] } -validator = { version = "0.16.0", features = ["derive"] } +serde = "1.0.219" +serde_with = { version = "3.18.0", features = ["time_0_3"] } +time = "0.3.47" +uuid = { version = "1.12.1", features = ["serde"] } +validator = { version = "0.20.0", features = ["derive"] } # Auxilliary crates anyhow = "1.0.58" -dotenvy = "0.15.1" -thiserror = "2.0.0" -tracing = "0.1.35" +dotenvy = "0.15.7" +thiserror = "2.0.18" +tracing = "0.1.37" [dev-dependencies] -serde_json = "1.0.82" -tower = "0.4.13" +http-body-util = "0.1" +serde_json = "1.0.142" +tower = "0.5.2" diff --git a/examples/postgres/axum-social-with-tests/src/http/error.rs b/examples/postgres/axum-social-with-tests/src/http/error.rs index fc4fd5ea1f..51e0208e5a 100644 --- a/examples/postgres/axum-social-with-tests/src/http/error.rs +++ b/examples/postgres/axum-social-with-tests/src/http/error.rs @@ -11,7 +11,7 @@ pub enum Error { /// A SQLx call returned an error. /// /// The exact error contents are not reported to the user in order to avoid leaking - /// information about databse internals. + /// information about database internals. #[error("an internal database error occurred")] Sqlx(#[from] sqlx::Error), diff --git a/examples/postgres/axum-social-with-tests/src/http/mod.rs b/examples/postgres/axum-social-with-tests/src/http/mod.rs index a871a93d7e..2a9297cbef 100644 --- a/examples/postgres/axum-social-with-tests/src/http/mod.rs +++ b/examples/postgres/axum-social-with-tests/src/http/mod.rs @@ -1,5 +1,5 @@ use anyhow::Context; -use axum::{Extension, Router}; +use axum::Router; use sqlx::PgPool; mod error; @@ -15,12 +15,12 @@ pub fn app(db: PgPool) -> Router { Router::new() .merge(user::router()) .merge(post::router()) - .layer(Extension(db)) + .with_state(db) } pub async fn serve(db: PgPool) -> anyhow::Result<()> { - axum::Server::bind(&"0.0.0.0:8080".parse().unwrap()) - .serve(app(db).into_make_service()) + let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?; + axum::serve(listener, app(db)) .await .context("failed to serve API") } diff --git a/examples/postgres/axum-social-with-tests/src/http/post/comment.rs b/examples/postgres/axum-social-with-tests/src/http/post/comment.rs index 630dedaa21..d54cb473d4 100644 --- a/examples/postgres/axum-social-with-tests/src/http/post/comment.rs +++ b/examples/postgres/axum-social-with-tests/src/http/post/comment.rs @@ -1,5 +1,5 @@ -use axum::extract::Path; -use axum::{Extension, Json, Router}; +use axum::extract::{Path, State}; +use axum::{Json, Router}; use axum::routing::get; @@ -15,9 +15,9 @@ use crate::http::Result; use time::format_description::well_known::Rfc3339; use uuid::Uuid; -pub fn router() -> Router { +pub fn router() -> Router { Router::new().route( - "/v1/post/:postId/comment", + "/v1/post/{postId}/comment", get(get_post_comments).post(create_post_comment), ) } @@ -44,7 +44,7 @@ struct Comment { // #[axum::debug_handler] // very useful! async fn create_post_comment( - db: Extension, + db: State, Path(post_id): Path, Json(req): Json, ) -> Result> { @@ -76,7 +76,7 @@ async fn create_post_comment( /// Returns comments in ascending chronological order. async fn get_post_comments( - db: Extension, + db: State, Path(post_id): Path, ) -> Result>> { // Note: normally you'd want to put a `LIMIT` on this as well, diff --git a/examples/postgres/axum-social-with-tests/src/http/post/mod.rs b/examples/postgres/axum-social-with-tests/src/http/post/mod.rs index 09c2fa44bb..4d2021e0f4 100644 --- a/examples/postgres/axum-social-with-tests/src/http/post/mod.rs +++ b/examples/postgres/axum-social-with-tests/src/http/post/mod.rs @@ -1,4 +1,5 @@ -use axum::{Extension, Json, Router}; +use axum::extract::State; +use axum::{Json, Router}; use axum::routing::get; @@ -16,7 +17,7 @@ use uuid::Uuid; mod comment; -pub fn router() -> Router { +pub fn router() -> Router { Router::new() .route("/v1/post", get(get_posts).post(create_post)) .merge(comment::router()) @@ -43,10 +44,7 @@ struct Post { } // #[axum::debug_handler] // very useful! -async fn create_post( - db: Extension, - Json(req): Json, -) -> Result> { +async fn create_post(db: State, Json(req): Json) -> Result> { req.validate()?; let user_id = req.auth.verify(&*db).await?; @@ -73,7 +71,7 @@ async fn create_post( } /// Returns posts in descending chronological order. -async fn get_posts(db: Extension) -> Result>> { +async fn get_posts(db: State) -> Result>> { // Note: normally you'd want to put a `LIMIT` on this as well, // though that would also necessitate implementing pagination. let posts = sqlx::query_as!( diff --git a/examples/postgres/axum-social-with-tests/src/http/user.rs b/examples/postgres/axum-social-with-tests/src/http/user.rs index cf4db77cdc..7b267bd5b9 100644 --- a/examples/postgres/axum-social-with-tests/src/http/user.rs +++ b/examples/postgres/axum-social-with-tests/src/http/user.rs @@ -1,6 +1,6 @@ +use axum::extract::State; use axum::http::StatusCode; -use axum::{routing::post, Extension, Json, Router}; -use rand::Rng; +use axum::{routing::post, Json, Router}; use regex::Regex; use std::{sync::LazyLock, time::Duration}; @@ -13,7 +13,7 @@ use crate::http::{Error, Result}; pub type UserId = Uuid; -pub fn router() -> Router { +pub fn router() -> Router { Router::new().route("/v1/user", post(create_user)) } @@ -24,7 +24,7 @@ static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[0-9A-Za #[derive(Deserialize, Validate)] #[serde(rename_all = "camelCase")] pub struct UserAuth { - #[validate(length(min = 3, max = 16), regex = "USERNAME_REGEX")] + #[validate(length(min = 3, max = 16), regex(path = USERNAME_REGEX))] username: String, #[validate(length(min = 8, max = 32))] password: String, @@ -32,7 +32,7 @@ pub struct UserAuth { // WARNING: this API has none of the checks that a normal user signup flow implements, // such as email or phone verification. -async fn create_user(db: Extension, Json(req): Json) -> Result { +async fn create_user(db: State, Json(req): Json) -> Result { req.validate()?; let UserAuth { username, password } = req; @@ -84,7 +84,7 @@ impl UserAuth { // Sleep a random amount of time to avoid leaking existence of a user in timing. let sleep_duration = - rand::thread_rng().gen_range(Duration::from_millis(100)..=Duration::from_millis(500)); + rand::random_range(Duration::from_millis(100)..=Duration::from_millis(500)); tokio::time::sleep(sleep_duration).await; Err(Error::UnprocessableEntity( diff --git a/examples/postgres/axum-social-with-tests/src/password.rs b/examples/postgres/axum-social-with-tests/src/password.rs index 44f1551f85..7115ce1d66 100644 --- a/examples/postgres/axum-social-with-tests/src/password.rs +++ b/examples/postgres/axum-social-with-tests/src/password.rs @@ -1,14 +1,12 @@ use anyhow::{anyhow, Context}; use tokio::task; -use argon2::password_hash::SaltString; use argon2::{password_hash, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; pub async fn hash(password: String) -> anyhow::Result { task::spawn_blocking(move || { - let salt = SaltString::generate(rand::thread_rng()); Ok(Argon2::default() - .hash_password(password.as_bytes(), &salt) + .hash_password(password.as_bytes()) .map_err(|e| anyhow!(e).context("failed to hash password"))? .to_string()) }) @@ -25,7 +23,7 @@ pub async fn verify(password: String, hash: String) -> anyhow::Result { match res { Ok(()) => Ok(true), - Err(password_hash::Error::Password) => Ok(false), + Err(password_hash::Error::PasswordInvalid) => Ok(false), Err(e) => Err(anyhow!(e).context("failed to verify password")), } }) diff --git a/examples/postgres/axum-social-with-tests/tests/common.rs b/examples/postgres/axum-social-with-tests/tests/common.rs index 2b7f169ae9..64da67091f 100644 --- a/examples/postgres/axum-social-with-tests/tests/common.rs +++ b/examples/postgres/axum-social-with-tests/tests/common.rs @@ -1,10 +1,11 @@ // This is imported by different tests that use different functions. #![allow(dead_code)] -use axum::body::{Body, BoxBody, HttpBody}; +use axum::body::Body; use axum::http::header::CONTENT_TYPE; use axum::http::{request, Request}; use axum::response::Response; +use http_body_util::BodyExt; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; use uuid::Uuid; @@ -27,7 +28,7 @@ impl RequestBuilderExt for request::Builder { } } -pub async fn response_json(resp: &mut Response) -> serde_json::Value { +pub async fn response_json(resp: &mut Response) -> serde_json::Value { assert_eq!( resp.headers() .get(CONTENT_TYPE) @@ -35,15 +36,11 @@ pub async fn response_json(resp: &mut Response) -> serde_json::Value { "application/json" ); - let body = resp.body_mut(); - - let mut bytes = Vec::new(); - - while let Some(res) = body.data().await { - let chunk = res.expect("error reading response body"); - - bytes.extend_from_slice(&chunk[..]); - } + let bytes = resp + .collect() + .await + .expect("error reading response body") + .to_bytes(); serde_json::from_slice(&bytes).expect("failed to read response body as json") } diff --git a/examples/postgres/chat/Cargo.toml b/examples/postgres/chat/Cargo.toml index ecacff7269..43e1893698 100644 --- a/examples/postgres/chat/Cargo.toml +++ b/examples/postgres/chat/Cargo.toml @@ -6,7 +6,7 @@ workspace = "../../../" [dependencies] sqlx = { path = "../../../", features = [ "postgres", "runtime-tokio", "tls-native-tls" ] } -tokio = { version = "1.20.0", features = [ "rt-multi-thread", "macros" ] } -ratatui = "0.27.0" -crossterm = "0.27.0" -unicode-width = "0.1" +tokio = { version = "1.25.0", features = [ "rt-multi-thread", "macros" ] } +ratatui = "0.30.0" +crossterm = "0.29.0" +unicode-width = "0.2.2" diff --git a/examples/postgres/chat/src/main.rs b/examples/postgres/chat/src/main.rs index 3b6ab48619..05840c65a0 100644 --- a/examples/postgres/chat/src/main.rs +++ b/examples/postgres/chat/src/main.rs @@ -38,7 +38,10 @@ impl ChatApp { mut self, terminal: &mut Terminal, mut listener: PgListener, - ) -> Result<(), Box> { + ) -> Result<(), Box> + where + ::Error: 'static, + { // setup listener task let messages = self.messages.clone(); tokio::spawn(async move { diff --git a/examples/postgres/files/Cargo.toml b/examples/postgres/files/Cargo.toml index 5b9ac6bd49..823c3576e6 100644 --- a/examples/postgres/files/Cargo.toml +++ b/examples/postgres/files/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0" +anyhow = "1.0.58" sqlx = { path = "../../../", features = [ "postgres", "runtime-tokio", "tls-native-tls" ] } -tokio = { version = "1.20.0", features = ["rt", "macros"]} -dotenvy = "0.15.0" +tokio = { version = "1.25.0", features = ["rt", "macros"]} +dotenvy = "0.15.7" diff --git a/examples/postgres/files/src/main.rs b/examples/postgres/files/src/main.rs index 65e92190d8..194d823fd7 100644 --- a/examples/postgres/files/src/main.rs +++ b/examples/postgres/files/src/main.rs @@ -30,7 +30,7 @@ impl Display for PostWithAuthorQuery { async fn main() -> anyhow::Result<()> { let pool = PgPool::connect(&dotenvy::var("DATABASE_URL")?).await?; - // we can use a tranditional wrapper around the `query!()` macro using files + // we can use a traditional wrapper around the `query!()` macro using files query_file!("queries/insert_seed_data.sql") .execute(&pool) .await?; diff --git a/examples/postgres/json/Cargo.toml b/examples/postgres/json/Cargo.toml index edca041d68..cb9e2f8b49 100644 --- a/examples/postgres/json/Cargo.toml +++ b/examples/postgres/json/Cargo.toml @@ -5,10 +5,10 @@ edition = "2021" workspace = "../../../" [dependencies] -anyhow = "1.0" -dotenvy = "0.15.0" -serde = { version = "1", features = ["derive"] } -serde_json = "1" +anyhow = "1.0.58" +dotenvy = "0.15.7" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.142" sqlx = { path = "../../../", features = [ "runtime-tokio", "postgres", "json" ] } -clap = { version = "4", features = ["derive"] } -tokio = { version = "1.20.0", features = ["rt", "macros"]} +clap = { version = "4.4.7", features = ["derive"] } +tokio = { version = "1.25.0", features = ["rt", "macros"]} diff --git a/examples/postgres/listen/Cargo.toml b/examples/postgres/listen/Cargo.toml index 636e8c380b..e575fab3e9 100644 --- a/examples/postgres/listen/Cargo.toml +++ b/examples/postgres/listen/Cargo.toml @@ -6,5 +6,5 @@ workspace = "../../../" [dependencies] sqlx = { path = "../../../", features = [ "runtime-tokio", "postgres" ] } -futures-util = { version = "0.3.1", default-features = false } -tokio = { version = "1.20.0", features = ["rt-multi-thread", "macros", "time"]} +futures-util = { version = "0.3.32", default-features = false } +tokio = { version = "1.25.0", features = ["rt-multi-thread", "macros", "time"]} diff --git a/examples/postgres/mockable-todos/Cargo.toml b/examples/postgres/mockable-todos/Cargo.toml index 59bb800224..4860d74c0e 100644 --- a/examples/postgres/mockable-todos/Cargo.toml +++ b/examples/postgres/mockable-todos/Cargo.toml @@ -5,10 +5,10 @@ edition = "2021" workspace = "../../../" [dependencies] -anyhow = "1.0" +anyhow = "1.0.58" sqlx = { path = "../../../", features = [ "postgres", "runtime-tokio", "tls-native-tls" ] } -clap = { version = "4", features = ["derive"] } -tokio = { version = "1.20.0", features = ["rt", "macros"]} -dotenvy = "0.15.0" -async-trait = "0.1.41" +clap = { version = "4.4.7", features = ["derive"] } +tokio = { version = "1.25.0", features = ["rt", "macros"]} +dotenvy = "0.15.7" +async-trait = "0.1.43" mockall = "0.11" diff --git a/examples/postgres/multi-database/Cargo.toml b/examples/postgres/multi-database/Cargo.toml index c5e01621b8..7fac3494e0 100644 --- a/examples/postgres/multi-database/Cargo.toml +++ b/examples/postgres/multi-database/Cargo.toml @@ -9,15 +9,15 @@ categories.workspace = true authors.workspace = true [dependencies] -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1.25.0", features = ["rt-multi-thread", "macros"] } -color-eyre = "0.6.3" +color-eyre = "0.6.5" dotenvy = "0.15.7" tracing-subscriber = "0.3.19" rust_decimal = "1.36.0" -rand = "0.8.5" +rand = "0.10.1" [dependencies.sqlx] # version = "0.9.0" diff --git a/examples/postgres/multi-database/accounts/Cargo.toml b/examples/postgres/multi-database/accounts/Cargo.toml index f7c04ca8b4..4b43f42757 100644 --- a/examples/postgres/multi-database/accounts/Cargo.toml +++ b/examples/postgres/multi-database/accounts/Cargo.toml @@ -5,18 +5,19 @@ edition = "2021" [dependencies] sqlx = { workspace = true, features = ["postgres", "time", "uuid", "macros", "sqlx-toml"] } -tokio = { version = "1", features = ["rt", "sync"] } +tokio = { version = "1.25.0", features = ["rt", "sync"] } -argon2 = { version = "0.5.3", features = ["password-hash"] } -password-hash = { version = "0.5", features = ["std"] } +# FIXME: update when `0.6.0` is released +argon2 = { version = "0.6.0-rc.8", features = ["password-hash"] } +password-hash = { version = "0.6.1", features = ["alloc"] } -uuid = { version = "1", features = ["serde"] } -thiserror = "1" -rand = "0.8" +uuid = { version = "1.12.1", features = ["serde"] } +thiserror = "2.0.18" +rand = "0.10.1" -time = { version = "0.3.37", features = ["serde"] } +time = { version = "0.3.47", features = ["serde"] } -serde = { version = "1.0.218", features = ["derive"] } +serde = { version = "1.0.219", features = ["derive"] } [dev-dependencies] sqlx = { workspace = true, features = ["runtime-tokio"] } diff --git a/examples/postgres/multi-database/accounts/sqlx.toml b/examples/postgres/multi-database/accounts/sqlx.toml index 0620c4686f..d9ebb85749 100644 --- a/examples/postgres/multi-database/accounts/sqlx.toml +++ b/examples/postgres/multi-database/accounts/sqlx.toml @@ -3,7 +3,7 @@ database-url-var = "ACCOUNTS_DATABASE_URL" [macros.table-overrides.'account'] 'account_id' = "crate::AccountId" -'password_hash' = "sqlx::types::Text" +'password_hash' = "sqlx::types::Text" [macros.table-overrides.'session'] 'session_token' = "crate::SessionToken" diff --git a/examples/postgres/multi-database/accounts/src/lib.rs b/examples/postgres/multi-database/accounts/src/lib.rs index a543d2fd45..e6f4c66881 100644 --- a/examples/postgres/multi-database/accounts/src/lib.rs +++ b/examples/postgres/multi-database/accounts/src/lib.rs @@ -1,6 +1,6 @@ use argon2::{password_hash, Argon2, PasswordHasher, PasswordVerifier}; -use password_hash::PasswordHashString; -use rand::distributions::{Alphanumeric, DistString}; +use password_hash::phc::PasswordHash; +use rand::distr::{Alphanumeric, SampleString}; use sqlx::PgPool; use std::sync::Arc; use uuid::Uuid; @@ -130,7 +130,7 @@ impl AccountsManager { }) } - async fn hash_password(&self, password: String) -> Result { + async fn hash_password(&self, password: String) -> Result { let guard = self .hashing_semaphore .clone() @@ -141,13 +141,7 @@ impl AccountsManager { // We transfer ownership to the blocking task and back to ensure Tokio doesn't spawn // excess threads. let (_guard, res) = tokio::task::spawn_blocking(move || { - let salt = password_hash::SaltString::generate(rand::thread_rng()); - ( - guard, - Argon2::default() - .hash_password(password.as_bytes(), &salt) - .map(|hash| hash.serialize()), - ) + (guard, Argon2::default().hash_password(password.as_bytes())) }) .await?; @@ -157,7 +151,7 @@ impl AccountsManager { async fn verify_password( &self, password: String, - hash: PasswordHashString, + hash: PasswordHash, ) -> Result<(), CreateSessionError> { let guard = self .hashing_semaphore @@ -169,13 +163,13 @@ impl AccountsManager { let (_guard, res) = tokio::task::spawn_blocking(move || { ( guard, - Argon2::default().verify_password(password.as_bytes(), &hash.password_hash()), + Argon2::default().verify_password(password.as_bytes(), &hash), ) }) .await .map_err(GeneralError::from)?; - if let Err(password_hash::Error::Password) = res { + if let Err(password_hash::Error::PasswordInvalid) = res { return Err(CreateSessionError::InvalidPassword); } @@ -200,7 +194,9 @@ impl AccountsManager { values (1,ドル 2ドル) \ returning account_id", email, - hash.as_str(), + // However, since arguments don't link back to the target column, + // SQLx doesn't know that `PasswordHash` would be a valid argument here. + hash.to_string(), ) .fetch_one(&self.pool) .await @@ -230,7 +226,7 @@ impl AccountsManager { // Thanks to `sqlx.toml`: // * `account_id` maps to `AccountId` - // * `password_hash` maps to `Text` + // * `password_hash` maps to `Text` // * `session_token` maps to `SessionToken` let maybe_account = sqlx::query!( // language=PostgreSQL @@ -288,6 +284,6 @@ impl SessionToken { const LEN: usize = 32; fn generate() -> Self { - SessionToken(Alphanumeric.sample_string(&mut rand::thread_rng(), Self::LEN)) + SessionToken(Alphanumeric.sample_string(&mut rand::rng(), Self::LEN)) } } diff --git a/examples/postgres/multi-database/payments/Cargo.toml b/examples/postgres/multi-database/payments/Cargo.toml index 853b32f624..fed2617026 100644 --- a/examples/postgres/multi-database/payments/Cargo.toml +++ b/examples/postgres/multi-database/payments/Cargo.toml @@ -9,7 +9,7 @@ sqlx = { workspace = true, features = ["postgres", "time", "uuid", "rust_decimal rust_decimal = "1.36.0" -time = "0.3.37" +time = "0.3.47" uuid = "1.12.1" [dependencies.accounts] diff --git a/examples/postgres/multi-database/setup.py b/examples/postgres/multi-database/setup.py new file mode 100644 index 0000000000..5b86398422 --- /dev/null +++ b/examples/postgres/multi-database/setup.py @@ -0,0 +1,10 @@ +from os import path + +def setup(database_url, cwd, sqlx): + accounts_url = f"{database_url}-accounts" + payments_url = f"{database_url}-payments" + + sqlx("db reset -y", accounts_url, cwd=path.join(cwd, "accounts")) + sqlx("db reset -y", payments_url, cwd=path.join(cwd, "payments")) + + return {"ACCOUNTS_DATABASE_URL": accounts_url, "PAYMENTS_DATABASE_URL": payments_url} diff --git a/examples/postgres/multi-database/src/main.rs b/examples/postgres/multi-database/src/main.rs index 263eff8e50..9fb24fe7f6 100644 --- a/examples/postgres/multi-database/src/main.rs +++ b/examples/postgres/multi-database/src/main.rs @@ -2,7 +2,7 @@ use accounts::AccountsManager; use color_eyre::eyre; use color_eyre::eyre::{Context, OptionExt}; use payments::PaymentsManager; -use rand::distributions::{Alphanumeric, DistString}; +use rand::distr::{Alphanumeric, SampleString}; use sqlx::Connection; #[tokio::main] @@ -42,7 +42,7 @@ async fn main() -> eyre::Result<()> { // POST /account let user_email = format!("user{}@example.com", rand::random::()); - let user_password = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); + let user_password = Alphanumeric.sample_string(&mut rand::rng(), 16); // Requires an externally managed transaction in case any application-specific records // should be created after the actual account record. diff --git a/examples/postgres/multi-tenant/Cargo.toml b/examples/postgres/multi-tenant/Cargo.toml index a219cce2b8..71bd270380 100644 --- a/examples/postgres/multi-tenant/Cargo.toml +++ b/examples/postgres/multi-tenant/Cargo.toml @@ -9,7 +9,7 @@ categories.workspace = true authors.workspace = true [dependencies] -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1.25.0", features = ["rt-multi-thread", "macros"] } color-eyre = "0.6.3" dotenvy = "0.15.7" @@ -17,7 +17,7 @@ tracing-subscriber = "0.3.19" rust_decimal = "1.36.0" -rand = "0.8.5" +rand = "0.10.1" [dependencies.sqlx] # version = "0.9.0" diff --git a/examples/postgres/multi-tenant/accounts/Cargo.toml b/examples/postgres/multi-tenant/accounts/Cargo.toml index 40c365c607..a1b3661f67 100644 --- a/examples/postgres/multi-tenant/accounts/Cargo.toml +++ b/examples/postgres/multi-tenant/accounts/Cargo.toml @@ -4,18 +4,19 @@ version = "0.1.0" edition = "2021" [dependencies] -tokio = { version = "1", features = ["rt", "sync"] } +tokio = { version = "1.25.0", features = ["rt", "sync"] } -argon2 = { version = "0.5.3", features = ["password-hash"] } -password-hash = { version = "0.5", features = ["std"] } +# FIXME: update to `0.6.0` when released +argon2 = { version = "0.6.0-rc.8", features = ["password-hash"] } +password-hash = { version = "0.6.1", features = ["alloc"] } -uuid = { version = "1", features = ["serde"] } -thiserror = "1" -rand = "0.8" +uuid = { version = "1.12.1", features = ["serde"] } +thiserror = "2.0.18" +rand = "0.10.1" -time = { version = "0.3.37", features = ["serde"] } +time = { version = "0.3.47", features = ["serde"] } -serde = { version = "1.0.218", features = ["derive"] } +serde = { version = "1.0.219", features = ["derive"] } [dependencies.sqlx] # version = "0.9.0" diff --git a/examples/postgres/multi-tenant/accounts/sqlx.toml b/examples/postgres/multi-tenant/accounts/sqlx.toml index 024f6395e5..aeed48043f 100644 --- a/examples/postgres/multi-tenant/accounts/sqlx.toml +++ b/examples/postgres/multi-tenant/accounts/sqlx.toml @@ -4,7 +4,7 @@ table-name = "accounts._sqlx_migrations" [macros.table-overrides.'accounts.account'] 'account_id' = "crate::AccountId" -'password_hash' = "sqlx::types::Text" +'password_hash' = "sqlx::types::Text" [macros.table-overrides.'accounts.session'] 'session_token' = "crate::SessionToken" diff --git a/examples/postgres/multi-tenant/accounts/src/lib.rs b/examples/postgres/multi-tenant/accounts/src/lib.rs index ad33735165..e9f719d9c0 100644 --- a/examples/postgres/multi-tenant/accounts/src/lib.rs +++ b/examples/postgres/multi-tenant/accounts/src/lib.rs @@ -1,6 +1,6 @@ use argon2::{password_hash, Argon2, PasswordHasher, PasswordVerifier}; -use password_hash::PasswordHashString; -use rand::distributions::{Alphanumeric, DistString}; +use password_hash::phc::PasswordHash; +use rand::distr::{Alphanumeric, SampleString}; use sqlx::{Acquire, Executor, PgTransaction, Postgres}; use std::sync::Arc; use uuid::Uuid; @@ -118,7 +118,7 @@ impl AccountsManager { }) } - async fn hash_password(&self, password: String) -> Result { + async fn hash_password(&self, password: String) -> Result { let guard = self .hashing_semaphore .clone() @@ -129,13 +129,7 @@ impl AccountsManager { // We transfer ownership to the blocking task and back to ensure Tokio doesn't spawn // excess threads. let (_guard, res) = tokio::task::spawn_blocking(move || { - let salt = password_hash::SaltString::generate(rand::thread_rng()); - ( - guard, - Argon2::default() - .hash_password(password.as_bytes(), &salt) - .map(|hash| hash.serialize()), - ) + (guard, Argon2::default().hash_password(password.as_bytes())) }) .await?; @@ -145,7 +139,7 @@ impl AccountsManager { async fn verify_password( &self, password: String, - hash: PasswordHashString, + hash: PasswordHash, ) -> Result<(), CreateSessionError> { let guard = self .hashing_semaphore @@ -157,13 +151,13 @@ impl AccountsManager { let (_guard, res) = tokio::task::spawn_blocking(move || { ( guard, - Argon2::default().verify_password(password.as_bytes(), &hash.password_hash()), + Argon2::default().verify_password(password.as_bytes(), &hash), ) }) .await .map_err(GeneralError::from)?; - if let Err(password_hash::Error::Password) = res { + if let Err(password_hash::Error::PasswordInvalid) = res { return Err(CreateSessionError::InvalidPassword); } @@ -189,7 +183,9 @@ impl AccountsManager { values (1,ドル 2ドル) \ returning account_id", email, - hash.as_str(), + // However, since arguments don't link back to the target column, + // SQLx doesn't know that `PasswordHash` would be a valid argument here. + hash.to_string(), ) .fetch_one(&mut **txn) .await @@ -220,7 +216,7 @@ impl AccountsManager { // Thanks to `sqlx.toml`: // * `account_id` maps to `AccountId` - // * `password_hash` maps to `Text` + // * `password_hash` maps to `Text` // * `session_token` maps to `SessionToken` let maybe_account = sqlx::query!( // language=PostgreSQL @@ -279,6 +275,6 @@ impl SessionToken { const LEN: usize = 32; fn generate() -> Self { - SessionToken(Alphanumeric.sample_string(&mut rand::thread_rng(), Self::LEN)) + SessionToken(Alphanumeric.sample_string(&mut rand::rng(), Self::LEN)) } } diff --git a/examples/postgres/multi-tenant/payments/Cargo.toml b/examples/postgres/multi-tenant/payments/Cargo.toml index de15b21828..6b98b1d7c0 100644 --- a/examples/postgres/multi-tenant/payments/Cargo.toml +++ b/examples/postgres/multi-tenant/payments/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" rust_decimal = "1.36.0" -time = "0.3.37" +time = "0.3.47" uuid = "1.12.1" [dependencies.sqlx] diff --git a/examples/postgres/multi-tenant/setup.py b/examples/postgres/multi-tenant/setup.py new file mode 100644 index 0000000000..4c75162370 --- /dev/null +++ b/examples/postgres/multi-tenant/setup.py @@ -0,0 +1,5 @@ +from os import path + +def setup(database_url, cwd, sqlx): + sqlx("mig run", database_url, cwd=path.join(cwd, "accounts")) + sqlx("mig run", database_url, cwd=path.join(cwd, "payments")) diff --git a/examples/postgres/multi-tenant/src/main.rs b/examples/postgres/multi-tenant/src/main.rs index 94a96fcf2b..fa3f9101a8 100644 --- a/examples/postgres/multi-tenant/src/main.rs +++ b/examples/postgres/multi-tenant/src/main.rs @@ -1,7 +1,7 @@ use accounts::AccountsManager; use color_eyre::eyre; use color_eyre::eyre::{Context, OptionExt}; -use rand::distributions::{Alphanumeric, DistString}; +use rand::distr::{Alphanumeric, SampleString}; use sqlx::Connection; #[tokio::main] @@ -31,7 +31,7 @@ async fn main() -> eyre::Result<()> { // POST /account let user_email = format!("user{}@example.com", rand::random::()); - let user_password = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); + let user_password = Alphanumeric.sample_string(&mut rand::rng(), 16); // Requires an externally managed transaction in case any application-specific records // should be created after the actual account record. diff --git a/examples/postgres/preferred-crates/Cargo.toml b/examples/postgres/preferred-crates/Cargo.toml index cb975e30df..2972a8203c 100644 --- a/examples/postgres/preferred-crates/Cargo.toml +++ b/examples/postgres/preferred-crates/Cargo.toml @@ -11,10 +11,10 @@ authors.workspace = true [dependencies] dotenvy.workspace = true -anyhow = "1" -chrono = "0.4" -serde = { version = "1", features = ["derive"] } -uuid = { version = "1", features = ["serde"] } +anyhow = "1.0.58" +chrono = "0.4.34" +serde = { version = "1.0.219", features = ["derive"] } +uuid = { version = "1.12.1", features = ["serde"] } [dependencies.tokio] workspace = true diff --git a/examples/postgres/preferred-crates/uses-rust-decimal/Cargo.toml b/examples/postgres/preferred-crates/uses-rust-decimal/Cargo.toml index 13c409ac84..09ca470caf 100644 --- a/examples/postgres/preferred-crates/uses-rust-decimal/Cargo.toml +++ b/examples/postgres/preferred-crates/uses-rust-decimal/Cargo.toml @@ -9,9 +9,9 @@ categories.workspace = true authors.workspace = true [dependencies] -chrono = "0.4" -rust_decimal = "1" -uuid = "1" +chrono = "0.4.34" +rust_decimal = "1.36.0" +uuid = "1.12.1" [dependencies.sqlx] workspace = true diff --git a/examples/postgres/preferred-crates/uses-time/Cargo.toml b/examples/postgres/preferred-crates/uses-time/Cargo.toml index 1dfb1dab7f..5f4d519632 100644 --- a/examples/postgres/preferred-crates/uses-time/Cargo.toml +++ b/examples/postgres/preferred-crates/uses-time/Cargo.toml @@ -9,9 +9,9 @@ categories.workspace = true authors.workspace = true [dependencies] -serde = "1" -time = "0.3" -uuid = "1" +serde = "1.0.219" +time = "0.3.47" +uuid = "1.12.1" [dependencies.sqlx] workspace = true diff --git a/examples/postgres/todos/Cargo.toml b/examples/postgres/todos/Cargo.toml index 0696ea57d6..30a24d09d2 100644 --- a/examples/postgres/todos/Cargo.toml +++ b/examples/postgres/todos/Cargo.toml @@ -5,8 +5,8 @@ edition = "2018" workspace = "../../../" [dependencies] -anyhow = "1.0" +anyhow = "1.0.58" sqlx = { path = "../../../", features = [ "postgres", "runtime-tokio", "tls-native-tls" ] } -clap = { version = "4", features = ["derive"] } -tokio = { version = "1.20.0", features = ["rt", "macros"]} -dotenvy = "0.15.0" +clap = { version = "4.4.7", features = ["derive"] } +tokio = { version = "1.25.0", features = ["rt", "macros"]} +dotenvy = "0.15.7" diff --git a/examples/postgres/transaction/Cargo.toml b/examples/postgres/transaction/Cargo.toml index b6bd4e7cad..6370044781 100644 --- a/examples/postgres/transaction/Cargo.toml +++ b/examples/postgres/transaction/Cargo.toml @@ -6,4 +6,4 @@ workspace = "../../../" [dependencies] sqlx = { path = "../../../", features = [ "postgres", "runtime-tokio", "tls-native-tls" ] } -tokio = { version = "1.20.0", features = ["rt-multi-thread", "macros"]} +tokio = { version = "1.25.0", features = ["rt-multi-thread", "macros"]} diff --git a/examples/sqlite/extension/Cargo.toml b/examples/sqlite/extension/Cargo.toml index fa2042e343..3ee7d2b493 100644 --- a/examples/sqlite/extension/Cargo.toml +++ b/examples/sqlite/extension/Cargo.toml @@ -10,8 +10,8 @@ authors.workspace = true [dependencies] sqlx = { path = "../../../", features = [ "sqlite", "runtime-tokio", "tls-native-tls", "sqlx-toml"] } -tokio = { version = "1.20.0", features = ["rt", "macros"]} -anyhow = "1.0" +tokio = { version = "1.25.0", features = ["rt", "macros"]} +anyhow = "1.0.58" [lints] workspace = true diff --git a/examples/sqlite/extension/download-extension.sh b/examples/sqlite/extension/download-extension.sh index ce7f23a486..83829b3ae9 100755 --- a/examples/sqlite/extension/download-extension.sh +++ b/examples/sqlite/extension/download-extension.sh @@ -6,4 +6,8 @@ # directory on the library search path, either by using the system # package manager or by compiling and installing it yourself. -mkdir /tmp/sqlite3-lib && wget -O /tmp/sqlite3-lib/ipaddr.so https://github.com/nalgeon/sqlean/releases/download/0.15.2/ipaddr.so +mkdir /tmp/sqlite3-lib && \ + wget -O /tmp/sqlean-linux-x64.zip https://github.com/nalgeon/sqlean/releases/download/0.28.0/sqlean-linux-x64.zip && \ + unzip /tmp/sqlean-linux-x64.zip -d /tmp/sqlite3-lib && \ + mv /tmp/sqlite3-lib/uuid.so /tmp/sqlite3-lib/uuid_renamed.so && \ + rm /tmp/sqlean-linux-x64.zip diff --git a/examples/sqlite/extension/migrations/20251115215857_uuid.sql b/examples/sqlite/extension/migrations/20251115215857_uuid.sql new file mode 100644 index 0000000000..c0a8673b4b --- /dev/null +++ b/examples/sqlite/extension/migrations/20251115215857_uuid.sql @@ -0,0 +1,10 @@ +create table uuids (uuid text); + +-- The `uuid4` function is provided by the +-- [uuid](https://github.com/nalgeon/sqlean/blob/main/docs/uuid.md) +-- sqlite extension, and so this migration can not run if that +-- extension is not loaded. +insert into uuids (uuid) values + (uuid4()), + (uuid4()), + (uuid4()); diff --git a/examples/sqlite/extension/sqlx.toml b/examples/sqlite/extension/sqlx.toml index 7c67dd160e..ada65b1a25 100644 --- a/examples/sqlite/extension/sqlx.toml +++ b/examples/sqlite/extension/sqlx.toml @@ -1,4 +1,4 @@ -[common.drivers.sqlite] +[drivers.sqlite] # Including the full path to the extension is somewhat unusual, # because normally an extension will be installed in a standard # directory which is part of the library search path. If that were the @@ -9,4 +9,7 @@ # * Provide the full path the the extension, as seen below. # * Add the non-standard location to the library search path, which on # Linux means adding it to the LD_LIBRARY_PATH environment variable. -unsafe-load-extensions = ["/tmp/sqlite3-lib/ipaddr"] +unsafe-load-extensions = [ + "/tmp/sqlite3-lib/ipaddr", + { path = "/tmp/sqlite3-lib/uuid_renamed", entrypoint = "sqlite3_uuid_init" }, +] diff --git a/examples/sqlite/extension/src/main.rs b/examples/sqlite/extension/src/main.rs index ee859c55b8..15a5b01ab8 100644 --- a/examples/sqlite/extension/src/main.rs +++ b/examples/sqlite/extension/src/main.rs @@ -7,24 +7,32 @@ use sqlx::{ #[tokio::main(flavor = "current_thread")] async fn main() -> anyhow::Result<()> { - let opts = SqliteConnectOptions::from_str(&std::env::var("DATABASE_URL")?)? - // The sqlx.toml file controls loading extensions for the CLI - // and for the query checking macros, *not* for the - // application while it's running. Thus, if we want the - // extension to be available during program execution, we need - // to load it. - // - // Note that while in this case the extension path is the same - // when checking the program (sqlx.toml) and when running it - // (here), this is not required. The runtime environment can - // be entirely different from the development one. - // - // The extension can be described with a full path, as seen - // here, but in many cases that will not be necessary. As long - // as the extension is installed in a directory on the library - // search path, it is sufficient to just provide the extension - // name, like "ipaddr" - .extension("/tmp/sqlite3-lib/ipaddr"); + let opts = SqliteConnectOptions::from_str(&std::env::var("DATABASE_URL")?)?; + // The sqlx.toml file controls loading extensions for the CLI + // and for the query checking macros, *not* for the + // application while it's running. Thus, if we want the + // extension to be available during program execution, we need + // to load it. + // + // Note that while in this case the extension paths are the + // same when checking the program (sqlx.toml) and when running + // it (here), this is not required. The runtime environment + // can be entirely different from the development one. + // + // The extension can be described with a full path, as seen + // here, but in many cases that will not be necessary. As long + // as the extension is installed in a directory on the library + // search path, it is sufficient to just provide the extension + // name, like "ipaddr" + let opts = unsafe { opts.extension("/tmp/sqlite3-lib/ipaddr") }; + // The entrypoint for an extension is usually inferred as + // `sqlite3_extension_init` or `sqlite3_X_init` where X is the + // lowercase, ASCII-only equivalent of the filename. For the + // extension below, this would be `sqlite3_uuidrenamed_init`. + // The entrypoint can instead be explicitly provided. + let opts = unsafe { + opts.extension_with_entrypoint("/tmp/sqlite3-lib/uuid_renamed", "sqlite3_uuid_init") + }; let db = SqlitePool::connect_with(opts).await?; @@ -41,7 +49,11 @@ async fn main() -> anyhow::Result<()> { .execute(&db) .await?; - println!("Query which requires the extension was successfully executed."); + query!("insert into uuids (uuid) values (uuid4())") + .execute(&db) + .await?; + + println!("Queries which require the extensions were successfully executed."); Ok(()) } diff --git a/examples/sqlite/serialize/Cargo.toml b/examples/sqlite/serialize/Cargo.toml new file mode 100644 index 0000000000..580d8bcef2 --- /dev/null +++ b/examples/sqlite/serialize/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sqlx-example-sqlite-serialize" +version = "0.1.0" +edition = "2024" +workspace = "../../../" + +[dependencies] +anyhow = "1.0.58" +sqlx = { path = "../../../", features = ["sqlite", "sqlite-deserialize", "runtime-tokio"] } +tokio = { version = "1.25.0", features = ["rt", "macros"] } diff --git a/examples/sqlite/serialize/src/main.rs b/examples/sqlite/serialize/src/main.rs new file mode 100644 index 0000000000..f3c0b709b5 --- /dev/null +++ b/examples/sqlite/serialize/src/main.rs @@ -0,0 +1,31 @@ +use sqlx::sqlite::SqliteOwnedBuf; +use sqlx::{Connection, SqliteConnection}; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + sqlx::raw_sql( + "create table notes(id integer primary key, body text not null); + insert into notes(body) values ('hello'), ('world');", + ) + .execute(&mut conn) + .await?; + + let snapshot: SqliteOwnedBuf = conn.serialize(None).await?; + let bytes: &[u8] = snapshot.as_ref(); + conn.close().await?; + + let owned = SqliteOwnedBuf::try_from(bytes)?; + let mut restored = SqliteConnection::connect("sqlite::memory:").await?; + restored.deserialize(None, owned, false).await?; + + let rows = sqlx::query_as::<_, (i64, String)>("select id, body from notes order by id") + .fetch_all(&mut restored) + .await?; + + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].1, "hello"); + assert_eq!(rows[1].1, "world"); + Ok(()) +} diff --git a/examples/sqlite/todos/Cargo.toml b/examples/sqlite/todos/Cargo.toml index 544ca2d8df..712a653bed 100644 --- a/examples/sqlite/todos/Cargo.toml +++ b/examples/sqlite/todos/Cargo.toml @@ -5,7 +5,7 @@ edition = "2018" workspace = "../../../" [dependencies] -anyhow = "1.0" +anyhow = "1.0.58" sqlx = { path = "../../../", features = [ "sqlite", "runtime-tokio", "tls-native-tls" ] } -clap = { version = "4", features = ["derive"] } -tokio = { version = "1.20.0", features = ["rt", "macros"]} +clap = { version = "4.4.7", features = ["derive"] } +tokio = { version = "1.25.0", features = ["rt", "macros"]} diff --git a/examples/x.py b/examples/x.py index aaf4170c77..46733d5e41 100755 --- a/examples/x.py +++ b/examples/x.py @@ -2,6 +2,7 @@ import sys import os +import glob from os import path # base dir of sqlx workspace @@ -16,6 +17,7 @@ import subprocess import time import argparse +import runpy from docker import start_database parser = argparse.ArgumentParser() @@ -34,7 +36,7 @@ def run(command, env=None, cwd=None, display=None): res = subprocess.run( command.split(" "), - env=dict(**os.environ, **env), + env=os.environ | env, cwd=cwd, ) @@ -43,7 +45,7 @@ def run(command, env=None, cwd=None, display=None): def sqlx(command, url, cwd=None): - run(f"cargo --quiet run -p sqlx-cli --bin sqlx -- {command}", cwd=cwd, env={"DATABASE_URL": url}, + run(f"cargo --quiet run -p sqlx-cli --bin sqlx -- {command} --database-url {url}", cwd=cwd, env={}, display=f"sqlx {command}") @@ -74,15 +76,41 @@ def project(name, database=None, driver=None): # database create sqlx("db create", database_url, cwd=cwd) + if path.exists(path.join(name, "setup.py")): + setup = runpy.run_path(path.join(name, "setup.py")) + res = setup["setup"](database_url=database_url,cwd=cwd,sqlx=sqlx) + + if type(res) is dict: + env |= res + # migrate - sqlx("migrate run", database_url, cwd=cwd) + if path.exists(path.join(name, "migrations")) or path.exists(path.join(name, "src/migrations")): + sqlx("migrate run", database_url, cwd=cwd) # check run("cargo check", cwd=cwd, env=env) -# todos -project("mysql/todos", driver="mysql_8", database="todos") -project("postgres/todos", driver="postgres_12", database="todos") -project("sqlite/todos", driver="sqlite", database="todos.db") -project("sqlite/extension", driver="sqlite", database="extension.db") +# MySQL +for example_path in sorted(glob.iglob("mysql/*")): + if not path.isdir(example_path): + continue + + example = path.relpath(example_path, start="mysql") + project(example_path, driver="mysql_8", database=f"example-{example}") + +# Postgres +for example_path in sorted(glob.iglob("postgres/*")): + if not path.isdir(example_path): + continue + + example = path.relpath(example_path, start="postgres") + project(example_path, driver="postgres_17", database=f"example-{example}") + +# SQLite +for example_path in sorted(glob.iglob("sqlite/*")): + if not path.isdir(example_path): + continue + + example = path.relpath(example_path, start="sqlite") + project(example_path, driver="sqlite", database=f"example-{example}.db") diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f406d73736..38c71d18e0 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ # Note: should NOT increase during a minor/patch release cycle [toolchain] -channel = "1.86" +channel = "1.94" profile = "minimal" diff --git a/sqlx-cli/Cargo.toml b/sqlx-cli/Cargo.toml index d69048e698..f283ba747b 100644 --- a/sqlx-cli/Cargo.toml +++ b/sqlx-cli/Cargo.toml @@ -26,20 +26,20 @@ name = "cargo-sqlx" path = "src/bin/cargo-sqlx.rs" [dependencies] -dotenvy = "0.15.0" -tokio = { version = "1.15.0", features = ["macros", "rt", "rt-multi-thread", "signal"] } -futures-util = { version = "0.3.19", features = ["alloc"] } -clap = { version = "4.3.10", features = ["derive", "env", "wrap_help"] } +dotenvy = "0.15.7" +tokio = { version = "1.25.0", features = ["macros", "rt", "rt-multi-thread", "signal"] } +futures-util = { version = "0.3.32", features = ["alloc"] } +clap = { version = "4.4.7", 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" +chrono = { version = "0.4.34", default-features = false, features = ["clock"] } +anyhow = "1.0.58" console = "0.15.0" dialoguer = { version = "0.11", default-features = false } -serde_json = "1.0.73" +serde_json = "1.0.142" glob = "0.3.0" -openssl = { version = "0.10.38", optional = true } -cargo_metadata = "0.18.1" -filetime = "0.2" +openssl = { version = "0.10.46", optional = true } +cargo_metadata = "0.23.1" +filetime = "0.2.25" backoff = { version = "0.4.0", features = ["futures", "tokio"] } @@ -61,6 +61,7 @@ native-tls = ["sqlx/tls-native-tls"] # databases mysql = ["sqlx/mysql"] +mysql-rsa = ["sqlx/mysql-rsa"] postgres = ["sqlx/postgres"] sqlite = ["sqlx/sqlite", "_sqlite"] sqlite-unbundled = ["sqlx/sqlite-unbundled", "_sqlite"] @@ -76,7 +77,7 @@ sqlx-toml = ["sqlx/sqlx-toml"] _sqlite = [] [dev-dependencies] -assert_cmd = "2.0.11" +assert_cmd = "2.1.1" tempfile = "3.10.1" [lints] diff --git a/sqlx-cli/README.md b/sqlx-cli/README.md index b20461b8fd..ae575d5b5f 100644 --- a/sqlx-cli/README.md +++ b/sqlx-cli/README.md @@ -22,8 +22,14 @@ $ cargo install sqlx-cli --no-default-features --features rustls # only for sqlite and use the system sqlite library $ cargo install sqlx-cli --no-default-features --features sqlite-unbundled + +# if you connect to MySQL/MariaDB without TLS and the server requires RSA auth +$ cargo install sqlx-cli --features mysql-rsa ``` +Add `mysql-rsa` only for non-TLS MySQL/MariaDB connections that use +`caching_sha2_password` or `sha256_password`. If you use TLS, it is not needed. + ## Usage All commands require that a database url is provided. This can be done either with the `--database-url` command line option or by setting `DATABASE_URL`, either in the environment or in a `.env` file diff --git a/sqlx-cli/src/database.rs b/sqlx-cli/src/database.rs index eaba46eed9..44d58eecca 100644 --- a/sqlx-cli/src/database.rs +++ b/sqlx-cli/src/database.rs @@ -62,7 +62,16 @@ pub async fn setup( connect_opts: &ConnectOpts, ) -> anyhow::Result<()> { create(connect_opts).await?; - migrate::run(config, migration_source, connect_opts, false, false, None).await + migrate::run( + config, + migration_source, + connect_opts, + false, + false, + None, + false, + ) + .await } async fn ask_to_continue_drop(db_url: String) -> bool { diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index bd3f11137c..9abd689e96 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -27,7 +27,7 @@ use futures_util::TryFutureExt; use sqlx::AnyConnection; use tokio::{select, signal}; -use crate::opt::{Command, ConnectOpts, DatabaseCommand, MigrateCommand}; +use crate::opt::{Command, ConnectOpts, DatabaseCommand, MigrateCommand, OverrideCommand}; pub mod database; pub mod metadata; @@ -49,7 +49,11 @@ pub fn maybe_apply_dotenv() { return; } - dotenvy::dotenv().ok(); + if let Err(e) = dotenvy::dotenv() { + if !e.not_found() { + eprintln!("Warning: error loading `.env` file: {e:?}"); + } + } } pub async fn run(opt: Opt) -> anyhow::Result<()> { @@ -95,6 +99,7 @@ async fn do_run(opt: Opt) -> anyhow::Result<()> { dry_run, *ignore_missing, target_version, + false, ) .await? } @@ -120,6 +125,30 @@ async fn do_run(opt: Opt) -> anyhow::Result<()> { ) .await? } + MigrateCommand::Override { command } => match command { + OverrideCommand::Skip { + source, + config, + mut connect_opts, + dry_run, + ignore_missing, + target_version, + } => { + let config = config.load_config().await?; + connect_opts.populate_db_url(&config)?; + + migrate::run( + &config, + &source, + &connect_opts, + dry_run, + *ignore_missing, + target_version, + true, + ) + .await? + } + }, MigrateCommand::Info { source, config, diff --git a/sqlx-cli/src/metadata.rs b/sqlx-cli/src/metadata.rs index e90d9c66c3..38afa9ba14 100644 --- a/sqlx-cli/src/metadata.rs +++ b/sqlx-cli/src/metadata.rs @@ -9,6 +9,7 @@ use std::{ use anyhow::Context; use cargo_metadata::{ Metadata as CargoMetadata, Package as MetadataPackage, PackageId as MetadataId, + PackageName as MetadataPackageName, }; /// The minimal amount of package information we care about @@ -17,13 +18,13 @@ use cargo_metadata::{ /// are used to trigger recompiles of packages within the workspace #[derive(Debug)] pub struct Package { - name: String, + name: MetadataPackageName, src_paths: Vec, } impl Package { pub fn name(&self) -> &str { - &self.name + self.name.as_str() } pub fn src_paths(&self) -> &[PathBuf] { diff --git a/sqlx-cli/src/migrate.rs b/sqlx-cli/src/migrate.rs index 926e264032..b26a9f46a3 100644 --- a/sqlx-cli/src/migrate.rs +++ b/sqlx-cli/src/migrate.rs @@ -220,6 +220,7 @@ pub async fn run( dry_run: bool, ignore_missing: bool, target_version: Option, + skip: bool, ) -> anyhow::Result<()> { let migrator = migration_source.resolve(config).await?; @@ -277,18 +278,23 @@ pub async fn run( } } None => { - let skip = + let exceeds_target = target_version.is_some_and(|target_version| migration.version> target_version); - let elapsed = if dry_run || skip { + let elapsed = if dry_run || exceeds_target { + Duration::new(0, 0) + } else if skip { + conn.skip(config.migrate.table_name(), migration).await?; Duration::new(0, 0) } else { conn.apply(config.migrate.table_name(), migration).await? }; - let text = if skip { + let text = if exceeds_target { "Skipped" } else if dry_run { "Can apply" + } else if skip { + "Skipped on request" } else { "Applied" }; diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index cb09bc2ff5..48ec0207ba 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -247,6 +247,12 @@ pub enum MigrateCommand { target_version: Option, }, + /// Override migration state, potentially dangerous operations. + Override { + #[clap(subcommand)] + command: OverrideCommand, + }, + /// Revert the latest migration with a down file. Revert { #[clap(flatten)] @@ -300,6 +306,33 @@ pub enum MigrateCommand { }, } +#[derive(clap::Subcommand, Debug)] +pub enum OverrideCommand { + /// Skip all pending migrations without running them. + Skip { + #[clap(flatten)] + source: MigrationSourceOpt, + + #[clap(flatten)] + config: ConfigOpt, + + #[clap(flatten)] + connect_opts: ConnectOpts, + + /// List all the migrations to be skipped without marking them as applied. + #[clap(long)] + dry_run: bool, + + #[clap(flatten)] + ignore_missing: IgnoreMissing, + + /// Apply migrations up to the specified version. If unspecified, apply all + /// pending migrations. If already at the target version, then no-op. + #[clap(long)] + target_version: Option, + }, +} + #[derive(Args, Debug)] pub struct AddMigrationOpts { pub description: String, diff --git a/sqlx-cli/src/prepare.rs b/sqlx-cli/src/prepare.rs index 9f3fc67da4..f3688add2a 100644 --- a/sqlx-cli/src/prepare.rs +++ b/sqlx-cli/src/prepare.rs @@ -161,7 +161,7 @@ fn run_prepare_step(ctx: &PrepareCtx, cache_dir: &Path) -> anyhow::Result<()> { let tmp_dir = ctx.metadata.target_directory().join("sqlx-tmp"); fs::create_dir_all(&tmp_dir).context(format!( "Failed to create temporary query cache directory: {:?}", - cache_dir + tmp_dir ))?; // Only delete sqlx-*.json files to avoid accidentally deleting any user data. diff --git a/sqlx-cli/tests/add.rs b/sqlx-cli/tests/add.rs index cebbb51d53..bf9085b85e 100644 --- a/sqlx-cli/tests/add.rs +++ b/sqlx-cli/tests/add.rs @@ -1,5 +1,5 @@ use anyhow::Context; -use assert_cmd::Command; +use assert_cmd::cargo_bin_cmd; use std::cmp::Ordering; use std::fs::read_dir; use std::ops::Index; @@ -117,7 +117,7 @@ impl AddMigrations { sequential: bool, expect_success: bool, ) -> anyhow::Result<&'_ Self> { - let cmd_result = Command::cargo_bin("cargo-sqlx")? + let cmd_result = cargo_bin_cmd!("cargo-sqlx") .current_dir(&self.tempdir) .args( [ diff --git a/sqlx-cli/tests/common/mod.rs b/sqlx-cli/tests/common/mod.rs index 66e7924859..e3bd9f6e83 100644 --- a/sqlx-cli/tests/common/mod.rs +++ b/sqlx-cli/tests/common/mod.rs @@ -13,6 +13,22 @@ pub struct TestDatabase { pub config_path: Option, } +pub enum MigrateCommand { + Run, + Revert, + Skip, +} + +impl AsRef for MigrateCommand { + fn as_ref(&self) -> &str { + match self { + MigrateCommand::Run => "run", + MigrateCommand::Revert => "revert", + MigrateCommand::Skip => "override skip", + } + } +} + impl TestDatabase { pub fn new(name: &str, migrations: &str) -> Self { // Note: only set when _building_ @@ -58,19 +74,17 @@ impl TestDatabase { format!("sqlite://{}", self.file_path.display()) } - pub fn run_migration(&self, revert: bool, version: Option, dry_run: bool) -> Assert { + pub fn run_migration( + &self, + migrate_command: MigrateCommand, + version: Option, + dry_run: bool, + ) -> Assert { let mut command = Command::cargo_bin("sqlx").unwrap(); command - .args([ - "migrate", - match revert { - true => "revert", - false => "run", - }, - "--database-url", - &self.connection_string(), - "--source", - ]) + .arg("migrate") + .args(migrate_command.as_ref().split_whitespace()) + .args(["--database-url", &self.connection_string(), "--source"]) .arg(&self.migrations_path); if let Some(config_path) = &self.config_path { diff --git a/sqlx-cli/tests/ignored-chars/sqlx.toml b/sqlx-cli/tests/ignored-chars/sqlx.toml index e5278d283f..f331e5b7a4 100644 --- a/sqlx-cli/tests/ignored-chars/sqlx.toml +++ b/sqlx-cli/tests/ignored-chars/sqlx.toml @@ -1,5 +1,5 @@ [migrate] -# Ignore common whitespace characters (beware syntatically significant whitespace!) +# Ignore common whitespace characters (beware syntactically significant whitespace!) # Space, tab, CR, LF, zero-width non-breaking space (U+FEFF) # # U+FEFF is added by some editors as a magic number at the beginning of a text file indicating it is UTF-8 encoded, diff --git a/sqlx-cli/tests/migrate.rs b/sqlx-cli/tests/migrate.rs index f33ee5eb0e..002aa4da96 100644 --- a/sqlx-cli/tests/migrate.rs +++ b/sqlx-cli/tests/migrate.rs @@ -1,5 +1,6 @@ mod common; +use common::MigrateCommand; use common::TestDatabase; #[tokio::test] @@ -14,7 +15,7 @@ async fn run_reversible_migrations() { // Without --target-version specified.k { let db = TestDatabase::new("run_reversible_latest", "migrations_reversible"); - db.run_migration(false, None, false).success(); + db.run_migration(MigrateCommand::Run, None, false).success(); assert_eq!(db.applied_migrations().await, all_migrations); } // With --target-version specified. @@ -22,17 +23,17 @@ async fn run_reversible_migrations() { let db = TestDatabase::new("run_reversible_latest_explicit", "migrations_reversible"); // Move to latest, explicitly specified. - db.run_migration(false, Some(20230501000000), false) + db.run_migration(MigrateCommand::Run, Some(20230501000000), false) .success(); assert_eq!(db.applied_migrations().await, all_migrations); // Move to latest when we're already at the latest. - db.run_migration(false, Some(20230501000000), false) + db.run_migration(MigrateCommand::Run, Some(20230501000000), false) .success(); assert_eq!(db.applied_migrations().await, all_migrations); // Upgrade to an old version. - db.run_migration(false, Some(20230301000000), false) + db.run_migration(MigrateCommand::Run, Some(20230301000000), false) .failure(); assert_eq!(db.applied_migrations().await, all_migrations); } @@ -41,26 +42,26 @@ async fn run_reversible_migrations() { let db = TestDatabase::new("run_reversible_incremental", "migrations_reversible"); // First version - db.run_migration(false, Some(20230101000000), false) + db.run_migration(MigrateCommand::Run, Some(20230101000000), false) .success(); assert_eq!(db.applied_migrations().await, vec![20230101000000]); // Dry run upgrade to latest. - db.run_migration(false, None, true).success(); + db.run_migration(MigrateCommand::Run, None, true).success(); assert_eq!(db.applied_migrations().await, vec![20230101000000]); // Dry run upgrade + 2 - db.run_migration(false, Some(20230301000000), true) + db.run_migration(MigrateCommand::Run, Some(20230301000000), true) .success(); assert_eq!(db.applied_migrations().await, vec![20230101000000]); // Upgrade to non-existent version. - db.run_migration(false, Some(20230901000000999), false) + db.run_migration(MigrateCommand::Run, Some(20230901000000999), false) .failure(); assert_eq!(db.applied_migrations().await, vec![20230101000000]); // Upgrade + 1 - db.run_migration(false, Some(20230201000000), false) + db.run_migration(MigrateCommand::Run, Some(20230201000000), false) .success(); assert_eq!( db.applied_migrations().await, @@ -68,7 +69,7 @@ async fn run_reversible_migrations() { ); // Upgrade + 2 - db.run_migration(false, Some(20230401000000), false) + db.run_migration(MigrateCommand::Run, Some(20230401000000), false) .success(); assert_eq!(db.applied_migrations().await, all_migrations[..4]); } @@ -87,65 +88,150 @@ async fn revert_migrations() { // Without --target-version { let db = TestDatabase::new("revert_incremental", "migrations_reversible"); - db.run_migration(false, None, false).success(); + db.run_migration(MigrateCommand::Run, None, false).success(); // Dry-run - db.run_migration(true, None, true).success(); + db.run_migration(MigrateCommand::Revert, None, true) + .success(); assert_eq!(db.applied_migrations().await, all_migrations); // Downgrade one - db.run_migration(true, None, false).success(); + db.run_migration(MigrateCommand::Revert, None, false) + .success(); assert_eq!(db.applied_migrations().await, all_migrations[..4]); // Downgrade one - db.run_migration(true, None, false).success(); + db.run_migration(MigrateCommand::Revert, None, false) + .success(); assert_eq!(db.applied_migrations().await, all_migrations[..3]); } // With --target-version { let db = TestDatabase::new("revert_incremental", "migrations_reversible"); - db.run_migration(false, None, false).success(); + db.run_migration(MigrateCommand::Run, None, false).success(); // Dry-run downgrade to version 3. - db.run_migration(true, Some(20230301000000), true).success(); + db.run_migration(MigrateCommand::Revert, Some(20230301000000), true) + .success(); assert_eq!(db.applied_migrations().await, all_migrations); // Downgrade to version 3. - db.run_migration(true, Some(20230301000000), false) + db.run_migration(MigrateCommand::Revert, Some(20230301000000), false) .success(); assert_eq!(db.applied_migrations().await, all_migrations[..3]); // Try downgrading to the same version. - db.run_migration(true, Some(20230301000000), false) + db.run_migration(MigrateCommand::Revert, Some(20230301000000), false) .success(); assert_eq!(db.applied_migrations().await, all_migrations[..3]); // Try downgrading to a newer version. - db.run_migration(true, Some(20230401000000), false) + db.run_migration(MigrateCommand::Revert, Some(20230401000000), false) .failure(); assert_eq!(db.applied_migrations().await, all_migrations[..3]); // Try downgrading to a non-existent version. - db.run_migration(true, Some(9999), false).failure(); + db.run_migration(MigrateCommand::Revert, Some(9999), false) + .failure(); assert_eq!(db.applied_migrations().await, all_migrations[..3]); // Ensure we can still upgrade - db.run_migration(false, Some(20230401000000), false) + db.run_migration(MigrateCommand::Run, Some(20230401000000), false) .success(); assert_eq!(db.applied_migrations().await, all_migrations[..4]); // Downgrade to zero. - db.run_migration(true, Some(0), false).success(); + db.run_migration(MigrateCommand::Revert, Some(0), false) + .success(); assert_eq!(db.applied_migrations().await, Vec::::new()); } } +#[tokio::test] +async fn skip_reversible_migrations() { + let all_migrations: Vec = vec![ + 20230101000000, + 20230201000000, + 20230301000000, + 20230401000000, + 20230501000000, + ]; + // Without --target-version specified. + { + let db = TestDatabase::new("migrate_skip_reversible_latest", "migrations_reversible"); + db.run_migration(MigrateCommand::Skip, None, false) + .success(); + assert_eq!(db.applied_migrations().await, all_migrations); + } + // With --target-version specified. + { + let db = TestDatabase::new( + "migrate_skip_reversible_latest_explicit", + "migrations_reversible", + ); + + // Move to latest, explicitly specified. + db.run_migration(MigrateCommand::Run, Some(20230501000000), false) + .success(); + assert_eq!(db.applied_migrations().await, all_migrations); + + // Skip to latest when we're already at the latest. + db.run_migration(MigrateCommand::Skip, Some(20230501000000), false) + .success(); + assert_eq!(db.applied_migrations().await, all_migrations); + + // Upgrade to an old version. + db.run_migration(MigrateCommand::Skip, Some(20230301000000), false) + .failure(); + assert_eq!(db.applied_migrations().await, all_migrations); + } + // With --target-version, incrementally upgrade. + { + let db = TestDatabase::new( + "migrate_skip_reversible_incremental", + "migrations_reversible", + ); + + // Run first version + db.run_migration(MigrateCommand::Run, Some(20230101000000), false) + .success(); + assert_eq!(db.applied_migrations().await, vec![20230101000000]); + + // Skip and dry run upgrade to latest. + db.run_migration(MigrateCommand::Skip, None, true).success(); + assert_eq!(db.applied_migrations().await, vec![20230101000000]); + + // Skip and dry run upgrade + 2 + db.run_migration(MigrateCommand::Skip, Some(20230301000000), true) + .success(); + assert_eq!(db.applied_migrations().await, vec![20230101000000]); + + // Skip to to non-existent version. + db.run_migration(MigrateCommand::Skip, Some(20230901000000999), false) + .failure(); + assert_eq!(db.applied_migrations().await, vec![20230101000000]); + + // Upgrade + 1 + db.run_migration(MigrateCommand::Run, Some(20230201000000), false) + .success(); + assert_eq!( + db.applied_migrations().await, + vec![20230101000000, 20230201000000] + ); + + // Skip + 2 + db.run_migration(MigrateCommand::Skip, Some(20230401000000), false) + .success(); + assert_eq!(db.applied_migrations().await, all_migrations[..4]); + } +} + #[tokio::test] async fn ignored_chars() { let mut db = TestDatabase::new("ignored-chars", "ignored-chars/LF"); db.config_path = Some("tests/ignored-chars/sqlx.toml".into()); - db.run_migration(false, None, false).success(); + db.run_migration(MigrateCommand::Run, None, false).success(); db.set_migrations("ignored-chars/CRLF"); @@ -155,13 +241,19 @@ async fn ignored_chars() { db.migrate_info().success().stdout(expected_info); // Running migration should be a no-op - db.run_migration(false, None, false).success().stdout(""); + db.run_migration(MigrateCommand::Run, None, false) + .success() + .stdout(""); db.set_migrations("ignored-chars/BOM"); db.migrate_info().success().stdout(expected_info); - db.run_migration(false, None, false).success().stdout(""); + db.run_migration(MigrateCommand::Run, None, false) + .success() + .stdout(""); db.set_migrations("ignored-chars/oops-all-tabs"); db.migrate_info().success().stdout(expected_info); - db.run_migration(false, None, false).success().stdout(""); + db.run_migration(MigrateCommand::Run, None, false) + .success() + .stdout(""); } diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index 58c5b67e05..90ed446b4b 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -56,12 +56,13 @@ tokio = { workspace = true, optional = true } native-tls = { version = "0.2.10", optional = true } rustls = { version = "0.23.24", default-features = false, features = ["std", "tls12"], optional = true } -webpki-roots = { version = "0.26", optional = true } +webpki-roots = { version = "1", optional = true } rustls-native-certs = { version = "0.8.0", optional = true } # Type Integrations bit-vec = { workspace = true, optional = true } bigdecimal = { workspace = true, optional = true } +chrono = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } time = { workspace = true, optional = true } ipnet = { workspace = true, optional = true } @@ -69,44 +70,50 @@ ipnetwork = { workspace = true, optional = true } mac_address = { workspace = true, optional = true } uuid = { workspace = true, optional = true } +# work around bug in async-fs 2.0.0, which references futures-lite dependency wrongly, see https://github.com/launchbadge/sqlx/pull/3791#issuecomment-3043363281 +async-fs = { version = "2.1", optional = true } async-io = { version = "2.4.1", optional = true } async-task = { version = "4.7.1", optional = true } -# work around bug in async-fs 2.0.0, which references futures-lite dependency wrongly, see https://github.com/launchbadge/sqlx/pull/3791#issuecomment-3043363281 -async-fs = { version = "2.1", optional = true } -base64 = { version = "0.22.0", default-features = false, features = ["std"] } -bytes = "1.1.0" +base64.workspace = true +bytes = "1.2.0" cfg-if = { workspace = true } -chrono = { version = "0.4.34", default-features = false, features = ["clock"], optional = true } crc = { version = "3", optional = true } crossbeam-queue = "0.3.2" either = "1.6.1" -futures-core = { version = "0.3.19", default-features = false } -futures-io = "0.3.24" +futures-core = { version = "0.3.32", default-features = false } +futures-io = "0.3.32" futures-intrusive = "0.5.0" -futures-util = { version = "0.3.19", default-features = false, features = ["alloc", "sink", "io"] } +futures-util = { version = "0.3.32", default-features = false, features = ["alloc", "sink", "io"] } log = { version = "0.4.18", default-features = false } -memchr = { version = "2.4.1", default-features = false } -percent-encoding = "2.1.0" -serde = { version = "1.0.132", features = ["derive", "rc"], optional = true } -serde_json = { version = "1.0.73", features = ["raw_value"], optional = true } +memchr = { version = "2.5.0", default-features = false } +percent-encoding = "2.3.0" +serde = { version = "1.0.219", features = ["derive", "rc"], optional = true } +serde_json = { version = "1.0.142", features = ["raw_value"], optional = true } toml = { version = "0.8.16", optional = true } -sha2 = { version = "0.10.0", default-features = false, optional = true } +sha2 = { workspace = true, optional = true } #sqlformat = "0.2.0" -thiserror = "2.0.0" tokio-stream = { version = "0.1.8", features = ["fs"], optional = true } tracing = { version = "0.1.37", features = ["log"] } -smallvec = "1.7.0" +smallvec = "1.13.1" url = { version = "2.2.2" } -bstr = { version = "1.0", default-features = false, features = ["std"], optional = true } -hashlink = "0.10.0" +bstr = { version = "1.0.1", default-features = false, features = ["std"], optional = true } +hashlink = "0.11.0" indexmap = "2.0" event-listener = "5.2.0" -hashbrown = "0.15.0" +hashbrown = "0.16.0" + +thiserror.workspace = true [dev-dependencies] -sqlx = { workspace = true, features = ["postgres", "sqlite", "mysql", "migrate", "macros", "time", "uuid"] } -tokio = { version = "1", features = ["rt"] } +tokio = { version = "1.25.0", features = ["rt"] } + +[dev-dependencies.sqlx] +# FIXME: https://github.com/rust-lang/cargo/issues/15622 +# workspace = true +path = ".." +default-features = false +features = ["postgres", "sqlite", "mysql", "migrate", "macros", "time", "uuid"] [lints] workspace = true diff --git a/sqlx-core/src/any/connection/backend.rs b/sqlx-core/src/any/connection/backend.rs index 5b3ea7bf2d..1ee743a37e 100644 --- a/sqlx-core/src/any/connection/backend.rs +++ b/sqlx-core/src/any/connection/backend.rs @@ -1,5 +1,4 @@ -use crate::any::{Any, AnyArguments, AnyQueryResult, AnyRow, AnyStatement, AnyTypeInfo}; -use crate::describe::Describe; +use crate::any::{AnyArguments, AnyQueryResult, AnyRow, AnyStatement, AnyTypeInfo}; use crate::sql_str::SqlStr; use either::Either; use futures_core::future::BoxFuture; @@ -114,5 +113,9 @@ pub trait AnyConnectionBackend: std::any::Any + Debug + Send + 'static { parameters: &[AnyTypeInfo], ) -> BoxFuture<'c, crate::Result>; - fn describe(&mut self, sql: SqlStr) -> BoxFuture<'_, crate::Result>>; + #[cfg(feature = "offline")] + fn describe( + &mut self, + sql: SqlStr, + ) -> BoxFuture<'_, crate::Result>>; } diff --git a/sqlx-core/src/any/connection/executor.rs b/sqlx-core/src/any/connection/executor.rs index a7c2cd33a0..88937668a6 100644 --- a/sqlx-core/src/any/connection/executor.rs +++ b/sqlx-core/src/any/connection/executor.rs @@ -1,5 +1,4 @@ use crate::any::{Any, AnyConnection, AnyQueryResult, AnyRow, AnyStatement, AnyTypeInfo}; -use crate::describe::Describe; use crate::error::Error; use crate::executor::{Execute, Executor}; use crate::sql_str::SqlStr; @@ -56,7 +55,11 @@ impl<'c> Executor<'c> for &'c mut AnyConnection { self.backend.prepare_with(sql, parameters) } - fn describe<'e>(self, sql: SqlStr) -> BoxFuture<'e, Result, Error>> + #[cfg(feature = "offline")] + fn describe<'e>( + self, + sql: SqlStr, + ) -> BoxFuture<'e, Result, Error>> where 'c: 'e, { diff --git a/sqlx-core/src/any/migrate.rs b/sqlx-core/src/any/migrate.rs index 7a894c68bc..deca501cb7 100644 --- a/sqlx-core/src/any/migrate.rs +++ b/sqlx-core/src/any/migrate.rs @@ -99,4 +99,12 @@ impl Migrate for AnyConnection { ) -> BoxFuture<'e, Result> { Box::pin(async { self.get_migrate()?.revert(table_name, migration).await }) } + + fn skip<'e>( + &'e mut self, + table_name: &'e str, + migration: &'e Migration, + ) -> BoxFuture<'e, Result<(), MigrateError>> { + Box::pin(async { self.get_migrate()?.skip(table_name, migration).await }) + } } diff --git a/sqlx-core/src/column.rs b/sqlx-core/src/column.rs index 132e7b0346..e9b6aec813 100644 --- a/sqlx-core/src/column.rs +++ b/sqlx-core/src/column.rs @@ -84,6 +84,11 @@ impl ColumnOrigin { /// This trait is implemented for strings which are used to look up a column by name, and for /// `usize` which is used as a positional index into the row. /// +/// *Caution*: The column index may differ between a [`Statement`] and a [`Row`] returned by the +/// statement. This can happen with some databases if, for example, the schema changes between +/// prepare and execute or if the database does not provide column information when the statement +/// is prepared. +/// /// [`Row`]: crate::row::Row /// [`Statement`]: crate::statement::Statement /// [`get`]: crate::row::Row::get diff --git a/sqlx-core/src/config/drivers.rs b/sqlx-core/src/config/drivers.rs index 5019f1f9f6..c3416541cd 100644 --- a/sqlx-core/src/config/drivers.rs +++ b/sqlx-core/src/config/drivers.rs @@ -102,7 +102,19 @@ pub struct SqliteConfig { /// [common.drivers.sqlite] /// unsafe-load-extensions = ["uuid", "vsv"] /// ``` - pub unsafe_load_extensions: Vec, + pub unsafe_load_extensions: Vec, +} + +/// Extension for the SQLite database driver. +#[derive(Debug, PartialEq)] +#[cfg_attr( + feature = "sqlx-toml", + derive(serde::Deserialize), + serde(untagged, deny_unknown_fields) +)] +pub enum SqliteExtension { + Path(String), + PathWithEntrypoint { path: String, entrypoint: String }, } /// Configuration for external database drivers. diff --git a/sqlx-core/src/config/mod.rs b/sqlx-core/src/config/mod.rs index 267a2f1ed1..960d0f434f 100644 --- a/sqlx-core/src/config/mod.rs +++ b/sqlx-core/src/config/mod.rs @@ -158,7 +158,16 @@ impl Config { /// * If the file exists but could not be read or parsed. /// * If the file exists but the `sqlx-toml` feature is disabled. pub fn try_from_crate_or_default() -> Result { - Self::read_from(get_crate_path()?).or_else(|e| { + Self::try_from_path_or_default(get_crate_path()?) + } + + /// Attempt to read `Config` from the path given, or return `Config::default()` if it does not exist. + /// + /// # Errors + /// * If the file exists but could not be read or parsed. + /// * If the file exists but the `sqlx-toml` feature is disabled. + pub fn try_from_path_or_default(path: PathBuf) -> Result { + Self::read_from(path).or_else(|e| { if let ConfigError::NotFound { .. } = e { Ok(Config::default()) } else { diff --git a/sqlx-core/src/config/reference.toml b/sqlx-core/src/config/reference.toml index f0acbdbe6a..618ac23c91 100644 --- a/sqlx-core/src/config/reference.toml +++ b/sqlx-core/src/config/reference.toml @@ -44,7 +44,10 @@ database-url-var = "FOO_DATABASE_URL" # It is not possible to provide a truly safe version of this API. # # Use this field with care, and only load extensions that you trust. -unsafe-load-extensions = ["uuid", "vsv"] +unsafe-load-extensions = [ + "uuid", + { path = "vsv_renamed", entrypoint = "sqlite3_vsv_init" }, +] # Configure external drivers in macros and sqlx-cli. # @@ -90,8 +93,8 @@ numeric = "rust_decimal" # or not. They only override the inner type used. [macros.type-overrides] # Override a built-in type (map all `UUID` columns to `crate::types::MyUuid`) -# Note: currently, the case of the type name MUST match. -# Built-in types are spelled in all-uppercase to match SQL convention. +# Note: currently, the case of the type name MUST match. +# Built-in types are spelled in all-uppercase to match SQL convention. 'UUID' = "crate::types::MyUuid" # Support an external or custom wrapper type (e.g. from the `isn` Postgres extension) @@ -206,7 +209,7 @@ migrations-dir = "foo/migrations" # Note that the TOML format requires double-quoted strings to process escapes. # ignored-chars = ["\r"] -# Ignore common whitespace characters (beware syntatically significant whitespace!) +# Ignore common whitespace characters (beware syntactically significant whitespace!) # Space, tab, CR, LF, zero-width non-breaking space (U+FEFF) # # U+FEFF is added by some editors as a magic number at the beginning of a text file indicating it is UTF-8 encoded, diff --git a/sqlx-core/src/config/tests.rs b/sqlx-core/src/config/tests.rs index 91e877afb6..d01b859e46 100644 --- a/sqlx-core/src/config/tests.rs +++ b/sqlx-core/src/config/tests.rs @@ -18,7 +18,16 @@ fn assert_common_config(config: &config::common::Config) { } fn assert_drivers_config(config: &config::drivers::Config) { - assert_eq!(config.sqlite.unsafe_load_extensions, ["uuid", "vsv"]); + assert_eq!( + config.sqlite.unsafe_load_extensions, + vec![ + config::drivers::SqliteExtension::Path("uuid".to_string()), + config::drivers::SqliteExtension::PathWithEntrypoint { + path: "vsv_renamed".to_string(), + entrypoint: "sqlite3_vsv_init".to_string() + } + ] + ); #[derive(Debug, Eq, PartialEq, serde::Deserialize)] #[serde(rename_all = "kebab-case")] diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index c6652aef75..8c6f424cdf 100644 --- a/sqlx-core/src/error.rs +++ b/sqlx-core/src/error.rs @@ -84,7 +84,7 @@ pub enum Error { source: BoxDynError, }, - /// Error occured while encoding a value. + /// Error occurred while encoding a value. #[error("error occurred while encoding a value: {0}")] Encode(#[source] BoxDynError), diff --git a/sqlx-core/src/executor.rs b/sqlx-core/src/executor.rs index e1c42fc706..e146c7a895 100644 --- a/sqlx-core/src/executor.rs +++ b/sqlx-core/src/executor.rs @@ -1,5 +1,4 @@ use crate::database::Database; -use crate::describe::Describe; use crate::error::{BoxDynError, Error}; use crate::sql_str::{SqlSafeStr, SqlStr}; @@ -31,6 +30,26 @@ use std::{fmt::Debug, future}; /// * `&mut transaction` -> `&mut *transaction` /// * `&mut connection` -> `&mut *connection` /// +/// # Note: Methods Not for General Use +/// This is a building-block trait that is exposed mainly for use as a trait bound. +/// +/// Instead of calling the methods of this trait, use the free functions in the `sqlx` crate root: +/// * [`sqlx::query()`]: use for DML queries (`SELECT`, `INSERT`, `UPDATE`, `DELETE`, etc.) +/// * uses and caches prepared statements internally per-connection +/// * may use argument placeholders (MySQL/MariaDB: `?`, Postgres/SQLite: `1ドル`, `2ドル`, etc.) +/// * one query per string +/// * [`sqlx::query_as()`], [`sqlx::query_scalar()`]: preferred for DML queries with type mapping +/// * [`sqlx::raw_sql()`]: for DDL (`CREATE`, `ALTER`, etc.), batch queries and administration functions +/// * accepts multiple queries separated by semicolons (`;`) in the same string +/// * never uses prepared statements +/// +/// Consider also the `query!()` family of macros, which offer queries and type mapping checked +/// at compile-time. +/// +/// [`sqlx::query()`]: crate::query::query +/// [`sqlx::query_as()`]: crate::query_as::query_as +/// [`sqlx::query_scalar()`]: crate::query_scalar::query_scalar +/// [`sqlx::raw_sql()`]: crate::raw_sql::raw_sql pub trait Executor<'c>: Send + Debug + Sized { type Database: Database; @@ -178,7 +197,11 @@ pub trait Executor<'c>: Send + Debug + Sized { /// This is used by compile-time verification in the query macros to /// power their type inference. #[doc(hidden)] - fn describe<'e>(self, sql: SqlStr) -> BoxFuture<'e, Result, Error>> + #[cfg(feature = "offline")] + fn describe<'e>( + self, + sql: SqlStr, + ) -> BoxFuture<'e, Result, Error>> where 'c: 'e; } diff --git a/sqlx-core/src/migrate/error.rs b/sqlx-core/src/migrate/error.rs index a04243963a..3c08f57301 100644 --- a/sqlx-core/src/migrate/error.rs +++ b/sqlx-core/src/migrate/error.rs @@ -42,4 +42,7 @@ pub enum MigrateError { #[error("database driver does not support creation of schemas at migrate time: {0}")] CreateSchemasNotSupported(String), + + #[error("database driver does not support skipping migrations")] + SkipNotSupported(), } diff --git a/sqlx-core/src/migrate/migrate.rs b/sqlx-core/src/migrate/migrate.rs index a3c365b807..de4bbc5f9d 100644 --- a/sqlx-core/src/migrate/migrate.rs +++ b/sqlx-core/src/migrate/migrate.rs @@ -78,4 +78,13 @@ pub trait Migrate { table_name: &'e str, migration: &'e Migration, ) -> BoxFuture<'e, Result>; + + // insert new row to [_migrations] table without running SQL from migration + fn skip<'e>( + &'e mut self, + _table_name: &'e str, + _migration: &'e Migration, + ) -> BoxFuture<'e, Result<(), MigrateError>> { + Box::pin(async move { Err(MigrateError::SkipNotSupported()) }) + } } diff --git a/sqlx-core/src/migrate/migration.rs b/sqlx-core/src/migrate/migration.rs index 79721d244d..878139fc6d 100644 --- a/sqlx-core/src/migrate/migration.rs +++ b/sqlx-core/src/migrate/migration.rs @@ -1,5 +1,6 @@ use sha2::{Digest, Sha384}; use std::borrow::Cow; +use std::cmp::Ordering; use crate::sql_str::SqlStr; @@ -15,6 +16,28 @@ pub struct Migration { pub no_tx: bool, } +impl PartialEq for Migration { + fn eq(&self, other: &Self) -> bool { + self.version == other.version && self.migration_type == other.migration_type + } +} + +impl Eq for Migration {} + +impl PartialOrd for Migration { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Migration { + fn cmp(&self, other: &Self) -> Ordering { + self.version + .cmp(&other.version) + .then_with(|| self.migration_type.cmp(&other.migration_type)) + } +} + impl Migration { pub fn new( version: i64, @@ -61,7 +84,7 @@ pub struct AppliedMigration { } pub fn checksum(sql: &str) -> Vec { - Vec::from(Sha384::digest(sql).as_slice()) + Sha384::digest(sql).to_vec() } pub fn checksum_fragments<'a>(fragments: impl Iterator) -> Vec { diff --git a/sqlx-core/src/migrate/migration_type.rs b/sqlx-core/src/migrate/migration_type.rs index 350ddb3f27..107cdd246c 100644 --- a/sqlx-core/src/migrate/migration_type.rs +++ b/sqlx-core/src/migrate/migration_type.rs @@ -1,7 +1,7 @@ use super::Migrator; /// Migration Type represents the type of migration -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum MigrationType { /// Simple migration are single file migrations with no up / down queries Simple, diff --git a/sqlx-core/src/migrate/migrator.rs b/sqlx-core/src/migrate/migrator.rs index 375c2af3fd..c64b0f211b 100644 --- a/sqlx-core/src/migrate/migrator.rs +++ b/sqlx-core/src/migrate/migrator.rs @@ -80,15 +80,14 @@ impl Migrator { /// // Define your migrations. /// // You can also use include_str!("./xxx.sql") instead of hard-coded SQL statements. /// let migrations = vec![ - /// Migration::new(1, "user".into(), ReversibleUp, "create table uesrs ( ... )".into_sql_str(), false), + /// Migration::new(1, "user".into(), ReversibleUp, "create table users ( ... )".into_sql_str(), false), /// Migration::new(2, "post".into(), ReversibleUp, "create table posts ( ... )".into_sql_str(), false), /// // add more... /// ]; /// let m = Migrator::with_migrations(migrations); /// ``` pub fn with_migrations(mut migrations: Vec) -> Self { - // Ensure that we are sorted by version in ascending order. - migrations.sort_by_key(|m| m.version); + migrations.sort(); Self { migrations: Cow::Owned(migrations), ..Self::DEFAULT @@ -180,7 +179,7 @@ impl Migrator { ::Target: Migrate, { let mut conn = migrator.acquire().await?; - self.run_direct(None, &mut *conn).await + self.run_direct(None, &mut *conn, false).await } pub async fn run_to<'a, A>(&self, target: i64, migrator: A) -> Result<(), MigrateError> @@ -189,12 +188,48 @@ impl Migrator { ::Target: Migrate, { let mut conn = migrator.acquire().await?; - self.run_direct(Some(target), &mut *conn).await + self.run_direct(Some(target), &mut *conn, false).await + } + + /// Skip any pending migrations until a specific version against the database; + /// Additionally validate previously applied migrations against the current migration + /// source to detect accidental changes in previously-applied migrations. + /// + /// Skipping entails not executing the SQL of the migrations, but marking them as + /// applied in the [_migrations] table. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use sqlx::migrate::MigrateError; + /// # fn main() -> Result<(), MigrateError> { + /// # sqlx::__rt::test_block_on(async move { + /// use sqlx::migrate::Migrator; + /// use sqlx::sqlite::SqlitePoolOptions; + /// + /// let m = Migrator::new(std::path::Path::new("./migrations")).await?; + /// let pool = SqlitePoolOptions::new().connect("sqlite::memory:").await?; + /// m.skip(&pool, Some(17)).await + /// # }) + /// # } + /// ``` + pub async fn skip<'a, A>(&self, migrator: A, target: Option) -> Result<(), MigrateError> + where + A: Acquire<'a>, + ::Target: Migrate, + { + let mut conn = migrator.acquire().await?; + self.run_direct(target, &mut *conn, true).await } // Getting around the annoying "implementation of `Acquire` is not general enough" error #[doc(hidden)] - pub async fn run_direct(&self, target: Option, conn: &mut C) -> Result<(), MigrateError> + pub async fn run_direct( + &self, + target: Option, + conn: &mut C, + skip: bool, + ) -> Result<(), MigrateError> where C: Migrate, { @@ -241,7 +276,11 @@ impl Migrator { } } None => { - conn.apply(&self.table_name, migration).await?; + if skip { + conn.skip(&self.table_name, migration).await?; + } else { + conn.apply(&self.table_name, migration).await?; + } } } } diff --git a/sqlx-core/src/migrate/source.rs b/sqlx-core/src/migrate/source.rs index 4648e53f1e..10fc7c7b8e 100644 --- a/sqlx-core/src/migrate/source.rs +++ b/sqlx-core/src/migrate/source.rs @@ -248,8 +248,7 @@ pub fn resolve_blocking_with_config( )); } - // Ensure that we are sorted by version in ascending order. - migrations.sort_by_key(|(m, _)| m.version); + migrations.sort(); Ok(migrations) } diff --git a/sqlx-core/src/net/tls/util.rs b/sqlx-core/src/net/tls/util.rs index 42e11f31be..99aa6384c6 100644 --- a/sqlx-core/src/net/tls/util.rs +++ b/sqlx-core/src/net/tls/util.rs @@ -2,7 +2,7 @@ use crate::net::Socket; use std::future; use std::io::{self, Read, Write}; -use std::task::{ready, Context, Poll}; +use std::task::{Context, Poll}; pub struct StdSocket { pub socket: S, @@ -19,20 +19,35 @@ impl StdSocket { } } + /// Returns `Ready` if a previously blocked read _or_ write may now proceed. + /// + /// If both a read and a write were attempted, to avoid deadlocks this returns `Ready` + /// when _either_ direction is ready, not necessarily both. pub fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - if self.wants_write { - ready!(self.socket.poll_write_ready(cx))?; + // Return `Ready` without waiting if the caller hasn't tried to do I/O in either direction. + let mut ready = !(self.wants_read || self.wants_write); + + if self.wants_write && self.socket.poll_write_ready(cx)?.is_ready() { self.wants_write = false; + ready |= true; } - if self.wants_read { - ready!(self.socket.poll_read_ready(cx))?; + if self.wants_read && self.socket.poll_read_ready(cx)?.is_ready() { self.wants_read = false; + ready |= true; } - Poll::Ready(Ok(())) + if ready { + Poll::Ready(Ok(())) + } else { + Poll::Pending + } } + /// Returns successfully if a previously blocked read _or_ write may now proceed. + /// + /// If both a read and a write were attempted, to avoid deadlocks this returns when _either_ + /// direction is ready, not necessarily both. pub async fn ready(&mut self) -> io::Result<()> { future::poll_fn(|cx| self.poll_ready(cx)).await } diff --git a/sqlx-core/src/pool/executor.rs b/sqlx-core/src/pool/executor.rs index c168a70f51..6fb06b9ec1 100644 --- a/sqlx-core/src/pool/executor.rs +++ b/sqlx-core/src/pool/executor.rs @@ -4,7 +4,6 @@ use futures_core::stream::BoxStream; use futures_util::TryStreamExt; use crate::database::Database; -use crate::describe::Describe; use crate::error::Error; use crate::executor::{Execute, Executor}; use crate::pool::Pool; @@ -63,7 +62,11 @@ where } #[doc(hidden)] - fn describe<'e>(self, sql: SqlStr) -> BoxFuture<'e, Result, Error>> { + #[cfg(feature = "offline")] + fn describe<'e>( + self, + sql: SqlStr, + ) -> BoxFuture<'e, Result, Error>> { let pool = self.clone(); Box::pin(async move { pool.acquire().await?.describe(sql).await }) @@ -127,6 +130,7 @@ where // } // // #[doc(hidden)] +// #[cfg(feature = "offline")] // #[inline] // fn describe<'e, 'q: 'e>( // self, diff --git a/sqlx-core/src/sql_str.rs b/sqlx-core/src/sql_str.rs index 100f270214..53e404268f 100644 --- a/sqlx-core/src/sql_str.rs +++ b/sqlx-core/src/sql_str.rs @@ -35,6 +35,15 @@ use std::sync::Arc; /// [injection]: https://en.wikipedia.org/wiki/SQL_injection /// [`query()`]: crate::query::query /// [`raw_sql()`]: crate::raw_sql::raw_sql +#[diagnostic::on_unimplemented( + label = "dynamic SQL string", + message = "dynamic SQL strings should be audited for possible injections", + note = "prefer literal SQL strings with bind parameters or `QueryBuilder` to add dynamic data to a query. + +To bypass this error, manually audit for potential injection vulnerabilities and wrap with `AssertSqlSafe()`. +For details, see the docs for `SqlSafeStr`.\n", + note = "this trait is only implemented for `&'static str`, not all `&str` like the compiler error may suggest" +)] pub trait SqlSafeStr { /// Convert `self` to a [`SqlStr`]. fn into_sql_str(self) -> SqlStr; diff --git a/sqlx-core/src/testing/mod.rs b/sqlx-core/src/testing/mod.rs index 17022b4652..379fac690e 100644 --- a/sqlx-core/src/testing/mod.rs +++ b/sqlx-core/src/testing/mod.rs @@ -257,7 +257,7 @@ async fn setup_test_db( if let Some(migrator) = args.migrator { migrator - .run_direct(None, &mut conn) + .run_direct(None, &mut conn, false) .await .expect("failed to apply migrations"); } diff --git a/sqlx-core/src/transaction.rs b/sqlx-core/src/transaction.rs index 09bb68ed02..46f89d25b2 100644 --- a/sqlx-core/src/transaction.rs +++ b/sqlx-core/src/transaction.rs @@ -189,6 +189,7 @@ where // } // // #[doc(hidden)] +// #[cfg(feature = "offline")] // fn describe<'e, 'q: 'e>( // self, // query: &'q str, diff --git a/sqlx-core/src/types/json.rs b/sqlx-core/src/types/json.rs index 3b94d8729b..6616ce1a7d 100644 --- a/sqlx-core/src/types/json.rs +++ b/sqlx-core/src/types/json.rs @@ -86,6 +86,13 @@ use crate::types::Type; #[serde(transparent)] pub struct Json(pub T); +impl Json { + /// Extract the inner value. + pub fn into_inner(self) -> T { + self.0 + } +} + impl From for Json { fn from(value: T) -> Self { Self(value) diff --git a/sqlx-macros-core/Cargo.toml b/sqlx-macros-core/Cargo.toml index 3bcbede6f4..42bf0f5e8c 100644 --- a/sqlx-macros-core/Cargo.toml +++ b/sqlx-macros-core/Cargo.toml @@ -26,13 +26,14 @@ _sqlite = [] # SQLx features derive = [] -macros = [] +macros = ["thiserror"] migrate = ["sqlx-core/migrate"] sqlx-toml = ["sqlx-core/sqlx-toml", "sqlx-sqlite?/sqlx-toml"] # database mysql = ["sqlx-mysql"] +mysql-rsa = ["mysql", "sqlx-mysql/rsa"] postgres = ["sqlx-postgres"] sqlite = ["_sqlite", "sqlx-sqlite/bundled"] sqlite-unbundled = ["_sqlite", "sqlx-sqlite/unbundled"] @@ -55,7 +56,7 @@ uuid = ["sqlx-core/uuid", "sqlx-mysql?/uuid", "sqlx-postgres?/uuid", "sqlx-sqlit [dependencies] sqlx-core = { workspace = true, features = ["offline"] } -sqlx-mysql = { workspace = true, features = ["offline", "migrate"], optional = true } +sqlx-mysql = { workspace = true, features = ["offline", "migrate"], optional = true, default-features = false } sqlx-postgres = { workspace = true, features = ["offline", "migrate"], optional = true } sqlx-sqlite = { workspace = true, features = ["offline", "migrate"], optional = true } @@ -66,16 +67,17 @@ tokio = { workspace = true, optional = true } cfg-if = { workspace = true} dotenvy = { workspace = true } +thiserror = { workspace = true, optional = true } hex = { version = "0.4.3" } heck = { version = "0.5" } either = "1.6.1" -proc-macro2 = { version = "1.0.79", default-features = false } -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"] } -quote = { version = "1.0.26", default-features = false } +proc-macro2 = { version = "1.0.83", default-features = false } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = { version = "1.0.142" } +sha2 = { workspace = true } +syn = { version = "2.0.87", default-features = false, features = ["full", "derive", "parsing", "printing", "clone-impls"] } +quote = { version = "1.0.35", default-features = false } url = { version = "2.2.2" } [lints.rust.unexpected_cfgs] diff --git a/sqlx-macros-core/clippy.toml b/sqlx-macros-core/clippy.toml new file mode 100644 index 0000000000..0c0f4fe996 --- /dev/null +++ b/sqlx-macros-core/clippy.toml @@ -0,0 +1,3 @@ +[[disallowed-methods]] +path = "std::env::var" +reason = "use `crate::env()` instead, which optionally calls `proc_macro::tracked::env_var()`" diff --git a/sqlx-macros-core/src/common.rs b/sqlx-macros-core/src/common.rs index b195a9ffd0..662dc12eff 100644 --- a/sqlx-macros-core/src/common.rs +++ b/sqlx-macros-core/src/common.rs @@ -1,5 +1,4 @@ use proc_macro2::Span; -use std::env; use std::path::{Path, PathBuf}; pub(crate) fn resolve_path(path: impl AsRef, err_span: Span) -> syn::Result { @@ -25,13 +24,9 @@ pub(crate) fn resolve_path(path: impl AsRef, err_span: Span) -> syn::Resul )); } - let base_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| { - syn::Error::new( - err_span, - "CARGO_MANIFEST_DIR is not set; please use Cargo to build", - ) - })?; - let base_dir_path = Path::new(&base_dir); + let mut out_path = crate::manifest_dir().map_err(|e| syn::Error::new(err_span, e))?; + + out_path.push(path); - Ok(base_dir_path.join(path)) + Ok(out_path) } diff --git a/sqlx-macros-core/src/lib.rs b/sqlx-macros-core/src/lib.rs index 9d4204f814..db6586200f 100644 --- a/sqlx-macros-core/src/lib.rs +++ b/sqlx-macros-core/src/lib.rs @@ -16,17 +16,21 @@ #![cfg_attr( any(sqlx_macros_unstable, procmacro2_semver_exempt), - feature(track_path) + feature(proc_macro_tracked_path, proc_macro_tracked_env) )] +#[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))] +extern crate proc_macro; + use cfg_if::cfg_if; +use std::path::PathBuf; #[cfg(feature = "macros")] use crate::query::QueryDriver; pub type Error = Box; -pub type Result = std::result::Result; +pub type Result = std::result::Result; mod common; pub mod database; @@ -84,3 +88,29 @@ where } } } + +pub fn env(var: &str) -> Result { + env_opt(var)? + .ok_or_else(|| format!("env var {var:?} must be set to use the query macros").into()) +} + +#[allow(clippy::disallowed_methods)] +pub fn env_opt(var: &str) -> Result> { + use std::env::VarError; + + #[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))] + let res: Result = proc_macro::tracked::env_var(var); + + #[cfg(not(any(sqlx_macros_unstable, procmacro2_semver_exempt)))] + let res: Result = std::env::var(var); + + match res { + Ok(val) => Ok(Some(val)), + Err(VarError::NotPresent) => Ok(None), + Err(VarError::NotUnicode(_)) => Err(format!("env var {var:?} is not valid UTF-8").into()), + } +} + +pub fn manifest_dir() -> Result { + Ok(env("CARGO_MANIFEST_DIR")?.into()) +} diff --git a/sqlx-macros-core/src/migrate.rs b/sqlx-macros-core/src/migrate.rs index b855703c22..6ef1582093 100644 --- a/sqlx-macros-core/src/migrate.rs +++ b/sqlx-macros-core/src/migrate.rs @@ -1,6 +1,3 @@ -#[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))] -extern crate proc_macro; - use std::path::{Path, PathBuf}; use proc_macro2::{Span, TokenStream}; @@ -132,7 +129,7 @@ pub fn expand_with_path(config: &Config, path: &Path) -> crate::Result { + inner: Mutex>>, +} + +pub struct MtimeCacheBuilder { + file_mtimes: Vec<(pathbuf, Option)>, +} + +struct MtimeCacheInner { + builder: MtimeCacheBuilder, + cached: T, +} + +impl MtimeCache { + pub fn new() -> Self { + MtimeCache { + inner: Mutex::new(None), + } + } + + /// Get the cached value, or (re)initialize it if it does not exist or a file's mtime has changed. + pub fn get_or_try_init( + &self, + init: impl FnOnce(&mut MtimeCacheBuilder) -> Result, + ) -> Result { + let mut inner = self.inner.lock().unwrap_or_else(|e| { + // Reset the cache on-panic. + let mut locked = e.into_inner(); + *locked = None; + locked + }); + + if let Some(inner) = &*inner { + if !inner.builder.any_modified() { + return Ok(inner.cached.clone()); + } + } + + let mut builder = MtimeCacheBuilder::new(); + + let value = init(&mut builder)?; + + *inner = Some(MtimeCacheInner { + builder, + cached: value.clone(), + }); + + Ok(value) + } +} + +impl MtimeCacheBuilder { + fn new() -> Self { + MtimeCacheBuilder { + file_mtimes: Vec::new(), + } + } + + /// Add a file path to watch. + /// + /// The cached value will be automatically invalidated if the modified-time of the file changes, + /// or if the file does not exist but is created sometime after this call. + pub fn add_path(&mut self, path: PathBuf) { + let mtime = get_mtime(&path); + + #[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))] + { + proc_macro::tracked::path(&path); + } + + self.file_mtimes.push((path, mtime)); + } + + fn any_modified(&self) -> bool { + for (path, expected_mtime) in &self.file_mtimes { + let actual_mtime = get_mtime(path); + + if expected_mtime != &actual_mtime { + return true; + } + } + + false + } +} + +fn get_mtime(path: &Path) -> Option { + std::fs::metadata(path) + .and_then(|metadata| metadata.modified()) + .ok() +} diff --git a/sqlx-macros-core/src/query/data.rs b/sqlx-macros-core/src/query/data.rs index 470f86f973..912236ae37 100644 --- a/sqlx-macros-core/src/query/data.rs +++ b/sqlx-macros-core/src/query/data.rs @@ -1,17 +1,18 @@ -use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter}; use std::fs; use std::io::Write as _; use std::marker::PhantomData; use std::path::{Path, PathBuf}; -use std::sync::{LazyLock, Mutex}; +use std::sync::{Arc, LazyLock, Mutex}; use serde::{Serialize, Serializer}; use sqlx_core::database::Database; use sqlx_core::describe::Describe; +use sqlx_core::HashMap; use crate::database::DatabaseExt; +use crate::query::cache::MtimeCache; #[derive(serde::Serialize)] #[serde(bound(serialize = "Describe: serde::Serialize"))] @@ -64,7 +65,7 @@ impl Serialize for SerializeDbName { } } -static OFFLINE_DATA_CACHE: LazyLock>> = +static OFFLINE_DATA_CACHE: LazyLock>>>> = LazyLock::new(Default::default); /// Offline query data @@ -79,47 +80,33 @@ pub struct DynQueryData { impl DynQueryData { /// Loads a query given the path to its "query-.json" file. Subsequent calls for the same /// path are retrieved from an in-memory cache. - pub fn from_data_file(path: impl AsRef, query: &str) -> crate::Result { - let path = path.as_ref(); - - let mut cache = OFFLINE_DATA_CACHE + pub fn from_data_file(path: &Path, query: &str) -> crate::Result { + let cache = OFFLINE_DATA_CACHE .lock() // Just reset the cache on error .unwrap_or_else(|poison_err| { let mut guard = poison_err.into_inner(); *guard = Default::default(); guard - }); - if let Some(cached) = cache.get(path).cloned() { - if query != cached.query { - return Err("hash collision for saved query data".into()); - } - return Ok(cached); - } - - #[cfg(procmacro2_semver_exempt)] - { - let path = path.as_ref().canonicalize()?; - let path = path.to_str().ok_or_else(|| { - format!( - "query-.json path cannot be represented as a string: {:?}", - path - ) - })?; + }) + .entry_ref(path) + .or_insert_with(|| Arc::new(MtimeCache::new())) + .clone(); - proc_macro::tracked_path::path(path); - } + cache.get_or_try_init(|builder| { + builder.add_path(path.into()); - let offline_data_contents = fs::read_to_string(path) - .map_err(|e| format!("failed to read saved query path {}: {}", path.display(), e))?; - let dyn_data: DynQueryData = serde_json::from_str(&offline_data_contents)?; + let offline_data_contents = fs::read_to_string(path).map_err(|e| { + format!("failed to read saved query path {}: {}", path.display(), e) + })?; + let dyn_data: DynQueryData = serde_json::from_str(&offline_data_contents)?; - if query != dyn_data.query { - return Err("hash collision for saved query data".into()); - } + if query != dyn_data.query { + return Err("hash collision for saved query data".into()); + } - let _ = cache.insert(path.to_owned(), dyn_data.clone()); - Ok(dyn_data) + Ok(dyn_data) + }) } } @@ -149,41 +136,71 @@ where } } - pub(super) fn save_in(&self, dir: impl AsRef) -> crate::Result<()> { + pub(super) fn save_in(&self, dir: &Path) -> crate::Result<()> { use std::io::ErrorKind; - let path = dir.as_ref().join(format!("query-{}.json", self.hash)); - match std::fs::remove_file(&path) { - Ok(()) => {} - Err(err) - if matches!( - err.kind(), - ErrorKind::NotFound | ErrorKind::PermissionDenied, - ) => {} - Err(err) => return Err(format!("failed to delete {path:?}: {err:?}").into()), + let path = dir.join(format!("query-{}.json", self.hash)); + + if let Err(err) = fs::remove_file(&path) { + match err.kind() { + ErrorKind::NotFound | ErrorKind::PermissionDenied => (), + ErrorKind::NotADirectory => { + return Err(format!( + "sqlx offline path exists, but is not a directory: {dir:?}" + ) + .into()); + } + _ => return Err(format!("failed to delete {path:?}: {err:?}").into()), + } } - let mut file = match std::fs::OpenOptions::new() + + // Prevent tearing from concurrent invocations possibly trying to write the same file + // by using the existence of the file itself as a mutex. + // + // By deleting the file first and then using `.create_new(true)`, + // we guarantee that this only succeeds if another invocation hasn't concurrently + // re-created the file. + let mut file = match fs::OpenOptions::new() .write(true) .create_new(true) .open(&path) { Ok(file) => file, - // We overlapped with a concurrent invocation and the other one succeeded. - Err(err) if matches!(err.kind(), ErrorKind::AlreadyExists) => return Ok(()), Err(err) => { - return Err(format!("failed to exclusively create {path:?}: {err:?}").into()) + return match err.kind() { + // We overlapped with a concurrent invocation and the other one succeeded. + ErrorKind::AlreadyExists => Ok(()), + ErrorKind::NotFound => { + Err(format!("sqlx offline path does not exist: {dir:?}").into()) + } + ErrorKind::NotADirectory => Err(format!( + "sqlx offline path exists, but is not a directory: {dir:?}" + ) + .into()), + _ => Err(format!("failed to exclusively create {path:?}: {err:?}").into()), + }; } }; - let data = serde_json::to_string_pretty(self) - .map_err(|err| format!("failed to serialize query data: {err:?}"))?; - file.write_all(data.as_bytes()) - .map_err(|err| format!("failed to write query data to file: {err:?}"))?; + // From a quick survey of the files generated by `examples/postgres/axum-social-with-tests`, + // which are generally in the 1-2 KiB range, this seems like a safe bet to avoid + // lots of reallocations without using too much memory. + // + // As of writing, `serde_json::to_vec_pretty()` only allocates 128 bytes up-front. + let mut data = Vec::with_capacity(4096); + + serde_json::to_writer_pretty(&mut data, self).expect("BUG: failed to serialize query data"); // Ensure there is a newline at the end of the JSON file to avoid // accidental modification by IDE and make github diff tool happier. - file.write_all(b"\n") - .map_err(|err| format!("failed to append a newline to file: {err:?}"))?; + data.push(b'\n'); + + // This ideally writes the data in as few syscalls as possible. + file.write_all(&data) + .map_err(|err| format!("failed to write query data to file {path:?}: {err:?}"))?; + + // We don't really need to call `.sync_data()` since it's trivial to re-run the macro + // in the event a power loss results in incomplete flushing of the data to disk. Ok(()) } diff --git a/sqlx-macros-core/src/query/input.rs b/sqlx-macros-core/src/query/input.rs index 63e35ec77d..7068403014 100644 --- a/sqlx-macros-core/src/query/input.rs +++ b/sqlx-macros-core/src/query/input.rs @@ -26,6 +26,7 @@ enum QuerySrc { File(String), } +#[allow(clippy::large_enum_variant)] pub enum RecordType { Given(Type), Scalar, diff --git a/sqlx-macros-core/src/query/metadata.rs b/sqlx-macros-core/src/query/metadata.rs new file mode 100644 index 0000000000..5d5d3885d7 --- /dev/null +++ b/sqlx-macros-core/src/query/metadata.rs @@ -0,0 +1,181 @@ +use sqlx_core::config::Config; +use std::hash::{BuildHasherDefault, DefaultHasher}; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +use crate::query::cache::{MtimeCache, MtimeCacheBuilder}; +use sqlx_core::HashMap; + +pub struct Metadata { + pub manifest_dir: PathBuf, + pub config: Config, + env: MtimeCache>, + workspace_root: Arc>>, +} + +pub struct MacrosEnv { + pub database_url: Option, + pub offline_dir: Option, + pub offline: Option, +} + +impl Metadata { + pub fn env(&self) -> crate::Result> { + let workspace_root = self.workspace_root(); + + self.env.get_or_try_init(|builder| { + load_env(&self.manifest_dir, &workspace_root, &self.config, builder) + }) + } + + pub fn workspace_root(&self) -> PathBuf { + let mut root = self.workspace_root.lock().unwrap(); + if root.is_none() { + use serde::Deserialize; + use std::process::Command; + + let cargo = crate::env("CARGO").unwrap(); + + let output = Command::new(cargo) + .args(["metadata", "--format-version=1", "--no-deps"]) + .current_dir(&self.manifest_dir) + .env_remove("__CARGO_FIX_PLZ") + .output() + .expect("Could not fetch metadata"); + + #[derive(Deserialize)] + struct CargoMetadata { + workspace_root: PathBuf, + } + + let metadata: CargoMetadata = + serde_json::from_slice(&output.stdout).expect("Invalid `cargo metadata` output"); + + *root = Some(metadata.workspace_root); + } + root.clone().unwrap() + } +} + +pub fn try_for_crate() -> crate::Result> { + /// The `MtimeCache` in this type covers the config itself, + /// any changes to which will indirectly invalidate the loaded env vars as well. + #[expect(clippy::type_complexity)] + static METADATA: Mutex< + HashMap>>, BuildHasherDefault>, +> = Mutex::new(HashMap::with_hasher(BuildHasherDefault::new())); + + let manifest_dir = crate::env("CARGO_MANIFEST_DIR")?; + + let cache = METADATA + .lock() + .expect("BUG: we shouldn't panic while holding this lock") + .entry_ref(&manifest_dir) + .or_insert_with(|| Arc::new(MtimeCache::new())) + .clone(); + + cache.get_or_try_init(|builder| { + let manifest_dir = PathBuf::from(manifest_dir); + let config_path = manifest_dir.join("sqlx.toml"); + + builder.add_path(config_path.clone()); + + let config = Config::try_from_path_or_default(config_path)?; + + Ok(Arc::new(Metadata { + manifest_dir, + config, + env: MtimeCache::new(), + workspace_root: Default::default(), + })) + }) +} + +fn load_env( + manifest_dir: &Path, + workspace_root: &Path, + config: &Config, + builder: &mut MtimeCacheBuilder, +) -> crate::Result> { + #[derive(thiserror::Error, Debug)] + #[error("error reading dotenv file {path:?}")] + struct DotenvError { + path: PathBuf, + #[source] + error: dotenvy::Error, + } + + let mut from_dotenv = MacrosEnv { + database_url: None, + offline_dir: None, + offline: None, + }; + + // https://github.com/launchbadge/sqlx/issues/4276 + let dirs = if manifest_dir.starts_with(workspace_root) { + // Often just `[manifest_dir, workspace_dir]` but project structures can absolutely + // be more complicated + manifest_dir + .ancestors() + .take_while(|dir| dir.starts_with(workspace_root)) + .collect::>() + } else { + // Thinking of edge cases, there's the possibility that the package directory + // isn't actually a child of the workspace directory. There isn't really any other sane + // thing to do here; we shouldn't traverse into unrelated paths. + [manifest_dir, workspace_root].to_vec() + }; + + for dir in dirs { + let path = dir.join(".env"); + + let dotenv = match dotenvy::from_path_iter(&path) { + Ok(iter) => { + builder.add_path(path.clone()); + iter + } + Err(dotenvy::Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => { + builder.add_path(dir.to_path_buf()); + continue; + } + Err(e) => { + builder.add_path(path.clone()); + return Err(DotenvError { path, error: e }.into()); + } + }; + + for res in dotenv { + let (name, val) = res.map_err(|e| DotenvError { + path: path.clone(), + error: e, + })?; + + match &*name { + "SQLX_OFFLINE_DIR" => from_dotenv.offline_dir = Some(val.into()), + "SQLX_OFFLINE" => from_dotenv.offline = Some(is_truthy_bool(&val)), + _ if name == config.common.database_url_var() => { + from_dotenv.database_url = Some(val) + } + _ => continue, + } + } + } + + Ok(Arc::new(MacrosEnv { + // Make set variables take precedent + database_url: crate::env_opt(config.common.database_url_var())? + .or(from_dotenv.database_url), + offline_dir: crate::env_opt("SQLX_OFFLINE_DIR")? + .map(PathBuf::from) + .or(from_dotenv.offline_dir), + offline: crate::env_opt("SQLX_OFFLINE")? + .map(|val| is_truthy_bool(&val)) + .or(from_dotenv.offline), + })) +} + +/// Returns `true` if `val` is `"true"`, +fn is_truthy_bool(val: &str) -> bool { + val.eq_ignore_ascii_case("true") || val == "1" +} diff --git a/sqlx-macros-core/src/query/mod.rs b/sqlx-macros-core/src/query/mod.rs index 060a24b847..84461f22b8 100644 --- a/sqlx-macros-core/src/query/mod.rs +++ b/sqlx-macros-core/src/query/mod.rs @@ -1,7 +1,4 @@ -use std::collections::{hash_map, HashMap}; use std::path::{Path, PathBuf}; -use std::sync::{Arc, LazyLock, Mutex}; -use std::{fs, io}; use proc_macro2::TokenStream; use syn::Type; @@ -14,20 +11,25 @@ use sqlx_core::{column::Column, describe::Describe, type_info::TypeInfo}; use crate::database::DatabaseExt; use crate::query::data::{hash_string, DynQueryData, QueryData}; use crate::query::input::RecordType; +use crate::query::metadata::MacrosEnv; use either::Either; +use metadata::Metadata; use sqlx_core::config::Config; use url::Url; mod args; +mod cache; mod data; mod input; +mod metadata; mod output; #[derive(Copy, Clone)] pub struct QueryDriver { db_name: &'static str, url_schemes: &'static [&'static str], - expand: fn(&Config, QueryMacroInput, QueryDataSource) -> crate::Result, + expand: + fn(&Config, QueryMacroInput, QueryDataSource, Option<&path>) -> crate::Result, } impl QueryDriver { @@ -68,138 +70,64 @@ impl<'a> QueryDataSource<'a> { } } } - -struct Metadata { - #[allow(unused)] - manifest_dir: PathBuf, - offline: bool, - database_url: Option, - offline_dir: Option, - config: Config, - workspace_root: Arc>>, -} - -impl Metadata { - pub fn workspace_root(&self) -> PathBuf { - let mut root = self.workspace_root.lock().unwrap(); - if root.is_none() { - use serde::Deserialize; - use std::process::Command; - - let cargo = env("CARGO").expect("`CARGO` must be set"); - - let output = Command::new(cargo) - .args(["metadata", "--format-version=1", "--no-deps"]) - .current_dir(&self.manifest_dir) - .env_remove("__CARGO_FIX_PLZ") - .output() - .expect("Could not fetch metadata"); - - #[derive(Deserialize)] - struct CargoMetadata { - workspace_root: PathBuf, - } - - let metadata: CargoMetadata = - serde_json::from_slice(&output.stdout).expect("Invalid `cargo metadata` output"); - - *root = Some(metadata.workspace_root); - } - root.clone().unwrap() - } -} - -static METADATA: LazyLock>> = LazyLock::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 -fn init_metadata(manifest_dir: &String) -> crate::Result { - let manifest_dir: PathBuf = manifest_dir.into(); - - 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 offline_dir = env("SQLX_OFFLINE_DIR").ok().or(offline_dir); - - let config = Config::try_from_crate_or_default()?; - - let database_url = env(config.common.database_url_var()).ok().or(database_url); - - Ok(Metadata { - manifest_dir, - offline, - database_url, - offline_dir, - config, - workspace_root: Arc::new(Mutex::new(None)), - }) -} - pub fn expand_input<'a>( input: QueryMacroInput, drivers: impl IntoIterator, ) -> crate::Result { - 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::try_for_crate()?; - let metadata = match metadata_lock.entry(manifest_dir) { - hash_map::Entry::Occupied(occupied) => occupied.into_mut(), - hash_map::Entry::Vacant(vacant) => { - let metadata = init_metadata(vacant.key())?; - vacant.insert(metadata) - } - }; + let metadata_env = metadata.env()?; - let data_source = match &metadata { - Metadata { - offline: false, + let data_source = match &*metadata_env { + MacrosEnv { + offline: None | Some(false), database_url: Some(db_url), .. - } => QueryDataSource::live(db_url)?, - Metadata { offline, .. } => { + } + // Allow `DATABASE_URL=''` + if !db_url.is_empty() => QueryDataSource::live(db_url)?, + MacrosEnv { + offline, + offline_dir, + .. + } => { // 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 = [ - |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")), + |_: &Metadata, offline_dir: Option<&path>| offline_dir.map(PathBuf::from), + |meta: &Metadata, _: Option<&path>| Some(meta.manifest_dir.join(".sqlx")), + |meta: &Metadata, _: Option<&path>| Some(meta.workspace_root().join(".sqlx")), ]; + let Some(data_file_path) = dirs .iter() - .filter_map(|path| path(metadata)) + .filter_map(|path| path(&metadata, offline_dir.as_deref())) .map(|path| path.join(&filename)) .find(|path| path.exists()) else { return Err( - if *offline { + if offline.unwrap_or(false) { "`SQLX_OFFLINE=true` but there is no cached data for this query, run `cargo sqlx prepare` to update the query cache or unset `SQLX_OFFLINE`" } else { "set `DATABASE_URL` to use query macros online, or run `cargo sqlx prepare` to update the query cache" }.into() ); }; + QueryDataSource::Cached(DynQueryData::from_data_file(&data_file_path, &input.sql)?) } }; for driver in drivers { if data_source.matches_driver(driver) { - return (driver.expand)(&metadata.config, input, data_source); + return (driver.expand)( + &metadata.config, + input, + data_source, + metadata_env.offline_dir.as_deref(), + ); } } @@ -224,19 +152,21 @@ fn expand_with( config: &Config, input: QueryMacroInput, data_source: QueryDataSource, + offline_dir: Option<&path>, ) -> crate::Result where Describe: DescribeExt, { - let (query_data, offline): (QueryData, bool) = match data_source { - QueryDataSource::Cached(dyn_data) => (QueryData::from_dyn_data(dyn_data)?, true), + let (query_data, save_dir): (QueryData, Option<&path>) = match data_source { + // If the build is offline, the cache is our input so it's pointless to also write data for it. + QueryDataSource::Cached(dyn_data) => (QueryData::from_dyn_data(dyn_data)?, None), QueryDataSource::Live { database_url, .. } => { let describe = DB::describe_blocking(&input.sql, database_url, &config.drivers)?; - (QueryData::from_describe(&input.sql, describe), false) + (QueryData::from_describe(&input.sql, describe), offline_dir) } }; - expand_with_data(config, input, query_data, offline) + expand_with_data(config, input, query_data, save_dir) } // marker trait for `Describe` that lets us conditionally require it to be `Serialize + Deserialize` @@ -257,7 +187,7 @@ fn expand_with_data( config: &Config, input: QueryMacroInput, data: QueryData, - offline: bool, + save_dir: Option<&path>, ) -> crate::Result where Describe: DescribeExt, @@ -380,99 +310,9 @@ where } }; - // Store query metadata only if offline support is enabled but the current build is online. - // If the build is offline, the cache is our input so it's pointless to also write data for it. - if !offline { - // Only save query metadata if SQLX_OFFLINE_DIR is set manually or by `cargo sqlx prepare`. - // Note: in a cargo workspace this path is relative to the root. - if let Ok(dir) = env("SQLX_OFFLINE_DIR") { - let path = PathBuf::from(&dir); - - match fs::metadata(&path) { - Err(e) => { - if e.kind() != io::ErrorKind::NotFound { - // Can't obtain information about .sqlx - return Err(format!("{e}: {dir}").into()); - } - // .sqlx doesn't exist. - return Err(format!("sqlx offline path does not exist: {dir}").into()); - } - Ok(meta) => { - if !meta.is_dir() { - return Err(format!( - "sqlx offline path exists, but is not a directory: {dir}" - ) - .into()); - } - - // .sqlx exists and is a directory, store data. - data.save_in(path)?; - } - } - } + if let Some(save_dir) = save_dir { + data.save_in(save_dir)?; } Ok(ret_tokens) } - -/// Get the value of an environment variable, telling the compiler about it if applicable. -fn env(name: &str) -> Result { - #[cfg(procmacro2_semver_exempt)] - { - proc_macro::tracked_env::var(name) - } - - #[cfg(not(procmacro2_semver_exempt))] - { - 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 987dcaa3cb..ccf3a37868 100644 --- a/sqlx-macros-core/src/query/output.rs +++ b/sqlx-macros-core/src/query/output.rs @@ -68,7 +68,8 @@ enum ColumnNullabilityOverride { } enum ColumnTypeOverride { - Exact(Type), + // Significantly larger variant + Exact(Box), Wildcard, None, } @@ -394,7 +395,7 @@ impl Parse for ColumnOverride { if let Type::Infer(_) = ty { ColumnTypeOverride::Wildcard } else { - ColumnTypeOverride::Exact(ty) + ColumnTypeOverride::Exact(ty.into()) } } else { ColumnTypeOverride::None diff --git a/sqlx-macros/Cargo.toml b/sqlx-macros/Cargo.toml index 95954d72ef..bd90da9608 100644 --- a/sqlx-macros/Cargo.toml +++ b/sqlx-macros/Cargo.toml @@ -34,6 +34,7 @@ sqlx-toml = ["sqlx-macros-core/sqlx-toml"] # database mysql = ["sqlx-macros-core/mysql"] +mysql-rsa = ["sqlx-macros-core/mysql-rsa"] postgres = ["sqlx-macros-core/postgres"] sqlite = ["sqlx-macros-core/sqlite"] sqlite-unbundled = ["sqlx-macros-core/sqlite-unbundled"] @@ -56,9 +57,9 @@ json = ["sqlx-macros-core/json"] sqlx-core = { workspace = true, features = ["any"] } sqlx-macros-core = { workspace = true } -proc-macro2 = { version = "1.0.36", default-features = false } -syn = { version = "2.0.52", default-features = false, features = ["parsing", "proc-macro"] } -quote = { version = "1.0.26", default-features = false } +proc-macro2 = { version = "1.0.83", default-features = false } +syn = { version = "2.0.87", default-features = false, features = ["parsing", "proc-macro"] } +quote = { version = "1.0.35", default-features = false } [lints] workspace = true diff --git a/sqlx-mysql/Cargo.toml b/sqlx-mysql/Cargo.toml index 52717c4207..7ffb529c8f 100644 --- a/sqlx-mysql/Cargo.toml +++ b/sqlx-mysql/Cargo.toml @@ -10,10 +10,12 @@ repository.workspace = true rust-version.workspace = true [features] +default = [] json = ["sqlx-core/json", "serde"] any = ["sqlx-core/any"] -offline = ["sqlx-core/offline", "serde/derive"] -migrate = ["sqlx-core/migrate"] +offline = ["sqlx-core/offline", "serde/derive", "bitflags/serde"] +migrate = ["sqlx-core/migrate", "dep:crc"] +rsa = ["dep:rand", "dep:rsa"] # Type Integration features bigdecimal = ["dep:bigdecimal", "sqlx-core/bigdecimal"] @@ -26,21 +28,16 @@ uuid = ["dep:uuid", "sqlx-core/uuid"] sqlx-core = { workspace = true } # Futures crates -futures-channel = { version = "0.3.19", default-features = false, features = ["sink", "alloc", "std"] } -futures-core = { version = "0.3.19", default-features = false } -futures-io = "0.3.24" -futures-util = { version = "0.3.19", default-features = false, features = ["alloc", "sink", "io"] } +futures-core = { version = "0.3.32", default-features = false } +futures-util = { version = "0.3.32", default-features = false, features = ["alloc", "sink", "io"] } # Cryptographic Primitives -crc = "3.0.0" -digest = { version = "0.10.0", default-features = false, features = ["std"] } -hkdf = "0.12.0" -hmac = { version = "0.12.0", default-features = false } -md-5 = { version = "0.10.0", default-features = false } -rand = { version = "0.8.4", default-features = false, features = ["std", "std_rng"] } -rsa = "0.9" -sha1 = { version = "0.10.1", default-features = false } -sha2 = { version = "0.10.0", default-features = false } +crc = { workspace = true, optional = true } +digest = { workspace = true } +rand = { workspace = true, optional = true } +rsa = { workspace = true, optional = true } +sha1 = { workspace = true } +sha2 = { workspace = true } # Type Integrations (versions inherited from `[workspace.dependencies]`) bigdecimal = { workspace = true, optional = true } @@ -50,29 +47,23 @@ time = { workspace = true, optional = true } uuid = { workspace = true, optional = true } # Misc -atoi = "2.0" -base64 = { version = "0.22.0", default-features = false, features = ["std"] } -bitflags = { version = "2", default-features = false, features = ["serde"] } +bitflags = { version = "2.4", default-features = false } byteorder = { version = "1.4.3", default-features = false, features = ["std"] } -bytes = "1.1.0" -dotenvy = "0.15.5" +bytes = "1.2.0" either = "1.6.1" generic-array = { version = "0.14.4", default-features = false } -hex = "0.4.3" -itoa = "1.0.1" log = "0.4.18" -memchr = { version = "2.4.1", default-features = false } -percent-encoding = "2.1.0" -smallvec = "1.7.0" -stringprep = "0.1.2" -thiserror = "2.0.0" +percent-encoding = "2.3.0" tracing = { version = "0.1.37", features = ["log"] } -whoami = { version = "1.2.1", default-features = false } -serde = { version = "1.0.144", optional = true } +dotenvy.workspace = true +thiserror.workspace = true + +serde = { version = "1.0.219", optional = true } [dev-dependencies] -sqlx = { workspace = true, features = ["mysql"] } +# FIXME: https://github.com/rust-lang/cargo/issues/15622 +sqlx = { path = "..", default-features = false, features = ["mysql"] } [lints] workspace = true diff --git a/sqlx-mysql/src/any.rs b/sqlx-mysql/src/any.rs index 241900560e..57c895826f 100644 --- a/sqlx-mysql/src/any.rs +++ b/sqlx-mysql/src/any.rs @@ -8,15 +8,15 @@ use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; use futures_util::{stream, FutureExt, StreamExt, TryFutureExt, TryStreamExt}; use sqlx_core::any::{ - Any, AnyArguments, AnyColumn, AnyConnectOptions, AnyConnectionBackend, AnyQueryResult, AnyRow, + AnyArguments, AnyColumn, AnyConnectOptions, AnyConnectionBackend, AnyQueryResult, AnyRow, AnyStatement, AnyTypeInfo, AnyTypeInfoKind, }; use sqlx_core::connection::Connection; use sqlx_core::database::Database; -use sqlx_core::describe::Describe; use sqlx_core::executor::Executor; use sqlx_core::sql_str::SqlStr; use sqlx_core::transaction::TransactionManager; +use sqlx_core::types::Type; use std::{future, pin::pin}; sqlx_core::declare_driver_with_optional_migrate!(DRIVER = MySql); @@ -141,7 +141,11 @@ impl AnyConnectionBackend for MySqlConnection { }) } - fn describe(&mut self, sql: SqlStr) -> BoxFuture<'_, sqlx_core::Result>> { + #[cfg(feature = "offline")] + fn describe( + &mut self, + sql: SqlStr, + ) -> BoxFuture<'_, sqlx_core::Result>> { Box::pin(async move { let describe = Executor::describe(self, sql).await?; describe.try_into_any() @@ -161,13 +165,9 @@ impl<'a> TryFrom<&'a MySqlTypeInfo> for AnyTypeInfo { ColumnType::LongLong => AnyTypeInfoKind::BigInt, ColumnType::Float => AnyTypeInfoKind::Real, ColumnType::Double => AnyTypeInfoKind::Double, - ColumnType::Blob - | ColumnType::TinyBlob - | ColumnType::MediumBlob - | ColumnType::LongBlob => AnyTypeInfoKind::Blob, - ColumnType::String | ColumnType::VarString | ColumnType::VarChar => { - AnyTypeInfoKind::Text - } + // Checks for any applicable type and compatible collations + _ if >::compatible(type_info) => AnyTypeInfoKind::Text, + _ if <[u8] as Type>::compatible(type_info) => AnyTypeInfoKind::Blob, _ => { return Err(sqlx_core::Error::AnyDriverError( format!("Any driver does not support MySql type {type_info:?}").into(), diff --git a/sqlx-mysql/src/column.rs b/sqlx-mysql/src/column.rs index 457cf991d3..1f38779ec4 100644 --- a/sqlx-mysql/src/column.rs +++ b/sqlx-mysql/src/column.rs @@ -13,6 +13,7 @@ pub struct MySqlColumn { #[cfg_attr(feature = "offline", serde(default))] pub(crate) origin: ColumnOrigin, + #[allow(unused)] #[cfg_attr(feature = "offline", serde(skip))] pub(crate) flags: Option, } diff --git a/sqlx-mysql/src/connection/auth.rs b/sqlx-mysql/src/connection/auth.rs index 613f8e702f..ad10e14e79 100644 --- a/sqlx-mysql/src/connection/auth.rs +++ b/sqlx-mysql/src/connection/auth.rs @@ -1,9 +1,6 @@ use bytes::buf::Chain; use bytes::Bytes; -use digest::{Digest, OutputSizeUser}; -use generic_array::GenericArray; -use rand::thread_rng; -use rsa::{pkcs8::DecodePublicKey, Oaep, RsaPublicKey}; +use digest::Digest; use sha1::Sha1; use sha2::Sha256; @@ -46,10 +43,12 @@ impl AuthPlugin { match self { AuthPlugin::CachingSha2Password if packet[0] == 0x01 => { match packet[1] { - // AUTH_OK - 0x03 => Ok(true), + // fast_auth_success — the server still sends a trailing + // OK_Packet, so yield back to the handshake loop and let + // it consume the OK on the next iteration. + 0x03 => Ok(false), - // AUTH_CONTINUE + // perform_full_authentication 0x04 => { let payload = encrypt_rsa(stream, 0x02, password, nonce).await?; @@ -60,7 +59,7 @@ impl AuthPlugin { } v => { - Err(err_protocol!("unexpected result from fast authentication 0x{:x} when expecting 0x03 (AUTH_OK) or 0x04 (AUTH_CONTINUE)", v)) + Err(err_protocol!("unexpected result from fast authentication 0x{:x} when expecting 0x03 (fast_auth_success) or 0x04 (perform_full_authentication)", v)) } } } @@ -74,10 +73,7 @@ impl AuthPlugin { } } -fn scramble_sha1( - password: &str, - nonce: &Chain, -) -> GenericArray::OutputSize> { +fn scramble_sha1(password: &str, nonce: &Chain) -> Vec { // SHA1( password ) ^ SHA1( seed + SHA1( SHA1( password ) ) ) // https://mariadb.com/kb/en/connection/#mysql_native_password-plugin @@ -99,15 +95,13 @@ fn scramble_sha1( xor_eq(&mut pw_hash, &pw_seed_hash_hash); - pw_hash + pw_hash.to_vec() } -fn scramble_sha256( - password: &str, - nonce: &Chain, -) -> GenericArray::OutputSize> { - // XOR(SHA256(password), SHA256(seed, SHA256(SHA256(password)))) - // https://mariadb.com/kb/en/caching_sha2_password-authentication-plugin/#sha-2-encrypted-password +fn scramble_sha256(password: &str, nonce: &Chain) -> Vec { + // XOR(SHA256(password), SHA256(SHA256(SHA256(password)), seed)) + // Order matches the server-side verification in MySQL's sha2_password + // (generate_sha2_scramble): stage2 digest first, then the nonce. let mut ctx = Sha256::new(); ctx.update(password); @@ -118,15 +112,15 @@ fn scramble_sha256( let pw_hash_hash = ctx.finalize_reset(); + ctx.update(pw_hash_hash); ctx.update(nonce.first_ref()); ctx.update(nonce.last_ref()); - ctx.update(pw_hash_hash); let pw_seed_hash_hash = ctx.finalize(); xor_eq(&mut pw_hash, &pw_seed_hash_hash); - pw_hash + pw_hash.to_vec() } async fn encrypt_rsa<'s>( @@ -161,10 +155,7 @@ async fn encrypt_rsa<'s>( xor_eq(&mut pass, &nonce); // client sends an RSA encrypted password - let pkey = parse_rsa_pub_key(rsa_pub_key)?; - let padding = Oaep::new::(); - pkey.encrypt(&mut thread_rng(), padding, &pass[..]) - .map_err(Error::protocol) + rsa_backend::encrypt(rsa_pub_key, &pass) } // XOR(x, y) @@ -185,13 +176,71 @@ fn to_asciz(s: &str) -> Vec { z.into_bytes() } -// https://docs.rs/rsa/0.3.0/rsa/struct.RSAPublicKey.html?search=#example-1 -fn parse_rsa_pub_key(key: &[u8]) -> Result { - let pem = std::str::from_utf8(key).map_err(Error::protocol)?; +#[cfg(feature = "rsa")] +mod rsa_backend { + use rsa::{pkcs8::DecodePublicKey, Oaep, RsaPublicKey}; + + use super::Error; + + pub(super) fn encrypt(rsa_pub_key: &[u8], pass: &[u8]) -> Result, Error> { + let pkey = parse_rsa_pub_key(rsa_pub_key)?; + let padding = Oaep::::new(); + pkey.encrypt(&mut rand::rng(), padding, pass) + .map_err(Error::protocol) + } + + // https://docs.rs/rsa/0.3.0/rsa/struct.RSAPublicKey.html?search=#example-1 + fn parse_rsa_pub_key(key: &[u8]) -> Result { + let pem = std::str::from_utf8(key).map_err(Error::protocol)?; + + // This takes advantage of the knowledge that we know + // we are receiving a PKCS#8 RSA Public Key at all + // times from MySQL + + RsaPublicKey::from_public_key_pem(pem).map_err(Error::protocol) + } +} + +#[cfg(not(feature = "rsa"))] +mod rsa_backend { + use super::Error; - // This takes advantage of the knowledge that we know - // we are receiving a PKCS#8 RSA Public Key at all - // times from MySQL + pub(super) fn encrypt(_rsa_pub_key: &[u8], _pass: &[u8]) -> Result, Error> { + Err(Error::Configuration( + "RSA auth backend disabled; enable feature `mysql-rsa` (or `rsa` if using sqlx-mysql directly) or use TLS.".into(), + )) + } +} - RsaPublicKey::from_public_key_pem(pem).map_err(Error::protocol) +#[cfg(test)] +mod tests { + use super::*; + use bytes::Buf; + use sha2::{Digest, Sha256}; + + // Regression test for https://github.com/launchbadge/sqlx/issues/4244: + // caching_sha2_password fast-auth requires the client scramble to be + // invertible by the server as XOR(scramble, SHA256(stage2 || nonce)) == stage1, + // where stage1 = SHA256(password) and stage2 = SHA256(stage1). + #[test] + fn scramble_sha256_is_invertible_by_server() { + let password = "my_pwd"; + let nonce_a = Bytes::from_static(b"0123456789"); + let nonce_b = Bytes::from_static(&[0xAB; 10]); + let nonce = nonce_a.clone().chain(nonce_b.clone()); + + let mut scramble = scramble_sha256(password, &nonce); + + let stage1 = Sha256::digest(password.as_bytes()); + let stage2 = Sha256::digest(stage1); + + let mut h = Sha256::new(); + h.update(stage2); + h.update(&nonce_a); + h.update(&nonce_b); + let xor_pad = h.finalize(); + + xor_eq(&mut scramble, &xor_pad); + assert_eq!(&scramble[..], &stage1[..]); + } } diff --git a/sqlx-mysql/src/connection/executor.rs b/sqlx-mysql/src/connection/executor.rs index 2b660b94b3..ee59d03d0a 100644 --- a/sqlx-mysql/src/connection/executor.rs +++ b/sqlx-mysql/src/connection/executor.rs @@ -1,6 +1,5 @@ use super::MySqlStream; use crate::connection::stream::Waiting; -use crate::describe::Describe; use crate::error::Error; use crate::executor::{Execute, Executor}; use crate::ext::ustr::UStr; @@ -10,7 +9,7 @@ use crate::protocol::response::Status; use crate::protocol::statement::{ BinaryRow, Execute as StatementExecute, Prepare, PrepareOk, StmtClose, }; -use crate::protocol::text::{ColumnDefinition, ColumnFlags, Query, TextRow}; +use crate::protocol::text::{ColumnDefinition, Query, TextRow}; use crate::statement::{MySqlStatement, MySqlStatementMetadata}; use crate::HashMap; use crate::{ @@ -121,7 +120,7 @@ impl MySqlConnection { // to re-use this memory freely between result sets let mut columns = Arc::new(Vec::new()); - let (mut column_names, format, mut needs_metadata) = if let Some(arguments) = arguments { + let format = if let Some(arguments) = arguments { if persistent && self.inner.cache_statement.is_enabled() { let (id, metadata) = self .get_or_prepare_statement(sql) @@ -145,7 +144,7 @@ impl MySqlConnection { }) .await?; - (metadata.column_names, MySqlValueFormat::Binary, false) + MySqlValueFormat::Binary } else { let (id, metadata) = self .prepare_statement(sql) @@ -171,13 +170,13 @@ impl MySqlConnection { self.inner.stream.send_packet(StmtClose { statement: id }).await?; - (metadata.column_names, MySqlValueFormat::Binary, false) + MySqlValueFormat::Binary } } else { // https://dev.mysql.com/doc/internals/en/com-query.html self.inner.stream.send_packet(Query(sql)).await?; - (Arc::default(), MySqlValueFormat::Text, true) + MySqlValueFormat::Text }; loop { @@ -213,19 +212,13 @@ impl MySqlConnection { // otherwise, this first packet is the start of the result-set metadata, *self.inner.stream.waiting.front_mut().unwrap() = Waiting::Row; - let num_columns = packet.get_uint_lenenc(); // column count + let num_columns = packet.get_uint_lenenc()?; // column count let num_columns = usize::try_from(num_columns) .map_err(|_| err_protocol!("column count overflows usize: {num_columns}"))?; - if needs_metadata { - column_names = Arc::new(recv_result_metadata(&mut self.inner.stream, num_columns, Arc::make_mut(&mut columns)).await?); - } else { - // next time we hit here, it'll be a new result set and we'll need the - // full metadata - needs_metadata = true; - - recv_result_columns(&mut self.inner.stream, num_columns, Arc::make_mut(&mut columns)).await?; - } + // Always reload column names, even for prepared statements (the schema + // may change between PREPARE and EXECUTE). + let column_names = Arc::new(recv_result_metadata(&mut self.inner.stream, num_columns, Arc::make_mut(&mut columns)).await?); // finally, there will be none or many result-rows loop { @@ -359,7 +352,11 @@ impl<'c> Executor<'c> for &'c mut MySqlConnection { } #[doc(hidden)] - fn describe<'e>(self, sql: SqlStr) -> BoxFuture<'e, Result, Error>> + #[cfg(feature = "offline")] + fn describe<'e>( + self, + sql: SqlStr, + ) -> BoxFuture<'e, Result, Error>> where 'c: 'e, { @@ -379,11 +376,11 @@ impl<'c> Executor<'c> for &'c mut MySqlConnection { .iter() .map(|col| { col.flags - .map(|flags| !flags.contains(ColumnFlags::NOT_NULL)) + .map(|flags| !flags.contains(crate::protocol::text::ColumnFlags::NOT_NULL)) }) .collect(); - Ok(Describe { + Ok(crate::describe::Describe { parameters: Some(Either::Right(metadata.parameters)), columns, nullable, @@ -392,25 +389,6 @@ impl<'c> Executor<'c> for &'c mut MySqlConnection { } } -async fn recv_result_columns( - stream: &mut MySqlStream, - num_columns: usize, - columns: &mut Vec, -) -> Result<(), Error> { - columns.clear(); - columns.reserve(num_columns); - - for ordinal in 0..num_columns { - columns.push(recv_next_result_column(&stream.recv().await?, ordinal)?); - } - - if num_columns> 0 { - stream.maybe_recv_eof().await?; - } - - Ok(()) -} - fn recv_next_result_column(def: &ColumnDefinition, ordinal: usize) -> Result { // if the alias is empty, use the alias // only then use the name diff --git a/sqlx-mysql/src/connection/stream.rs b/sqlx-mysql/src/connection/stream.rs index ff931b2f46..e6aa8b48c8 100644 --- a/sqlx-mysql/src/connection/stream.rs +++ b/sqlx-mysql/src/connection/stream.rs @@ -194,7 +194,7 @@ impl MySqlStream { } async fn skip_result_metadata(&mut self, mut packet: Packet) -> Result<(), Error> { - let num_columns: u64 = packet.get_uint_lenenc(); // column count + let num_columns: u64 = packet.get_uint_lenenc()?; // column count for _ in 0..num_columns { let _ = self.recv_packet().await?; diff --git a/sqlx-mysql/src/io/buf.rs b/sqlx-mysql/src/io/buf.rs index 685d5bfda7..6b3c11b3f5 100644 --- a/sqlx-mysql/src/io/buf.rs +++ b/sqlx-mysql/src/io/buf.rs @@ -8,7 +8,7 @@ pub trait MySqlBufExt: Buf { // NOTE: 0xfb or NULL is only returned for binary value encoding to indicate NULL. // NOTE: 0xff is only returned during a result set to indicate ERR. // - fn get_uint_lenenc(&mut self) -> u64; + fn get_uint_lenenc(&mut self) -> Result; // Read a length-encoded string. #[allow(dead_code)] @@ -19,18 +19,46 @@ pub trait MySqlBufExt: Buf { } impl MySqlBufExt for Bytes { - fn get_uint_lenenc(&mut self) -> u64 { + fn get_uint_lenenc(&mut self) -> Result { + if self.remaining() < 1 { + return Err(err_protocol!("lenenc int: no bytes remaining")); + } + match self.get_u8() { - 0xfc => u64::from(self.get_u16_le()), - 0xfd => self.get_uint_le(3), - 0xfe => self.get_u64_le(), + 0xfc => { + if self.remaining() < 2 { + return Err(err_protocol!( + "lenenc int: need 2 more bytes, have {}", + self.remaining() + )); + } + Ok(u64::from(self.get_u16_le())) + } + 0xfd => { + if self.remaining() < 3 { + return Err(err_protocol!( + "lenenc int: need 3 more bytes, have {}", + self.remaining() + )); + } + Ok(self.get_uint_le(3)) + } + 0xfe => { + if self.remaining() < 8 { + return Err(err_protocol!( + "lenenc int: need 8 more bytes, have {}", + self.remaining() + )); + } + Ok(self.get_u64_le()) + } - v => u64::from(v), + v => Ok(u64::from(v)), } } fn get_str_lenenc(&mut self) -> Result { - let size = self.get_uint_lenenc(); + let size = self.get_uint_lenenc()?; let size = usize::try_from(size) .map_err(|_| err_protocol!("string length overflows usize: {size}"))?; @@ -38,7 +66,7 @@ impl MySqlBufExt for Bytes { } fn get_bytes_lenenc(&mut self) -> Result { - let size = self.get_uint_lenenc(); + let size = self.get_uint_lenenc()?; let size = usize::try_from(size) .map_err(|_| err_protocol!("string length overflows usize: {size}"))?; diff --git a/sqlx-mysql/src/migrate.rs b/sqlx-mysql/src/migrate.rs index 0176f93c26..54cf1cf660 100644 --- a/sqlx-mysql/src/migrate.rs +++ b/sqlx-mysql/src/migrate.rs @@ -193,7 +193,7 @@ CREATE TABLE IF NOT EXISTS {table_name} ( migration: &'e Migration, ) -> BoxFuture<'e, Result> { Box::pin(async move { - // Use a single transaction for the actual migration script and the essential bookeeping so we never + // Use a single transaction for the actual migration script and the essential bookkeeping so we never // execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966. // The `execution_time` however can only be measured for the whole transaction. This value _only_ exists for // data lineage and debugging reasons, so it is not super important if it is lost. So we initialize it to -1 @@ -268,7 +268,7 @@ CREATE TABLE IF NOT EXISTS {table_name} ( migration: &'e Migration, ) -> BoxFuture<'e, Result> { Box::pin(async move { - // Use a single transaction for the actual migration script and the essential bookeeping so we never + // Use a single transaction for the actual migration script and the essential bookkeeping so we never // execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966. let mut tx = self.begin().await?; let start = Instant::now(); @@ -308,6 +308,29 @@ CREATE TABLE IF NOT EXISTS {table_name} ( Ok(elapsed) }) } + + fn skip<'e>( + &'e mut self, + table_name: &'e str, + migration: &'e Migration, + ) -> BoxFuture<'e, Result<(), MigrateError>> { + Box::pin(async move { + // language=MySQL + let _ = query(AssertSqlSafe(format!( + r#" + INSERT INTO {table_name} ( version, description, success, checksum, execution_time ) + VALUES ( ?, ?, TRUE, ?, -1 ) + "# + ))) + .bind(migration.version) + .bind(&*migration.description) + .bind(&*migration.checksum) + .execute(self) + .await?; + + Ok(()) + }) + } } async fn current_database(conn: &mut MySqlConnection) -> Result { diff --git a/sqlx-mysql/src/protocol/response/ok.rs b/sqlx-mysql/src/protocol/response/ok.rs index 74c4abded7..86fea9b4cf 100644 --- a/sqlx-mysql/src/protocol/response/ok.rs +++ b/sqlx-mysql/src/protocol/response/ok.rs @@ -24,8 +24,16 @@ impl ProtocolDecode<'_> for OkPacket { )); } - let affected_rows = buf.get_uint_lenenc(); - let last_insert_id = buf.get_uint_lenenc(); + let affected_rows = buf.get_uint_lenenc()?; + let last_insert_id = buf.get_uint_lenenc()?; + + if buf.remaining() < 4 { + return Err(err_protocol!( + "OK_Packet too short: expected at least 4 more bytes for status+warnings, got {}", + buf.remaining() + )); + } + let status = Status::from_bits_truncate(buf.get_u16_le()); let warnings = buf.get_u16_le(); @@ -76,3 +84,11 @@ fn test_decode_ok_packet_with_extended_info() { assert_eq!(p.warnings, 1); assert!(p.status.contains(Status::SERVER_STATUS_AUTOCOMMIT)); } + +#[test] +fn test_decode_ok_packet_truncated() { + const DATA: &[u8] = b"\x00\x00\x00\x01"; + + let err = OkPacket::decode(DATA.into()).unwrap_err(); + assert!(matches!(err, Error::Protocol(_)), "{err}"); +} diff --git a/sqlx-mysql/src/protocol/statement/row.rs b/sqlx-mysql/src/protocol/statement/row.rs index 3007884c72..4e9cc2bb9c 100644 --- a/sqlx-mysql/src/protocol/statement/row.rs +++ b/sqlx-mysql/src/protocol/statement/row.rs @@ -18,7 +18,7 @@ impl<'de> ProtocolDecode<'de, &'de [MySqlColumn]> for BinaryRow { let header = buf.get_u8(); if header != 0 { return Err(err_protocol!( - "exepcted 0x00 (ROW) but found 0x{:02x}", + "expected 0x00 (ROW) but found 0x{:02x}", header )); } @@ -76,7 +76,7 @@ impl<'de> ProtocolDecode<'de, &'de [MySqlColumn]> for BinaryRow { | ColumnType::Decimal | ColumnType::Json | ColumnType::NewDecimal => { - let size = buf.get_uint_lenenc(); + let size = buf.get_uint_lenenc()?; usize::try_from(size) .map_err(|_| err_protocol!("BLOB length out of range: {size}"))? } diff --git a/sqlx-mysql/src/protocol/text/column.rs b/sqlx-mysql/src/protocol/text/column.rs index 6ff65b3fcc..b7c9c7e639 100644 --- a/sqlx-mysql/src/protocol/text/column.rs +++ b/sqlx-mysql/src/protocol/text/column.rs @@ -41,7 +41,7 @@ bitflags! { /// Field is an enumeration. const ENUM = 256; - /// Field is an auto-incement field. + /// Field is an auto-increment field. const AUTO_INCREMENT = 512; /// Field is a timestamp. @@ -147,7 +147,7 @@ impl ProtocolDecode<'_, Capabilities> for ColumnDefinition { let table = buf.get_bytes_lenenc()?; let alias = buf.get_bytes_lenenc()?; let name = buf.get_bytes_lenenc()?; - let _next_len = buf.get_uint_lenenc(); // always 0x0c + let _next_len = buf.get_uint_lenenc()?; // always 0x0c let collation = buf.get_u16_le(); let max_size = buf.get_u32_le(); let type_id = buf.get_u8(); diff --git a/sqlx-mysql/src/protocol/text/row.rs b/sqlx-mysql/src/protocol/text/row.rs index e5f820c653..53f7c472ee 100644 --- a/sqlx-mysql/src/protocol/text/row.rs +++ b/sqlx-mysql/src/protocol/text/row.rs @@ -22,7 +22,7 @@ impl<'de> ProtocolDecode<'de, &'de [MySqlColumn]> for TextRow { values.push(None); buf.advance(1); } else { - let size = buf.get_uint_lenenc(); + 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?", diff --git a/sqlx-postgres/Cargo.toml b/sqlx-postgres/Cargo.toml index a70fb37d72..701cabff85 100644 --- a/sqlx-postgres/Cargo.toml +++ b/sqlx-postgres/Cargo.toml @@ -11,9 +11,9 @@ rust-version.workspace = true [features] any = ["sqlx-core/any"] -json = ["sqlx-core/json"] -migrate = ["sqlx-core/migrate"] -offline = ["sqlx-core/offline"] +json = ["dep:serde", "dep:serde_json", "sqlx-core/json"] +migrate = ["sqlx-core/migrate", "dep:crc"] +offline = ["json", "sqlx-core/offline", "smallvec/serde"] # Type Integration features bigdecimal = ["dep:bigdecimal", "dep:num-bigint", "sqlx-core/bigdecimal"] @@ -28,17 +28,17 @@ uuid = ["dep:uuid", "sqlx-core/uuid"] [dependencies] # Futures crates -futures-channel = { version = "0.3.19", default-features = false, features = ["sink", "alloc", "std"] } -futures-core = { version = "0.3.19", default-features = false } -futures-util = { version = "0.3.19", default-features = false, features = ["alloc", "sink", "io"] } +futures-channel = { version = "0.3.32", default-features = false, features = ["sink", "alloc", "std"] } +futures-core = { version = "0.3.32", default-features = false } +futures-util = { version = "0.3.32", default-features = false, features = ["alloc", "sink", "io"] } # Cryptographic Primitives -crc = "3.0.0" -hkdf = "0.12.0" -hmac = { version = "0.12.0", default-features = false, features = ["reset"]} -md-5 = { version = "0.10.0", default-features = false } -rand = { version = "0.8.4", default-features = false, features = ["std", "std_rng"] } -sha2 = { version = "0.10.0", default-features = false } +crc = { workspace = true, optional = true } +hkdf = { workspace = true } +hmac = { workspace = true } +md-5 = { workspace = true } +rand = { workspace = true } +sha2 = { workspace = true } # Type Integrations (versions inherited from `[workspace.dependencies]`) bigdecimal = { workspace = true, optional = true } @@ -53,36 +53,36 @@ uuid = { workspace = true, optional = true } # Misc atoi = "2.0" -base64 = { version = "0.22.0", default-features = false, features = ["std"] } -bitflags = { version = "2", default-features = false } +base64.workspace = true +bitflags = { version = "2.4", default-features = false } byteorder = { version = "1.4.3", default-features = false, features = ["std"] } -dotenvy = { workspace = true } hex = "0.4.3" -home = "0.5.5" -itoa = "1.0.1" +itoa = "1.0.5" log = "0.4.18" -memchr = { version = "2.4.1", default-features = false } +memchr = { version = "2.5.0", default-features = false } num-bigint = { version = "0.4.3", optional = true } -smallvec = { version = "1.7.0", features = ["serde"] } +smallvec = { version = "1.13.1" } stringprep = "0.1.2" -thiserror = "2.0.0" tracing = { version = "0.1.37", features = ["log"] } -whoami = { version = "1.2.1", default-features = false } +whoami = { version = "2.0.2", features = ["std"], default-features = false } -serde = { version = "1.0.144", features = ["derive"] } -serde_json = { version = "1.0.85", features = ["raw_value"] } +dotenvy.workspace = true +thiserror.workspace = true + +serde = { version = "1.0.219", optional = true, features = ["derive"] } +serde_json = { version = "1.0.142", optional = true, features = ["raw_value"] } [dependencies.sqlx-core] workspace = true -# We use JSON in the driver implementation itself so there's no reason not to enable it here. -features = ["json"] [dev-dependencies.sqlx] -workspace = true +# FIXME: https://github.com/rust-lang/cargo/issues/15622 +# workspace = true +path = ".." features = ["postgres", "derive"] [target.'cfg(target_os = "windows")'.dependencies] -etcetera = "0.10.0" +etcetera = "0.11.0" [lints] workspace = true diff --git a/sqlx-postgres/src/advisory_lock.rs b/sqlx-postgres/src/advisory_lock.rs index 84cad2bfdd..979aca06ce 100644 --- a/sqlx-postgres/src/advisory_lock.rs +++ b/sqlx-postgres/src/advisory_lock.rs @@ -3,6 +3,8 @@ use crate::Either; use crate::PgConnection; use hkdf::Hkdf; use sha2::Sha256; +use sqlx_core::executor::Executor; +use sqlx_core::sql_str::SqlSafeStr; use std::ops::{Deref, DerefMut}; use std::sync::Arc; use std::sync::OnceLock; @@ -199,27 +201,36 @@ impl PgAdvisoryLock { /// See [Postgres' documentation for the Advisory Lock Functions][advisory-funcs] for details. /// /// [advisory-funcs]: https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS + /// + /// # Cancel Safety + /// + /// This method is cancel safe. If the future is dropped before the query completes, a + /// `pg_advisory_unlock()` call is queued and run the next time the connection is used. pub async fn acquire>( &self, mut conn: C, ) -> Result> { + let query = match &self.key { + PgAdvisoryLockKey::BigInt(_) => "SELECT pg_advisory_lock(1ドル)", + PgAdvisoryLockKey::IntPair(_, _) => "SELECT pg_advisory_lock(1,ドル 2ドル)", + }; + + let stmt = conn.as_mut().prepare(query.into_sql_str()).await?; + let query = crate::query::query_statement(&stmt); + + // We're wrapping the connection in a `PgAdvisoryLockGuard` early here on purpose. If this + // future is dropped, the lock will be released in the drop impl. + let mut guard = PgAdvisoryLockGuard::new(self.clone(), conn); + let conn = guard.conn.as_mut().unwrap(); + match &self.key { - PgAdvisoryLockKey::BigInt(key) => { - crate::query::query("SELECT pg_advisory_lock(1ドル)") - .bind(key) - .execute(conn.as_mut()) - .await?; - } - PgAdvisoryLockKey::IntPair(key1, key2) => { - crate::query::query("SELECT pg_advisory_lock(1,ドル 2ドル)") - .bind(key1) - .bind(key2) - .execute(conn.as_mut()) - .await?; - } + PgAdvisoryLockKey::BigInt(key) => query.bind(key), + PgAdvisoryLockKey::IntPair(key1, key2) => query.bind(key1).bind(key2), } + .execute(conn.as_mut()) + .await?; - Ok(PgAdvisoryLockGuard::new(self.clone(), conn)) + Ok(guard) } /// Acquires an exclusive lock using `pg_try_advisory_lock()`, returning immediately @@ -242,6 +253,12 @@ impl PgAdvisoryLock { /// See [Postgres' documentation for the Advisory Lock Functions][advisory-funcs] for details. /// /// [advisory-funcs]: https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS + /// + /// # Cancel Safety + /// + /// This method is **not** cancel safe. If the future is dropped while the query is in-flight, + /// it is not possible to know whether the lock was acquired, so it cannot be safely released. + /// The lock may remain held until the connection is closed. pub async fn try_acquire>( &self, mut conn: C, diff --git a/sqlx-postgres/src/any.rs b/sqlx-postgres/src/any.rs index 51eb15d2a7..62b3dedbac 100644 --- a/sqlx-postgres/src/any.rs +++ b/sqlx-postgres/src/any.rs @@ -9,14 +9,13 @@ use sqlx_core::sql_str::SqlStr; use std::{future, pin::pin}; use sqlx_core::any::{ - Any, AnyArguments, AnyColumn, AnyConnectOptions, AnyConnectionBackend, AnyQueryResult, AnyRow, + AnyArguments, AnyColumn, AnyConnectOptions, AnyConnectionBackend, AnyQueryResult, AnyRow, AnyStatement, AnyTypeInfo, AnyTypeInfoKind, }; use crate::type_info::PgType; use sqlx_core::connection::Connection; use sqlx_core::database::Database; -use sqlx_core::describe::Describe; use sqlx_core::executor::Executor; use sqlx_core::ext::ustr::UStr; use sqlx_core::transaction::TransactionManager; @@ -84,7 +83,7 @@ impl AnyConnectionBackend for PgConnection { query: SqlStr, persistent: bool, arguments: Option, - ) -> BoxStream>> { + ) -> BoxStream<'_, sqlx_core::Result>> { let persistent = persistent && arguments.is_some(); let arguments = match arguments.map(AnyArguments::convert_into).transpose() { Ok(arguments) => arguments, @@ -110,7 +109,7 @@ impl AnyConnectionBackend for PgConnection { query: SqlStr, persistent: bool, arguments: Option, - ) -> BoxFuture>> { + ) -> BoxFuture<'_, sqlx_core::Result>> { let persistent = persistent && arguments.is_some(); let arguments = arguments .map(AnyArguments::convert_into) @@ -136,12 +135,16 @@ impl AnyConnectionBackend for PgConnection { ) -> BoxFuture<'c, sqlx_core::Result> { Box::pin(async move { let statement = Executor::prepare_with(self, sql, &[]).await?; - let colunn_names = statement.metadata.column_names.clone(); - AnyStatement::try_from_statement(statement, colunn_names) + let column_names = statement.metadata.column_names.clone(); + AnyStatement::try_from_statement(statement, column_names) }) } - fn describe<'c>(&mut self, sql: SqlStr) -> BoxFuture<'_, sqlx_core::Result>> { + #[cfg(feature = "offline")] + fn describe<'c>( + &mut self, + sql: SqlStr, + ) -> BoxFuture<'_, sqlx_core::Result>> { Box::pin(async move { let describe = Executor::describe(self, sql).await?; @@ -172,7 +175,7 @@ impl AnyConnectionBackend for PgConnection { None => None, }; - Ok(Describe { + Ok(sqlx_core::describe::Describe { columns, parameters, nullable: describe.nullable, diff --git a/sqlx-postgres/src/arguments.rs b/sqlx-postgres/src/arguments.rs index c0db982c7d..90cdd85057 100644 --- a/sqlx-postgres/src/arguments.rs +++ b/sqlx-postgres/src/arguments.rs @@ -4,11 +4,9 @@ use std::sync::Arc; use crate::encode::{Encode, IsNull}; use crate::error::Error; -use crate::ext::ustr::UStr; use crate::types::Type; use crate::{PgConnection, PgTypeInfo, Postgres}; -use crate::type_info::PgArrayOf; pub(crate) use sqlx_core::arguments::Arguments; use sqlx_core::error::BoxDynError; @@ -43,13 +41,10 @@ pub struct PgArgumentBuffer { // This is done for Records and Arrays as the OID is needed well before we are in an async // function and can just ask postgres. // - type_holes: Vec<(usize, HoleKind)>, // Vec<{ offset, type_name }> -} - -#[derive(Debug, Clone)] -enum HoleKind { - Type { name: UStr }, - Array(Arc), + hole_offsets: Vec, + // Separate vecator so that we don't have to generify or duplicate the logic in + // `PgConnection::resolve_types()`. + hole_types: Vec, } #[derive(Clone)] @@ -114,7 +109,8 @@ impl PgArguments { ) -> Result<(), Error> { let PgArgumentBuffer { ref patches, - ref type_holes, + ref hole_types, + ref hole_offsets, ref mut buffer, .. } = self.buffer; @@ -126,12 +122,10 @@ impl PgArguments { (patch.callback)(buf, ty); } - for (offset, kind) in type_holes { - let oid = match kind { - HoleKind::Type { name } => conn.fetch_type_id_by_name(name).await?, - HoleKind::Array(array) => conn.fetch_array_type_id(array).await?, - }; - buffer[*offset..(*offset + 4)].copy_from_slice(&oid.0.to_be_bytes()); + let resolved_holes = conn.resolve_types(hole_types).await?; + + for (&offset, oid) in hole_offsets.iter().zip(resolved_holes) { + buffer[offset..][..4].copy_from_slice(&oid.0.to_be_bytes()); } Ok(()) @@ -195,8 +189,8 @@ impl PgArgumentBuffer { } // Adds a callback to be invoked later when we know the parameter type - #[allow(dead_code)] - pub(crate) fn patch(&mut self, callback: F) + #[cfg_attr(not(feature = "json"), expect(dead_code))] + pub(crate) fn patch_with(&mut self, callback: F) where F: Fn(&mut [u8], &PgTypeInfo) + 'static + Send + Sync, { @@ -212,23 +206,12 @@ impl PgArgumentBuffer { // Extends the inner buffer by enough space to have an OID // Remembers where the OID goes and type name for the OID - pub(crate) fn patch_type_by_name(&mut self, type_name: &UStr) { + pub(crate) fn push_hole(&mut self, type_info: PgTypeInfo) { let offset = self.len(); self.extend_from_slice(&0_u32.to_be_bytes()); - self.type_holes.push(( - offset, - HoleKind::Type { - name: type_name.clone(), - }, - )); - } - - pub(crate) fn patch_array_type(&mut self, array: Arc) { - let offset = self.len(); - - self.extend_from_slice(&0_u32.to_be_bytes()); - self.type_holes.push((offset, HoleKind::Array(array))); + self.hole_offsets.push(offset); + self.hole_types.push(type_info); } fn snapshot(&self) -> PgArgumentBufferSnapshot { @@ -236,14 +219,15 @@ impl PgArgumentBuffer { buffer, count, patches, - type_holes, + hole_offsets, + .. } = self; PgArgumentBufferSnapshot { buffer_length: buffer.len(), count: *count, patches_length: patches.len(), - type_holes_length: type_holes.len(), + type_holes_length: hole_offsets.len(), } } @@ -259,7 +243,8 @@ impl PgArgumentBuffer { self.buffer.truncate(buffer_length); self.count = count; self.patches.truncate(patches_length); - self.type_holes.truncate(type_holes_length); + self.hole_offsets.truncate(type_holes_length); + self.hole_types.truncate(type_holes_length); } } diff --git a/sqlx-postgres/src/bind_iter.rs b/sqlx-postgres/src/bind_iter.rs index 0f44f19e3d..c0cb54f2b0 100644 --- a/sqlx-postgres/src/bind_iter.rs +++ b/sqlx-postgres/src/bind_iter.rs @@ -1,4 +1,4 @@ -use crate::{type_info::PgType, PgArgumentBuffer, PgHasArrayType, PgTypeInfo, Postgres}; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, Postgres}; use core::cell::Cell; use sqlx_core::{ database::Database, @@ -95,13 +95,10 @@ where buf.extend(&1_i32.to_be_bytes()); // number of dimensions buf.extend(&0_i32.to_be_bytes()); // flags - match type_info.0 { - PgType::DeclareWithName(name) => buf.patch_type_by_name(&name), - PgType::DeclareArrayOf(array) => buf.patch_array_type(array), - - ty => { - buf.extend(&ty.oid().0.to_be_bytes()); - } + if let Some(oid) = type_info.oid() { + buf.extend(oid.0.to_be_bytes()); + } else { + buf.push_hole(type_info); } let len_start = buf.len(); diff --git a/sqlx-postgres/src/connection/describe.rs b/sqlx-postgres/src/connection/describe.rs index dfe5286458..f08e213783 100644 --- a/sqlx-postgres/src/connection/describe.rs +++ b/sqlx-postgres/src/connection/describe.rs @@ -1,494 +1,14 @@ -use crate::connection::TableColumns; use crate::error::Error; -use crate::ext::ustr::UStr; use crate::io::StatementId; -use crate::message::{ParameterDescription, RowDescription}; use crate::query_as::query_as; -use crate::query_scalar::query_scalar; use crate::statement::PgStatementMetadata; -use crate::type_info::{PgArrayOf, PgCustomType, PgType, PgTypeKind}; use crate::types::Json; -use crate::types::Oid; -use crate::HashMap; -use crate::{PgColumn, PgConnection, PgTypeInfo}; +use crate::PgConnection; use smallvec::SmallVec; -use sqlx_core::column::{ColumnOrigin, TableColumn}; use sqlx_core::query_builder::QueryBuilder; use sqlx_core::sql_str::AssertSqlSafe; -use std::sync::Arc; - -/// Describes the type of the `pg_type.typtype` column -/// -/// See -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum TypType { - Base, - Composite, - Domain, - Enum, - Pseudo, - Range, -} - -impl TryFrom for TypType { - type Error = (); - - fn try_from(t: i8) -> Result { - let t = u8::try_from(t).or(Err(()))?; - - let t = match t { - b'b' => Self::Base, - b'c' => Self::Composite, - b'd' => Self::Domain, - b'e' => Self::Enum, - b'p' => Self::Pseudo, - b'r' => Self::Range, - _ => return Err(()), - }; - Ok(t) - } -} - -/// Describes the type of the `pg_type.typcategory` column -/// -/// See -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum TypCategory { - Array, - Boolean, - Composite, - DateTime, - Enum, - Geometric, - Network, - Numeric, - Pseudo, - Range, - String, - Timespan, - User, - BitString, - Unknown, -} - -impl TryFrom for TypCategory { - type Error = (); - - fn try_from(c: i8) -> Result { - let c = u8::try_from(c).or(Err(()))?; - - let c = match c { - b'A' => Self::Array, - b'B' => Self::Boolean, - b'C' => Self::Composite, - b'D' => Self::DateTime, - b'E' => Self::Enum, - b'G' => Self::Geometric, - b'I' => Self::Network, - b'N' => Self::Numeric, - b'P' => Self::Pseudo, - b'R' => Self::Range, - b'S' => Self::String, - b'T' => Self::Timespan, - b'U' => Self::User, - b'V' => Self::BitString, - b'X' => Self::Unknown, - _ => return Err(()), - }; - Ok(c) - } -} impl PgConnection { - pub(super) async fn handle_row_description( - &mut self, - desc: Option, - fetch_type_info: bool, - fetch_column_description: bool, - ) -> Result<(vec, HashMap), Error> { - let mut columns = Vec::new(); - let mut column_names = HashMap::new(); - - let desc = if let Some(desc) = desc { - desc - } else { - // no rows - return Ok((columns, column_names)); - }; - - columns.reserve(desc.fields.len()); - column_names.reserve(desc.fields.len()); - - for (index, field) in desc.fields.into_iter().enumerate() { - let name = UStr::from(field.name); - - let type_info = self - .maybe_fetch_type_info_by_oid(field.data_type_id, fetch_type_info) - .await?; - - let origin = if let (Some(relation_oid), Some(attribute_no)) = - (field.relation_id, field.relation_attribute_no) - { - self.maybe_fetch_column_origin(relation_oid, attribute_no, fetch_column_description) - .await? - } else { - ColumnOrigin::Expression - }; - - let column = PgColumn { - ordinal: index, - name: name.clone(), - type_info, - relation_id: field.relation_id, - relation_attribute_no: field.relation_attribute_no, - origin, - }; - - columns.push(column); - column_names.insert(name, index); - } - - Ok((columns, column_names)) - } - - pub(super) async fn handle_parameter_description( - &mut self, - desc: ParameterDescription, - ) -> Result, Error> { - let mut params = Vec::with_capacity(desc.types.len()); - - for ty in desc.types { - params.push(self.maybe_fetch_type_info_by_oid(ty, true).await?); - } - - Ok(params) - } - - async fn maybe_fetch_type_info_by_oid( - &mut self, - oid: Oid, - should_fetch: bool, - ) -> Result { - // first we check if this is a built-in type - // in the average application, the vast majority of checks should flow through this - if let Some(info) = PgTypeInfo::try_from_oid(oid) { - return Ok(info); - } - - // next we check a local cache for user-defined type names <-> object id - if let Some(info) = self.inner.cache_type_info.get(&oid) { - return Ok(info.clone()); - } - - // fallback to asking the database directly for a type name - if should_fetch { - // we're boxing this future here so we can use async recursion - let info = Box::pin(async { self.fetch_type_by_oid(oid).await }).await?; - - // cache the type name <-> oid relationship in a paired hashmap - // so we don't come down this road again - self.inner.cache_type_info.insert(oid, info.clone()); - self.inner - .cache_type_oid - .insert(info.0.name().to_string().into(), oid); - - Ok(info) - } else { - // we are not in a place that *can* run a query - // this generally means we are in the middle of another query - // this _should_ only happen for complex types sent through the TEXT protocol - // we're open to ideas to correct this.. but it'd probably be more efficient to figure - // out a way to "prime" the type cache for connections rather than make this - // fallback work correctly for complex user-defined types for the TEXT protocol - Ok(PgTypeInfo(PgType::DeclareWithOid(oid))) - } - } - - async fn maybe_fetch_column_origin( - &mut self, - relation_id: Oid, - attribute_no: i16, - should_fetch: bool, - ) -> Result { - if let Some(origin) = self - .inner - .cache_table_to_column_names - .get(&relation_id) - .and_then(|table_columns| { - let column_name = table_columns.columns.get(&attribute_no).cloned()?; - - Some(ColumnOrigin::Table(TableColumn { - table: table_columns.table_name.clone(), - name: column_name, - })) - }) - { - return Ok(origin); - } - - if !should_fetch { - return Ok(ColumnOrigin::Unknown); - } - - // Looking up the table name _may_ end up being redundant, - // but the round-trip to the server is by far the most expensive part anyway. - let Some((table_name, column_name)): Option<(string, String)> = query_as( - // language=PostgreSQL - "SELECT 1ドル::oid::regclass::text, attname \ - FROM pg_catalog.pg_attribute \ - WHERE attrelid = 1ドル AND attnum = 2ドル", - ) - .bind(relation_id) - .bind(attribute_no) - .fetch_optional(&mut *self) - .await? - else { - // The column/table doesn't exist anymore for whatever reason. - return Ok(ColumnOrigin::Unknown); - }; - - let table_columns = self - .inner - .cache_table_to_column_names - .entry(relation_id) - .or_insert_with(|| TableColumns { - table_name: table_name.into(), - columns: Default::default(), - }); - - let column_name = table_columns - .columns - .entry(attribute_no) - .or_insert(column_name.into()); - - Ok(ColumnOrigin::Table(TableColumn { - table: table_columns.table_name.clone(), - name: Arc::clone(column_name), - })) - } - - async fn fetch_type_by_oid(&mut self, oid: Oid) -> Result { - let (name, typ_type, category, relation_id, element, base_type): ( - String, - i8, - i8, - Oid, - Oid, - Oid, - ) = query_as( - // Converting the OID to `regtype` and then `text` will give us the name that - // the type will need to be found at by search_path. - "SELECT oid::regtype::text, \ - typtype, \ - typcategory, \ - typrelid, \ - typelem, \ - typbasetype \ - FROM pg_catalog.pg_type \ - WHERE oid = 1ドル", - ) - .bind(oid) - .fetch_one(&mut *self) - .await?; - - let typ_type = TypType::try_from(typ_type); - let category = TypCategory::try_from(category); - - match (typ_type, category) { - (Ok(TypType::Domain), _) => self.fetch_domain_by_oid(oid, base_type, name).await, - - (Ok(TypType::Base), Ok(TypCategory::Array)) => { - Ok(PgTypeInfo(PgType::Custom(Arc::new(PgCustomType { - kind: PgTypeKind::Array( - self.maybe_fetch_type_info_by_oid(element, true).await?, - ), - name: name.into(), - oid, - })))) - } - - (Ok(TypType::Pseudo), Ok(TypCategory::Pseudo)) => { - Ok(PgTypeInfo(PgType::Custom(Arc::new(PgCustomType { - kind: PgTypeKind::Pseudo, - name: name.into(), - oid, - })))) - } - - (Ok(TypType::Range), Ok(TypCategory::Range)) => { - self.fetch_range_by_oid(oid, name).await - } - - (Ok(TypType::Enum), Ok(TypCategory::Enum)) => self.fetch_enum_by_oid(oid, name).await, - - (Ok(TypType::Composite), Ok(TypCategory::Composite)) => { - self.fetch_composite_by_oid(oid, relation_id, name).await - } - - _ => Ok(PgTypeInfo(PgType::Custom(Arc::new(PgCustomType { - kind: PgTypeKind::Simple, - name: name.into(), - oid, - })))), - } - } - - async fn fetch_enum_by_oid(&mut self, oid: Oid, name: String) -> Result { - let variants: Vec = query_scalar( - r#" -SELECT enumlabel -FROM pg_catalog.pg_enum -WHERE enumtypid = 1ドル -ORDER BY enumsortorder - "#, - ) - .bind(oid) - .fetch_all(self) - .await?; - - Ok(PgTypeInfo(PgType::Custom(Arc::new(PgCustomType { - oid, - name: name.into(), - kind: PgTypeKind::Enum(Arc::from(variants)), - })))) - } - - async fn fetch_composite_by_oid( - &mut self, - oid: Oid, - relation_id: Oid, - name: String, - ) -> Result { - let raw_fields: Vec<(string, Oid)> = query_as( - r#" -SELECT attname, atttypid -FROM pg_catalog.pg_attribute -WHERE attrelid = 1ドル -AND NOT attisdropped -AND attnum> 0 -ORDER BY attnum - "#, - ) - .bind(relation_id) - .fetch_all(&mut *self) - .await?; - - let mut fields = Vec::new(); - - for (field_name, field_oid) in raw_fields.into_iter() { - let field_type = self.maybe_fetch_type_info_by_oid(field_oid, true).await?; - - fields.push((field_name, field_type)); - } - - Ok(PgTypeInfo(PgType::Custom(Arc::new(PgCustomType { - oid, - name: name.into(), - kind: PgTypeKind::Composite(Arc::from(fields)), - })))) - } - - async fn fetch_domain_by_oid( - &mut self, - oid: Oid, - base_type: Oid, - name: String, - ) -> Result { - let base_type = self.maybe_fetch_type_info_by_oid(base_type, true).await?; - - Ok(PgTypeInfo(PgType::Custom(Arc::new(PgCustomType { - oid, - name: name.into(), - kind: PgTypeKind::Domain(base_type), - })))) - } - - async fn fetch_range_by_oid(&mut self, oid: Oid, name: String) -> Result { - let element_oid: Oid = query_scalar( - r#" -SELECT rngsubtype -FROM pg_catalog.pg_range -WHERE rngtypid = 1ドル - "#, - ) - .bind(oid) - .fetch_one(&mut *self) - .await?; - - let element = self.maybe_fetch_type_info_by_oid(element_oid, true).await?; - - Ok(PgTypeInfo(PgType::Custom(Arc::new(PgCustomType { - kind: PgTypeKind::Range(element), - name: name.into(), - oid, - })))) - } - - pub(crate) async fn resolve_type_id(&mut self, ty: &PgType) -> Result { - if let Some(oid) = ty.try_oid() { - return Ok(oid); - } - - match ty { - PgType::DeclareWithName(name) => self.fetch_type_id_by_name(name).await, - PgType::DeclareArrayOf(array) => self.fetch_array_type_id(array).await, - // `.try_oid()` should return `Some()` or it should be covered here - _ => unreachable!("(bug) OID should be resolvable for type {ty:?}"), - } - } - - pub(crate) async fn fetch_type_id_by_name(&mut self, name: &str) -> Result { - if let Some(oid) = self.inner.cache_type_oid.get(name) { - return Ok(*oid); - } - - // language=SQL - let (oid,): (Oid,) = query_as("SELECT 1ドル::regtype::oid") - .bind(name) - .fetch_optional(&mut *self) - .await? - .ok_or_else(|| Error::TypeNotFound { - type_name: name.into(), - })?; - - self.inner - .cache_type_oid - .insert(name.to_string().into(), oid); - Ok(oid) - } - - pub(crate) async fn fetch_array_type_id(&mut self, array: &PgArrayOf) -> Result { - if let Some(oid) = self - .inner - .cache_type_oid - .get(&array.elem_name) - .and_then(|elem_oid| self.inner.cache_elem_type_to_array.get(elem_oid)) - { - return Ok(*oid); - } - - // language=SQL - let (elem_oid, array_oid): (Oid, Oid) = - query_as("SELECT oid, typarray FROM pg_catalog.pg_type WHERE oid = 1ドル::regtype::oid") - .bind(&*array.elem_name) - .fetch_optional(&mut *self) - .await? - .ok_or_else(|| Error::TypeNotFound { - type_name: array.name.to_string(), - })?; - - // Avoids copying `elem_name` until necessary - self.inner - .cache_type_oid - .entry_ref(&array.elem_name) - .insert(elem_oid); - self.inner - .cache_elem_type_to_array - .insert(elem_oid, array_oid); - - Ok(array_oid) - } - /// Check whether EXPLAIN statements are supported by the current connection fn is_explain_available(&self) -> bool { let parameter_statuses = &self.inner.stream.parameter_statuses; diff --git a/sqlx-postgres/src/connection/establish.rs b/sqlx-postgres/src/connection/establish.rs index 634b71de4b..3c2f516533 100644 --- a/sqlx-postgres/src/connection/establish.rs +++ b/sqlx-postgres/src/connection/establish.rs @@ -148,7 +148,7 @@ impl PgConnection { cache_type_oid: HashMap::new(), cache_type_info: HashMap::new(), cache_elem_type_to_array: HashMap::new(), - cache_table_to_column_names: HashMap::new(), + cache_table_data: HashMap::new(), log_settings: options.log_settings.clone(), }), }) diff --git a/sqlx-postgres/src/connection/executor.rs b/sqlx-postgres/src/connection/executor.rs index ba4cffa647..e0f4c3d44a 100644 --- a/sqlx-postgres/src/connection/executor.rs +++ b/sqlx-postgres/src/connection/executor.rs @@ -1,11 +1,10 @@ -use crate::describe::Describe; use crate::error::Error; use crate::executor::{Execute, Executor}; use crate::io::{PortalId, StatementId}; use crate::logger::QueryLogger; use crate::message::{ self, BackendMessageFormat, Bind, Close, CommandComplete, DataRow, ParameterDescription, Parse, - ParseComplete, Query, RowDescription, + ParseComplete, RowDescription, }; use crate::statement::PgStatementMetadata; use crate::{ @@ -24,10 +23,10 @@ use std::{pin::pin, sync::Arc}; async fn prepare( conn: &mut PgConnection, sql: &str, - parameters: &[PgTypeInfo], + arg_types: &[PgTypeInfo], metadata: Option>, persistent: bool, - fetch_column_origin: bool, + resolve_column_origin: bool, ) -> Result<(statementid, Arc), Error> { let id = if persistent { let id = conn.inner.next_statement_id; @@ -40,12 +39,7 @@ async fn prepare( // 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 // additional queries here to get any missing OIDs - - let mut param_types = Vec::with_capacity(parameters.len()); - - for ty in parameters { - param_types.push(conn.resolve_type_id(&ty.0).await?); - } + let param_types = conn.resolve_types(arg_types).await?; // flush and wait until we are re-ready conn.wait_until_ready().await?; @@ -80,26 +74,20 @@ async fn prepare( } else { let parameters = recv_desc_params(conn).await?; - let rows = recv_desc_rows(conn).await?; + let row_desc = recv_desc_rows(conn).await?; // each SYNC produces one READY FOR QUERY conn.recv_ready_for_query().await?; - let parameters = conn.handle_parameter_description(parameters).await?; - - let (columns, column_names) = conn - .handle_row_description(rows, true, fetch_column_origin) + let metadata = conn + .resolve_statement_metadata::(Some(parameters), row_desc, resolve_column_origin) .await?; // ensure that if we did fetch custom data, we wait until we are fully ready before // continuing conn.wait_until_ready().await?; - Arc::new(PgStatementMetadata { - parameters, - columns, - column_names: Arc::new(column_names), - }) + metadata }; Ok((id, metadata)) @@ -177,7 +165,7 @@ impl PgConnection { // optional metadata that was provided by the user, this means they are reusing // a statement object metadata: Option>, - fetch_column_origin: bool, + resolve_column_origin: bool, ) -> Result<(statementid, Arc), Error> { if let Some(statement) = self.inner.cache_statement.get_mut(sql) { return Ok((*statement).clone()); @@ -189,7 +177,7 @@ impl PgConnection { parameters, metadata, persistent, - fetch_column_origin, + resolve_column_origin, ) .await?; @@ -293,8 +281,7 @@ impl PgConnection { PgValueFormat::Binary } else { // Query will trigger a ReadyForQuery - self.inner.stream.write_msg(Query(sql))?; - self.inner.pending_ready_for_query_count += 1; + self.queue_simple_query(sql)?; // metadata starts out as "nothing" metadata = Arc::new(PgStatementMetadata::default()); @@ -344,17 +331,15 @@ impl PgConnection { // incomplete query execution has finished BackendMessageFormat::PortalSuspended => {} + // indicates that a *new* set of rows are about to be returned BackendMessageFormat::RowDescription => { - // indicates that a *new* set of rows are about to be returned - let (columns, column_names) = self - .handle_row_description(Some(message.decode()?), false, false) - .await?; - - metadata = Arc::new(PgStatementMetadata { - column_names: Arc::new(column_names), - columns, - parameters: Vec::default(), - }); + let new_metadata = self.resolve_statement_metadata::( + None, + Some(message.decode()?), + false, + ).await?; + + metadata = new_metadata; } BackendMessageFormat::DataRow => { @@ -475,7 +460,11 @@ impl<'c> Executor<'c> for &'c mut PgConnection { }) } - fn describe<'e>(self, sql: SqlStr) -> BoxFuture<'e, Result, Error>> + #[cfg(feature = "offline")] + fn describe<'e>( + self, + sql: SqlStr, + ) -> BoxFuture<'e, Result, Error>> where 'c: 'e, { @@ -488,7 +477,7 @@ impl<'c> Executor<'c> for &'c mut PgConnection { let nullable = self.get_nullable_for_columns(stmt_id, &metadata).await?; - Ok(Describe { + Ok(crate::describe::Describe { columns: metadata.columns.clone(), nullable, parameters: Some(Either::Left(metadata.parameters.clone())), diff --git a/sqlx-postgres/src/connection/mod.rs b/sqlx-postgres/src/connection/mod.rs index 4e05cd867b..d594585b6c 100644 --- a/sqlx-postgres/src/connection/mod.rs +++ b/sqlx-postgres/src/connection/mod.rs @@ -23,9 +23,11 @@ use sqlx_core::sql_str::SqlSafeStr; pub use self::stream::PgStream; -pub(crate) mod describe; +#[cfg(feature = "offline")] +mod describe; mod establish; mod executor; +mod resolve; mod sasl; mod stream; mod tls; @@ -64,7 +66,7 @@ pub struct PgConnectionInner { cache_type_info: HashMap, cache_type_oid: HashMap, cache_elem_type_to_array: HashMap, - cache_table_to_column_names: HashMap, + cache_table_data: HashMap, // number of ReadyForQuery messages that we are currently expecting pub(crate) pending_ready_for_query_count: usize, @@ -76,7 +78,7 @@ pub struct PgConnectionInner { log_settings: LogSettings, } -pub(crate) struct TableColumns { +pub(crate) struct TableData { table_name: Arc, /// Attribute number -> name. columns: BTreeMap>, diff --git a/sqlx-postgres/src/connection/resolve.rs b/sqlx-postgres/src/connection/resolve.rs new file mode 100644 index 0000000000..e47c318926 --- /dev/null +++ b/sqlx-postgres/src/connection/resolve.rs @@ -0,0 +1,699 @@ +use crate::connection::TableData; +use crate::error::Error; +use crate::ext::ustr::UStr; +use crate::message::{ParameterDescription, RowDescription}; +use crate::statement::PgStatementMetadata; +use crate::type_info::{PgCustomType, PgType, PgTypeKind}; +use crate::types::Oid; +use crate::{HashMap, PgRow, PgValueRef, Postgres}; +use crate::{PgColumn, PgConnection, PgTypeInfo}; +use sqlx_core::column::{ColumnOrigin, TableColumn}; +use sqlx_core::decode::Decode; +use sqlx_core::error::BoxDynError; +use sqlx_core::from_row::FromRow; +use sqlx_core::raw_sql::raw_sql; +use sqlx_core::row::Row; +use sqlx_core::sql_str::AssertSqlSafe; +use sqlx_core::types::Type; +use std::collections::{BTreeMap, VecDeque}; +use std::fmt::Display; +use std::mem; +use std::ops::ControlFlow; +use std::sync::Arc; +// NOTE: we should only use raw queries in this module because this may occur in the middle +// of an existing extended query flow. Additionally, some third-party implementations don't +// support named prepared statements, so to execute these statements with the extended query flow, +// we'd have to replace the unnamed prepared statement which is already the one the user wanted +// to execute. This means we'd have to immediately re-prepare it, adding an extra round trip. + +impl PgConnection { + pub(super) async fn resolve_statement_metadata( + &mut self, + param_desc: Option, + row_desc: Option, + resolve_column_origin: bool, + ) -> Result, Error> { + let param_types = param_desc.map_or_else(Default::default, |desc| desc.types); + + let fields = row_desc.map_or_else(Default::default, |desc| desc.fields); + + if QUERIES_ALLOWED { + let mut type_resolver = TypeResolver::default(); + let mut column_resolver = ColumnResolver::default(); + + for ty in ¶m_types { + if self.try_oid_to_type(*ty).is_none() { + type_resolver.push_type("NULL", ty.0); + } + } + + for field in &fields { + if self.try_oid_to_type(field.data_type_id).is_none() { + type_resolver.push_type("NULL", field.data_type_id.0); + } + + if let (Some(relation_oid), Some(attribute_no)) = + (field.relation_id, field.relation_attribute_no) + { + if resolve_column_origin && !self.has_table_column(relation_oid, attribute_no) { + column_resolver.push_column(relation_oid, attribute_no); + } + } + } + + // No-op if `.push_type()` was not called + type_resolver.fill_cache(self).await?; + + // No-op if `.push_column()` was not called + column_resolver.fill_cache(self).await?; + } + + let mut parameters = Vec::with_capacity(param_types.len()); + + for ty in param_types { + if let Some(type_info) = self.try_oid_to_type(ty) { + parameters.push(type_info); + } else { + parameters.push(PgTypeInfo(PgType::DeclareWithOid(ty))); + } + } + + let mut columns = Vec::with_capacity(fields.len()); + let mut column_names = HashMap::with_capacity(fields.len()); + + for field in fields { + let name = UStr::from(field.name); + let ordinal = columns.len(); + + let type_info = self + .try_oid_to_type(field.data_type_id) + .unwrap_or(PgTypeInfo(PgType::DeclareWithOid(field.data_type_id))); + + let origin = field.relation_id.zip(field.relation_attribute_no).map_or( + ColumnOrigin::Expression, + |(relation_oid, attribue_no)| { + self.try_table_column(relation_oid, attribue_no) + .map_or(ColumnOrigin::Unknown, ColumnOrigin::Table) + }, + ); + + columns.push(PgColumn { + ordinal, + name: name.clone(), + type_info, + origin, + relation_id: field.relation_id, + relation_attribute_no: field.relation_attribute_no, + }); + + column_names.insert(name, ordinal); + } + + Ok(Arc::new(PgStatementMetadata { + columns, + column_names: column_names.into(), + parameters, + })) + } + + fn try_table_column(&self, relation_oid: Oid, attribute_no: i16) -> Option { + let table_columns = self.inner.cache_table_data.get(&relation_oid)?; + + let column = table_columns.columns.get(&attribute_no)?; + + Some(TableColumn { + table: table_columns.table_name.clone(), + name: column.clone(), + }) + } + + fn has_table_column(&self, relation_oid: Oid, attribute_no: i16) -> bool { + self.inner + .cache_table_data + .get(&relation_oid) + .is_some_and(|data| data.columns.contains_key(&attribute_no)) + } + + pub(crate) async fn resolve_types(&mut self, types: &[PgTypeInfo]) -> Result, Error> { + let mut oids = Vec::with_capacity(types.len()); + + let mut unresolved_types = types.iter().peekable(); + + // Eagerly try to resolve types, stopping at the first unresolved type + while let Some(ty) = unresolved_types.peek() { + let Some(oid) = self.try_type_to_oid(ty) else { + break; + }; + + oids.push(oid); + unresolved_types.next(); + } + + // Fast-path: all types resolved + if oids.len() == types.len() { + return Ok(oids); + } + + let mut resolver = TypeResolver::default(); + + for ty in unresolved_types.clone() { + // Skip over subsequent types that are already resolved + if self.try_type_to_oid(ty).is_some() { + continue; + } + + if let PgType::DeclareArrayOf(array_of) = &ty.0 { + // Eagerly bring the element type into cache for array types declared by-name + resolver.push_type( + format_args!("E'{}'", array_of.elem_name), + format_args!("to_regtype(E'{}')", array_of.elem_name), + ); + } + + resolver.push_type( + // `escape_default()` should produce a valid SQL string literal + // https://doc.rust-lang.org/stable/std/primitive.char.html#method.escape_default + // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE + format_args!("E'{}'", ty.name().escape_default()), + // `to_regtype()` evaluates to `NULL` if the type does not exist, + // instead of throwing an exception like `''::regtype` does. + format_args!("to_regtype(E'{}')::oid", ty.name().escape_default()), + ); + } + + resolver.fill_cache(self).await?; + + for ty in unresolved_types { + oids.push( + self.try_type_to_oid(ty) + .ok_or_else(|| Error::TypeNotFound { + type_name: ty.name().to_string(), + })?, + ); + } + + Ok(oids) + } + + pub(crate) fn try_type_to_oid(&self, ty: &PgTypeInfo) -> Option { + if let Some(oid) = ty.try_oid() { + return Some(oid); + } + + match &ty.0 { + PgType::DeclareWithName(name) => self.inner.cache_type_oid.get(name).copied(), + PgType::DeclareArrayOf(array) => { + let typelem = self.inner.cache_type_oid.get(&array.elem_name).copied()?; + self.inner.cache_elem_type_to_array.get(&typelem).copied() + } + // `.try_oid()` should return `Some()` or it should be covered here + _ => unreachable!("(bug) OID should be resolvable for type {ty:?}"), + } + } + + fn try_oid_to_type(&self, oid: Oid) -> Option { + PgTypeInfo::try_from_oid(oid).or_else(|| self.inner.cache_type_info.get(&oid).cloned()) + } + + fn try_cache_type(&mut self, ty: &TypeResolverRow) -> Result, Error> { + if self.try_oid_to_type(ty.oid).is_some() { + // We hit this code path because one of these names didn't resolve, + // cache them both. + self.inner + .cache_type_oid + .insert(UStr::new(&ty.catalog_name), ty.oid); + self.inner + .cache_type_oid + .insert(UStr::new(&ty.pretty_name), ty.oid); + + if let Some(original_name) = &ty.original_name { + self.inner + .cache_type_oid + .insert(UStr::new(original_name), ty.oid); + } + + if let Some(elem_oid) = ty.typelem { + if self.try_oid_to_type(elem_oid).is_some() { + self.inner.cache_elem_type_to_array.insert(elem_oid, ty.oid); + } else { + return Ok(ControlFlow::Break(elem_oid)); + } + } + + return Ok(ControlFlow::Continue(())); + } + + if self.inner.cache_type_info.contains_key(&ty.oid) { + return Ok(ControlFlow::Continue(())); + } + + let custom_type_kind = match (ty.typtype, ty.typcategory) { + (TypType::Domain, _) => { + let typbasetype = ty.typbasetype.ok_or_else(|| { + err_protocol!( + "type category is listed as domain, but no base type was found: {ty:?}" + ) + })?; + + let Some(base_type) = self.try_oid_to_type(typbasetype) else { + return Ok(ControlFlow::Break(typbasetype)); + }; + + PgTypeKind::Domain(base_type) + } + + (TypType::Base, TypCategory::Array) => { + let typelem = ty.typelem.ok_or_else(|| { + err_protocol!( + "type category is listed as array, but no element type was found: {ty:?}" + ) + })?; + + let Some(elem_type) = self.try_oid_to_type(typelem) else { + return Ok(ControlFlow::Break(typelem)); + }; + + self.inner.cache_elem_type_to_array.insert(typelem, ty.oid); + + PgTypeKind::Array(elem_type) + } + + (TypType::Pseudo, _) => PgTypeKind::Pseudo, + + (TypType::Range, _) => { + let rngsubtype = ty.rngsubtype.ok_or_else(|| { + err_protocol!( + "type category is listed as range, but no subtype was found: {ty:?}" + ) + })?; + + let Some(sub_type) = self.try_oid_to_type(rngsubtype) else { + return Ok(ControlFlow::Break(rngsubtype)); + }; + + PgTypeKind::Range(sub_type) + } + + (TypType::Enum, _) => PgTypeKind::Enum(ty.enum_labels.iter().cloned().collect()), + + (TypType::Composite, _) => { + let mut attributes = Vec::with_capacity(ty.record_attributes.len()); + + for (name, oid) in &ty.record_attributes { + let Some(attribute_type) = self.try_oid_to_type(*oid) else { + return Ok(ControlFlow::Break(*oid)); + }; + + attributes.push((name.clone(), attribute_type)); + } + + PgTypeKind::Composite(attributes.into()) + } + + _ => PgTypeKind::Simple, + }; + + let typname = UStr::new(&ty.pretty_name); + + self.inner + .cache_type_oid + .entry_ref(&typname) + .or_insert(ty.oid); + + if ty.pretty_name != ty.catalog_name { + self.inner + .cache_type_oid + .entry(UStr::new(&ty.catalog_name)) + .or_insert(ty.oid); + } + + if let Some(original_name) = &ty.original_name { + self.inner + .cache_type_oid + .entry(UStr::new(original_name)) + .or_insert(ty.oid); + } + + self.inner.cache_type_info.entry(ty.oid).or_insert_with(|| { + PgTypeInfo(PgType::Custom(Arc::new(PgCustomType { + kind: custom_type_kind, + name: typname.clone(), + oid: ty.oid, + }))) + }); + + Ok(ControlFlow::Continue(())) + } +} + +#[derive(Default)] +struct TypeResolver { + query: String, +} + +impl TypeResolver { + fn push_type(&mut self, original_name: impl Display, oid_expr: impl Display) { + use std::fmt::Write; + + tracing::trace!(%original_name, %oid_expr, "push_type"); + + // Lazily push the preamble to `self.query` so we don't allocate in the fast path + // (all types already known) + if self.query.is_empty() { + write!( + &mut self.query, + // Postgres 13 would return `0` instead of `NULL` for `typelem`, `typbasetype` + "SELECT pg_type.oid,\n\ + pg_type.oid::regtype::text pretty_name,\n\ + typname catalog_name,\n\ + original_name,\n\ + typtype,\n\ + typcategory,\n\ + NULLIF(typelem, 0::oid) typelem,\n\ + NULLIF(typbasetype, 0::oid) typbasetype,\n\ + rngsubtype,\n\ + COALESCE(\ + (SELECT array_agg(enumlabel) FROM (SELECT *\n\ + FROM pg_catalog.pg_enum\n\ + WHERE enumtypid = pg_type.oid\n\ + ORDER BY enumsortorder) labels),\n\ + '{{}}') enum_labels,\n\ + COALESCE(\n\ + (SELECT array_agg((attname, atttypid)) FROM (SELECT *\n\ + FROM pg_catalog.pg_attribute\n\ + WHERE attrelid = pg_type.typrelid\n\ + AND NOT attisdropped\n\ + AND attnum> 0\n\ + ORDER BY attnum) attributes),\n\ + '{{}}') record_attributes\n\ + FROM (SELECT DISTINCT ON(lookup_oid) original_name, lookup_oid\n\ + FROM (VALUES ({original_name}, {oid_expr})" + ) + .expect("error writing type expression to query string") + } else { + write!(&mut self.query, ", ({original_name}, {oid_expr})") + .expect("error writing type expression to query string") + } + } + + async fn fill_cache(&mut self, conn: &mut PgConnection) -> Result<(), Error> { + let mut missing_dependencies = HashMap::>::new(); + + // Iteratively resolve types until all are resolved, or we hit a dead-end. + // We statically cap the number of iterations in case we somehow encounter a circular type + // dependency, which I *assume* Postgres should forbid. + for _ in 0..64 { + if self.query.is_empty() { + break; + } + + // * Cancel-safety + // * Makes this type reusable if we want to for whatever reason + // * Avoids an allocation when converting to `SqlStr` + let mut query = mem::take(&mut self.query); + query.push_str( + ") lookup_inner(original_name, lookup_oid)\n\ + ORDER BY lookup_oid) type_lookup\n\ + INNER JOIN pg_catalog.pg_type ON type_lookup.lookup_oid = pg_type.oid\n\ + LEFT JOIN pg_catalog.pg_range ON pg_type.oid = pg_range.rngtypid", + ); + + tracing::trace!(query, "fill_cache"); + + let types = raw_sql(AssertSqlSafe(query)).fetch_all(&mut *conn).await?; + + 'outer: for row in types { + let mut type_row = TypeResolverRow::from_row(&row)?; + + tracing::trace!("type_row: {type_row:?}"); + + let mut resolved_dependencies = VecDeque::new(); + + loop { + if let ControlFlow::Break(missing_oid) = conn.try_cache_type(&type_row)? { + tracing::trace!( + ty_name = type_row.catalog_name, + missing_oid = missing_oid.0, + "type missing dependency" + ); + + missing_dependencies + .entry(missing_oid) + .or_default() + .push(type_row); + + self.push_type("NULL", missing_oid.0); + + continue 'outer; + } + + resolved_dependencies.extend( + missing_dependencies + .remove(&type_row.oid) + .unwrap_or_default(), + ); + + // Iteratively mark existing dependencies as resolved + if let Some(next_row) = resolved_dependencies.pop_back() { + tracing::trace!( + resolved_oid = type_row.oid.0, + ty_name = next_row.catalog_name, + "resolved dependency" + ); + + type_row = next_row + } else { + break; + } + } + } + } + + if !missing_dependencies.is_empty() { + return Err(Error::Protocol(format!( + "unable to resolve type OIDs: {:?}", + missing_dependencies.keys() + ))); + } + + Ok(()) + } +} + +#[derive(Debug)] +struct TypeResolverRow { + oid: Oid, + // Most of the time, these are the same but not necessarily for arrays + pretty_name: String, + catalog_name: String, + original_name: Option, + typtype: TypType, + typcategory: TypCategory, + typelem: Option, + typbasetype: Option, + rngsubtype: Option, + enum_labels: Vec, + record_attributes: Vec<(string, Oid)>, +} + +// Can't use `#[derive(FromRow)]` here +impl<'r> FromRow<'r, PgRow> for TypeResolverRow { + fn from_row(row: &'r PgRow) -> Result { + Ok(Self { + oid: row.try_get("oid")?, + pretty_name: row.try_get("pretty_name")?, + catalog_name: row.try_get("catalog_name")?, + original_name: row.try_get("original_name")?, + typtype: row.try_get("typtype")?, + typcategory: row.try_get("typcategory")?, + typelem: row.try_get("typelem")?, + typbasetype: row.try_get("typbasetype")?, + rngsubtype: row.try_get("rngsubtype")?, + enum_labels: row.try_get("enum_labels")?, + record_attributes: row.try_get("record_attributes")?, + }) + } +} + +#[derive(Default)] +struct ColumnResolver { + query: String, +} + +impl ColumnResolver { + fn push_column(&mut self, table_oid: Oid, attribute_no: i16) { + use std::fmt::Write; + + if self.query.is_empty() { + write!( + self.query, + // Postgres 13 does not accept `(attnum,attname)` without `ROW` + "SELECT\n\ + attrelid table_oid,\n\ + attrelid::regclass::text table_name,\n\ + array_agg(ROW(attnum, attname)) AS columns\n\ + FROM (VALUES ({}, {attribute_no})", + table_oid.0, + ) + .expect("writing to a `String` should be infallible") + } else { + write!(self.query, ", ({}, {attribute_no})", table_oid.0) + .expect("writing to a `String` should be infallible") + } + } + + async fn fill_cache(&mut self, conn: &mut PgConnection) -> Result<(), Error> { + if self.query.is_empty() { + return Ok(()); + } + + let mut query = mem::take(&mut self.query); + query.push_str( + ") lookup(table_oid, attribute_num)\n\ + INNER JOIN pg_catalog.pg_attribute ON lookup.table_oid = attrelid AND lookup.attribute_num = attnum\n\ + GROUP BY attrelid" + ); + + let rows = raw_sql(AssertSqlSafe(query)).fetch_all(&mut *conn).await?; + + for row in rows { + let row = ColumnResolverRow::from_row(&row)?; + + let table_columns = conn + .inner + .cache_table_data + .entry(row.table_oid) + .or_insert_with(|| TableData { + table_name: row.table_name.clone(), + columns: BTreeMap::new(), + }); + + table_columns.columns.extend(row.columns); + } + + Ok(()) + } +} + +#[derive(Debug)] +struct ColumnResolverRow { + table_oid: Oid, + table_name: Arc, + columns: Vec<(i16, Arc)>, +} + +impl<'r> FromRow<'r, PgRow> for ColumnResolverRow { + fn from_row(row: &'r PgRow) -> Result { + Ok(Self { + table_oid: row.try_get("table_oid")?, + table_name: row.try_get("table_name")?, + columns: row.try_get("columns")?, + }) + } +} + +/// Describes the type of the `pg_type.typtype` column +/// +/// See +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum TypType { + Base, + Composite, + Domain, + Enum, + Pseudo, + Range, +} + +impl TryFrom for TypType { + type Error = String; + + fn try_from(t: i8) -> Result { + let t = u8::try_from(t).map_err(|_| format!("unknown type code {t}"))?; + + let t = match t { + b'b' => Self::Base, + b'c' => Self::Composite, + b'd' => Self::Domain, + b'e' => Self::Enum, + b'p' => Self::Pseudo, + b'r' => Self::Range, + _ => return Err(format!("unknown type code {t}")), + }; + Ok(t) + } +} + +impl<'r> Decode<'r, Postgres> for TypType { + fn decode(value: PgValueRef<'r>) -> Result { + Ok(i8::decode(value)?.try_into()?) + } +} + +impl Type for TypType { + fn type_info() -> PgTypeInfo { + PgTypeInfo(PgType::Char) + } +} + +/// Describes the type of the `pg_type.typcategory` column +/// +/// See +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum TypCategory { + Array, + Boolean, + Composite, + DateTime, + Enum, + Geometric, + Network, + Numeric, + Pseudo, + Range, + String, + Timespan, + User, + BitString, + Unknown, +} + +impl TryFrom for TypCategory { + type Error = String; + + fn try_from(c: i8) -> Result { + let c = u8::try_from(c).map_err(|_| format!("invalid category code {c}"))?; + + let c = match c { + b'A' => Self::Array, + b'B' => Self::Boolean, + b'C' => Self::Composite, + b'D' => Self::DateTime, + b'E' => Self::Enum, + b'G' => Self::Geometric, + b'I' => Self::Network, + b'N' => Self::Numeric, + b'P' => Self::Pseudo, + b'R' => Self::Range, + b'S' => Self::String, + b'T' => Self::Timespan, + b'U' => Self::User, + b'V' => Self::BitString, + b'X' => Self::Unknown, + _ => return Err(format!("invalid category code {c}")), + }; + Ok(c) + } +} + +impl<'r> Decode<'r, Postgres> for TypCategory { + fn decode(value: PgValueRef<'r>) -> Result { + Ok(i8::decode(value)?.try_into()?) + } +} + +impl Type for TypCategory { + fn type_info() -> PgTypeInfo { + PgTypeInfo(PgType::Char) + } +} diff --git a/sqlx-postgres/src/connection/sasl.rs b/sqlx-postgres/src/connection/sasl.rs index 94fdfc689f..157e1214ab 100644 --- a/sqlx-postgres/src/connection/sasl.rs +++ b/sqlx-postgres/src/connection/sasl.rs @@ -4,11 +4,12 @@ use crate::message::{Authentication, AuthenticationSasl, SaslInitialResponse, Sa use crate::rt; use crate::PgConnectOptions; use hmac::{Hmac, Mac}; -use rand::Rng; +use hmac::{HmacReset, KeyInit}; use sha2::{Digest, Sha256}; use stringprep::saslprep; use base64::prelude::{Engine as _, BASE64_STANDARD}; +use rand::RngExt; const GS2_HEADER: &str = "n,,"; const CHANNEL_ATTR: &str = "c"; @@ -56,8 +57,11 @@ pub(crate) async fn authenticate( let username = format!("{}={}", USERNAME_ATTR, options.username); let username = match saslprep(&username) { Ok(v) => v, - // TODO(danielakhterov): Remove panic when we have proper support for configuration errors - Err(_) => panic!("Failed to saslprep username"), + Err(error) => { + return Err(Error::Configuration( + format!("Failed to saslprep username: {:?}", error).into(), + )) + } }; // nonce = "r=" c-nonce [s-nonce] ;; Second part provided by server. @@ -86,13 +90,19 @@ pub(crate) async fn authenticate( } }; + // Normalize(password): + let password = options.password.as_deref().unwrap_or_default(); + let password = match saslprep(password) { + Ok(v) => v, + Err(error) => { + return Err(Error::Configuration( + format!("Failed to saslprep password: {:?}", error).into(), + )) + } + }; + // SaltedPassword := Hi(Normalize(password), salt, i) - let salted_password = hi( - options.password.as_deref().unwrap_or_default(), - &cont.salt, - cont.iterations, - ) - .await?; + let salted_password = hi(&password, &cont.salt, cont.iterations).await?; // ClientKey := HMAC(SaltedPassword, "Client Key") let mut mac = Hmac::::new_from_slice(&salted_password).map_err(Error::protocol)?; @@ -163,8 +173,8 @@ pub(crate) async fn authenticate( // nonce is a sequence of random printable bytes fn gen_nonce() -> String { - let mut rng = rand::thread_rng(); - let count = rng.gen_range(64..128); + let mut rng = rand::rng(); + let count = rng.random_range(64..128); // printable = %x21-2B / %x2D-7E // ;; Printable ASCII except ",". @@ -172,10 +182,10 @@ fn gen_nonce() -> String { // ;; a valid "value". let nonce: String = std::iter::repeat(()) .map(|()| { - let mut c = rng.gen_range(0x21u8..0x7F); + let mut c = rng.random_range(0x21u8..0x7F); while c == 0x2C { - c = rng.gen_range(0x21u8..0x7F); + c = rng.random_range(0x21u8..0x7F); } c @@ -184,13 +194,12 @@ fn gen_nonce() -> String { .map(|c| c as char) .collect(); - rng.gen_range(32..128); format!("{NONCE_ATTR}={nonce}") } // Hi(str, salt, i): async fn hi<'a>(s: &'a str, salt: &'a [u8], iter_count: u32) -> Result<[u8; 32], Error> { - let mut mac = Hmac::::new_from_slice(s.as_bytes()).map_err(Error::protocol)?; + let mut mac = HmacReset::::new_from_slice(s.as_bytes()).map_err(Error::protocol)?; mac.update(salt); mac.update(&1u32.to_be_bytes()); diff --git a/sqlx-postgres/src/error.rs b/sqlx-postgres/src/error.rs index 7b5a03f2b3..7f787a4e5e 100644 --- a/sqlx-postgres/src/error.rs +++ b/sqlx-postgres/src/error.rs @@ -156,7 +156,11 @@ impl Debug for PgDatabaseError { impl Display for PgDatabaseError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str(self.message()) + f.write_str(self.message())?; + if let Some(line) = self.line() { + write!(f, " at line {line}")?; + } + Ok(()) } } diff --git a/sqlx-postgres/src/io/mod.rs b/sqlx-postgres/src/io/mod.rs index 72f2a978c8..00de302296 100644 --- a/sqlx-postgres/src/io/mod.rs +++ b/sqlx-postgres/src/io/mod.rs @@ -16,6 +16,7 @@ pub(crate) struct PortalId(IdInner); #[derive(Debug, Copy, Clone, PartialEq, Eq)] struct IdInner(Option); +#[allow(unused)] pub(crate) struct DisplayId { prefix: &'static str, id: NonZeroU32, @@ -43,6 +44,7 @@ impl StatementId { /// Get a type to format this statement ID with [`Display`]. /// /// Returns `None` if this is the unnamed statement. + #[allow(unused)] #[inline(always)] pub fn display(&self) -> Option { self.0.display(Self::NAME_PREFIX) @@ -104,6 +106,7 @@ impl IdInner { ) } + #[allow(unused)] #[inline(always)] fn display(&self, prefix: &'static str) -> Option { self.0.map(|id| DisplayId { prefix, id }) diff --git a/sqlx-postgres/src/listener.rs b/sqlx-postgres/src/listener.rs index 639ec95441..f9b9b98b1f 100644 --- a/sqlx-postgres/src/listener.rs +++ b/sqlx-postgres/src/listener.rs @@ -12,7 +12,6 @@ use sqlx_core::transaction::Transaction; use sqlx_core::Either; use tracing::Instrument; -use crate::describe::Describe; use crate::error::Error; use crate::executor::{Execute, Executor}; use crate::message::{BackendMessageFormat, Notification}; @@ -37,6 +36,7 @@ pub struct PgListener { } /// An asynchronous notification from Postgres. +#[derive(Clone)] pub struct PgNotification(Notification); impl PgListener { @@ -336,7 +336,7 @@ impl PgListener { /// /// This is helpful if you want to retrieve all buffered notifications and process them in batches. pub fn next_buffered(&mut self) -> Option { - if let Ok(Some(notification)) = self.buffer_rx.try_next() { + if let Ok(notification) = self.buffer_rx.try_recv() { Some(PgNotification(notification)) } else { None @@ -439,7 +439,11 @@ impl<'c> Executor<'c> for &'c mut PgListener { } #[doc(hidden)] - fn describe<'e>(self, query: SqlStr) -> BoxFuture<'e, Result, Error>> + #[cfg(feature = "offline")] + fn describe<'e>( + self, + query: SqlStr, + ) -> BoxFuture<'e, Result, Error>> where 'c: 'e, { diff --git a/sqlx-postgres/src/message/flush.rs b/sqlx-postgres/src/message/flush.rs index d1dfabbfaf..49f9b4f2a1 100644 --- a/sqlx-postgres/src/message/flush.rs +++ b/sqlx-postgres/src/message/flush.rs @@ -8,6 +8,7 @@ use std::num::Saturating; /// A Flush must be sent after any extended-query command except Sync, if the /// frontend wishes to examine the results of that command before issuing more commands. #[derive(Debug)] +#[expect(dead_code)] pub struct Flush; impl FrontendMessage for Flush { diff --git a/sqlx-postgres/src/message/mod.rs b/sqlx-postgres/src/message/mod.rs index e62f9bebb3..dedfe7c1bb 100644 --- a/sqlx-postgres/src/message/mod.rs +++ b/sqlx-postgres/src/message/mod.rs @@ -73,6 +73,7 @@ pub enum FrontendMessageFormat { CopyFail = b'f', Describe = b'D', Execute = b'E', + #[expect(dead_code)] Flush = b'H', Parse = b'P', /// This message format is polymorphic. It's used for: diff --git a/sqlx-postgres/src/message/notification.rs b/sqlx-postgres/src/message/notification.rs index 7bf029839c..8eafcc27f2 100644 --- a/sqlx-postgres/src/message/notification.rs +++ b/sqlx-postgres/src/message/notification.rs @@ -4,7 +4,7 @@ use crate::error::Error; use crate::io::BufExt; use crate::message::{BackendMessage, BackendMessageFormat}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Notification { pub(crate) process_id: u32, pub(crate) channel: Bytes, diff --git a/sqlx-postgres/src/message/password.rs b/sqlx-postgres/src/message/password.rs index 4eaaeb15af..ce2ef23786 100644 --- a/sqlx-postgres/src/message/password.rs +++ b/sqlx-postgres/src/message/password.rs @@ -2,7 +2,6 @@ use crate::io::BufMutExt; use crate::message::{FrontendMessage, FrontendMessageFormat}; use md5::{Digest, Md5}; use sqlx_core::Error; -use std::fmt::Write; use std::num::Saturating; #[derive(Debug)] @@ -65,16 +64,15 @@ impl FrontendMessage for Password<'_> { hasher.update(password); hasher.update(username); - let mut output = String::with_capacity(35); - - let _ = write!(output, "{:x}", hasher.finalize_reset()); + let mut output = hex::encode(hasher.finalize_reset()); hasher.update(&output); hasher.update(salt); - output.clear(); - - let _ = write!(output, "md5{:x}", hasher.finalize()); + // This is not really optimal but hopefully this is an uncommon code path. + // MD5 password hashing should really be phased out anyway. + output = hex::encode(hasher.finalize()); + output.insert_str(0, "md5"); buf.put_str_nul(&output); } diff --git a/sqlx-postgres/src/message/sasl.rs b/sqlx-postgres/src/message/sasl.rs index 9d393189bf..5593a9367a 100644 --- a/sqlx-postgres/src/message/sasl.rs +++ b/sqlx-postgres/src/message/sasl.rs @@ -41,7 +41,7 @@ impl FrontendMessage for SaslInitialResponse<'_> { let response_len = i32::try_from(self.response.len()).map_err(|_| { err_protocol!( - "SASL Initial Response length too long for protcol: {}", + "SASL Initial Response length too long for protocol: {}", self.response.len() ) })?; diff --git a/sqlx-postgres/src/migrate.rs b/sqlx-postgres/src/migrate.rs index 49104672c7..4afa2046c9 100644 --- a/sqlx-postgres/src/migrate.rs +++ b/sqlx-postgres/src/migrate.rs @@ -231,7 +231,7 @@ CREATE TABLE IF NOT EXISTS {table_name} ( if migration.no_tx { execute_migration(self, table_name, migration).await?; } else { - // Use a single transaction for the actual migration script and the essential bookeeping so we never + // Use a single transaction for the actual migration script and the essential bookkeeping so we never // execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966. // The `execution_time` however can only be measured for the whole transaction. This value _only_ exists for // data lineage and debugging reasons, so it is not super important if it is lost. So we initialize it to -1 @@ -288,6 +288,28 @@ CREATE TABLE IF NOT EXISTS {table_name} ( Ok(elapsed) }) } + + fn skip<'e>( + &'e mut self, + table_name: &'e str, + migration: &'e Migration, + ) -> BoxFuture<'e, Result<(), MigrateError>> { + Box::pin(async move { + // language=SQL + let _ = query(AssertSqlSafe(format!( + r#" + INSERT INTO {table_name} ( version, description, success, checksum, execution_time ) + VALUES ( 1,ドル 2,ドル TRUE, 3,ドル -1 ) + "# + ))) + .bind(migration.version) + .bind(&*migration.description) + .bind(&*migration.checksum) + .execute(self) + .await?; + Ok(()) + }) + } } async fn execute_migration( diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index efbc43989b..21e6628cae 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -64,7 +64,14 @@ impl PgConnectOptions { .or_else(|| var("PGHOST").ok()) .unwrap_or_else(|| default_host(port)); - let username = var("PGUSER").ok().unwrap_or_else(whoami::username); + let username = if let Ok(username) = var("PGUSER") { + username + } else if let Ok(username) = whoami::username() { + username + } else { + // keep the same fallback as previous version + "unknown".to_string() + }; let database = var("PGDATABASE").ok(); diff --git a/sqlx-postgres/src/options/pgpass.rs b/sqlx-postgres/src/options/pgpass.rs index bf16559548..b8e0e4847f 100644 --- a/sqlx-postgres/src/options/pgpass.rs +++ b/sqlx-postgres/src/options/pgpass.rs @@ -4,6 +4,12 @@ use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] +pub enum PGPassLineParseError { + #[error("Unexpected end of line")] + UnexpectedEOL, +} + /// try to load a password from the various pgpass file locations pub fn load_password( host: &str, @@ -21,14 +27,16 @@ pub fn load_password( } #[cfg(not(target_os = "windows"))] - let default_file = home::home_dir().map(|path| path.join(".pgpass")); + // home_dir fixed in 1.85 (rust-lang/rust#132515) and un-deprecated in 1.87 (rust-lang/rust#137327) + #[allow(deprecated)] + let default_file = std::env::home_dir().map(|path| path.join(".pgpass")); #[cfg(target_os = "windows")] let default_file = { use etcetera::BaseStrategy; etcetera::base_strategy::Windows::new() .ok() - .map(|basedirs| basedirs.data_dir().join("postgres").join("pgpass.conf")) + .map(|basedirs| basedirs.data_dir().join("postgresql").join("pgpass.conf")) }; load_password_from_file(default_file?, host, port, username, database) } @@ -112,8 +120,12 @@ fn load_password_from_reader( } else { // try to load password from line trim_newline(&mut line); - if let Some(password) = load_password_from_line(&line, host, port, username, database) { - return Some(password); + match load_password_from_line(&line, host, port, username, database) { + Err(err) => { + tracing::warn!(line = line, "Malformed line in pgpass file: {err}"); + } + Ok(Some(password)) => return Some(password), + Ok(None) => (), } } @@ -130,60 +142,72 @@ fn load_password_from_line( port: u16, username: &str, database: Option<&str>, -) -> Option { - let whole_line = line; - +) -> Result, PGPassLineParseError> { // Pgpass line ordering: hostname, port, database, username, password // See: https://www.postgresql.org/docs/9.3/libpq-pgpass.html - match line.trim_start().chars().next() { - None | Some('#') => None, - _ => { - matches_next_field(whole_line, &mut line, host)?; - matches_next_field(whole_line, &mut line, &port.to_string())?; - matches_next_field(whole_line, &mut line, database.unwrap_or_default())?; - matches_next_field(whole_line, &mut line, username)?; - Some(line.to_owned()) - } + + if let None | Some('#') = line.trim_end().chars().next() { + return Ok(None); + } + + let line_matches = matches_next_field(&mut line, host)? + && matches_next_field(&mut line, &port.to_string())? + && matches_next_field(&mut line, database.unwrap_or_default())? + && matches_next_field(&mut line, username)?; + + if !line_matches { + return Ok(None); } + + Ok(Some(unescape_password(line))) } -/// check if the next field matches the provided value -fn matches_next_field(whole_line: &str, line: &mut &str, value: &str) -> Option<()> { - let field = find_next_field(line); - match field { - Some(field) => { - if field == "*" || field == value { - Some(()) - } else { - None +/// Unescape occurrences of `:` and `\` in the given password’s. +fn unescape_password(password_escaped: &str) -> String { + let mut result = String::new(); + + let mut it = password_escaped.chars(); + while let Some(char) = it.next() { + if char != '\\' { + result.push(char); + } else if let Some(c) = it.next() { + if c != ':' && c != '\\' { + tracing::warn!("Superfluous escape in pgpass file"); } - } - None => { - tracing::warn!(line = whole_line, "Malformed line in pgpass file"); - None + result.push(c); + } else { + tracing::warn!("Superfluous escape at EOL in pgpass file"); } } + + result +} + +/// check if the next field matches the provided value +fn matches_next_field(line: &mut &str, value: &str) -> Result { + let field = find_next_field(line)?; + Ok(field == "*" || field == value) } /// extract the next value from a line in a pgpass file /// /// `line` will get updated to point behind the field and delimiter -fn find_next_field<'a>(line: &mut &'a str) -> Option> { +fn find_next_field<'a>(line: &mut &'a str) -> Result, PGPassLineParseError> { let mut escaping = false; let mut escaped_string = None; let mut last_added = 0; - let char_indicies = line.char_indices(); - for (idx, c) in char_indicies { + let char_indices = line.char_indices(); + for (idx, c) in char_indices { if c == ':' && !escaping { let (field, rest) = line.split_at(idx); *line = &rest[1..]; if let Some(mut escaped_string) = escaped_string { escaped_string += &field[last_added..]; - return Some(Cow::Owned(escaped_string)); + return Ok(Cow::Owned(escaped_string)); } else { - return Some(Cow::Borrowed(field)); + return Ok(Cow::Borrowed(field)); } } else if c == '\\' { let s = escaped_string.get_or_insert_with(String::new); @@ -197,66 +221,73 @@ fn find_next_field<'a>(line: &mut &'a str) -> Option> { escaping = !escaping; last_added = idx + 1; } else { + if escaping && c != '\\' && c != ':' { + tracing::warn!("Superfluous escape in in pgpass file"); + } escaping = false; } } - None + Err(PGPassLineParseError::UnexpectedEOL) } #[cfg(test)] mod tests { - use super::{find_next_field, load_password_from_line, load_password_from_reader}; + use super::*; use std::borrow::Cow; #[test] fn test_find_next_field() { - fn test_case<'a>(mut input: &'a str, result: Option>, rest: &str) { + fn test_case<'a>( + mut input: &'a str, + result: Result, PGPassLineParseError>, + rest: &str, + ) { assert_eq!(find_next_field(&mut input), result); assert_eq!(input, rest); } // normal field - test_case("foo:bar:baz", Some(Cow::Borrowed("foo")), "bar:baz"); + test_case("foo:bar:baz", Ok(Cow::Borrowed("foo")), "bar:baz"); // \ escaped test_case( "foo\\\\:bar:baz", - Some(Cow::Owned("foo\\".to_owned())), + Ok(Cow::Owned("foo\\".to_owned())), "bar:baz", ); // : escaped test_case( "foo\\::bar:baz", - Some(Cow::Owned("foo:".to_owned())), + Ok(Cow::Owned("foo:".to_owned())), "bar:baz", ); // unnecessary escape test_case( "foo\\a:bar:baz", - Some(Cow::Owned("fooa".to_owned())), + Ok(Cow::Owned("fooa".to_owned())), "bar:baz", ); // other text after escape test_case( "foo\\\\a:bar:baz", - Some(Cow::Owned("foo\\a".to_owned())), + Ok(Cow::Owned("foo\\a".to_owned())), "bar:baz", ); // double escape test_case( "foo\\\\\\\\a:bar:baz", - Some(Cow::Owned("foo\\\\a".to_owned())), + Ok(Cow::Owned("foo\\\\a".to_owned())), "bar:baz", ); // utf8 support - test_case("🦀:bar:baz", Some(Cow::Borrowed("🦀")), "bar:baz"); + test_case("🦀:bar:baz", Ok(Cow::Borrowed("🦀")), "bar:baz"); // missing delimiter (eof) - test_case("foo", None, "foo"); + test_case("foo", Err(PGPassLineParseError::UnexpectedEOL), "foo"); // missing delimiter after escape - test_case("foo\\:", None, "foo\\:"); + test_case("foo\\:", Err(PGPassLineParseError::UnexpectedEOL), "foo\\:"); // missing delimiter after unused trailing escape - test_case("foo\\", None, "foo\\"); + test_case("foo\\", Err(PGPassLineParseError::UnexpectedEOL), "foo\\"); } #[test] @@ -268,19 +299,19 @@ mod tests { "localhost", 5432, "foo", - Some("bar") + Some("bar"), ), - Some("baz".to_owned()) + Ok(Some("baz".to_owned())) ); // wildcard assert_eq!( load_password_from_line("*:5432:bar:foo:baz", "localhost", 5432, "foo", Some("bar")), - Some("baz".to_owned()) + Ok(Some("baz".to_owned())) ); // accept wildcard with missing db assert_eq!( load_password_from_line("localhost:5432:*:foo:baz", "localhost", 5432, "foo", None), - Some("baz".to_owned()) + Ok(Some("baz".to_owned())) ); // doesn't match @@ -292,7 +323,7 @@ mod tests { "foo", Some("bar") ), - None + Ok(None) ); // malformed entry assert_eq!( @@ -303,7 +334,32 @@ mod tests { "foo", Some("bar") ), - None + Err(PGPassLineParseError::UnexpectedEOL) + ); + // Password with trailing whitespace + assert_eq!( + load_password_from_line("*:*:*:*:baz ", "localhost", 5432, "foo", Some("bar")), + Ok(Some("baz ".to_owned())) + ); + // Password with escaped colon + assert_eq!( + load_password_from_line("*:*:*:*:ba\\:z", "localhost", 5432, "foo", Some("bar")), + Ok(Some("ba:z".to_owned())) + ); + // Password with escaped backslash + assert_eq!( + load_password_from_line("*:*:*:*:ba\\\\z", "localhost", 5432, "foo", Some("bar")), + Ok(Some("ba\\z".to_owned())) + ); + // Password with superfluous escape + assert_eq!( + load_password_from_line("*:*:*:*:ba\\z", "localhost", 5432, "foo", Some("bar")), + Ok(Some("baz".to_owned())) + ); + // Password with trailing escape + assert_eq!( + load_password_from_line("*:*:*:*:baz\\", "localhost", 5432, "foo", Some("bar")), + Ok(Some("baz".to_owned())) ); } diff --git a/sqlx-postgres/src/types/cube.rs b/sqlx-postgres/src/types/cube.rs index d7ddbd1723..df88e962e2 100644 --- a/sqlx-postgres/src/types/cube.rs +++ b/sqlx-postgres/src/types/cube.rs @@ -206,7 +206,7 @@ impl PgCube { } fn read_vec(bytes: &mut &[u8]) -> Result, String> { - if bytes.len() % BYTE_WIDTH != 0 { + if !bytes.len().is_multiple_of(BYTE_WIDTH) { return Err(format!( "data length not divisible by {BYTE_WIDTH}: {}", bytes.len() diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 4f99e7e983..c8bfddf8a3 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -131,7 +131,7 @@ impl PgPath { .into()); } - if bytes.len() % BYTE_WIDTH * 2 != 0 { + if !bytes.len().is_multiple_of(BYTE_WIDTH * 2) { return Err(format!( "data length not divisible by pairs of {BYTE_WIDTH}: {}", bytes.len() diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index e612b93499..7d352cf420 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -15,7 +15,7 @@ const BYTE_WIDTH: usize = mem::size_of::(); /// 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. +/// Polygons are represented by lists of points (the vertices 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: /// @@ -128,7 +128,7 @@ impl PgPolygon { .into()); } - if bytes.len() % BYTE_WIDTH * 2 != 0 { + if !bytes.len().is_multiple_of(BYTE_WIDTH * 2) { return Err(format!( "data length not divisible by pairs of {BYTE_WIDTH}: {}", bytes.len() diff --git a/sqlx-postgres/src/types/hstore.rs b/sqlx-postgres/src/types/hstore.rs index a03970fb30..2d504904da 100644 --- a/sqlx-postgres/src/types/hstore.rs +++ b/sqlx-postgres/src/types/hstore.rs @@ -12,7 +12,6 @@ use crate::{ types::Type, PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef, Postgres, }; -use serde::{Deserialize, Serialize}; use sqlx_core::bytes::Buf; /// Key-value support (`hstore`) for Postgres. @@ -88,7 +87,8 @@ use sqlx_core::bytes::Buf; /// } /// ``` /// -#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))] pub struct PgHstore(pub BTreeMap>); impl Deref for PgHstore { diff --git a/sqlx-postgres/src/types/interval.rs b/sqlx-postgres/src/types/interval.rs index af4810323d..0266ad4b69 100644 --- a/sqlx-postgres/src/types/interval.rs +++ b/sqlx-postgres/src/types/interval.rs @@ -100,7 +100,7 @@ impl TryFrom for PgInterval { /// This returns an error if there is a loss of precision using nanoseconds or if there is a /// microsecond overflow. fn try_from(value: std::time::Duration) -> Result { - if value.as_nanos() % 1000 != 0 { + if !value.as_nanos().is_multiple_of(1000) { return Err("PostgreSQL `INTERVAL` does not support nanoseconds precision".into()); } diff --git a/sqlx-postgres/src/types/json.rs b/sqlx-postgres/src/types/json.rs index 32f886c781..d575c1df03 100644 --- a/sqlx-postgres/src/types/json.rs +++ b/sqlx-postgres/src/types/json.rs @@ -61,7 +61,7 @@ where fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { // we have a tiny amount of dynamic behavior depending if we are resolved to be JSON // instead of JSONB - buf.patch(|buf, ty: &PgTypeInfo| { + buf.patch_with(|buf, ty: &PgTypeInfo| { if *ty == PgTypeInfo::JSON || *ty == PgTypeInfo::JSON_ARRAY { buf[0] = b' '; } @@ -85,11 +85,12 @@ where let mut buf = value.as_bytes()?; if value.format() == PgValueFormat::Binary && value.type_info == PgTypeInfo::JSONB { - assert_eq!( - buf[0], 1, - "unsupported JSONB format version {}; please open an issue", - buf[0] - ); + // Check JSONB version byte - PostgreSQL currently only supports version 1 + if buf[0] != 1 { + return Err( + format!("unsupported JSONB format version {} (expected 1)", buf[0]).into(), + ); + } buf = &buf[1..]; } diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 0faefbb482..f4ff7ef0e4 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -211,7 +211,9 @@ use crate::type_info::PgTypeKind; use crate::{PgTypeInfo, Postgres}; -pub(crate) use sqlx_core::types::{Json, Type}; +#[cfg(feature = "json")] +pub(crate) use sqlx_core::types::Json; +pub(crate) use sqlx_core::types::Type; mod array; mod bool; @@ -221,10 +223,10 @@ mod float; mod hstore; mod int; mod interval; +#[cfg(feature = "json")] +mod json; mod lquery; mod ltree; -// Not behind a Cargo feature because we require JSON in the driver implementation. -mod json; mod money; mod oid; mod range; diff --git a/sqlx-postgres/src/types/oid.rs b/sqlx-postgres/src/types/oid.rs index 04c5ef837a..9ef5dc970b 100644 --- a/sqlx-postgres/src/types/oid.rs +++ b/sqlx-postgres/src/types/oid.rs @@ -1,5 +1,4 @@ use byteorder::{BigEndian, ByteOrder}; -use serde::{de::Deserializer, ser::Serializer, Deserialize, Serialize}; use crate::decode::Decode; use crate::encode::{Encode, IsNull}; @@ -46,19 +45,21 @@ impl Decode<'_, Postgres> for Oid { } } -impl Serialize for Oid { +#[cfg(feature = "offline")] +impl serde::Serialize for Oid { fn serialize(&self, serializer: S) -> Result where - S: Serializer, + S: serde::Serializer, { self.0.serialize(serializer) } } -impl<'de> Deserialize<'de> for Oid { +#[cfg(feature = "offline")] +impl<'de> serde::Deserialize<'de> for Oid { fn deserialize(deserializer: D) -> Result where - D: Deserializer<'de>, + D: serde::Deserializer<'de>, { u32::deserialize(deserializer).map(Self) } diff --git a/sqlx-postgres/src/types/record.rs b/sqlx-postgres/src/types/record.rs index a5410caadd..2ad2480066 100644 --- a/sqlx-postgres/src/types/record.rs +++ b/sqlx-postgres/src/types/record.rs @@ -41,13 +41,10 @@ impl<'a> PgRecordEncoder<'a> { { let ty = value.produces().unwrap_or_else(T::type_info); - match ty.0 { - // push a hole for this type ID - // to be filled in on query execution - PgType::DeclareWithName(name) => self.buf.patch_type_by_name(&name), - PgType::DeclareArrayOf(array) => self.buf.patch_array_type(array), - // write type id - pg_type => self.buf.extend(&pg_type.oid().0.to_be_bytes()), + if let Some(oid) = ty.oid() { + self.buf.extend(oid.0.to_be_bytes()) + } else { + self.buf.push_hole(ty); } self.buf.encode(value)?; diff --git a/sqlx-sqlite/Cargo.toml b/sqlx-sqlite/Cargo.toml index 4508e19ff6..ffac1b9d14 100644 --- a/sqlx-sqlite/Cargo.toml +++ b/sqlx-sqlite/Cargo.toml @@ -56,7 +56,7 @@ _unstable-docs = [ [dependencies.libsqlite3-sys] # See `sqlx-sqlite/src/lib.rs` for details. -version = ">=0.30.0, <0.36.0" +version = ">=0.30.1, <0.38.0" default-features = false features = [ "pkg-config", @@ -64,37 +64,42 @@ features = [ ] [dependencies] -futures-core = { version = "0.3.19", default-features = false } -futures-channel = { version = "0.3.19", default-features = false, features = ["sink", "alloc", "std"] } +futures-core = { version = "0.3.32", default-features = false } +futures-channel = { version = "0.3.32", default-features = false, features = ["sink", "alloc", "std"] } # used by the SQLite worker thread to block on the async mutex that locks the database handle -futures-executor = { version = "0.3.19" } +futures-executor = { version = "0.3.32" } futures-intrusive = "0.5.0" -futures-util = { version = "0.3.19", default-features = false, features = ["alloc", "sink"] } +futures-util = { version = "0.3.32", default-features = false, features = ["alloc", "sink"] } chrono = { workspace = true, optional = true } time = { workspace = true, optional = true } uuid = { workspace = true, optional = true } url = { version = "2.2.2" } -percent-encoding = "2.1.0" -serde_urlencoded = "0.7" +percent-encoding = "2.3.0" +form_urlencoded = "1.2.2" -flume = { version = "0.11.0", default-features = false, features = ["async"] } +flume = { version = "0.12.0", default-features = false, features = ["async"] } 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 } +thiserror.workspace = true + +serde = { version = "1.0.219", features = ["derive"], optional = true } +regex = { version = "1.6.0", optional = true } [dependencies.sqlx-core] workspace = true -[dev-dependencies] -sqlx = { workspace = true, features = ["macros", "runtime-tokio", "tls-none", "sqlite"] } +[dev-dependencies.sqlx] +# FIXME: https://github.com/rust-lang/cargo/issues/15622 +# workspace = true +path = ".." +default-features = false +features = ["macros", "runtime-tokio", "tls-none", "sqlite"] [lints] workspace = true diff --git a/sqlx-sqlite/src/any.rs b/sqlx-sqlite/src/any.rs index 83b141decd..b3a5af5543 100644 --- a/sqlx-sqlite/src/any.rs +++ b/sqlx-sqlite/src/any.rs @@ -7,7 +7,7 @@ use futures_core::stream::BoxStream; use futures_util::{FutureExt, StreamExt, TryFutureExt, TryStreamExt}; use sqlx_core::any::{ - Any, AnyArguments, AnyColumn, AnyConnectOptions, AnyConnectionBackend, AnyQueryResult, AnyRow, + AnyArguments, AnyColumn, AnyConnectOptions, AnyConnectionBackend, AnyQueryResult, AnyRow, AnyStatement, AnyTypeInfo, AnyTypeInfoKind, AnyValueKind, }; use sqlx_core::sql_str::SqlStr; @@ -16,7 +16,6 @@ use crate::arguments::SqliteArgumentsBuffer; use crate::type_info::DataType; use sqlx_core::connection::{ConnectOptions, Connection}; use sqlx_core::database::Database; -use sqlx_core::describe::Describe; use sqlx_core::executor::Executor; use sqlx_core::transaction::TransactionManager; use std::pin::pin; @@ -140,7 +139,11 @@ impl AnyConnectionBackend for SqliteConnection { }) } - fn describe(&mut self, sql: SqlStr) -> BoxFuture<'_, sqlx_core::Result>> { + #[cfg(feature = "offline")] + fn describe( + &mut self, + sql: SqlStr, + ) -> BoxFuture<'_, sqlx_core::Result>> { Box::pin(async move { Executor::describe(self, sql).await?.try_into_any() }) } } diff --git a/sqlx-sqlite/src/connection/collation.rs b/sqlx-sqlite/src/connection/collation.rs index e7422138bc..229e780adf 100644 --- a/sqlx-sqlite/src/connection/collation.rs +++ b/sqlx-sqlite/src/connection/collation.rs @@ -3,7 +3,6 @@ use std::ffi::CString; use std::fmt::{self, Debug, Formatter}; use std::os::raw::{c_int, c_void}; use std::slice; -use std::str::from_utf8_unchecked; use std::sync::Arc; use libsqlite3_sys::{sqlite3_create_collation_v2, SQLITE_OK, SQLITE_UTF8}; @@ -137,15 +136,19 @@ where let right_len = usize::try_from(right_len) .unwrap_or_else(|_| panic!("right_len out of range: {right_len}")); + // SQLite explicitly documents that invalid UTF-8 may be passed into + // application-defined collating sequences. The safe `Fn(&str, &str)` + // signature exposed to users must never observe invalid UTF-8, so + // lossily coerce the raw bytes here. let s1 = { let c_slice = slice::from_raw_parts(left_ptr as *const u8, left_len); - from_utf8_unchecked(c_slice) + String::from_utf8_lossy(c_slice) }; let s2 = { let c_slice = slice::from_raw_parts(right_ptr as *const u8, right_len); - from_utf8_unchecked(c_slice) + String::from_utf8_lossy(c_slice) }; - let t = (*boxed_f)(s1, s2); + let t = (*boxed_f)(&s1, &s2); match t { Ordering::Less => -1, diff --git a/sqlx-sqlite/src/connection/establish.rs b/sqlx-sqlite/src/connection/establish.rs index d811275409..cedf752e6a 100644 --- a/sqlx-sqlite/src/connection/establish.rs +++ b/sqlx-sqlite/src/connection/establish.rs @@ -93,10 +93,15 @@ impl EstablishParams { if !query_params.is_empty() { filename = format!( - "file:{}?{}", + "file:{}?", percent_encoding::percent_encode(filename.as_bytes(), NON_ALPHANUMERIC), - serde_urlencoded::to_string(&query_params).unwrap() ); + + // Suffix serializer automatically handles `&` separators for us. + let filename_len = filename.len(); + filename = form_urlencoded::Serializer::for_suffix(filename, filename_len) + .extend_pairs(query_params) + .finish(); } let filename = CString::new(filename).map_err(|_| { diff --git a/sqlx-sqlite/src/connection/execute.rs b/sqlx-sqlite/src/connection/execute.rs index 733a1abbe6..f26a540510 100644 --- a/sqlx-sqlite/src/connection/execute.rs +++ b/sqlx-sqlite/src/connection/execute.rs @@ -108,7 +108,15 @@ impl Iterator for ExecuteIter<'_> { Ok(false) => { let last_insert_rowid = self.handle.last_insert_rowid(); - let changes = statement.handle.changes(); + // `sqlite3_changes()` returns the row count for the most recently completed + // INSERT/UPDATE/DELETE on the connection, not necessarily this statement. + // For read-only statements (SELECT, BEGIN, COMMIT, etc.) we must report 0. + // See https://sqlite.org/c3ref/changes.html + let changes = if statement.handle.read_only() { + 0 + } else { + statement.handle.changes() + }; self.logger.increase_rows_affected(changes); let done = SqliteQueryResult { diff --git a/sqlx-sqlite/src/connection/executor.rs b/sqlx-sqlite/src/connection/executor.rs index 0bc88cf14e..a62d3349a5 100644 --- a/sqlx-sqlite/src/connection/executor.rs +++ b/sqlx-sqlite/src/connection/executor.rs @@ -4,7 +4,6 @@ use crate::{ use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; use futures_util::{stream, FutureExt, StreamExt, TryFutureExt, TryStreamExt}; -use sqlx_core::describe::Describe; use sqlx_core::error::Error; use sqlx_core::executor::{Execute, Executor}; use sqlx_core::sql_str::SqlStr; @@ -89,7 +88,11 @@ impl<'c> Executor<'c> for &'c mut SqliteConnection { } #[doc(hidden)] - fn describe<'e>(self, sql: SqlStr) -> BoxFuture<'e, Result, Error>> + #[cfg(feature = "offline")] + fn describe<'e>( + self, + sql: SqlStr, + ) -> BoxFuture<'e, Result, Error>> where 'c: 'e, { diff --git a/sqlx-sqlite/src/connection/explain.rs b/sqlx-sqlite/src/connection/explain.rs index edd65ece49..550f21557e 100644 --- a/sqlx-sqlite/src/connection/explain.rs +++ b/sqlx-sqlite/src/connection/explain.rs @@ -43,6 +43,7 @@ const OP_IDX_GE: &str = "IdxGE"; const OP_IDX_GT: &str = "IdxGT"; const OP_IDX_LE: &str = "IdxLE"; const OP_IDX_LT: &str = "IdxLT"; +const OP_IDX_ROWID: &str = "IdxRowid"; const OP_IF: &str = "If"; const OP_IF_NO_HOPE: &str = "IfNoHope"; const OP_IF_NOT: &str = "IfNot"; @@ -361,7 +362,9 @@ fn opcode_to_type(op: &str) -> DataType { OP_REAL => DataType::Float, OP_BLOB => DataType::Blob, OP_AND | OP_OR => DataType::Bool, - OP_NEWROWID | OP_ROWID | OP_COUNT | OP_INT64 | OP_INTEGER => DataType::Integer, + OP_NEWROWID | OP_IDX_ROWID | OP_ROWID | OP_COUNT | OP_INT64 | OP_INTEGER => { + DataType::Integer + } OP_STRING8 => DataType::Text, OP_COLUMN | _ => DataType::Null, } @@ -676,7 +679,7 @@ pub(super) fn explain( //nobranch if maybe not null let might_not_branch = match state.mem.r.get(&p1) { - Some(r_p1) => !matches!(r_p1.map_to_datatype(), DataType::Null), + Some(r_p1) => r_p1.map_to_nullable() != Some(false), _ => false, }; @@ -1379,7 +1382,8 @@ pub(super) fn explain( state.mem.r.insert(p2, RegDataType::Int(p1)); } - OP_BLOB | OP_COUNT | OP_REAL | OP_STRING8 | OP_ROWID | OP_NEWROWID => { + OP_BLOB | OP_COUNT | OP_REAL | OP_STRING8 | OP_ROWID | OP_IDX_ROWID + | OP_NEWROWID => { // r[p2] = state.mem.r.insert( p2, @@ -1778,3 +1782,63 @@ fn test_root_block_columns_has_types() { ); } } + +#[test] +fn test_explain() { + use crate::SqliteConnectOptions; + use std::str::FromStr; + let conn_options = SqliteConnectOptions::from_str("sqlite::memory:").unwrap(); + let mut conn = super::EstablishParams::from_options(&conn_options) + .unwrap() + .establish() + .unwrap(); + + assert!(execute::iter( + &mut conn, + r"CREATE TABLE an_alias(a INTEGER PRIMARY KEY);", + None, + false + ) + .unwrap() + .next() + .is_some()); + + assert!(execute::iter( + &mut conn, + r"CREATE TABLE not_an_alias(a INT PRIMARY KEY);", + None, + false + ) + .unwrap() + .next() + .is_some()); + + assert!( + if let Ok((ty, nullable)) = explain(&mut conn, "SELECT * FROM an_alias") { + ty == [SqliteTypeInfo(DataType::Integer)] && nullable == [Some(false)] + } else { + false + } + ); + assert!( + if let Ok((ty, nullable)) = explain(&mut conn, "SELECT * FROM not_an_alias") { + ty == [SqliteTypeInfo(DataType::Integer)] && nullable == [Some(true)] + } else { + false + } + ); + assert!( + if let Ok((ty, nullable)) = explain(&mut conn, "SELECT rowid FROM an_alias") { + ty == [SqliteTypeInfo(DataType::Integer)] && nullable == [Some(false)] + } else { + false + } + ); + assert!( + if let Ok((ty, nullable)) = explain(&mut conn, "SELECT rowid FROM not_an_alias") { + ty == [SqliteTypeInfo(DataType::Integer)] && nullable == [Some(false)] + } else { + false + } + ); +} diff --git a/sqlx-sqlite/src/connection/handle.rs b/sqlx-sqlite/src/connection/handle.rs index 7df3b1b717..e89179b30e 100644 --- a/sqlx-sqlite/src/connection/handle.rs +++ b/sqlx-sqlite/src/connection/handle.rs @@ -71,10 +71,14 @@ impl ConnectionHandle { &mut self, call: impl FnOnce(*mut sqlite3) -> c_int, ) -> Result<(), SqliteError> { - if call(self.as_ptr()) == SQLITE_OK { + let res = call(self.as_ptr()); + + if res == SQLITE_OK { Ok(()) } else { - Err(self.expect_error()) + Err(self + .last_error() + .unwrap_or_else(|| SqliteError::from_code(res))) } } diff --git a/sqlx-sqlite/src/connection/intmap.rs b/sqlx-sqlite/src/connection/intmap.rs index fb0553fb30..f8308067cc 100644 --- a/sqlx-sqlite/src/connection/intmap.rs +++ b/sqlx-sqlite/src/connection/intmap.rs @@ -65,7 +65,7 @@ impl IntMap { pub(crate) fn insert(&mut self, idx: i64, value: V) -> Option { let idx: usize = self.expand(idx); - std::mem::replace(&mut self.0[idx], Some(value)) + self.0[idx].replace(value) } pub(crate) fn remove(&mut self, idx: &i64) -> Option { diff --git a/sqlx-sqlite/src/connection/worker.rs b/sqlx-sqlite/src/connection/worker.rs index 11c8778cc9..2585dc1312 100644 --- a/sqlx-sqlite/src/connection/worker.rs +++ b/sqlx-sqlite/src/connection/worker.rs @@ -8,18 +8,16 @@ use futures_intrusive::sync::{Mutex, MutexGuard}; use sqlx_core::sql_str::SqlStr; use tracing::span::Span; -use sqlx_core::describe::Describe; use sqlx_core::error::Error; use sqlx_core::transaction::{ begin_ansi_transaction_sql, commit_ansi_transaction_sql, rollback_ansi_transaction_sql, }; use sqlx_core::Either; -use crate::connection::describe::describe; use crate::connection::establish::EstablishParams; use crate::connection::execute; use crate::connection::ConnectionState; -use crate::{Sqlite, SqliteArguments, SqliteQueryResult, SqliteRow, SqliteStatement}; +use crate::{SqliteArguments, SqliteQueryResult, SqliteRow, SqliteStatement}; #[cfg(feature = "deserialize")] use crate::connection::deserialize::{deserialize, serialize, SchemaName, SqliteOwnedBuf}; @@ -57,9 +55,10 @@ enum Command { query: SqlStr, tx: oneshot::Sender>, }, + #[cfg(feature = "offline")] Describe { query: SqlStr, - tx: oneshot::Sender, Error>>, + tx: oneshot::Sender, Error>>, }, Execute { query: SqlStr, @@ -157,8 +156,9 @@ impl ConnectionWorker { &shared.cached_statements_size, ); } + #[cfg(feature = "offline")] Command::Describe { query, tx } => { - tx.send(describe(&mut conn, query)).ok(); + tx.send(crate::connection::describe::describe(&mut conn, query)).ok(); } Command::Execute { query, @@ -352,7 +352,11 @@ impl ConnectionWorker { .await? } - pub(crate) async fn describe(&mut self, query: SqlStr) -> Result, Error> { + #[cfg(feature = "offline")] + pub(crate) async fn describe( + &mut self, + query: SqlStr, + ) -> Result, Error> { self.oneshot_cmd(|tx| Command::Describe { query, tx }) .await? } diff --git a/sqlx-sqlite/src/error.rs b/sqlx-sqlite/src/error.rs index b4373d7a07..5510e25d97 100644 --- a/sqlx-sqlite/src/error.rs +++ b/sqlx-sqlite/src/error.rs @@ -39,7 +39,7 @@ impl SqliteError { let msg = sqlite3_errmsg(handle); debug_assert!(!msg.is_null()); - str::from_utf8_unchecked(CStr::from_ptr(msg).to_bytes()).to_owned() + String::from_utf8_lossy(CStr::from_ptr(msg).to_bytes()).into_owned() }; Some(Self { diff --git a/sqlx-sqlite/src/migrate.rs b/sqlx-sqlite/src/migrate.rs index dd9611b873..5864023891 100644 --- a/sqlx-sqlite/src/migrate.rs +++ b/sqlx-sqlite/src/migrate.rs @@ -221,6 +221,29 @@ CREATE TABLE IF NOT EXISTS {table_name} ( Ok(elapsed) }) } + + fn skip<'e>( + &'e mut self, + table_name: &'e str, + migration: &'e Migration, + ) -> BoxFuture<'e, Result<(), MigrateError>> { + Box::pin(async move { + // language=SQLite + let _ = query(AssertSqlSafe(format!( + r#" + INSERT INTO {table_name} ( version, description, success, checksum, execution_time ) + VALUES ( ?1, ?2, TRUE, ?3, -1 ) + "# + ))) + .bind(migration.version) + .bind(&*migration.description) + .bind(&*migration.checksum) + .execute(self) + .await?; + + Ok(()) + }) + } } async fn execute_migration( diff --git a/sqlx-sqlite/src/options/mod.rs b/sqlx-sqlite/src/options/mod.rs index b2849f243c..0cafc4d08c 100644 --- a/sqlx-sqlite/src/options/mod.rs +++ b/sqlx-sqlite/src/options/mod.rs @@ -511,7 +511,7 @@ impl SqliteConnectOptions { /// .extension("vsv") /// .extension("mod_spatialite"); /// } - /// + /// /// # Ok(options) /// # } /// ``` @@ -646,7 +646,15 @@ impl SqliteConnectOptions { #[cfg(feature = "load-extension")] for extension in &config.unsafe_load_extensions { // SAFETY: the documentation warns the user about loading extensions - self = unsafe { self.extension(extension.clone()) }; + match extension { + config::drivers::SqliteExtension::Path(path) => { + self = unsafe { self.extension(path.clone()) } + } + config::drivers::SqliteExtension::PathWithEntrypoint { path, entrypoint } => { + self = + unsafe { self.extension_with_entrypoint(path.clone(), entrypoint.clone()) } + } + } } #[cfg(not(feature = "load-extension"))] diff --git a/sqlx-sqlite/src/regexp.rs b/sqlx-sqlite/src/regexp.rs index eb14fffc77..735200e9be 100644 --- a/sqlx-sqlite/src/regexp.rs +++ b/sqlx-sqlite/src/regexp.rs @@ -68,12 +68,12 @@ unsafe extern "C" fn sqlite3_regexp_func( } // arg0: Regex - let Some(regex) = get_regex_from_arg(ctx, *args.offset(0), 0) else { + let Some(regex) = get_regex_from_arg(ctx, *args, 0) else { return; }; // arg1: value - let Some(value) = get_text_from_arg(ctx, *args.offset(1)) else { + let Some(value) = get_text_from_arg(ctx, *args.add(1)) else { return; }; @@ -136,8 +136,11 @@ unsafe fn get_regex_from_arg( Some(regex) } -/// Get a text reference of the value of `arg`. If this value is not a string value, an error is printed and `None` is -/// returned. +/// Get a text reference of the value of `arg`. Returns `None` for NULL values. +/// +/// For non-NULL values, `sqlite3_value_text()` is called directly, which lets SQLite +/// coerce INTEGER, REAL, and BLOB values to their text representation. This matches +/// the coercion behavior documented at . /// /// The returned `&str` is valid for lifetime `'a` which can be determined by the caller. This lifetime should **not** /// outlive `ctx`. @@ -146,20 +149,19 @@ unsafe fn get_text_from_arg<'a>( arg: *mut ffi::sqlite3_value, ) -> Option<&'a str> { let ty = ffi::sqlite3_value_type(arg); - if ty == ffi::SQLITE_TEXT { - let ptr = ffi::sqlite3_value_text(arg); - let len = ffi::sqlite3_value_bytes(arg); - let slice = std::slice::from_raw_parts(ptr.cast(), len as usize); - match std::str::from_utf8(slice) { - Ok(result) => Some(result), - Err(e) => { - log::error!("Incoming text is not valid UTF8: {e:?}"); - ffi::sqlite3_result_error_code(ctx, ffi::SQLITE_CONSTRAINT_FUNCTION); - None - } + if ty == ffi::SQLITE_NULL { + return None; + } + let ptr = ffi::sqlite3_value_text(arg); + let len = ffi::sqlite3_value_bytes(arg); + let slice = std::slice::from_raw_parts(ptr.cast(), len as usize); + match std::str::from_utf8(slice) { + Ok(result) => Some(result), + Err(e) => { + log::error!("Incoming text is not valid UTF8: {e:?}"); + ffi::sqlite3_result_error_code(ctx, ffi::SQLITE_CONSTRAINT_FUNCTION); + None } - } else { - None } } @@ -222,6 +224,52 @@ mod tests { assert!(result.is_empty()); } + #[sqlx::test] + async fn test_regexp_coerces_non_text_values() { + let mut conn = crate::SqliteConnectOptions::from_str("sqlite://:memory:") + .unwrap() + .with_regexp() + .connect() + .await + .unwrap(); + + // INTEGER coercion + let result: Option = sqlx::query_scalar("SELECT 123 REGEXP '23'") + .fetch_one(&mut conn) + .await + .unwrap(); + assert_eq!(result, Some(1)); + + // REAL coercion + let result: Option = sqlx::query_scalar("SELECT 12.5 REGEXP '12\\.5'") + .fetch_one(&mut conn) + .await + .unwrap(); + assert_eq!(result, Some(1)); + + // INTEGER column + sqlx::query("CREATE TABLE int_test (x INTEGER NOT NULL)") + .execute(&mut conn) + .await + .unwrap(); + sqlx::query("INSERT INTO int_test VALUES (123), (45)") + .execute(&mut conn) + .await + .unwrap(); + let rows: Vec = sqlx::query_scalar("SELECT x FROM int_test WHERE x REGEXP '23'") + .fetch_all(&mut conn) + .await + .unwrap(); + assert_eq!(rows, vec![123]); + + // NULL should return NULL, not match + let result: Option = sqlx::query_scalar("SELECT NULL REGEXP '.*'") + .fetch_one(&mut conn) + .await + .unwrap(); + assert_eq!(result, None); + } + #[sqlx::test] async fn test_invalid_regexp_should_fail() { let mut conn = test_db().await; diff --git a/sqlx-sqlite/src/statement/handle.rs b/sqlx-sqlite/src/statement/handle.rs index 7985ff9d36..c78ce98414 100644 --- a/sqlx-sqlite/src/statement/handle.rs +++ b/sqlx-sqlite/src/statement/handle.rs @@ -21,7 +21,7 @@ use std::os::raw::{c_char, c_int}; use std::ptr; use std::ptr::NonNull; use std::slice::from_raw_parts; -use std::str::{from_utf8, from_utf8_unchecked}; +use std::str::from_utf8; use std::sync::Arc; #[derive(Debug)] @@ -77,7 +77,8 @@ impl StatementHandle { let raw = sqlite3_sql(self.0.as_ptr()); debug_assert!(!raw.is_null()); - from_utf8_unchecked(CStr::from_ptr(raw).to_bytes()) + from_utf8(CStr::from_ptr(raw).to_bytes()) + .expect("sqlite3_sql() returned non-UTF-8 string") } } @@ -107,7 +108,8 @@ impl StatementHandle { let name = sqlite3_column_name(self.0.as_ptr(), check_col_idx!(index)); debug_assert!(!name.is_null()); - from_utf8_unchecked(CStr::from_ptr(name).to_bytes()) + from_utf8(CStr::from_ptr(name).to_bytes()) + .expect("sqlite3_column_name() returned non-UTF-8 column name") } } @@ -139,7 +141,10 @@ impl StatementHandle { let db_name = sqlite3_column_database_name(self.0.as_ptr(), check_col_idx!(index)); if !db_name.is_null() { - Some(from_utf8_unchecked(CStr::from_ptr(db_name).to_bytes())) + Some( + from_utf8(CStr::from_ptr(db_name).to_bytes()) + .expect("sqlite3_column_database_name() returned non-UTF-8 string"), + ) } else { None } @@ -151,7 +156,10 @@ impl StatementHandle { let table_name = sqlite3_column_table_name(self.0.as_ptr(), check_col_idx!(index)); if !table_name.is_null() { - Some(from_utf8_unchecked(CStr::from_ptr(table_name).to_bytes())) + Some( + from_utf8(CStr::from_ptr(table_name).to_bytes()) + .expect("sqlite3_column_table_name() returned non-UTF-8 string"), + ) } else { None } @@ -163,7 +171,10 @@ impl StatementHandle { let origin_name = sqlite3_column_origin_name(self.0.as_ptr(), check_col_idx!(index)); if !origin_name.is_null() { - Some(from_utf8_unchecked(CStr::from_ptr(origin_name).to_bytes())) + Some( + from_utf8(CStr::from_ptr(origin_name).to_bytes()) + .expect("sqlite3_column_origin_name() returned non-UTF-8 string"), + ) } else { None } @@ -191,17 +202,23 @@ impl StatementHandle { return None; } - let decl = from_utf8_unchecked(CStr::from_ptr(decl).to_bytes()); + let decl = from_utf8(CStr::from_ptr(decl).to_bytes()) + .expect("sqlite3_column_decltype() returned non-UTF-8 string"); let ty: DataType = decl.parse().ok()?; Some(SqliteTypeInfo(ty)) } } + /// Use sqlite3_column_metadata to determine if a specific column is nullable. + /// + /// Returns None in the case of INTEGER PRIMARY KEYs + /// This is because this column is an alias to rowid if the table does not use a compound + /// primary key. In this case the row is not nullable, and the output of + /// sqlite3_column_metadata may be incorrect. pub(crate) fn column_nullable(&self, index: usize) -> Result, Error> { unsafe { let index = check_col_idx!(index); - // https://sqlite.org/c3ref/column_database_name.html // // ### Note @@ -212,12 +229,13 @@ impl StatementHandle { let db_name = sqlite3_column_database_name(self.0.as_ptr(), index); let table_name = sqlite3_column_table_name(self.0.as_ptr(), index); let origin_name = sqlite3_column_origin_name(self.0.as_ptr(), index); - if db_name.is_null() || table_name.is_null() || origin_name.is_null() { return Ok(None); } let mut not_null: c_int = 0; + let mut datatype: *const c_char = ptr::null(); + let mut primary_key: c_int = 0; // https://sqlite.org/c3ref/table_column_metadata.html let status = sqlite3_table_column_metadata( @@ -225,11 +243,11 @@ impl StatementHandle { db_name, table_name, origin_name, + &mut datatype, // function docs state to provide NULL for return values you don't care about ptr::null_mut(), - ptr::null_mut(), &mut not_null, - ptr::null_mut(), + &mut primary_key, ptr::null_mut(), ); @@ -245,7 +263,19 @@ impl StatementHandle { return Err(SqliteError::new(self.db_handle()).into()); } - Ok(Some(not_null == 0)) + let datatype = CStr::from_ptr(datatype); + + Ok( + if primary_key != 0 + && datatype + .to_bytes() + .eq_ignore_ascii_case("integer".as_bytes()) + { + None + } else { + Some(not_null == 0) + }, + ) } } @@ -267,7 +297,10 @@ impl StatementHandle { return None; } - Some(from_utf8_unchecked(CStr::from_ptr(name).to_bytes())) + Some( + from_utf8(CStr::from_ptr(name).to_bytes()) + .expect("sqlite3_bind_parameter_name() returned non-UTF-8 string"), + ) } } diff --git a/sqlx-test/Cargo.toml b/sqlx-test/Cargo.toml index 32a341adcb..47a17ca0fb 100644 --- a/sqlx-test/Cargo.toml +++ b/sqlx-test/Cargo.toml @@ -8,8 +8,8 @@ rust-version.workspace = true [dependencies] sqlx = { default-features = false, path = ".." } env_logger = "0.11" -dotenvy = "0.15.0" -anyhow = "1.0.26" +dotenvy = "0.15.7" +anyhow = "1.0.58" [lints] workspace = true diff --git a/tests/README.md b/tests/README.md index bc2dc2327c..019f4c5d4d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -5,14 +5,55 @@ SQLx uses docker to run many compatible database systems for integration testing $ docker run hello-world -Start the databases with `docker-compose` before running tests: +Start the databases with `docker compose` (or `docker-compose`) before running tests: - $ docker-compose up + $ docker compose up -d -Run all tests against all supported databases using: +Run clippy for the check matrix: + + $ ./x.py --clippy + +This runs only the check/clippy matrix and skips unit and integration tests. + +For the full test matrix, run all tests against all supported databases using: $ ./x.py +### Limiting the Matrix + +The full matrix (runtimes, TLS backends, and DB versions) is large. Use the filters in `x.py` to keep runs small. + +List all targets (tags): + + $ ./x.py --list-targets + +Run by prefix (uses `tag.startswith`): + + $ ./x.py --target sqlite_tokio + $ ./x.py --target postgres_17_tokio + $ ./x.py --target mysql_8 + $ ./x.py --target mariadb_10_11 + +Note: integration tags do not include TLS, so a target like `postgres_17_tokio` +still runs all `TLS_VARIANTS`. To limit TLS locally, edit `TLS_VARIANTS` (and +`CHECK_TLS` for the check phase). + +Run exactly one target: + + $ ./x.py --target-exact mysql_8_client_ssl_no_password_tokio + +Run only one integration test binary: + + $ ./x.py --test sqlite + $ ./x.py --test any + +Pass extra args to cargo: + + $ ./x.py -- --nocapture + +To shrink the matrix globally, edit the lists at the top of `tests/x.py`: +`CHECK_TLS`, `TLS_VARIANTS`, `POSTGRES_VERSIONS`, `MYSQL_VERSIONS`, `MARIADB_VERSIONS`. + If you see test failures, or want to run a more specific set of tests against a specific database, you can specify both the features to be tests and the DATABASE_URL. e.g. - $ DATABASE_URL=mysql://root:password@127.0.0.1:49183/sqlx cargo test --no-default-features --features macros,offline,any,all-types,mysql,runtime-async-std-native-tls + $ DATABASE_URL=mysql://root:password@127.0.0.1:49183/sqlx cargo test --no-default-features --features macros,offline,any,all-types,mysql,runtime-async-std-native-tls \ No newline at end of file diff --git a/tests/any/any.rs b/tests/any/any.rs index 71c561cadb..2c57a237ec 100644 --- a/tests/any/any.rs +++ b/tests/any/any.rs @@ -155,12 +155,14 @@ async fn it_can_query_by_string_args() -> sqlx::Result<()> { let ref tuple = ("Hello, world!".to_string(),); #[cfg(feature = "postgres")] - const SQL: &str = - "SELECT 'Hello, world!' as string where 'Hello, world!' in (1,ドル 2,ドル 3,ドル 4,ドル 5,ドル 6,ドル 7ドル)"; + const SQL: &str = "SELECT 'Hello, world!' \ + FROM (SELECT 1) AS t \ + WHERE 'Hello, world!' IN (1,ドル 2,ドル 3,ドル 4,ドル 5,ドル 6,ドル 7ドル)"; #[cfg(not(feature = "postgres"))] - const SQL: &str = - "SELECT 'Hello, world!' as string where 'Hello, world!' in (?, ?, ?, ?, ?, ?, ?)"; + const SQL: &str = "SELECT 'Hello, world!' \ + FROM (SELECT 1) AS t \ + WHERE 'Hello, world!' IN (?, ?, ?, ?, ?, ?, ?)"; { let query = sqlx::query(SQL) diff --git a/tests/certs/README.md b/tests/certs/README.md index add100625b..78bc0bc87a 100644 --- a/tests/certs/README.md +++ b/tests/certs/README.md @@ -14,6 +14,12 @@ These certificates should be valid until the year 2035. RusTLS requires TLS certificates to be x509v3. OpenSSL 3.2 and up create v3 certificates by default. +### MySQL 5.7 (RSA) + +The default test certificates in this directory use Ed25519, which MySQL 5.7 +cannot load. We keep a separate RSA CA/client/server set under +`tests/certs/rsa` and use it only for the MySQL 5.7 client-SSL targets. + ## (Re)generating When generating certificates, OpenSSL prompts for a number of fields: diff --git a/tests/certs/rsa/ca.crt b/tests/certs/rsa/ca.crt new file mode 100644 index 0000000000..bafecf8bfa --- /dev/null +++ b/tests/certs/rsa/ca.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDizCCAnOgAwIBAgIUYWAIZEOv172fTkUC1LZvQbdmUcYwDQYJKoZIhvcNAQEL +BQAwVTELMAkGA1UEBhMCdXMxEzARBgNVBAgMCmNhbGlmb3JuaWExEDAOBgNVBAoM +B1NRTHgucnMxHzAdBgNVBAMMFlNRTHggTXlTUUwgNS43IFRlc3QgQ0EwHhcNMjYw +MTE3MjA0OTQzWhcNMzYwMTE1MjA0OTQzWjBVMQswCQYDVQQGEwJ1czETMBEGA1UE +CAwKY2FsaWZvcm5pYTEQMA4GA1UECgwHU1FMeC5yczEfMB0GA1UEAwwWU1FMeCBN +eVNRTCA1LjcgVGVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AJlTKGk0hm+kuaX7AXFPt7z3EgkAakqEF7zpz6oyKohgQ/favYUgFW36A27VCpLs +eUhzRvlYUaLjdbMiSZMcDLyFMPQysFR6XFmB/loUppEhWGUCY/2qbUlnCd3jj9vB +qSuJ4KkVHo9HMeivLhAxEiFQ60iYgQ1dC+cOXxXckDUAqr1ZRVCwpkqo5VKyi5Ft +2jo2I8q20pAbnPteQJcWwF4zdKD989WoToZyFEsHGSNJvx7qsXnbbQV9ZfVD2gQO +3ac8rO746lq/Mv4hvX7UmRE3N/wrJvsE6wkHLOGwqs79AlWfUR/7PDxf2Qolaujf +e7ZSWVmvsG4YsHLYbFgt0rECAwEAAaNTMFEwHQYDVR0OBBYEFOJi5+EmSMZYTpnK +IEdk7FyA6RsUMB8GA1UdIwQYMBaAFOJi5+EmSMZYTpnKIEdk7FyA6RsUMA8GA1Ud +EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABiC7AG9C8HmMEHEFgK0Vu96 +JKnWrObjINR07VfxLNpQ8lCSTdxVj6f4gNA4rfWuoai5+9us/nVQEHWwul7OZM5s +nrrh6G4xF6gWETpCj7Psro33tA3P9gjYanf9pBA8cEnjSN9mZdR+EToNkWwqYa25 +KnNUSV8dAmOGWkFinqQh1buR5BL55muffKL91rDtsbjVuS+FKfXUF6RR7+MwHuGz +KSuK5jN2lXuXryyuCLqKdEP5Hzi453M1EbSnbFPNnNihVsO7IEbyrvsrdDoo2KwQ +kZsWZxdT7CnVkVVUDrgJTql4suDYdcJkY/wAm8lPDf+oHyEBEXoNCmW31pKu6o8= +-----END CERTIFICATE----- diff --git a/tests/certs/rsa/client.crt b/tests/certs/rsa/client.crt new file mode 100644 index 0000000000..b18a9268ef --- /dev/null +++ b/tests/certs/rsa/client.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJzCCAg8CFAxjCSTLziRQ3tsnEl9kRYWx+mDmMA0GCSqGSIb3DQEBCwUAMFUx +CzAJBgNVBAYTAnVzMRMwEQYDVQQIDApjYWxpZm9ybmlhMRAwDgYDVQQKDAdTUUx4 +LnJzMR8wHQYDVQQDDBZTUUx4IE15U1FMIDUuNyBUZXN0IENBMB4XDTI2MDExNzIw +NTAxMFoXDTM2MDExNTIwNTAxMFowSzELMAkGA1UEBhMCdXMxEzARBgNVBAgMCmNh +bGlmb3JuaWExEDAOBgNVBAoMB1NRTHgucnMxFTATBgNVBAMMDG15c3FsLWNsaWVu +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALMvA+vqxPuTVtX/9uFH +Up7Wkx5iaFBiC2x3wTLZX1WavknU/aw8jYQabgwVbiCaCvjoncPtQt1abtzBvPbL +HpXC6h91D0hpMabzr8HyRutPQ09MK2OkIQvIJZPFu8CKbDdSsD2uqgSEjvAeFxKv +v1JdL24ha+hoojTnG+Of5qzPgIKeuQrUa5lvB0RVpxxs86tjENZXsqgYVtCD3i9u +ne6DGcTn0sMZTjQiHll0HGbTPEOY0CzIp+xoAOrTfVXeAVhi+B90U31q371X70+H +qa6R8KmsJ4f+qWn26yJoZacLEcO97IPZLxo+zEegDSDIWfiCDb8o+JGdjpMDmsvS +XIECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAOBdK+gBtdy8hAF313FCBCNAhe5eF +jSVUt0B+sknqXh5W2cx8J0cfyiL2/HgID5t4d1JaNOUt+3Pv2XDWgp8zQ7Tqob+j +Rp0M//IuBAJgyBkN6E+Xok+4sIX8pRJ0fYPzHPU7LbnCcKb6tc5MnY6wjtLM1I8F +ayCwXIEdXcsvPey98kWnmwJu4QHpjBkvAs6NEGWbW2ZLdm0URdAUdBlgv/sSNnV9 +4UTHVvjk1/aZOC2BTtcNvLO+8qXRkeWy/YMEFKMWtcF19uXOS39jTtjeRnyNhosy +g6yFsDbcEU+TnVtRA0lji+DsRJ3JBLT6UKmIY45tMYMDqlDm77MwLcH1sw== +-----END CERTIFICATE----- diff --git a/tests/certs/rsa/keys/ca.key b/tests/certs/rsa/keys/ca.key new file mode 100644 index 0000000000..e8ac8881a8 --- /dev/null +++ b/tests/certs/rsa/keys/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCZUyhpNIZvpLml ++wFxT7e89xIJAGpKhBe86c+qMiqIYEP32r2FIBVt+gNu1QqS7HlIc0b5WFGi43Wz +IkmTHAy8hTD0MrBUelxZgf5aFKaRIVhlAmP9qm1JZwnd44/bwakrieCpFR6PRzHo +ry4QMRIhUOtImIENXQvnDl8V3JA1AKq9WUVQsKZKqOVSsouRbdo6NiPKttKQG5z7 +XkCXFsBeM3Sg/fPVqE6GchRLBxkjSb8e6rF5220FfWX1Q9oEDt2nPKzu+OpavzL+ +Ib1+1JkRNzf8Kyb7BOsJByzhsKrO/QJVn1Ef+zw8X9kKJWro33u2UllZr7BuGLBy +2GxYLdKxAgMBAAECggEAHNJYfNpOUStOaKiT/1hkaiWpor6MvIAzNCRhkJVIkIVE +EZHxYVaEIL3IKmvqxm6kZ92foFydT/jhFbDi0sAJluCUsLrckazEsmCwzv8lxo9V +nftCj5sbWxp+7NKLptwzMEeFT1N0gKt58ssHZizLQy8CY42jaL8ubxsw/ZuOEiBI +4bz+tr17UWxA08K4jmxM+un+wUSAa1X6+j8DqqdvAyGXoHRoyweQQ8gC87c7pIal +GMIk+Bk1izILR2SdgoOdLiTASpYQEr6x3/+A/ZGixlD6tlM2twuKWD3LuGhDCZ/V +I0+3MQxO8PPKdb3DaB5ReRyVyuNTpaH7SxKnbV9vzwKBgQDQzWTxnsEDf2zCfsO+ +yDntH9tTigI+WMt6nVX4lV3jy6fnqa+6zPuYxes6RLRMkJZMud1S3b35FNW9lQHd +CYxS4eRO1DnrRWFgj8VGJQ97d4wqSg5YLoB/de/h6JN7C2SctsmCXm9qL0L5wXrx +DS5ZWLe//gKIDPU8m8yMEvbkjwKBgQC7+31p/Lgu39DqhiRCrbwMShOgu9JqTlmN +LroJJ2eZA931gnK3wysWlHpJBu6+/ObippEW9IJ+NRecYHe/jMyL1fCCm6iCUu6o +E8s2u8lzltOQSskC6PBn9p/kM3mmC+mhlA2qFYoInT+2OMZhbFup96fb1Yc5FmPR +/k0QKE/0vwKBgQDP97OSANgn3rP56H6YuB8R8gfm5e+kH5bTkn/9bvAsIj0jPVx9 +RwtVN9Q5nhKiq+Q3mWw6zAcaXskg4ZgQiyELsFhQt4rUra72mVwYqHMKO6EMweQV +qoNr8JCzxo2WIVvdxyVfxyVbcqVX04DbNJC0huvFu37T+WwNKPSLk5v7OwKBgCis +TYJ1L9TUkHtt8sKKnLl7/as1eF2P/khR5+a7I+szrv7D7tZb4CLOlXbfjSC9z6cS +qynwVZvBGQ64wLAtYsSO0a8wxtEL6J9tSPbawsfDxprd04hRplKYRhg2GwgWY8KW +Ki624lriyzo+Jo5Fx7+K2kLyfIOZmJeDEmGAl2w5AoGALtFWXrHFNuHqFAk7oRBk +m6IgHrWf4SAH13fbReROk3nAhZwRBUIoFBSBJA8i0z3kjg01lm3rk64h5SwB7WIg +As+x0QYVlQifR7Lv0kQbtN55GsQYYyj4DUPTqVvZXSm88yzJ0TqFWLd5RdhFZviC +ZPF8adFIzEAnRXoEH6d98Og= +-----END PRIVATE KEY----- diff --git a/tests/certs/rsa/keys/client.key b/tests/certs/rsa/keys/client.key new file mode 100644 index 0000000000..1591d585f8 --- /dev/null +++ b/tests/certs/rsa/keys/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzLwPr6sT7k1bV +//bhR1Ke1pMeYmhQYgtsd8Ey2V9Vmr5J1P2sPI2EGm4MFW4gmgr46J3D7ULdWm7c +wbz2yx6VwuofdQ9IaTGm86/B8kbrT0NPTCtjpCELyCWTxbvAimw3UrA9rqoEhI7w +HhcSr79SXS9uIWvoaKI05xvjn+asz4CCnrkK1GuZbwdEVaccbPOrYxDWV7KoGFbQ +g94vbp3ugxnE59LDGU40Ih5ZdBxm0zxDmNAsyKfsaADq031V3gFYYvgfdFN9at+9 +V+9Ph6mukfCprCeH/qlp9usiaGWnCxHDveyD2S8aPsxHoA0gyFn4gg2/KPiRnY6T +A5rL0lyBAgMBAAECggEABX/ZnvZMgyMCh0XBlyh0qdkQqt4RMPiszswCtEOuhM2E +Li2kdu33IM2u3B3MQ30JIlFvGG/CC1JNgnkCCpEMzPmcNe09QbKLHLk5YpXocr4g +m+C6mMIDHRBQqThoZjdHGv3cauOoVDIGejOWytd1dX9mrB81535zMuNSrqnL/O94 +0lmIn3iIwqGY0Hcc1Oed4GnOp0mSbjQfJ9SrBNTYS0BrbnYeO0B+E0W6nc/AfTRw +JLri5zWl+iQpiKjtvu9ABmLMeI5NrFzsZaUOPnTaJ3XKAr5tUWfB/pnjWf07JhRZ +5z97qjx1UTcjNiy/xCS0BzHm11FlOviBtNLF9NFEAQKBgQDnl4Pxg4Fmo/aCDL4E +so/YMfXJEH1qaGmtaI8pwsWtvlTQeflFYvNNZqSihe+YzkdrOWdIqBQS5vyjS/pl +yDhUU9ZUNWrtJBgo7bPHLKXP5t2asfvvHEp7YPZH6WWsJW2P8cslWmyu9lHIO9S5 +rdcTHfd+pKTK5fVZGCBsxWEXfwKBgQDGEX7pADH4xjBfbWt4KCJH1Yoy+p+8ftkt +/nN4KS+0P6dTne9Dmekt/wrX4n0qrJLYTzCO0LU7KDOgD7K/3JMwqgIYSYnFSEPt +LNaSKXhRhazgOhr55YfR4WGNfixApgrkICwB2iKqnkSZCu0wIqogr1BWE1vj84I7 +WNzcxKaL/wKBgQDEeEeZJkUq/EJuRb0WYx2g/ZFUB8c99GJimGeLuA7XvLZbPn74 +HF/n9AILVrDS43y3PDWg7+ZHuuns5tIAcwFGmPEk80RI9ewBHNb9S6VHYMXzLLdc +PJX7YWDN1PVKO15dVXVPtQyqyZDL2+Y1t4LUVwHV0Ht1He0srkkjvbcGpQKBgQCS +c5lNGzHX6mMWDEfsfnBqgQBAlYPK0lgvY/dpH7sAIhjNAPhLGeCKfAw+eF9oUFX7 +zwHud2+poB4b+b+Hkcbbsrj90FIoJzjig8bcKAGo9ZhP62bK4+a7T1TcVDDQVHW1 +G/yuGeaMFZ5PMv8SGm+E31wdaQ8Gy6S90QTt0BH9bQKBgFuOaNc+ByJl6zLpxoXI +Xtdd0jtzIwMHz6XOi+fn1BaCaadB+WWKbp8Si6WnkHXAckFIRLt6n86Pb4v79kVB +GKY/saNvtv8FWx9IfKCc/utrTm27J+OhAQhvChrrb4kGpbm8iBqX1mNstDcaKS5x +I6RrWqv3aLoo0Gxvy01sRaC0 +-----END PRIVATE KEY----- diff --git a/tests/certs/rsa/keys/server.key b/tests/certs/rsa/keys/server.key new file mode 100644 index 0000000000..8d7a8e5419 --- /dev/null +++ b/tests/certs/rsa/keys/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQMXlx6SQejGgE +viUQMUb40KkFixRo0FlHb4mONLPbMZ+TCNGt1OziLe2pPJnnfJPdq3miKza2iuxX +sB8e9wL2XVra3HtD1qlbl12vkPx8DHO2NNh3BYFgDij9CNZNAaD2O0IjDtBkhcv+ +yOYUoEnehtu4KyoiYcZbD1iASxJtiyK6fNWIWFRxYR+AF6t0BKEQKSivbiXtkawc +asXxelC8nupIwijgEms3CRabyDnMkJnWJ3wPuTDzQr7g3xW9NQ/rkKfqnw96rQJV +K1OxLr3c4D7/7T5Dt6glJijghCqgUrc2I8Z1cEdDWEJ2rED4a9CztW5nTenfc9pb +hw/yTue5AgMBAAECggEABa4/PZxSdy/BKVWiXNfJXiyRsL7ny0iEZ8fLdC5sNL2H +g1YCOaOQZJyt9M2Rt1B4V5Fa7TxlCWlO6QFwsfIPD2ztQmucb0Bjfdt/wQn8LQAp +JUY5x1Kb7M8/b1BkdN9dNobCp4Kl7UOTf76DJ6TW1dFucURNPvkAa2zmucEX+JY+ +ug4ZvKY7EH7vV5Hr96tfCz2oCcbtJn3NBODE0Nq6FYxzZsEMsMmAFDDutncBZVQl +3K77te2v2UussYuFqae0t2+VUADJ6Jc3oNHWKAvGG0zSa/W4yAFVc5feJzD4I/qD +vEVZOQRkUmjcDZwI5mGgwFdSf/I8c/HFh71W1H/1MQKBgQDzoWntgSU8TNfhj2s1 +bq+hCxOs9ytuH7TALd4DoqGTr7y79d0PCYUCOHHY7cXdVUTIRgyshAKHkiqsmE1w +M3CsyU1uPvp0zIqIHeddjdwy8rNrrvlQ0KaUE/KElSfXhjPtE5TdFm1xkrCJBW9W +TLbcwklXzyWLt778QMOyeSXWEQKBgQDaw3cKcjpxQdqrZzbU71B+69E7yNMU2Jnb +B/9A2xPlb4omkFdqi0naEENySClPCeXR0E9v6+hq87pzx6JlfYu22CJk5f78lUpS +dbZQYaSQPKO5Unik5oPJHY6oA5iYYAaZxxEj9GsAMHWNV27FVHPoz5J9Z40Hm12X +YDt7/UqvKQKBgFqUUrvY3i0zLLhSCDwPcQDhC2mtY9pHs34YD4kueABewD7pxEyI +74jJz5olnQETaMVFNgUV95LMB02wOmpS1buIBF/OznOKcJ7270RbL9lJXufUYCFp +0eUQHYSpp+x7muaz9w7T/dDSBwyKlsBxOTOOkJIzE/SEVl+W/KtoW2bhAoGBAKUx +72WbBpjZ4teGNHitUrrVNoYPy521RtGIg28lQCwEg21FmE1ja1xY5aWZ6l++GKbM +x/+7RCHndMfTW8WJ/YQQSECrEVcJITuNmiOu6EbnE7dxGJtlWuT3Be/H72Y5NSLQ +mRfujRJyhYI7IPGwKWsHvBYoqO2ynAUgbSrfBZOpAoGAPUTE0jjRchngnR9hdfQZ +ztwXq2vpfaQd8vyYiNkihNGu3h4MVGvm86zw7288AMwd2ArEYVccqcqM9KdPulee +WcPxKHV3dQirldwlk2ZErUhFRDO3BORwChm4uIkjN9yfOOaX4kOixllTMTVb19WX +SE6T72rWH84Kev/aPsHFMao= +-----END PRIVATE KEY----- diff --git a/tests/certs/rsa/server.crt b/tests/certs/rsa/server.crt new file mode 100644 index 0000000000..7ebc7f7454 --- /dev/null +++ b/tests/certs/rsa/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfzCCAmegAwIBAgIUDGMJJMvOJFDe2ycSX2RFhbH6YOUwDQYJKoZIhvcNAQEL +BQAwVTELMAkGA1UEBhMCdXMxEzARBgNVBAgMCmNhbGlmb3JuaWExEDAOBgNVBAoM +B1NRTHgucnMxHzAdBgNVBAMMFlNRTHggTXlTUUwgNS43IFRlc3QgQ0EwHhcNMjYw +MTE3MjA0OTU2WhcNMzYwMTE1MjA0OTU2WjBGMQswCQYDVQQGEwJ1czETMBEGA1UE +CAwKY2FsaWZvcm5pYTEQMA4GA1UECgwHU1FMeC5yczEQMA4GA1UEAwwHc3FseC5y +czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAxeXHpJB6MaAS+JRAx +RvjQqQWLFGjQWUdviY40s9sxn5MI0a3U7OIt7ak8med8k92reaIrNraK7FewHx73 +AvZdWtrce0PWqVuXXa+Q/HwMc7Y02HcFgWAOKP0I1k0BoPY7QiMO0GSFy/7I5hSg +Sd6G27grKiJhxlsPWIBLEm2LIrp81YhYVHFhH4AXq3QEoRApKK9uJe2RrBxqxfF6 +ULye6kjCKOASazcJFpvIOcyQmdYnfA+5MPNCvuDfFb01D+uQp+qfD3qtAlUrU7Eu +vdzgPv/tPkO3qCUmKOCEKqBStzYjxnVwR0NYQnasQPhr0LO1bmdN6d9z2luHD/JO +57kCAwEAAaNWMFQwEgYDVR0RBAswCYIHc3FseC5yczAdBgNVHQ4EFgQU+AloROBZ +cbZ3TX5lZGxB6E8IF+EwHwYDVR0jBBgwFoAU4mLn4SZIxlhOmcogR2TsXIDpGxQw +DQYJKoZIhvcNAQELBQADggEBADS2doCZzWhSaqHjycgs4KkmCd3rORNL+U6QbPTk +5fjPvO0Ni6kXOYfCnyhocelyhc3dKt8Kvkusqg/fNmeRAj3BNo4cKlsnycdCezvg +vZBxVB9DnV2NcCmFRN4uKSqGszJ5mtCHbgWxJWnP+48qpWVUNi0dZSL7SYRr/eou +3WancEOJ3OkUAZssUNSNloXkhpyzvFkLd1btLCSnyGpE2pACfpX5JiRrX/qkRKaM +IP2l5s/5QDRhRde85PvKqWCQkzIh5wR5qwZfKx/n0ZjtEWDvu0fzdpFdgag03Q2L +vZZ8+9RqwQ1E8KB6dLQjZSbi71UbUqVN97535w9oYPOLY8k= +-----END CERTIFICATE----- diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index c2ccdabef6..e03eeeea67 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -47,6 +47,8 @@ services: dockerfile: mysql/Dockerfile args: IMAGE: mysql:5.7 + SSL_CERT_DIR: certs/rsa + SSL_KEY_DIR: certs/rsa/keys volumes: - "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql:z" ports: diff --git a/tests/docker.py b/tests/docker.py index 5e8c74fb1f..6d7c2663b0 100644 --- a/tests/docker.py +++ b/tests/docker.py @@ -12,6 +12,14 @@ # start database server and return a URL to use to connect +def docker_compose_command(): + if shutil.which("docker-compose"): + return ["docker-compose"] + if shutil.which("docker"): + return ["docker", "compose"] + return None + + def start_database(driver, database, cwd): if driver == "sqlite": database = path.join(cwd, database) @@ -22,8 +30,13 @@ def start_database(driver, database, cwd): # short-circuit for sqlite return f"sqlite://{path.join(cwd, new_database)}?mode=rwc" + compose_cmd = docker_compose_command() + if compose_cmd is None: + raise FileNotFoundError("docker-compose or docker compose not found") + + compose_args = [*compose_cmd, "-p", "sqlx"] res = subprocess.run( - ["docker-compose", "up", "-d", driver], + [*compose_args, "up", "-d", driver], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dir_tests, @@ -35,6 +48,21 @@ def start_database(driver, database, cwd): if b"done" in res.stderr: time.sleep(30) + res = subprocess.run( + [*compose_args, "ps", "-q", driver], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=dir_tests, + ) + + if res.returncode != 0: + print(res.stderr, file=sys.stderr) + raise RuntimeError(f"failed to resolve container for {driver}") + + container_id = res.stdout.strip().decode() + if not container_id: + raise RuntimeError(f"no container found for {driver}") + # determine appropriate port for driver if driver.startswith("mysql") or driver.startswith("mariadb"): port = 3306 @@ -46,9 +74,9 @@ def start_database(driver, database, cwd): raise NotImplementedError # find port + format_arg = f"{{{{(index (index .NetworkSettings.Ports \"{port}/tcp\") 0).HostPort}}}}" res = subprocess.run( - ["docker", "inspect", f"-f='{{{{(index (index .NetworkSettings.Ports \"{port}/tcp\") 0).HostPort}}}}'", - f"sqlx_{driver}_1"], + ["docker", "inspect", "-f", format_arg, container_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dir_tests, @@ -57,18 +85,23 @@ def start_database(driver, database, cwd): if res.returncode != 0: print(res.stderr, file=sys.stderr) - port = int(res.stdout[1:-2].decode()) + port = int(res.stdout.decode().strip()) # need additional permissions to connect to MySQL when using SSL - res = subprocess.run( - ["docker", "exec", f"sqlx_{driver}_1", "mysql", "-u", "root", "-e", "GRANT ALL PRIVILEGES ON *.* TO 'root' WITH GRANT OPTION;"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=dir_tests, - ) - - if res.returncode != 0: - print(res.stderr, file=sys.stderr) + if driver.startswith("mysql") or driver.startswith("mariadb"): + mysql_args = ["docker", "exec", container_id, "mysql", "-u", "root"] + if not driver.endswith("client_ssl"): + mysql_args.append("-ppassword") + mysql_args.extend(["-e", "GRANT ALL PRIVILEGES ON *.* TO 'root' WITH GRANT OPTION;"]) + res = subprocess.run( + mysql_args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=dir_tests, + ) + + if res.returncode != 0: + print(res.stderr, file=sys.stderr) # do not set password in URL if authenticating using SSL key file if driver.endswith("client_ssl"): diff --git a/tests/mysql/Dockerfile b/tests/mysql/Dockerfile index b80618230f..46eddd8f6f 100644 --- a/tests/mysql/Dockerfile +++ b/tests/mysql/Dockerfile @@ -1,11 +1,15 @@ ARG IMAGE FROM ${IMAGE} +# Allow using alternate cert sets for older server images (e.g. MySQL 5.7). +ARG SSL_CERT_DIR=certs +ARG SSL_KEY_DIR=certs/keys + # Copy SSL certificate (and key) -COPY certs/server.crt /etc/mysql/ssl/server.crt -COPY certs/ca.crt /etc/mysql/ssl/ca.crt -COPY certs/keys/server.key /etc/mysql/ssl/server.key -COPY mysql/my.cnf /etc/mysql/my.cnf +COPY ${SSL_CERT_DIR}/server.crt /etc/mysql/ssl/server.crt +COPY ${SSL_CERT_DIR}/ca.crt /etc/mysql/ssl/ca.crt +COPY ${SSL_KEY_DIR}/server.key /etc/mysql/ssl/server.key +COPY mysql/my.cnf /etc/mysql/conf.d/sqlx-ssl.cnf # Fix permissions RUN chown mysql:mysql /etc/mysql/ssl/server.crt /etc/mysql/ssl/server.key diff --git a/tests/mysql/error.rs b/tests/mysql/error.rs index f75e9513a6..bd2cbb9ec0 100644 --- a/tests/mysql/error.rs +++ b/tests/mysql/error.rs @@ -1,6 +1,26 @@ use sqlx::{error::ErrorKind, mysql::MySql, Connection, Error}; use sqlx_test::new; +fn mysql_supports_check_constraints(version: &str) -> bool { + if version.contains("MariaDB") { + return true; + } + + let numeric = match version.split(|c| c == '-' || c == ' ').next() { + Some(numeric) => numeric, + None => return false, + }; + let mut parts = numeric.split('.'); + let major: u64 = match parts.next().and_then(|part| part.parse().ok()) { + Some(major) => major, + None => return false, + }; + let minor: u64 = parts.next().unwrap_or("0").parse().unwrap_or_default(); + let patch: u64 = parts.next().unwrap_or("0").parse().unwrap_or_default(); + + (major, minor, patch)>= (8, 0, 16) +} + #[sqlx_macros::test] async fn it_fails_with_unique_violation() -> anyhow::Result<()> { let mut conn = new::().await?; @@ -62,6 +82,13 @@ async fn it_fails_with_check_violation() -> anyhow::Result<()> { let mut conn = new::().await?; let mut tx = conn.begin().await?; + let version: String = sqlx::query_scalar("SELECT VERSION()") + .fetch_one(&mut *tx) + .await?; + if !mysql_supports_check_constraints(&version) { + return Ok(()); + } + let res: Result<_, sqlx::Error> = sqlx::query("INSERT INTO products VALUES (1, 'Product 1', 0);") .execute(&mut *tx) diff --git a/tests/mysql/migrate.rs b/tests/mysql/migrate.rs index 97caa38005..4a0759c81c 100644 --- a/tests/mysql/migrate.rs +++ b/tests/mysql/migrate.rs @@ -66,6 +66,51 @@ async fn reversible(mut conn: PoolConnection) -> anyhow::Result<()> { Ok(()) } +#[sqlx::test(migrations = false)] +async fn skip(mut conn: PoolConnection) -> anyhow::Result<()> { + clean_up(&mut conn).await?; + let migrator = Migrator::new(Path::new("tests/mysql/migrations_reversible")).await?; + + // get to the state of after the first migration manually + let sql = include_str!("migrations_reversible/20220721124650_add_table.up.sql"); + let statements: Vec<&str> = sql.split(';').filter(|s| !s.trim().is_empty()).collect(); + for statement in statements { + conn.execute(statement).await?; + } + + // skip first migration + migrator.skip(&mut conn, Some(20220721124650)).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 100); + + // run remaining migration + migrator.run(&mut conn).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 101); + + // roll back one version + migrator.undo(&mut conn, 20220721124650).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 100); + + Ok(()) +} + /// Ensure that we have a clean initial state. async fn clean_up(conn: &mut MySqlConnection) -> anyhow::Result<()> { conn.execute("DROP TABLE migrations_simple_test").await.ok(); diff --git a/tests/mysql/mysql.rs b/tests/mysql/mysql.rs index 5d6a5ef233..5374e651c8 100644 --- a/tests/mysql/mysql.rs +++ b/tests/mysql/mysql.rs @@ -3,6 +3,7 @@ use futures_util::TryStreamExt; use sqlx::mysql::{MySql, MySqlConnection, MySqlPool, MySqlPoolOptions, MySqlRow}; use sqlx::{Column, Connection, Executor, Row, SqlSafeStr, Statement, TypeInfo}; use sqlx_core::connection::ConnectOptions; +use sqlx_core::types::Type; use sqlx_mysql::MySqlConnectOptions; use sqlx_test::{new, setup_if_needed}; use std::env; @@ -636,3 +637,93 @@ async fn issue_3200() -> anyhow::Result<()> { Ok(()) } + +#[cfg(mariadb)] +#[sqlx_macros::test] +async fn it_can_name_columns_issue_2206() -> anyhow::Result<()> { + let mut conn = new::().await?; + + sqlx::raw_sql( + "\ + CREATE TABLE IF NOT EXISTS issue_2206 + ( + `id` BIGINT AUTO_INCREMENT, + `name` VARCHAR(128) NOT NULL, + PRIMARY KEY (id) + ); + ", + ) + .execute(&mut conn) + .await?; + + let row = sqlx::query("INSERT INTO issue_2206 (name) VALUES (?) RETURNING *") + .bind("Alice") + .fetch_one(&mut conn) + .await?; + let _id: i64 = row.get("id"); + let name: String = row.get("name"); + + assert_eq!(&name, "Alice"); + + Ok(()) +} + +#[cfg(feature = "any")] +#[sqlx_macros::test] +async fn any_blob_conversions() -> anyhow::Result<()> { + use sqlx::Any; + use sqlx::Decode; + use sqlx::ValueRef; + + sqlx::any::install_default_drivers(); + + let mut conn = new::().await?; + + sqlx::raw_sql( + r#" + CREATE TEMPORARY TABLE any_blob_conversions( + id INTEGER PRIMARY KEY, + regular_text TEXT NOT NULL, + text_binary TEXT COLLATE "utf8mb4_bin" NOT NULL, + binary_blob BLOB NOT NULL + ); + + INSERT INTO any_blob_conversions(id, regular_text, text_binary, binary_blob) + VALUES (1, 'Hello, world!', 'Lorem ipsum dolor sit amet', X'01020304DEADBEEF'); + "#, + ) + .execute(&mut conn) + .await?; + + let row = sqlx::query("SELECT * FROM any_blob_conversions") + .fetch_one(&mut conn) + .await?; + + let id = row.try_get_raw("id")?; + let regular_text = row.try_get_raw("regular_text")?; + let text_binary = row.try_get_raw("text_binary")?; + let binary_blob = row.try_get_raw("binary_blob")?; + + assert_eq!(*id.type_info(), >::type_info()); + assert_eq!(>::decode(id).unwrap(), 1); + + assert_eq!(*regular_text.type_info(), >::type_info()); + assert_eq!( + <&str as Decode>::decode(regular_text).unwrap(), + "Hello, world!" + ); + + assert_eq!(*text_binary.type_info(), >::type_info()); + assert_eq!( + <&str as Decode>::decode(text_binary).unwrap(), + "Lorem ipsum dolor sit amet" + ); + + assert_eq!(*binary_blob.type_info(), <[u8] as Type>::type_info()); + assert_eq!( + <&[u8] as Decode>::decode(binary_blob).unwrap(), + [0x01, 0x02, 0x03, 0x04, 0xDE, 0xAD, 0xBE, 0xEF], + ); + + Ok(()) +} diff --git a/tests/postgres/derives.rs b/tests/postgres/derives.rs index 96687e3113..58d09edbc3 100644 --- a/tests/postgres/derives.rs +++ b/tests/postgres/derives.rs @@ -145,10 +145,19 @@ struct InventoryItem { #[sqlx(type_name = "float_range")] struct FloatRange(PgRange); -// Custom domain type -#[derive(sqlx::Type, Debug)] -#[sqlx(type_name = "int4rangeL0pC")] -struct RangeInclusive(PgRange); +// Manual `PgHasArrayType` impl +#[derive(sqlx::Type)] +#[sqlx(no_pg_array)] +pub struct User { + pub id: i32, + pub username: String, +} + +impl PgHasArrayType for User { + fn array_type_info() -> sqlx::postgres::PgTypeInfo { + sqlx::postgres::PgTypeInfo::array_of("User") + } +} test_type!(transparent_tuple(Postgres, "0" == Transparent(0), @@ -809,18 +818,6 @@ async fn test_from_row_hygiene() -> anyhow::Result<()> { #[sqlx_macros::test] async fn test_custom_pg_array() -> anyhow::Result<()> { - #[derive(sqlx::Type)] - #[sqlx(no_pg_array)] - pub struct User { - pub id: i32, - pub username: String, - } - - impl PgHasArrayType for User { - fn array_type_info() -> sqlx::postgres::PgTypeInfo { - sqlx::postgres::PgTypeInfo::array_of("User") - } - } Ok(()) } diff --git a/tests/postgres/migrate.rs b/tests/postgres/migrate.rs index 636dffe860..c08e24c731 100644 --- a/tests/postgres/migrate.rs +++ b/tests/postgres/migrate.rs @@ -66,6 +66,51 @@ async fn reversible(mut conn: PoolConnection) -> anyhow::Result<()> { Ok(()) } +#[sqlx::test(migrations = false)] +async fn skip(mut conn: PoolConnection) -> anyhow::Result<()> { + clean_up(&mut conn).await?; + let migrator = Migrator::new(Path::new("tests/postgres/migrations_reversible")).await?; + + // get to the state of after the first migration manually + let sql = include_str!("migrations_reversible/20220721124650_add_table.up.sql"); + let statements: Vec<&str> = sql.split(';').filter(|s| !s.trim().is_empty()).collect(); + for statement in statements { + conn.execute(statement).await?; + } + + // skip first migration + migrator.skip(&mut conn, Some(20220721124650)).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 100); + + // run remaining migration + migrator.run(&mut conn).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 101); + + // roll back one version + migrator.undo(&mut conn, 20220721124650).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 100); + + Ok(()) +} + #[sqlx::test(migrations = false)] async fn no_tx(mut conn: PoolConnection) -> anyhow::Result<()> { clean_up(&mut conn).await?; diff --git a/tests/postgres/postgres.rs b/tests/postgres/postgres.rs index 8324982b67..126771565a 100644 --- a/tests/postgres/postgres.rs +++ b/tests/postgres/postgres.rs @@ -417,7 +417,7 @@ async fn copy_can_work_with_failed_transactions() -> anyhow::Result<()> { tx.rollback().await?; - // conn should be usable again, as we explictly rolled back the transaction + // conn should be usable again, as we explicitly rolled back the transaction let got: i32 = sqlx::query_scalar("SELECT 1") .fetch_one(conn.as_mut()) .await?; @@ -445,7 +445,7 @@ async fn it_can_work_with_failed_transactions() -> anyhow::Result<()> { .is_err()); tx.rollback().await?; - // conn should be usable again, as we explictly rolled back the transaction + // conn should be usable again, as we explicitly rolled back the transaction let got: i32 = sqlx::query_scalar("SELECT 1") .fetch_one(conn.as_mut()) .await?; @@ -2086,6 +2086,7 @@ async fn test_issue_3052() { } #[sqlx_macros::test] +#[cfg(feature = "chrono")] async fn test_bind_iter() -> anyhow::Result<()> { use sqlx::postgres::PgBindIterExt; use sqlx::types::chrono::{DateTime, Utc}; diff --git a/tests/postgres/setup.sql b/tests/postgres/setup.sql index 49b7fcfeea..6a1b925abe 100644 --- a/tests/postgres/setup.sql +++ b/tests/postgres/setup.sql @@ -1,3 +1,6 @@ +-- Create extra user to be used on runner +CREATE USER runner WITH SUPERUSER PASSWORD 'runner-password'; + -- https://www.postgresql.org/docs/current/ltree.html CREATE EXTENSION IF NOT EXISTS ltree; diff --git a/tests/sqlite/describe.rs b/tests/sqlite/describe.rs index 4c0768a5e2..49bdbd35e7 100644 --- a/tests/sqlite/describe.rs +++ b/tests/sqlite/describe.rs @@ -591,6 +591,50 @@ async fn it_describes_table_order_by() -> anyhow::Result<()> { Ok(()) } +// Regression test for https://github.com/launchbadge/sqlx/issues/4147 +// ORDER BY + LIMIT routes data through an ephemeral sorter table; +// the NOT NULL constraint must survive the round-trip. +#[sqlx_macros::test] +async fn it_describes_order_by_with_limit() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let info = conn + .describe("SELECT text FROM tweet ORDER BY id DESC LIMIT 10".into_sql_str()) + .await?; + assert_eq!(info.column(0).type_info().name(), "TEXT"); + assert_eq!( + info.nullable(0), + Some(false), + "NOT NULL column should stay NOT NULL with ORDER BY + LIMIT" + ); + + let info = conn + .describe("SELECT text, is_sent FROM tweet ORDER BY id DESC LIMIT 10000".into_sql_str()) + .await?; + assert_eq!( + info.nullable(0), + Some(false), + "text should be NOT NULL with ORDER BY DESC + large LIMIT" + ); + assert_eq!( + info.nullable(1), + Some(false), + "is_sent should be NOT NULL with ORDER BY DESC + large LIMIT" + ); + + // nullable column must remain nullable + let info = conn + .describe("SELECT owner_id FROM tweet ORDER BY id DESC LIMIT 10".into_sql_str()) + .await?; + assert_eq!( + info.nullable(0), + Some(true), + "nullable column should stay nullable with ORDER BY + LIMIT" + ); + + Ok(()) +} + #[sqlx_macros::test] async fn it_describes_union() -> anyhow::Result<()> { async fn assert_union_described( diff --git a/tests/sqlite/migrate.rs b/tests/sqlite/migrate.rs index ed5835225c..9b5dc38bfa 100644 --- a/tests/sqlite/migrate.rs +++ b/tests/sqlite/migrate.rs @@ -66,6 +66,51 @@ async fn reversible(mut conn: PoolConnection) -> anyhow::Result<()> { Ok(()) } +#[sqlx::test(migrations = false)] +async fn skip(mut conn: PoolConnection) -> anyhow::Result<()> { + clean_up(&mut conn).await?; + let migrator = Migrator::new(Path::new("tests/sqlite/migrations_reversible")).await?; + + // get to the state of after the first migration manually + let sql = include_str!("migrations_reversible/20220721124650_add_table.up.sql"); + let statements: Vec<&str> = sql.split(';').filter(|s| !s.trim().is_empty()).collect(); + for statement in statements { + conn.execute(statement).await?; + } + + // skip first migration + migrator.skip(&mut conn, Some(20220721124650)).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 100); + + // run remaining migration + migrator.run(&mut conn).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 101); + + // roll back one version + migrator.undo(&mut conn, 20220721124650).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 100); + + Ok(()) +} + #[sqlx::test(migrations = false)] async fn no_tx(mut conn: PoolConnection) -> anyhow::Result<()> { clean_up(&mut conn).await?; diff --git a/tests/sqlite/migrations_issue_4300/000_init.sql b/tests/sqlite/migrations_issue_4300/000_init.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/sqlite/sqlite.rs b/tests/sqlite/sqlite.rs index d8f8ee492c..8791443b4b 100644 --- a/tests/sqlite/sqlite.rs +++ b/tests/sqlite/sqlite.rs @@ -1,6 +1,5 @@ use futures_util::TryStreamExt; -use rand::{Rng, SeedableRng}; -use rand_xoshiro::Xoshiro256PlusPlus; +use rand::{rngs::Xoshiro256PlusPlus, RngExt, SeedableRng}; use sqlx::sqlite::{SqliteConnectOptions, SqliteOperation, SqlitePoolOptions}; use sqlx::SqlSafeStr; use sqlx::{ @@ -664,8 +663,8 @@ async fn issue_1467() -> anyhow::Result<()> { if i % 1_000 == 0 { println!("{i}"); } - let key = rng.gen_range(0..1_000); - let value = rng.gen_range(0..1_000); + let key = rng.random_range(0..1_000); + let value = rng.random_range(0..1_000); let mut tx = conn.begin().await?; let exists = sqlx::query("SELECT 1 FROM kv WHERE k = ?") @@ -1438,3 +1437,35 @@ async fn issue_3982() -> anyhow::Result<()> { Ok(()) } + +#[sqlx_macros::test] +async fn issue_4300() -> anyhow::Result<()> { + use sqlx::migrate::Migrator; + use sqlx::sqlite::SqlitePoolOptions; + use std::path::Path; + + let pool = SqlitePoolOptions::new().connect("sqlite::memory:").await?; + + let migrator = Migrator::new(Path::new("tests/sqlite/migrations_issue_4300")).await?; + migrator.run(&pool).await?; + + sqlx::query("CREATE TABLE my_table ( qqq TEXT )") + .execute(&pool) + .await?; + + sqlx::query("CREATE TABLE other_table ( www TEXT )") + .execute(&pool) + .await?; + + sqlx::query("INSERT INTO my_table (qqq) VALUES ('temporary')") + .execute(&pool) + .await?; + + let result = sqlx::query("BEGIN TRANSACTION; COMMIT;") + .execute(&pool) + .await?; + + assert_eq!(result.rows_affected(), 0); + + Ok(()) +} diff --git a/tests/x.py b/tests/x.py index e1308f2fa4..66130e6f99 100755 --- a/tests/x.py +++ b/tests/x.py @@ -15,15 +15,25 @@ parser.add_argument("-e", "--target-exact") parser.add_argument("-l", "--list-targets", action="store_true") parser.add_argument("--test") +parser.add_argument("--clippy", action="store_true") argv, unknown = parser.parse_known_args() +_list_targets_seen = set() + # base dir of sqlx workspace dir_workspace = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # dir of tests dir_tests = os.path.join(dir_workspace, "tests") +RUNTIMES = ["async-std", "async-global-executor", "smol", "tokio"] +CHECK_TLS = ["native-tls", "rustls", "rustls-ring", "rustls-aws-lc-rs", "none"] +TLS_VARIANTS = ["native-tls", "rustls-ring", "rustls-aws-lc-rs", "none"] +POSTGRES_VERSIONS = ["17", "16", "15", "14", "13"] +MYSQL_VERSIONS = ["8", "5_7"] +MARIADB_VERSIONS = ["verylatest", "11_8", "11_4", "10_11", "10_6"] + def maybe_fetch_sqlite_extension(): """ @@ -55,10 +65,37 @@ def maybe_fetch_sqlite_extension(): return filename.split(".")[0] +def required_feature_for_test(test_name): + for feature in ["postgres", "mysql", "sqlite", "any"]: + if test_name.startswith(feature): + return feature + return None + + +def extract_features(command): + tokens = command.split(" ") + for i, token in enumerate(tokens): + if token == "--features" and i + 1 < len(tokens): + return set(tokens[i + 1].split(",")) + return None + + +def core_tls_features(tls): + if tls == "rustls": + return ["_tls-rustls-ring-webpki"] + if tls == "rustls-ring": + return ["_tls-rustls-ring-webpki", "_tls-rustls-ring-native-roots"] + if tls == "rustls-aws-lc-rs": + return ["_tls-rustls-aws-lc-rs"] + return [f"_tls-{tls}"] + + def run(command, comment=None, env=None, service=None, tag=None, args=None, database_url_args=None): if argv.list_targets: if tag: - print(f"{tag}") + if tag not in _list_targets_seen: + print(f"{tag}") + _list_targets_seen.add(tag) return @@ -100,7 +137,17 @@ def run(command, comment=None, env=None, service=None, tag=None, args=None, data command_args = [] if argv.test: - command_args.extend(["--test", argv.test]) + if command.startswith("cargo c") or command.startswith("cargo check") or command.startswith("cargo clippy"): + return + if "--manifest-path" in command: + return + required = required_feature_for_test(argv.test) + if required is not None: + features = extract_features(command) + if features is None or (required not in features and "all-databases" not in features): + return + if command.startswith("cargo test"): + command_args.extend(["--test", argv.test]) if unknown: command_args.extend(["--", *unknown]) @@ -124,6 +171,17 @@ def run(command, comment=None, env=None, service=None, tag=None, args=None, data sys.exit(res.returncode) +def postgres_env(version): + env = {} + rustflags = os.environ.get("RUSTFLAGS", "").strip() + version_flag = f'--cfg postgres="{version}"' + if rustflags: + env["RUSTFLAGS"] = f"{rustflags} {version_flag}" + else: + env["RUSTFLAGS"] = version_flag + return env + + # before we start, we clean previous profile data # keeping these around can cause weird errors for path in glob(os.path.join(os.path.dirname(__file__), "target/**/*.gc*"), recursive=True): @@ -133,120 +191,190 @@ def run(command, comment=None, env=None, service=None, tag=None, args=None, data # check # -for runtime in ["async-std", "tokio"]: - for tls in ["native-tls", "rustls", "none"]: +CHECK_CMD = "cargo clippy" if argv.clippy else "cargo c" + +for runtime in RUNTIMES: + for tls in CHECK_TLS: run( - f"cargo c --no-default-features --features all-databases,_unstable-all-types,macros,runtime-{runtime},tls-{tls}", - comment="check with async-std", - tag=f"check_{runtime}_{tls}" + f"{CHECK_CMD} --no-default-features --features all-databases,_unstable-all-types,macros,sqlite-preupdate-hook,runtime-{runtime},tls-{tls}", + comment=f"check {runtime} {tls}", + tag=f"check_{runtime}_{tls}", ) +if argv.clippy: + sys.exit(0) + # # unit test # -for runtime in ["async-std", "tokio"]: - for tls in ["native-tls", "rustls", "none"]: +for runtime in RUNTIMES: + for tls in TLS_VARIANTS: + core_features = [ + "json", + "offline", + "migrate", + "sqlx-toml", + f"_rt-{runtime}", + *core_tls_features(tls), + ] run( - f"cargo test --no-default-features --manifest-path sqlx-core/Cargo.toml --features json,offline,migrate,_rt-{runtime},_tls-{tls}", - comment="unit test core", - tag=f"unit_{runtime}_{tls}" + "cargo test --no-default-features --manifest-path sqlx-core/Cargo.toml " + f"--features {','.join(core_features)}", + comment=f"unit test core {runtime} {tls}", + tag=f"unit_{runtime}_{tls}", ) +run( + "cargo test -p sqlx-mysql --no-default-features --features rsa --lib", + comment="unit test sqlx-mysql rsa", + tag="unit_mysql_rsa", +) + # # integration tests # -for runtime in ["async-std", "tokio"]: - for tls in ["native-tls", "rustls", "none"]: - +for runtime in RUNTIMES: + for tls in TLS_VARIANTS: # # sqlite # run( - f"cargo test --no-default-features --features any,sqlite,macros,_unstable-all-types,runtime-{runtime},tls-{tls}", - comment=f"test sqlite", + f"cargo test --no-default-features " + f"--features any,sqlite,macros,migrate,sqlite-preupdate-hook,_unstable-all-types,runtime-{runtime},tls-{tls}", + comment="test sqlite", + env={"RUST_TEST_THREADS": "1"}, service="sqlite", - tag=f"sqlite" if runtime == "async-std" else f"sqlite_{runtime}", + tag=f"sqlite_{runtime}", ) # # postgres # - for version in ["17", "16", "15", "14", "13"]: + for version in POSTGRES_VERSIONS: run( - f"cargo test --no-default-features --features any,postgres,macros,_unstable-all-types,runtime-{runtime},tls-{tls}", + f"cargo test --no-default-features " + f"--features any,postgres,macros,migrate,_unstable-all-types,runtime-{runtime},tls-{tls}", comment=f"test postgres {version}", + env=postgres_env(version), service=f"postgres_{version}", - tag=f"postgres_{version}" if runtime == "async-std" else f"postgres_{version}_{runtime}", + tag=f"postgres_{version}_{runtime}", ) if tls != "none": ## +ssl run( - f"cargo test --no-default-features --features any,postgres,macros,_unstable-all-types,runtime-{runtime},tls-{tls}", + f"cargo test --no-default-features " + f"--features any,postgres,macros,migrate,_unstable-all-types,runtime-{runtime},tls-{tls}", comment=f"test postgres {version} ssl", database_url_args="sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt", + env=postgres_env(version), service=f"postgres_{version}", - tag=f"postgres_{version}_ssl" if runtime == "async-std" else f"postgres_{version}_ssl_{runtime}", + tag=f"postgres_{version}_ssl_{runtime}", ) ## +client-ssl run( - f"cargo test --no-default-features --features any,postgres,macros,_unstable-all-types,runtime-{runtime},tls-{tls}", + f"cargo test --no-default-features " + f"--features any,postgres,macros,migrate,_unstable-all-types,runtime-{runtime},tls-{tls}", comment=f"test postgres {version}_client_ssl no-password", - database_url_args="sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt&sslkey=%2Ftests%2Fcerts%2Fkeys%2Fclient.key&sslcert=.%2Ftests%2Fcerts%2Fclient.crt", + database_url_args="sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt&sslkey=.%2Ftests%2Fcerts%2Fkeys%2Fclient.key&sslcert=.%2Ftests%2Fcerts%2Fclient.crt", + env=postgres_env(version), service=f"postgres_{version}_client_ssl", - tag=f"postgres_{version}_client_ssl_no_password" if runtime == "async-std" else f"postgres_{version}_client_ssl_no_password_{runtime}", + tag=f"postgres_{version}_client_ssl_no_password_{runtime}", ) # # mysql # - for version in ["8", "5_7"]: + for version in MYSQL_VERSIONS: + base_features = f"any,mysql,macros,migrate,_unstable-all-types,runtime-{runtime},tls-{tls}" + rsa_features = f"any,mysql,mysql-rsa,macros,migrate,_unstable-all-types,runtime-{runtime},tls-{tls}" + features = rsa_features if tls == "none" else base_features + base_url_args = "ssl-mode=disabled" if tls == "none" else "ssl-mode=required" + client_ssl_ca = ".%2Ftests%2Fcerts%2Fca.crt" + client_ssl_key = ".%2Ftests%2Fcerts%2Fkeys%2Fclient.key" + client_ssl_cert = ".%2Ftests%2Fcerts%2Fclient.crt" + if version == "5_7": + # MySQL 5.7 cannot load Ed25519 certs; use the RSA set for client-SSL targets. + client_ssl_ca = ".%2Ftests%2Fcerts%2Frsa%2Fca.crt" + client_ssl_key = ".%2Ftests%2Fcerts%2Frsa%2Fkeys%2Fclient.key" + client_ssl_cert = ".%2Ftests%2Fcerts%2Frsa%2Fclient.crt" + client_ssl_args = ( + f"sslmode=verify_ca&ssl-ca={client_ssl_ca}" + f"&ssl-key={client_ssl_key}&ssl-cert={client_ssl_cert}" + ) + # Since docker mysql 5.7 using yaSSL(It only supports TLSv1.1), avoid running when using rustls. # https://github.com/docker-library/mysql/issues/567 - if not(version == "5_7" and tls == "rustls"): + # only run when using native-tls + if not (version == "5_7" and tls in ["rustls-ring", "rustls-aws-lc-rs"]): run( - f"cargo test --no-default-features --features any,mysql,macros,_unstable-all-types,runtime-{runtime},tls-{tls}", + f"cargo test --no-default-features --features {features}", comment=f"test mysql {version}", + database_url_args=base_url_args, service=f"mysql_{version}", - tag=f"mysql_{version}" if runtime == "async-std" else f"mysql_{version}_{runtime}", + tag=f"mysql_{version}_{runtime}", ) - ## +client-ssl - if tls != "none" and not(version == "5_7" and tls == "rustls"): + ## +client-ssl + if tls != "none" and not (version == "5_7" and tls in ["rustls-ring", "rustls-aws-lc-rs"]): + run( + f"cargo test --no-default-features --features {base_features}", + comment=f"test mysql {version}_client_ssl no-password", + database_url_args=client_ssl_args, + service=f"mysql_{version}_client_ssl", + tag=f"mysql_{version}_client_ssl_no_password_{runtime}", + ) + + if tls == "native-tls" and runtime == "tokio" and version == "8": run( - f"cargo test --no-default-features --features any,mysql,macros,_unstable-all-types,runtime-{runtime},tls-{tls}", - comment=f"test mysql {version}_client_ssl no-password", - database_url_args="sslmode=verify_ca&ssl-ca=.%2Ftests%2Fcerts%2Fca.crt&ssl-key=.%2Ftests%2Fcerts%2Fkeys%2Fclient.key&ssl-cert=.%2Ftests%2Fcerts%2Fclient.crt", - service=f"mysql_{version}_client_ssl", - tag=f"mysql_{version}_client_ssl_no_password" if runtime == "async-std" else f"mysql_{version}_client_ssl_no_password_{runtime}", + f"cargo test --no-default-features --features {rsa_features}", + comment=f"test mysql {version} tls with rsa", + database_url_args="ssl-mode=required", + service=f"mysql_{version}", + tag=f"mysql_{version}_tls_rsa_{runtime}", ) # # mariadb # - for version in ["verylatest", "10_11", "10_6", "10_5", "10_4"]: + for version in MARIADB_VERSIONS: + base_features = f"any,mysql,macros,migrate,_unstable-all-types,runtime-{runtime},tls-{tls}" + rsa_features = f"any,mysql,mysql-rsa,macros,migrate,_unstable-all-types,runtime-{runtime},tls-{tls}" + features = rsa_features if tls == "none" else base_features + base_url_args = "ssl-mode=disabled" if tls == "none" else "ssl-mode=required" + run( - f"cargo test --no-default-features --features any,mysql,macros,_unstable-all-types,runtime-{runtime},tls-{tls}", + f"cargo test --no-default-features --features {features}", comment=f"test mariadb {version}", + database_url_args=base_url_args, service=f"mariadb_{version}", - tag=f"mariadb_{version}" if runtime == "async-std" else f"mariadb_{version}_{runtime}", + tag=f"mariadb_{version}_{runtime}", ) ## +client-ssl if tls != "none": run( - f"cargo test --no-default-features --features any,mysql,macros,_unstable-all-types,runtime-{runtime},tls-{tls}", + f"cargo test --no-default-features --features {base_features}", comment=f"test mariadb {version}_client_ssl no-password", database_url_args="sslmode=verify_ca&ssl-ca=.%2Ftests%2Fcerts%2Fca.crt&ssl-key=%2Ftests%2Fcerts%2Fkeys%2Fclient.key&ssl-cert=.%2Ftests%2Fcerts%2Fclient.crt", service=f"mariadb_{version}_client_ssl", - tag=f"mariadb_{version}_client_ssl_no_password" if runtime == "async-std" else f"mariadb_{version}_client_ssl_no_password_{runtime}", + tag=f"mariadb_{version}_client_ssl_no_password_{runtime}", + ) + + if tls == "native-tls" and runtime == "tokio" and version == "10_11": + run( + f"cargo test --no-default-features --features {rsa_features}", + comment=f"test mariadb {version} tls with rsa", + database_url_args="ssl-mode=required", + service=f"mariadb_{version}", + tag=f"mariadb_{version}_tls_rsa_{runtime}", ) # TODO: Use [grcov] if available

AltStyle によって変換されたページ (->オリジナル) /