diff --git a/.github/workflows/go_test.yaml b/.github/workflows/go_test.yaml index 0b2903f..1aaa79e 100644 --- a/.github/workflows/go_test.yaml +++ b/.github/workflows/go_test.yaml @@ -121,7 +121,6 @@ jobs: pixi-version: v0.63.2 run-install: false - - name: Build working-directory: go run: | diff --git a/.github/workflows/rust_test.yaml b/.github/workflows/rust_test.yaml new file mode 100644 index 0000000..1c2d1cd --- /dev/null +++ b/.github/workflows/rust_test.yaml @@ -0,0 +1,609 @@ +# Copyright (c) 2025-2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +name: Rust Test + +on: + pull_request: + branches: + - main + paths: + - "rust/**" + - .github/workflows/rust_test.yaml + push: + branches: + - main + paths: + - "rust/**" + - .github/workflows/rust_test.yaml + + workflow_call: + inputs: + repository: + description: "The repository to checkout (in owner/repo short format)" + required: true + type: string + ref: + description: "The ref to checkout" + required: true + type: string + secrets: + SNOWFLAKE_DATABASE: + required: true + SNOWFLAKE_SCHEMA: + required: true + SNOWFLAKE_URI: + required: true + SNOWFLAKE_SCHEMA_SECONDARY: + required: true + SNOWFLAKE_DATABASE_SECONDARY: + required: true + SNOWFLAKE_DATABASE_SECONDARY_SCHEMA: + required: true + +concurrency: + group: ${{ github.repository }}-${{ github.ref }}-Snowflake-Rust-CI + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + test: + name: "Test/${{ matrix.platform }}_${{ matrix.arch }}" + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - { platform: linux, arch: amd64, runner: ubuntu-latest } + - { platform: macos, arch: arm64, runner: macos-latest } + # Disabled temporarily due to openssl-sys build failures on Windows + # - { platform: windows, arch: amd64, runner: windows-latest } + environment: Snowflake CI + env: + CARGO_INCREMENTAL: 0 + permissions: + contents: read + steps: + - name: free up disk space + if: runner.os != 'Windows' + run: | + # Preinstalled tools use a lot of disk space, free up some space + # https://github.com/actions/runner-images/issues/2840 + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name != 'workflow_dispatch' + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - name: "checkout remote" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'workflow_dispatch' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 + with: + toolchain: stable + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: rust + + - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 + with: + pixi-version: v0.63.2 + run-install: false + + - name: Build + working-directory: rust + run: | + if [[ -f ci/scripts/pre-build.sh ]]; then + echo "Loading pre-build" + ./ci/scripts/pre-build.sh test ${{ matrix.platform }} ${{ matrix.arch }} + fi + + set -a + if [[ -f .env.build ]]; then + echo "Loading .env.build" + source .env.build + fi + set +a + cargo build + + - name: Start Test Dependencies + # Can't use Docker on macOS AArch64 runners, and Windows containers + # work but often the container doesn't support Windows + if: runner.os == 'Linux' + working-directory: rust + run: | + if [[ -f compose.yaml ]]; then + if ! docker compose up --detach --wait test-service; then + echo "Service failed to start" + echo "Logs:" + docker compose logs test-service + exit 1 + fi + fi + + - name: Test + working-directory: rust + env: + SNOWFLAKE_DATABASE: ${{ secrets.SNOWFLAKE_DATABASE }} + SNOWFLAKE_SCHEMA: ${{ secrets.SNOWFLAKE_SCHEMA }} + SNOWFLAKE_URI: ${{ secrets.SNOWFLAKE_URI }} + SNOWFLAKE_SCHEMA_SECONDARY: ${{ secrets.SNOWFLAKE_SCHEMA_SECONDARY }} + SNOWFLAKE_DATABASE_SECONDARY: ${{ secrets.SNOWFLAKE_DATABASE_SECONDARY }} + SNOWFLAKE_DATABASE_SECONDARY_SCHEMA: ${{ secrets.SNOWFLAKE_DATABASE_SECONDARY_SCHEMA }} + run: | + set -a + if [[ -f .env ]]; then + source .env + fi + if [[ -f .env.${{ matrix.platform }} ]]; then + source .env.${{ matrix.platform }} + fi + if [[ -f .env.ci ]]; then + source .env.ci + fi + set +a + + if [[ -f ci/scripts/pre-test.sh ]]; then + echo "Loading pre-test" + ./ci/scripts/pre-test.sh ${{ matrix.platform }} ${{ matrix.arch }} + fi + + cargo test + + if [[ -f ci/scripts/post-test.sh ]]; then + ./ci/scripts/post-test.sh + fi + + - name: Lint + if: runner.os == 'Linux' + working-directory: rust + run: | + cargo fmt --check + cargo clippy -- -D warnings + + validate: + name: "Validate/${{ matrix.platform }}_${{ matrix.arch }}" + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + # I think we only need to test one platform, but we can change that later + - { platform: linux, arch: amd64, runner: ubuntu-latest } + environment: Snowflake CI + env: + CARGO_INCREMENTAL: 0 + permissions: + contents: read + steps: + - name: free up disk space + if: runner.os != 'Windows' + run: | + # Preinstalled tools use a lot of disk space, free up some space + # https://github.com/actions/runner-images/issues/2840 + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name != 'workflow_dispatch' + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - name: "checkout remote" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'workflow_dispatch' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 + with: + toolchain: stable + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: rust + + - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 + with: + pixi-version: v0.63.2 + run-install: false + + - name: Log in to ghcr.io + if: runner.os == 'Linux' + env: + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin + + - name: Build Library + working-directory: rust + run: | + if [[ -f ci/scripts/pre-build.sh ]]; then + ./ci/scripts/pre-build.sh test ${{ matrix.platform }} ${{ matrix.arch }} + fi + set -a + if [[ -f .env.build ]]; then + echo "Loading .env.build" + source .env.build + fi + if [[ -f .env.test ]]; then + source .env.test + fi + set +a + pixi run adbc-make build DEBUG=true VERBOSE=true DRIVER=snowflake IMPL_LANG=rust BUILD_TAGS=minicore_disabled + + - name: Start Test Dependencies + # Can't use Docker on macOS AArch64 runners, and windows containers + # work but often the container doesn't support Windows + if: runner.os == 'Linux' + working-directory: rust + run: | + if [[ -f compose.yaml ]]; then + if ! docker compose up --detach --wait test-service; then + echo "Service failed to start" + echo "Logs:" + docker compose logs test-service + exit 1 + fi + fi + + - name: Validate + if: runner.os == 'Linux' + env: + SNOWFLAKE_DATABASE: ${{ secrets.SNOWFLAKE_DATABASE }} + SNOWFLAKE_SCHEMA: ${{ secrets.SNOWFLAKE_SCHEMA }} + SNOWFLAKE_URI: ${{ secrets.SNOWFLAKE_URI }} + SNOWFLAKE_SCHEMA_SECONDARY: ${{ secrets.SNOWFLAKE_SCHEMA_SECONDARY }} + SNOWFLAKE_DATABASE_SECONDARY: ${{ secrets.SNOWFLAKE_DATABASE_SECONDARY }} + SNOWFLAKE_DATABASE_SECONDARY_SCHEMA: ${{ secrets.SNOWFLAKE_DATABASE_SECONDARY_SCHEMA }} + working-directory: rust + run: | + set -a + if [[ -f .env ]]; then + source .env + fi + if [[ -f .env.${{ matrix.platform }} ]]; then + source .env.${{ matrix.platform }} + fi + if [[ -f .env.ci ]]; then + source .env.ci + fi + set +a + + if [[ -f ci/scripts/pre-test.sh ]]; then + echo "Loading pre-test" + ./ci/scripts/pre-test.sh ${{ matrix.platform }} ${{ matrix.arch }} + fi + + docker ps + pixi run validate + + if [[ -f ci/scripts/post-test.sh ]]; then + ./ci/scripts/post-test.sh + fi + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: validation-report + path: "rust/validation-report.xml" + retention-days: 7 + + - name: Generate docs + working-directory: rust + run: | + pixi run gendocs --output generated + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: docs + path: "rust/generated/snowflake.md" + retention-days: 2 + + build: + name: "Build adbc-snowflake/${{ matrix.platform }}_${{ matrix.arch }}" + needs: test + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - { platform: linux, arch: amd64, runner: ubuntu-latest } + - { platform: linux, arch: arm64, runner: ubuntu-24.04-arm } + - { platform: macos, arch: arm64, runner: macos-latest } + # Disabled temporarily due to openssl-sys build failures on Windows + # - { platform: windows, arch: amd64, runner: windows-latest } + permissions: + contents: read + packages: read + env: + CARGO_INCREMENTAL: 0 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name != 'workflow_dispatch' + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - name: "checkout remote" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'workflow_dispatch' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 + with: + toolchain: stable + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: rust + + - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 + with: + pixi-version: v0.63.2 + run-install: false + + - name: Install dev tools + working-directory: rust + run: | + pixi install + + - name: Log in to ghcr.io + if: runner.os == 'Linux' + env: + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin + + - name: Build Library + working-directory: rust + run: | + if [[ -f ci/scripts/pre-build.sh ]]; then + ./ci/scripts/pre-build.sh release ${{ matrix.platform }} ${{ matrix.arch }} + fi + set -a + if [[ -f .env.build ]]; then + echo "Loading .env.build" + source .env.build + fi + if [[ -f .env.release ]]; then + source .env.release + fi + set +a + pixi run adbc-make check CI=true VERBOSE=true DRIVER=snowflake IMPL_LANG=rust BUILD_TAGS=minicore_disabled + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: drivers-${{ matrix.platform }}-${{ matrix.arch }} + path: rust/build/libadbc_driver_snowflake.* + retention-days: 2 + + package: + name: "Generate Packages" + runs-on: ubuntu-latest + needs: build + permissions: + contents: read + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name != 'workflow_dispatch' + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - name: "checkout remote" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'workflow_dispatch' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 + with: + toolchain: stable + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: rust + + - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 + with: + pixi-version: v0.63.2 + run-install: false + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: "drivers-*" + path: "~/drivers" + + - name: Install cargo-about + run: | + mkdir -p ~/.cargo/bin + curl -LsSf https://github.com/EmbarkStudios/cargo-about/releases/download/0.8.4/cargo-about-0.8.4-x86_64-unknown-linux-musl.tar.gz | tar zxf - -C ~/.cargo/bin --strip-components=1 cargo-about-0.8.4-x86_64-unknown-linux-musl/cargo-about + + - name: Generate packages + working-directory: rust + run: | + pixi install + + pixi run adbc-gen-package \ + --name snowflake \ + --root $(pwd) \ + --manifest-template $(pwd)/manifest.toml \ + ${{ (inputs.release && '--release') || '' }}\ + -o ~/packages \ + ~/drivers/drivers-*-*/ + + ls -laR ~/packages + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: all-packages + path: ~/packages + retention-days: 7 + + test-packages: + name: "Test Packages/${{ matrix.platform }}_${{ matrix.arch }}" + runs-on: ${{ matrix.runner }} + needs: + - package + strategy: + fail-fast: false + matrix: + include: + - { platform: linux, arch: amd64, runner: ubuntu-latest } + - { platform: macos, arch: arm64, runner: macos-latest } + # Disabled temporarily due to openssl-sys build failures on Windows + # - { platform: windows, arch: amd64, runner: windows-latest } + steps: + # for now, install dbc from main + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version: 'stable' + - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 + with: + pixi-version: v0.63.2 + run-install: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: "all-packages" + path: "~/packages" + - name: install package + # dbc uses the filename as the driver name + run: | + echo "Installing dbc" + git clone --depth 1 https://github.com/columnar-tech/dbc + cd dbc + go build ./cmd/dbc + echo "Installed dbc" + ls -laR ~/packages + driver_pkg=$(find ~/packages -name '*_${{ matrix.platform }}_${{ matrix.arch }}_*.tar.gz') + echo "Installing ${driver_pkg}" + ./dbc install --no-verify "${driver_pkg}" + echo "Installed ${driver_pkg}" + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name != 'workflow_dispatch' + with: + fetch-depth: 1 + persist-credentials: false + submodules: 'recursive' + - name: "checkout remote" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'workflow_dispatch' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + fetch-depth: 1 + persist-credentials: false + submodules: 'recursive' + - name: load package + working-directory: rust + run: | + if [[ -f ci/scripts/pre-build.sh ]]; then + echo "Loading pre-build" + ./ci/scripts/pre-build.sh test ${{ matrix.platform }} ${{ matrix.arch }} + fi + set -a + if [[ -f .env.build ]]; then + echo "Loading .env.build" + source .env.build + fi + if [[ -f .env.ci ]]; then + source .env.ci + fi + set +a + pixi exec -s adbc-driver-manager -s pyarrow -s pytest python -m pytest -vs ci/test_package.py + + release: + name: "Release (Dry Run)" + runs-on: ubuntu-latest + needs: + - package + - test-packages + - validate + permissions: + contents: read + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name != 'workflow_dispatch' + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - name: "checkout remote" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'workflow_dispatch' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 + with: + pixi-version: v0.63.2 + run-install: false + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: "all-packages" + path: "~/packages" + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: "docs" + path: "~/packages" + + - name: Release (dry-run) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: rust + run: | + git tag go/v1000.0.0 + tag=go/v1000.0.0 + + pixi run release --dry-run $(pwd) $tag + echo gh release upload $tag $(find ~/packages -name '*.tar.gz') $(find ~/packages -name 'manifest.yaml') $(find ~/packages -name '*.md') diff --git a/.rat-excludes b/.rat-excludes index 2ce365a..2296e28 100644 --- a/.rat-excludes +++ b/.rat-excludes @@ -16,9 +16,15 @@ .gitmodules */go.sum */pixi.lock +*/Cargo.lock */*.csproj */*.sln go/license.tpl go/validation/queries/*/*.json go/validation/queries/*/*.sql +rust/license.tpl +rust/validation/queries/*/*.json +rust/validation/queries/*/*.sql +rust/validation/queries/*/*/*.json +rust/validation/queries/*/*/*.sql csharp/test/Interop/Resources/snowflakeconfig.json diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml new file mode 100644 index 0000000..e7ac05c --- /dev/null +++ b/rust/.cargo/config.toml @@ -0,0 +1,18 @@ +# Copyright (c) 2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +# Disable dynamic linking to avoid linker errors from sf_core's dylib crate-type +# when building test binaries on Linux. +[build] +rustflags = ["-C", "prefer-dynamic=no"] diff --git a/rust/.gitattributes b/rust/.gitattributes new file mode 100644 index 0000000..402a99b --- /dev/null +++ b/rust/.gitattributes @@ -0,0 +1,16 @@ +# Copyright (c) 2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..a11c36d --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,17 @@ +# Copyright (c) 2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +# pixi environments +.pixi/* +!.pixi/config.toml diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..19e804e --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,5532 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adbc-driver-snowflake" +version = "0.1.0" +dependencies = [ + "adbc_core", + "adbc_ffi", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-cast 58.1.0", + "arrow-schema 58.1.0", + "env_logger 0.10.2", + "log", + "openssl", + "percent-encoding", + "sf_core", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "adbc_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46b169525a7c41670fe95874103c7c6ce713ac699123f81a200bc31f9ad3b02e" +dependencies = [ + "arrow-array 58.1.0", + "arrow-schema 58.1.0", +] + +[[package]] +name = "adbc_ffi" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6851c2ab953511cf7a244aadcbc0586442fd3c67dfe371457369048880dd513" +dependencies = [ + "adbc_core", + "arrow-array 58.1.0", + "arrow-schema 58.1.0", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +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_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrow" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e833808ff2d94ed40d9379848a950d995043c7fb3e81a30b383f4c6033821cc" +dependencies = [ + "arrow-arith", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-cast 56.2.0", + "arrow-csv", + "arrow-data 56.2.0", + "arrow-ipc", + "arrow-json", + "arrow-ord 56.2.0", + "arrow-row", + "arrow-schema 56.2.0", + "arrow-select 56.2.0", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad08897b81588f60ba983e3ca39bda2b179bdd84dced378e7df81a5313802ef8" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "chrono", + "num", +] + +[[package]] +name = "arrow-array" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8548ca7c070d8db9ce7aa43f37393e4bfcf3f2d3681df278490772fd1673d08d" +dependencies = [ + "ahash", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "chrono", + "half", + "hashbrown 0.16.1", + "num", +] + +[[package]] +name = "arrow-array" +version = "58.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772bd34cacdda8baec9418d80d23d0fb4d50ef0735685bd45158b83dfeb6e62d" +dependencies = [ + "ahash", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", + "chrono", + "chrono-tz", + "half", + "hashbrown 0.16.1", + "num-complex", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-buffer" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e003216336f70446457e280807a73899dd822feaf02087d31febca1363e2fccc" +dependencies = [ + "bytes", + "half", + "num", +] + +[[package]] +name = "arrow-buffer" +version = "58.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898f4cf1e9598fdb77f356fdf2134feedfd0ee8d5a4e0a5f573e7d0aec16baa4" +dependencies = [ + "bytes", + "half", + "num-bigint", + "num-traits", +] + +[[package]] +name = "arrow-cast" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919418a0681298d3a77d1a315f625916cb5678ad0d74b9c60108eb15fd083023" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "arrow-select 56.2.0", + "atoi", + "base64 0.22.1", + "chrono", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-cast" +version = "58.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0127816c96533d20fc938729f48c52d3e48f99717e7a0b5ade77d742510736d" +dependencies = [ + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-ord 58.1.0", + "arrow-schema 58.1.0", + "arrow-select 58.1.0", + "atoi", + "base64 0.22.1", + "chrono", + "half", + "lexical-core", + "num-traits", + "ryu", +] + +[[package]] +name = "arrow-csv" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa9bf02705b5cf762b6f764c65f04ae9082c7cfc4e96e0c33548ee3f67012eb" +dependencies = [ + "arrow-array 56.2.0", + "arrow-cast 56.2.0", + "arrow-schema 56.2.0", + "chrono", + "csv", + "csv-core", + "regex", +] + +[[package]] +name = "arrow-data" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5c64fff1d142f833d78897a772f2e5b55b36cb3e6320376f0961ab0db7bd6d0" +dependencies = [ + "arrow-buffer 56.2.0", + "arrow-schema 56.2.0", + "half", + "num", +] + +[[package]] +name = "arrow-data" +version = "58.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d10beeab2b1c3bb0b53a00f7c944a178b622173a5c7bcabc3cb45d90238df4" +dependencies = [ + "arrow-buffer 58.1.0", + "arrow-schema 58.1.0", + "half", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-ipc" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3594dcddccc7f20fd069bc8e9828ce37220372680ff638c5e00dea427d88f5" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "arrow-select 56.2.0", + "flatbuffers", +] + +[[package]] +name = "arrow-json" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88cf36502b64a127dc659e3b305f1d993a544eab0d48cce704424e62074dc04b" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-cast 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "chrono", + "half", + "indexmap", + "lexical-core", + "memchr", + "num", + "serde", + "serde_json", + "simdutf8", +] + +[[package]] +name = "arrow-ord" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8f82583eb4f8d84d4ee55fd1cb306720cddead7596edce95b50ee418edf66f" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "arrow-select 56.2.0", +] + +[[package]] +name = "arrow-ord" +version = "58.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a7ba279b20b52dad300e68cfc37c17efa65e68623169076855b3a9e941ca5" +dependencies = [ + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", + "arrow-select 58.1.0", +] + +[[package]] +name = "arrow-row" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d07ba24522229d9085031df6b94605e0f4b26e099fb7cdeec37abd941a73753" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "half", +] + +[[package]] +name = "arrow-schema" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3aa9e59c611ebc291c28582077ef25c97f1975383f1479b12f3b9ffee2ffabe" +dependencies = [ + "bitflags", +] + +[[package]] +name = "arrow-schema" +version = "58.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c30a1365d7a7dc50cc847e54154e6af49e4c4b0fddc9f607b687f29212082743" +dependencies = [ + "bitflags", +] + +[[package]] +name = "arrow-select" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c41dbbd1e97bfcaee4fcb30e29105fb2c75e4d82ae4de70b792a5d3f66b2e7a" +dependencies = [ + "ahash", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "num", +] + +[[package]] +name = "arrow-select" +version = "58.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78694888660a9e8ac949853db393af2a8b8fc82c19ce333132dfa2e72cc1a7fe" +dependencies = [ + "ahash", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", + "num-traits", +] + +[[package]] +name = "arrow-string" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53f5183c150fbc619eede22b861ea7c0eebed8eaac0333eaa7f6da5205fd504d" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "arrow-select 56.2.0", + "memchr", + "num", + "regex", + "regex-syntax", +] + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-compression" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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-config" +version = "1.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.4.0", + "sha1", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.129.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d4e8410fadbc0ee453145dd77a4958227b18b05bf67c2795d0a8b8596c9aa0f" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "http-body 1.0.1", + "lru 0.16.3", + "percent-encoding", + "regex-lite", + "sha2", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aadc669e184501caaa6beafb28c6267fc1baef0810fb58f9b205485ca3f2567" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1342a7db8f358d3de0aed2007a0b54e875458e39848d54cc1d46700b2bfcb0a8" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab41ad64e4051ecabeea802d6a17845a91e83287e1dd249e6963ea1ba78c428a" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "p256", + "percent-encoding", + "ring", + "sha2", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.64.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6750f3dd509b0694a4377f0293ed2f9630d710b1cebe281fa8bac8f099f88bc6" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "md-5", + "pin-project-lite", + "sha1", + "sha2", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.13", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.9.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[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 = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[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 = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[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.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[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 = "crc-fast" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" +dependencies = [ + "crc", + "digest", + "rustversion", + "spin", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[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.117", +] + +[[package]] +name = "doc-comment" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve", + "rfc6979", + "signature 1.6.4", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error_trace" +version = "0.1.0" +source = "git+https://github.com/snowflakedb/universal-driver?rev=1b8fc700c6fc10464851407da90ac5f9645758f7#1b8fc700c6fc10464851407da90ac5f9645758f7" +dependencies = [ + "error_trace_derive", + "snafu 0.8.9", +] + +[[package]] +name = "error_trace_derive" +version = "0.1.0" +source = "git+https://github.com/snowflakedb/universal-driver?rev=1b8fc700c6fc10464851407da90ac5f9645758f7#1b8fc700c6fc10464851407da90ac5f9645758f7" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "faststr" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca7d44d22004409a61c393afb3369c8f7bb74abcae49fe249ee01dcc3002113" +dependencies = [ + "bytes", + "rkyv", + "serde", + "simdutf8", +] + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" +dependencies = [ + "bitflags", + "rustc_version", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[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 = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[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 = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[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" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[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 = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[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", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.9.0", + "hyper-util", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.9.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.9.0", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +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 = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jwt" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" +dependencies = [ + "base64 0.13.1", + "crypto-common", + "digest", + "hmac", + "openssl", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "linux-keyutils", + "log", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-keyutils" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[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", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 3.7.0", + "security-framework-sys", + "tempfile", +] + +[[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 = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[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-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[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-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "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 = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "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.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-src" +version = "300.6.0+3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "opentelemetry" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +dependencies = [ + "async-trait", + "bytes", + "http 1.4.0", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" +dependencies = [ + "http 1.4.0", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost 0.13.5", + "reqwest", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost 0.13.5", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d059a296a47436748557a353c5e6c5705b9470ef6c95cfc52c21a8814ddac2" + +[[package]] +name = "opentelemetry_sdk" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +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 = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive 0.12.6", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive 0.14.3", +] + +[[package]] +name = "prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +dependencies = [ + "heck 0.5.0", + "itertools 0.14.0", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost 0.14.3", + "prost-types", + "regex", + "syn 2.0.117", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost 0.14.3", +] + +[[package]] +name = "proto_generator" +version = "0.1.0" +source = "git+https://github.com/snowflakedb/universal-driver?rev=1b8fc700c6fc10464851407da90ac5f9645758f7#1b8fc700c6fc10464851407da90ac5f9645758f7" +dependencies = [ + "clap", + "env_logger 0.11.10", + "log", + "prost 0.12.6", + "prost-build", + "serde", + "serde_json", + "snafu 0.7.5", + "tempfile", + "walkdir", +] + +[[package]] +name = "proto_utils" +version = "0.1.0" +source = "git+https://github.com/snowflakedb/universal-driver?rev=1b8fc700c6fc10464851407da90ac5f9645758f7#1b8fc700c6fc10464851407da90ac5f9645758f7" + +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.37", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls 0.23.37", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.3", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.9.0", + "hyper-rustls 0.27.7", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.37", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.26.4", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + +[[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.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a30e631b7f4a03dee9056b8ef6982e8ba371dd5bedb74d3ec86df4499132c70" +dependencies = [ + "bytes", + "hashbrown 0.16.1", + "indexmap", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.11", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der 0.6.1", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[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 = "sf_core" +version = "0.0.0" +source = "git+https://github.com/snowflakedb/universal-driver?rev=1b8fc700c6fc10464851407da90ac5f9645758f7#1b8fc700c6fc10464851407da90ac5f9645758f7" +dependencies = [ + "arrow", + "arrow-ipc", + "aws-config", + "aws-credential-types", + "aws-lc-rs", + "aws-sdk-s3", + "base64 0.22.1", + "chrono", + "clap", + "const-oid", + "der 0.7.10", + "der-parser", + "dirs", + "error_trace", + "flate2", + "futures", + "glob", + "hex", + "html-escape", + "infer", + "jwt", + "keyring", + "libc", + "lru 0.12.5", + "memchr", + "num-traits", + "once_cell", + "openssl", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "pkcs1", + "prost 0.14.3", + "proto_generator", + "proto_utils", + "rand", + "reqwest", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-pemfile", + "rustls-webpki 0.102.8", + "serde", + "serde_json", + "sha2", + "signature 2.2.0", + "snafu 0.8.9", + "sonic-rs", + "spki 0.7.3", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "toml", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", + "url", + "urlencoding", + "uuid", + "x509-cert", + "x509-parser", + "zeroize", +] + +[[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 = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive 0.7.5", +] + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive 0.8.9", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sonic-number" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3775c3390edf958191f1ab1e8c5c188907feebd0f3ce1604cb621f72961dbf32" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "sonic-rs" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d971cc77a245ccf1756dbd1a87c3e7f709c0191464096510d43eec056d0f2c4f" +dependencies = [ + "ahash", + "bumpalo", + "bytes", + "cfg-if", + "faststr", + "itoa", + "ref-cast", + "serde", + "simdutf8", + "sonic-number", + "sonic-simd", + "thiserror 2.0.18", + "zmij", +] + +[[package]] +name = "sonic-simd" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f99e664ecd2d85a68c87e3c7a3cfe691f647ea9e835de984aba4d54a41f817d4" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[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.117", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[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.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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 = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio" +version = "1.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.37", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "slab", + "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", + "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 = "tonic" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project", + "prost 0.13.5", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "async-compression", + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "iri-string", + "pin-project-lite", + "tokio", + "tokio-util", + "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.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[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-opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "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 = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[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.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[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 = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[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 = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +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.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der 0.7.10", + "spki 0.7.3", + "tls_codec", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..e34f618 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,43 @@ +# Copyright (c) 2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +[package] +name = "adbc-driver-snowflake" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +adbc_core = ">=0.22.0" +adbc_ffi = ">=0.22.0" +sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core", rev = "1b8fc700c6fc10464851407da90ac5f9645758f7" } +arrow-array = { version = "58.1.0", default-features = false, features = ["ffi", "chrono-tz"] } +arrow-buffer = { version = "58.1.0", default-features = false } +arrow-schema = { version = "58.1.0", default-features = false } +arrow-cast = { version = "58.1.0", default-features = false } +log = "0.4.22" +tokio = { version = "1", features = ["rt-multi-thread"] } +percent-encoding = "2.3.2" +# Force vendored (static) OpenSSL for manylinux compatibility. +# This crate doesn't call openssl APIs directly; it exists to ensure +# all transitive deps link statically. Bump manually on OpenSSL CVEs. +openssl = { version = "0.10.76", features = ["vendored"] } + +[dev-dependencies] +env_logger = "0.10" +tracing = "0.1.44" +tracing-subscriber = "0.3.23" diff --git a/rust/about.toml b/rust/about.toml new file mode 100644 index 0000000..abf2eeb --- /dev/null +++ b/rust/about.toml @@ -0,0 +1,55 @@ +# Copyright (c) 2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +accepted = [ + "Apache-2.0", + "MIT", + "ISC", + "BSD-3-Clause", + "Zlib", + "Unicode-3.0", + "MPL-2.0", + "CC0-1.0", + "CDLA-Permissive-2.0" +] + +[error_trace.clarify] +license = "Apache-2.0" +[[error_trace.clarify.files]] +path = "../LICENSE.txt" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[error_trace_derive.clarify] +license = "Apache-2.0" +[[error_trace_derive.clarify.files]] +path = "../LICENSE.txt" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[proto_generator.clarify] +license = "Apache-2.0" +[[proto_generator.clarify.files]] +path = "../LICENSE.txt" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[proto_utils.clarify] +license = "Apache-2.0" +[[proto_utils.clarify.files]] +path = "../LICENSE.txt" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[sf_core.clarify] +license = "Apache-2.0" +[[sf_core.clarify.files]] +path = "../LICENSE.txt" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" diff --git a/rust/build.rs b/rust/build.rs new file mode 100644 index 0000000..0841528 --- /dev/null +++ b/rust/build.rs @@ -0,0 +1,20 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +fn main() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set"); + if target_os == "linux" || target_os == "android" { + println!("cargo:rustc-link-arg=-Wl,--exclude-libs,ALL"); + } +} diff --git a/rust/ci/scripts/pre-build.sh b/rust/ci/scripts/pre-build.sh new file mode 100755 index 0000000..dd582de --- /dev/null +++ b/rust/ci/scripts/pre-build.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +set -ex + +if [[ "$2" == "windows" ]]; then + choco install openssl -y + echo "OPENSSL_DIR='C:\Program Files\OpenSSL'" > .env.build +elif [[ "$2" == "linux" ]]; then + echo "Checking if we need to patch adbc-drivers-dev manylinux-rust Dockerfile" + + # Ensure pixi environment is set up so we can find adbc_drivers_dev + pixi install + + ADBC_DEV_DIR=$(pixi run python -c "import os, adbc_drivers_dev; print(os.path.dirname(adbc_drivers_dev.__file__))") + DOCKERFILE="$ADBC_DEV_DIR/compose/manylinux-rust/Dockerfile" + COMPOSEFILE="$ADBC_DEV_DIR/compose.yaml" + + if [ -f "$DOCKERFILE" ] && [ -f "$COMPOSEFILE" ]; then + echo "Patching $DOCKERFILE to include perl modules for openssl vendored build" + sed -i 's/wget openssl openssl-devel openssl-static/wget openssl openssl-devel openssl-static perl-IPC-Cmd perl-Time-Piece/g' "$DOCKERFILE" + + echo "Patching $COMPOSEFILE to use local image tag and set HOME=/tmp" + sed -i 's/image: ghcr.io\/adbc-drivers\/dev/image: local-patched\/adbc-drivers-dev/g' "$COMPOSEFILE" + grep -q 'HOME=/tmp' "$COMPOSEFILE" || \ + sed -i 's/^ volumes:/ environment:\n - HOME=\/tmp\n volumes:/' "$COMPOSEFILE" + else + echo "Could not find Dockerfile at $DOCKERFILE or compose.yaml at $COMPOSEFILE — aborting." + exit 1 + fi +fi diff --git a/rust/ci/test_package.py b/rust/ci/test_package.py new file mode 100644 index 0000000..b78021d --- /dev/null +++ b/rust/ci/test_package.py @@ -0,0 +1,27 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +import adbc_driver_manager.dbapi +import pytest + + +def test_package() -> None: + uri = "snowflake://example:foo@nonexistent/test" + # Verify the driver loads and reaches the auth phase (not a load failure). + with pytest.raises( + adbc_driver_manager.dbapi.ProgrammingError, + match="(?i)(?:failed to (?:auth|login)|unauthenticated)", + ): + with adbc_driver_manager.dbapi.connect(driver="snowflake", uri=uri): + pass diff --git a/rust/docs/snowflake.md b/rust/docs/snowflake.md new file mode 100644 index 0000000..083afe7 --- /dev/null +++ b/rust/docs/snowflake.md @@ -0,0 +1,113 @@ +--- +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. +--- + +{{ cross_reference|safe }} +# Snowflake Driver {{ version }} + +{{ heading|safe }} + +This driver provides access to [Snowflake][snowflake], a cloud-based data warehouse platform. + +## Installation & Quickstart + +The driver can be installed with [dbc](https://docs.columnar.tech/dbc): + +```bash +dbc install snowflake +``` + +## Pre-requisites + +Using the Snowflake driver requires a Snowflake account and authentication. See [Getting Started With Snowflake](https://docs.snowflake.com/en/user-guide-getting-started) for instructions. + +## Connecting + +To connect, replace the Snowflake options below with the appropriate values for your situation and run the following: + +```python +from adbc_driver_manager import dbapi + +conn = dbapi.connect( + driver="snowflake", + db_kwargs={ + "username": "USER", + + ### for username/password authentication: ### + "adbc.snowflake.sql.auth_type": "auth_snowflake", + "password": "PASS", + + ### for JWT authentication: ### + #"adbc.snowflake.sql.auth_type": "auth_jwt", + #"adbc.snowflake.sql.client_option.jwt_private_key": "/path/to/rsa_key.p8", + + "adbc.snowflake.sql.account": "ACCOUNT-IDENT", + "adbc.snowflake.sql.db": "SNOWFLAKE_SAMPLE_DATA", + "adbc.snowflake.sql.schema": "TPCH_SF1", + "adbc.snowflake.sql.warehouse": "MY_WAREHOUSE", + "adbc.snowflake.sql.role": "MY_ROLE" + } +) +``` + +Note: The example above is for Python using the [adbc-driver-manager](https://pypi.org/project/adbc-driver-manager) package but the process will be similar for other driver managers. See [adbc-quickstarts](https://github.com/columnar-tech/adbc-quickstarts). + +The driver supports connecting with individual options or connection strings. + +### Connection String Format + +Snowflake URI syntax: + +``` +snowflake://user[:password]@host[:port]/database[/schema][?param1=value1¶m2=value2] +``` + +This follows the [Go Snowflake Driver Connection String](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String) format with the addition of the `snowflake://` scheme. + +Components: + +- `scheme`: `snowflake://` (required) +- `user/password`: (optional) For username/password authentication +- `host`: (required) The Snowflake account identifier string (e.g., myorg-account1) OR the full hostname (e.g., private.network.com). If a full hostname is used, the actual Snowflake account identifier must be provided separately via the account query parameter (see example 3). +- `port`: The port is optional and defaults to 443. +- `database`: Database name (required) +- `schema`: Schema name (optional) +- `Query Parameters`: Additional configuration options. For a complete list of parameters, see the [Go Snowflake Driver Connection Parameters](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_Parameters) + +:::{note} +Reserved characters in URI elements must be URI-encoded. For example, `@` becomes `%40`. +::: + +Examples: + +- `snowflake://jane.doe:MyS3cr3t!@myorg-account1/ANALYTICS_DB/SALES_DATA?warehouse=WH_XL&role=ANALYST` +- `snowflake://service_user@myorg-account2/RAW_DATA_LAKE?authenticator=oauth&application=ADBC_APP` +- `snowflake://sys_admin@private.network.com:443/OPS_MONITOR/DBA?account=vpc-id-1234&insecureMode=true&client_session_keep_alive=true` (Uses full hostname, requires explicit account parameter) + +## Feature & Type Support + +{{ features|safe }} + +### Types + +{{ types|safe }} + +{{ footnotes|safe }} + +## Previous Versions + +There are no previous published versions of this driver yet. + +[snowflake]: https://www.snowflake.com/ diff --git a/rust/license.tpl b/rust/license.tpl new file mode 100644 index 0000000..68d9adb --- /dev/null +++ b/rust/license.tpl @@ -0,0 +1,228 @@ +{{! + License template for binary artifacts. + + To use: + cargo about generate ./license.tpl > ./LICENSE.txt +}} + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + 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 [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + 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. + +-------------------------------------------------------------------------------- + +This project includes code from Apache Arrow ADBC. + +Copyright: 2022 The Apache Software Foundation. +Home page: https://arrow.apache.org/ +License: http://www.apache.org/licenses/LICENSE-2.0 + +{{#each licenses}} +{{#each used_by}} +-------------------------------------------------------------------------------- + +3rdparty dependency {{crate.name}} ({{crate.version}}) +is statically linked in certain binary distributions, like the Python wheels. +{{crate.name}} is under the {{../name}} license. +{{/each}} +{{#if (not (eq name "Apache-2.0"))}} +{{text}} +{{/if}} +{{/each}} diff --git a/rust/manifest.toml b/rust/manifest.toml new file mode 100644 index 0000000..46d65f2 --- /dev/null +++ b/rust/manifest.toml @@ -0,0 +1,21 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +name = "ADBC Driver Foundry Driver for Snowflake (rust)" +description = "An ADBC driver for Snowflake developed by the ADBC Driver Foundry" +publisher = "ADBC Drivers Contributors" +license = "Apache-2.0" + +[ADBC] +version = "v1.1.0" diff --git a/rust/pixi.lock b/rust/pixi.lock new file mode 100644 index 0000000..c279f49 --- /dev/null +++ b/rust/pixi.lock @@ -0,0 +1,1707 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-lazy-fixtures-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - pypi: https://files.pythonhosted.org/packages/ac/7d/3e131221995aef7edfd4dd0b09f14b7e51772d28eb362a0e6c3b8301a22a/adbc_driver_manager-1.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: git+https://github.com/adbc-drivers/dev#ffdbb1a1237b89cafb15a7420667f54838986681 + - pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/13/b58d718415cde993823a54952ea511d2612302f1d2bc220549d0cef752a4/duckdb-1.5.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d5/80/1a87f6e043e04cfa125380a73ef9f87a8c58292b7d4a6ed2e6203b4cd534/pygit2-1.19.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c6/13/b57ab75b0f60b5ee8cb8924bc01a5c419ed3221e00f8f11f8c059a707eb7/sqlglot-30.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: git+https://github.com/adbc-drivers/validation#80cd7910a2c7495301d617363a006c435cb6d044 + - pypi: https://files.pythonhosted.org/packages/56/5c/8d6dc529595b5387f5727cd6c2c5b8615851d95fec5c599a61ef239cc1b3/whenever-0.9.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + linux-aarch64: + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.3-hcab7f73_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.45.1-default_h1979696_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.4-hfae3067_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libmpdec-4.0.0-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.52.0-h10b116e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.3-h1022ec0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.1-h546c87b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-lazy-fixtures-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.13.12-h4c0d347_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h0dc03b3_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + - pypi: https://files.pythonhosted.org/packages/52/7b/2c076500e60cac3c2761eeecc82afed42af22d3a65cf3cd8d8034ffd75ad/adbc_driver_manager-1.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: git+https://github.com/adbc-drivers/dev#ffdbb1a1237b89cafb15a7420667f54838986681 + - pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/53/07/1390e69db922423b2e111e32ed342b3e8fad0a31c144db70681ea1ba4d56/duckdb-1.5.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/a7/02/02f0f56b9b0b044018d9047adf68ba842ebda662ba43ace942ed904f8e9d/pygit2-1.19.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/c6/13/b57ab75b0f60b5ee8cb8924bc01a5c419ed3221e00f8f11f8c059a707eb7/sqlglot-30.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: git+https://github.com/adbc-drivers/validation#80cd7910a2c7495301d617363a006c435cb6d044 + - pypi: https://files.pythonhosted.org/packages/b5/dc/090732e6e75f15a6084700d3247db6aa1f885971b637531529c62c4ba1c6/whenever-0.9.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.4-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.2-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.52.0-h1ae2325_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.1-hd24854e_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-lazy-fixtures-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.12-h20e6be0_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - pypi: https://files.pythonhosted.org/packages/d8/9c/6f9929b53cd578bef06b8d000e0ab829b982bcf5b22a6c99acfbad2aab34/adbc_driver_manager-1.10.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl + - pypi: git+https://github.com/adbc-drivers/dev#ffdbb1a1237b89cafb15a7420667f54838986681 + - pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/fc/c916e928606946209c20fb50898dabf120241fb528a244e2bd8cde1bd9e2/duckdb-1.5.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e2/1f/f67ec7f78a34ed14dbd3acf05ed23c4c8c2336ba6f3ca78d6b9962878435/pygit2-1.19.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/c6/13/b57ab75b0f60b5ee8cb8924bc01a5c419ed3221e00f8f11f8c059a707eb7/sqlglot-30.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: git+https://github.com/adbc-drivers/validation#80cd7910a2c7495301d617363a006c435cb6d044 + - pypi: https://files.pythonhosted.org/packages/ea/81/d59f0e226ef542fc4bc86567d7b9e2bf9016c353b1f83661ee3913a140a7/whenever-0.9.5-cp313-cp313-macosx_11_0_arm64.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.4-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.2-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.52.0-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.1-hf411b9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-lazy-fixtures-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.12-h09917c8_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - pypi: https://files.pythonhosted.org/packages/97/c2/2ed6c856dd56bbc0a45aaab67f6b1f0a846296f20d5ad625a3c5e7084e4f/adbc_driver_manager-1.10.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl + - pypi: git+https://github.com/adbc-drivers/dev#ffdbb1a1237b89cafb15a7420667f54838986681 + - pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/96/4460429651e371eb5ff745a4790e7fa0509c7a58c71fc4f0f893404c9646/duckdb-1.5.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/6d/01/98f74ecbe92f042d27e4de3cd7f093422d523cc67fdc74e6a65dbe4efbb8/pygit2-1.19.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c6/13/b57ab75b0f60b5ee8cb8924bc01a5c419ed3221e00f8f11f8c059a707eb7/sqlglot-30.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: git+https://github.com/adbc-drivers/validation#80cd7910a2c7495301d617363a006c435cb6d044 + - pypi: https://files.pythonhosted.org/packages/81/b4/17d4bc76ca73c21eb5b7883d10d8bacb7ce7a30a8f36501db2373c63ffb3/whenever-0.9.5-cp313-cp313-win_amd64.whl +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de + depends: + - __glibc >=2.17,<3.0.a0 + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 28948 + timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: a2527b1d81792a0ccd2c05850960df119c2b6d8f5fdec97f2db7d25dc23b1068 + md5: 468fd3bb9e1f671d36c2cbc677e56f1d + depends: + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 28926 + timestamp: 1770939656741 +- pypi: https://files.pythonhosted.org/packages/52/7b/2c076500e60cac3c2761eeecc82afed42af22d3a65cf3cd8d8034ffd75ad/adbc_driver_manager-1.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + name: adbc-driver-manager + version: 1.10.0 + sha256: ae24386989dfa055a09c800d13d5278d5d0399aee2548f071f414e6b8af63fc8 + requires_dist: + - typing-extensions + - pandas ; extra == 'dbapi' + - pyarrow>=14.0.1 ; extra == 'dbapi' + - duckdb ; extra == 'test' + - pandas ; extra == 'test' + - polars ; extra == 'test' + - pyarrow>=14.0.1 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/97/c2/2ed6c856dd56bbc0a45aaab67f6b1f0a846296f20d5ad625a3c5e7084e4f/adbc_driver_manager-1.10.0-cp313-cp313-win_amd64.whl + name: adbc-driver-manager + version: 1.10.0 + sha256: 564a95617bda8907a0ad0a8bc8fea0c2cf951cea747c0d750a4b1740c828b1ef + requires_dist: + - typing-extensions + - pandas ; extra == 'dbapi' + - pyarrow>=14.0.1 ; extra == 'dbapi' + - duckdb ; extra == 'test' + - pandas ; extra == 'test' + - polars ; extra == 'test' + - pyarrow>=14.0.1 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ac/7d/3e131221995aef7edfd4dd0b09f14b7e51772d28eb362a0e6c3b8301a22a/adbc_driver_manager-1.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: adbc-driver-manager + version: 1.10.0 + sha256: 97e06da4235dabbd29244c8bd83f769c8995c25abed5d0c2ee2d95ec76d48b8a + requires_dist: + - typing-extensions + - pandas ; extra == 'dbapi' + - pyarrow>=14.0.1 ; extra == 'dbapi' + - duckdb ; extra == 'test' + - pandas ; extra == 'test' + - polars ; extra == 'test' + - pyarrow>=14.0.1 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d8/9c/6f9929b53cd578bef06b8d000e0ab829b982bcf5b22a6c99acfbad2aab34/adbc_driver_manager-1.10.0-cp313-cp313-macosx_11_0_arm64.whl + name: adbc-driver-manager + version: 1.10.0 + sha256: 94cc8b279c90c66f60a499996651340c17eb40d2fd7ad22e1fe73969ab4db1ee + requires_dist: + - typing-extensions + - pandas ; extra == 'dbapi' + - pyarrow>=14.0.1 ; extra == 'dbapi' + - duckdb ; extra == 'test' + - pandas ; extra == 'test' + - polars ; extra == 'test' + - pyarrow>=14.0.1 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.10' +- pypi: git+https://github.com/adbc-drivers/dev#ffdbb1a1237b89cafb15a7420667f54838986681 + name: adbc-drivers-dev + version: '0.1' + requires_dist: + - doit + - jinja2 + - packaging + - platformdirs + - pydantic>=2.0 + - pygit2 + - requests + - ruamel-yaml>=0.18.11,<0.19 + - tomlkit>=0.13.2,<0.14 +- pypi: git+https://github.com/adbc-drivers/validation#80cd7910a2c7495301d617363a006c435cb6d044 + name: adbc-drivers-validation + version: '0.1' + requires_dist: + - adbc-driver-manager>=1.6.0,<2 + - bidict>=0.23.1,<1.0.0 + - duckdb>=1.4.1,<2 + - jinja2>=3.1.6,<4 + - pyarrow>=23.0.0,<24 + - pydantic>=2.12.0,<3 + - pytest>=9.0.0,<10 + - sqlglot>=28.5.0 + - whenever>=0.9.3,<0.10 + requires_python: '>=3.13' +- pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + name: annotated-types + version: 0.7.0 + sha256: 1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 + requires_dist: + - typing-extensions>=4.0.0 ; python_full_version < '3.9' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + name: bidict + version: 0.23.1 + sha256: 5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5 + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 + md5: d2ffd7602c02f2b316fd921d39876885 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 260182 + timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda + sha256: b3495077889dde6bb370938e7db82be545c73e8589696ad0843a32221520ad4c + md5: 840d8fc0d7b3209be93080bc20e07f2d + depends: + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 192412 + timestamp: 1771350241232 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + sha256: 540fe54be35fac0c17feefbdc3e29725cce05d7367ffedfaaa1bdda234b019df + md5: 620b85a3f45526a8bc4d23fd78fc22f0 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 124834 + timestamp: 1771350416561 +- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + sha256: 76dfb71df5e8d1c4eded2dbb5ba15bb8fb2e2b0fe42d94145d5eed4c75c35902 + md5: 4cb8e6b48f67de0b018719cdf1136306 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 56115 + timestamp: 1771350256444 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-h4c7d964_0.conda + sha256: 37950019c59b99585cee5d30dbc2cc9696ed4e11f5742606a4db1621ed8f94d6 + md5: f001e6e220355b7f87403a4d0e5bf1ca + depends: + - __win + license: ISC + purls: [] + size: 147734 + timestamp: 1772006322223 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda + sha256: 67cc7101b36421c5913a1687ef1b99f85b5d6868da3abbf6ec1a4181e79782fc + md5: 4492fd26db29495f0ba23f146cd5638d + depends: + - __unix + license: ISC + purls: [] + size: 147413 + timestamp: 1772006283803 +- pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl + name: certifi + version: 2026.2.25 + sha256: 027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl + name: cffi + version: 2.0.0 + sha256: 19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl + name: cffi + version: 2.0.0 + sha256: 45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: cffi + version: 2.0.0 + sha256: c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + name: cffi + version: 2.0.0 + sha256: d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl + name: charset-normalizer + version: 3.4.6 + sha256: 11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: charset-normalizer + version: 3.4.6 + sha256: 530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: charset-normalizer + version: 3.4.6 + sha256: 423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl + name: charset-normalizer + version: 3.4.6 + sha256: 572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389 + requires_python: '>=3.7' +- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 + md5: 962b9857ee8e7018c22f2776ffa0b2d7 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/colorama?source=hash-mapping + size: 27011 + timestamp: 1733218222191 +- pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl + name: doit + version: 0.37.0 + sha256: a9f181566aa90faac515e276f85e6526019554ed7e13c12cf9dc094ffecf3e1b + requires_dist: + - tomli ; python_full_version < '3.11' and extra == 'toml' + - cloudpickle ; platform_python_implementation != 'PyPy' and extra == 'cloudpickle' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/53/07/1390e69db922423b2e111e32ed342b3e8fad0a31c144db70681ea1ba4d56/duckdb-1.5.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + name: duckdb + version: 1.5.0 + sha256: 9409ed1184b363ddea239609c5926f5148ee412b8d9e5ffa617718d755d942f6 + requires_dist: + - ipython ; extra == 'all' + - fsspec ; extra == 'all' + - numpy ; extra == 'all' + - pandas ; extra == 'all' + - pyarrow ; extra == 'all' + - adbc-driver-manager ; extra == 'all' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/54/13/b58d718415cde993823a54952ea511d2612302f1d2bc220549d0cef752a4/duckdb-1.5.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: duckdb + version: 1.5.0 + sha256: 1df8c4f9c853a45f3ec1e79ed7fe1957a203e5ec893bbbb853e727eb93e0090f + requires_dist: + - ipython ; extra == 'all' + - fsspec ; extra == 'all' + - numpy ; extra == 'all' + - pandas ; extra == 'all' + - pyarrow ; extra == 'all' + - adbc-driver-manager ; extra == 'all' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/76/fc/c916e928606946209c20fb50898dabf120241fb528a244e2bd8cde1bd9e2/duckdb-1.5.0-cp313-cp313-macosx_11_0_arm64.whl + name: duckdb + version: 1.5.0 + sha256: 0ee4dabe03ed810d64d93927e0fd18cd137060b81ee75dcaeaaff32cbc816656 + requires_dist: + - ipython ; extra == 'all' + - fsspec ; extra == 'all' + - numpy ; extra == 'all' + - pandas ; extra == 'all' + - pyarrow ; extra == 'all' + - adbc-driver-manager ; extra == 'all' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/e0/96/4460429651e371eb5ff745a4790e7fa0509c7a58c71fc4f0f893404c9646/duckdb-1.5.0-cp313-cp313-win_amd64.whl + name: duckdb + version: 1.5.0 + sha256: 9a3d3dfa2d8bc74008ce3ad9564761ae23505a9e4282f6a36df29bd87249620b + requires_dist: + - ipython ; extra == 'all' + - fsspec ; extra == 'all' + - numpy ; extra == 'all' + - pandas ; extra == 'all' + - pyarrow ; extra == 'all' + - adbc-driver-manager ; extra == 'all' + requires_python: '>=3.10.0' +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 + md5: 8e662bd460bda79b1ea39194e3c4c9ab + depends: + - python >=3.10 + - typing_extensions >=4.6.0 + license: MIT and PSF-2.0 + purls: + - pkg:pypi/exceptiongroup?source=hash-mapping + size: 21333 + timestamp: 1763918099466 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + sha256: fbf86c4a59c2ed05bbffb2ba25c7ed94f6185ec30ecb691615d42342baa1a16a + md5: c80d8a3b84358cb967fa81e7075fbc8a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 12723451 + timestamp: 1773822285671 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.3-hcab7f73_0.conda + sha256: 49ba6aed2c6b482bb0ba41078057555d29764299bc947b990708617712ef6406 + md5: 546da38c2fa9efacf203e2ad3f987c59 + depends: + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 12837286 + timestamp: 1773822650615 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + sha256: 3a7907a17e9937d3a46dfd41cffaf815abad59a569440d1e25177c15fd0684e5 + md5: f1182c91c0de31a7abd40cedf6a5ebef + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 12361647 + timestamp: 1773822915649 +- pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + name: idna + version: '3.11' + sha256: 771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea + requires_dist: + - ruff>=0.6.2 ; extra == 'all' + - mypy>=1.11.2 ; extra == 'all' + - pytest>=8.3.2 ; extra == 'all' + - flake8>=7.1.1 ; extra == 'all' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + sha256: e1a9e3b1c8fe62dc3932a616c284b5d8cbe3124bbfbedcf4ce5c828cb166ee19 + md5: 9614359868482abba1bd15ce465e3c42 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/iniconfig?source=compressed-mapping + size: 13387 + timestamp: 1760831448842 +- pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + name: jinja2 + version: 3.1.6 + sha256: 85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + requires_dist: + - markupsafe>=2.0 + - babel>=2.7 ; extra == 'i18n' + requires_python: '>=3.7' +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda + sha256: 565941ac1f8b0d2f2e8f02827cbca648f4d18cd461afc31f15604cd291b5c5f3 + md5: 12bd9a3f089ee6c9266a37dab82afabd + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.45.1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 725507 + timestamp: 1770267139900 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.45.1-default_h1979696_101.conda + sha256: 44527364aa333be631913451c32eb0cae1e09343827e9ce3ccabd8d962584226 + md5: 35b2ae7fadf364b8e5fb8185aaeb80e5 + depends: + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-aarch64 2.45.1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 875924 + timestamp: 1770267209884 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-hecca717_0.conda + sha256: d78f1d3bea8c031d2f032b760f36676d87929b18146351c4464c66b0869df3f5 + md5: e7f7ce06ec24cfcfb9e36d28cf82ba57 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.7.4.* + license: MIT + license_family: MIT + purls: [] + size: 76798 + timestamp: 1771259418166 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.4-hfae3067_0.conda + sha256: 995ce3ad96d0f4b5ed6296b051a0d7b6377718f325bc0e792fbb96b0e369dad7 + md5: 57f3b3da02a50a1be2a6fe847515417d + depends: + - libgcc >=14 + constrains: + - expat 2.7.4.* + license: MIT + license_family: MIT + purls: [] + size: 76564 + timestamp: 1771259530958 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.4-hf6b4638_0.conda + sha256: 03887d8080d6a8fe02d75b80929271b39697ecca7628f0657d7afaea87761edf + md5: a92e310ae8dfc206ff449f362fc4217f + depends: + - __osx >=11.0 + constrains: + - expat 2.7.4.* + license: MIT + license_family: MIT + purls: [] + size: 68199 + timestamp: 1771260020767 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.4-hac47afa_0.conda + sha256: b31f6fb629c4e17885aaf2082fb30384156d16b48b264e454de4a06a313b533d + md5: 1c1ced969021592407f16ada4573586d + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - expat 2.7.4.* + license: MIT + license_family: MIT + purls: [] + size: 70323 + timestamp: 1771259521393 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 + md5: a360c33a5abe61c07959e449fa1453eb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 58592 + timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda + sha256: 3df4c539449aabc3443bbe8c492c01d401eea894603087fca2917aa4e1c2dea9 + md5: 2f364feefb6a7c00423e80dcb12db62a + depends: + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 55952 + timestamp: 1769456078358 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7 + md5: 43c04d9cb46ef176bb2a4c77e324d599 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 40979 + timestamp: 1769456747661 +- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + sha256: 59d01f2dfa8b77491b5888a5ab88ff4e1574c9359f7e229da254cdfe27ddc190 + md5: 720b39f5ec0610457b725eb3f396219a + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + size: 45831 + timestamp: 1769456418774 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + sha256: faf7d2017b4d718951e3a59d081eb09759152f93038479b768e3d612688f83f5 + md5: 0aa00f03f9e39fb9876085dee11a85d4 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_18 + - libgomp 15.2.0 he0feb66_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 1041788 + timestamp: 1771378212382 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_18.conda + sha256: 43df385bedc1cab11993c4369e1f3b04b4ca5d0ea16cba6a0e7f18dbc129fcc9 + md5: 552567ea2b61e3a3035759b2fdb3f9a6 + depends: + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_18 + - libgomp 15.2.0 h8acb6b2_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 622900 + timestamp: 1771378128706 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + sha256: 21337ab58e5e0649d869ab168d4e609b033509de22521de1bfed0c031bfc5110 + md5: 239c5e9546c38a1e884d69effcf4c882 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 603262 + timestamp: 1771378117851 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_18.conda + sha256: fc716f11a6a8525e27a5d332ef6a689210b0d2a4dd1133edc0f530659aa9faa6 + md5: 4faa39bf919939602e594253bd673958 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 588060 + timestamp: 1771378040807 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda + sha256: 755c55ebab181d678c12e49cced893598f2bab22d582fbbf4d8b83c18be207eb + md5: c7c83eecbb72d88b940c249af56c8b17 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 113207 + timestamp: 1768752626120 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.2-he30d5cf_0.conda + sha256: 843c46e20519651a3e357a8928352b16c5b94f4cd3d5481acc48be2e93e8f6a3 + md5: 96944e3c92386a12755b94619bae0b35 + depends: + - libgcc >=14 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 125916 + timestamp: 1768754941722 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.2-h8088a28_0.conda + sha256: 7bfc7ffb2d6a9629357a70d4eadeadb6f88fa26ebc28f606b1c1e5e5ed99dc7e + md5: 009f0d956d7bfb00de86901d16e486c7 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 92242 + timestamp: 1768752982486 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.2-hfd05255_0.conda + sha256: f25bf293f550c8ed2e0c7145eb404324611cfccff37660869d97abf526eb957c + md5: ba0bfd4c3cf73f299ffe46ff0eaeb8e3 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 106169 + timestamp: 1768752763559 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 + md5: 2c21e66f50753a083cbe6b80f38268fa + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 92400 + timestamp: 1769482286018 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libmpdec-4.0.0-he30d5cf_1.conda + sha256: 57c0dd12d506e84541c4e877898bd2a59cca141df493d34036f18b2751e0a453 + md5: 7b9813e885482e3ccb1fa212b86d7fd0 + depends: + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 114056 + timestamp: 1769482343003 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + sha256: 1089c7f15d5b62c622625ec6700732ece83be8b705da8c6607f4dabb0c4bd6d2 + md5: 57c4be259f5e0b99a5983799a228ae55 + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 73690 + timestamp: 1769482560514 +- conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + sha256: 40dcd0b9522a6e0af72a9db0ced619176e7cfdb114855c7a64f278e73f8a7514 + md5: e4a9fc2bba3b022dad998c78856afe47 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 89411 + timestamp: 1769482314283 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_0.conda + sha256: d716847b7deca293d2e49ed1c8ab9e4b9e04b9d780aea49a97c26925b28a7993 + md5: fd893f6a3002a635b5e50ceb9dd2c0f4 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.2,<79.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 951405 + timestamp: 1772818874251 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.52.0-h10b116e_0.conda + sha256: 1ddaf91b44fae83856276f4cb7ce544ffe41d4b55c1e346b504c6b45f19098d6 + md5: 77891484f18eca74b8ad83694da9815e + depends: + - icu >=78.2,<79.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 952296 + timestamp: 1772818881550 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.52.0-h1ae2325_0.conda + sha256: beb0fd5594d6d7c7cd42c992b6bb4d66cbb39d6c94a8234f15956da99a04306c + md5: f6233a3fddc35a2ec9f617f79d6f3d71 + depends: + - __osx >=11.0 + - icu >=78.2,<79.0a0 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 918420 + timestamp: 1772819478684 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.52.0-hf5d6505_0.conda + sha256: 5fccf1e4e4062f8b9a554abf4f9735a98e70f82e2865d0bfdb47b9de94887583 + md5: 8830689d537fda55f990620680934bb1 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: blessing + purls: [] + size: 1297302 + timestamp: 1772818899033 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + sha256: 78668020064fdaa27e9ab65cd2997e2c837b564ab26ce3bf0e58a2ce1a525c6e + md5: 1b08cd684f34175e4514474793d44bcb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_18 + constrains: + - libstdcxx-ng ==15.2.0=*_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 5852330 + timestamp: 1771378262446 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_18.conda + sha256: 31fdb9ffafad106a213192d8319b9f810e05abca9c5436b60e507afb35a6bc40 + md5: f56573d05e3b735cb03efeb64a15f388 + depends: + - libgcc 15.2.0 h8acb6b2_18 + constrains: + - libstdcxx-ng ==15.2.0=*_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 5541411 + timestamp: 1771378162499 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda + sha256: 1a7539cfa7df00714e8943e18de0b06cceef6778e420a5ee3a2a145773758aee + md5: db409b7c1720428638e7c0d509d3e1b5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 40311 + timestamp: 1766271528534 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.3-h1022ec0_0.conda + sha256: c37a8e89b700646f3252608f8368e7eb8e2a44886b92776e57ad7601fc402a11 + md5: cf2861212053d05f27ec49c3784ff8bb + depends: + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 43453 + timestamp: 1766271546875 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 + md5: edb0dca6bc32e4f4789199455a1dbeb8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 60963 + timestamp: 1727963148474 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + sha256: 5a2c1eeef69342e88a98d1d95bff1603727ab1ff4ee0e421522acd8813439b84 + md5: 08aad7cbe9f5a6b460d0976076b6ae64 + depends: + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 66657 + timestamp: 1727963199518 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b + md5: 369964e85dc26bfe78f41399b366c435 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 46438 + timestamp: 1727963202283 +- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 + md5: 41fbfac52c601159df6c01f875de31b9 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 55476 + timestamp: 1727963768015 +- pypi: https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: markupsafe + version: 3.0.3 + sha256: 133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl + name: markupsafe + version: 3.0.3 + sha256: 9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl + name: markupsafe + version: 3.0.3 + sha256: 116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: markupsafe + version: 3.0.3 + sha256: ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 + md5: 47e340acb35de30501a76c7c799c41d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 891641 + timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + sha256: 91cfb655a68b0353b2833521dc919188db3d8a7f4c64bea2c6a7557b24747468 + md5: 182afabe009dc78d8b73100255ee6868 + depends: + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 926034 + timestamp: 1738196018799 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 + md5: 068d497125e4bf8a66bf707254fff5ae + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 797030 + timestamp: 1738196177597 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + sha256: 44c877f8af015332a5d12f5ff0fb20ca32f896526a7d0cdb30c769df1144fb5c + md5: f61eb8cd60ff9057122a3d338b99c00f + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3164551 + timestamp: 1769555830639 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.1-h546c87b_1.conda + sha256: 7f8048c0e75b2620254218d72b4ae7f14136f1981c5eb555ef61645a9344505f + md5: 25f5885f11e8b1f075bccf4a2da91c60 + depends: + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3692030 + timestamp: 1769557678657 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.1-hd24854e_1.conda + sha256: 361f5c5e60052abc12bdd1b50d7a1a43e6a6653aab99a2263bf2288d709dcf67 + md5: f4f6ad63f98f64191c3e77c5f5f29d76 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3104268 + timestamp: 1769556384749 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.1-hf411b9b_1.conda + sha256: 53a5ad2e5553b8157a91bb8aa375f78c5958f77cb80e9d2ce59471ea8e5c0bd6 + md5: eb585509b815415bc964b2c7e11c7eb3 + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 9343023 + timestamp: 1769557547888 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + sha256: c1fc0f953048f743385d31c468b4a678b3ad20caffdeaa94bed85ba63049fd58 + md5: b76541e68fea4d511b1ac46a28dcd2c6 + depends: + - python >=3.8 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/packaging?source=compressed-mapping + size: 72010 + timestamp: 1769093650580 +- pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + name: platformdirs + version: 4.9.4 + sha256: 68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868 + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + sha256: e14aafa63efa0528ca99ba568eaf506eb55a0371d12e6250aaaa61718d2eb62e + md5: d7585b6550ad04c8c5e21097ada2888e + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/pluggy?source=compressed-mapping + size: 25877 + timestamp: 1764896838868 +- pypi: https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl + name: pyarrow + version: 23.0.1 + sha256: 6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl + name: pyarrow + version: 23.0.1 + sha256: 9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl + name: pyarrow + version: 23.0.1 + sha256: cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl + name: pyarrow + version: 23.0.1 + sha256: 71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + name: pycparser + version: '3.0' + sha256: b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + name: pydantic + version: 2.12.5 + sha256: e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d + requires_dist: + - annotated-types>=0.6.0 + - pydantic-core==2.41.5 + - typing-extensions>=4.14.1 + - typing-inspection>=0.4.2 + - email-validator>=2.0.0 ; extra == 'email' + - tzdata ; python_full_version >= '3.9' and sys_platform == 'win32' and extra == 'timezone' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: pydantic-core + version: 2.41.5 + sha256: 0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl + name: pydantic-core + version: 2.41.5 + sha256: 112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl + name: pydantic-core + version: 2.41.5 + sha256: 79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pydantic-core + version: 2.41.5 + sha256: 406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/6d/01/98f74ecbe92f042d27e4de3cd7f093422d523cc67fdc74e6a65dbe4efbb8/pygit2-1.19.1-cp313-cp313-win_amd64.whl + name: pygit2 + version: 1.19.1 + sha256: 6d73aedffad280f6b655394e303533fcff15545d4d8f322011179c9474bb1b13 + requires_dist: + - cffi>=2.0 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/a7/02/02f0f56b9b0b044018d9047adf68ba842ebda662ba43ace942ed904f8e9d/pygit2-1.19.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + name: pygit2 + version: 1.19.1 + sha256: cb4da746c92e23281890e865887d83f24e662fc3e1c481420e4993c5a13203fe + requires_dist: + - cffi>=2.0 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/d5/80/1a87f6e043e04cfa125380a73ef9f87a8c58292b7d4a6ed2e6203b4cd534/pygit2-1.19.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: pygit2 + version: 1.19.1 + sha256: ef18f1208422d3cac1c109417a5fc6143704cfff8e5de4e1665fa4a89ffe3902 + requires_dist: + - cffi>=2.0 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/e2/1f/f67ec7f78a34ed14dbd3acf05ed23c4c8c2336ba6f3ca78d6b9962878435/pygit2-1.19.1-cp313-cp313-macosx_11_0_arm64.whl + name: pygit2 + version: 1.19.1 + sha256: ed39106f1d9560709191093ed5251471dfb6b9e4aa35299dde45f4b91f7c984e + requires_dist: + - cffi>=2.0 + requires_python: '>=3.11' +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + sha256: 5577623b9f6685ece2697c6eb7511b4c9ac5fb607c9babc2646c811b428fd46a + md5: 6b6ece66ebcae2d5f326c77ef2c5a066 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/pygments?source=hash-mapping + size: 889287 + timestamp: 1750615908735 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + sha256: 9e749fb465a8bedf0184d8b8996992a38de351f7c64e967031944978de03a520 + md5: 2b694bad8a50dc2f712f5368de866480 + depends: + - pygments >=2.7.2 + - python >=3.10 + - iniconfig >=1.0.1 + - packaging >=22 + - pluggy >=1.5,<2 + - tomli >=1 + - colorama >=0.4 + - exceptiongroup >=1 + - python + constrains: + - pytest-faulthandler >=2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest?source=hash-mapping + size: 299581 + timestamp: 1765062031645 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-lazy-fixtures-1.4.0-pyhd8ed1ab_0.conda + sha256: d8299885069ea20b0e48c4d6b97b2f508c26040e5740c9ce8b3d8abae4d68652 + md5: 04673c65f0c6cf3c2fe3927df657d0ee + depends: + - pytest >=7 + - python >=3.10,<4.0.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest-lazy-fixtures?source=hash-mapping + size: 14600 + timestamp: 1758096611127 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda + build_number: 100 + sha256: 8a08fe5b7cb5a28aa44e2994d18dbf77f443956990753a4ca8173153ffb6eb56 + md5: 4c875ed0e78c2d407ec55eadffb8cf3d + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.5,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 37364553 + timestamp: 1770272309861 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.13.12-h4c0d347_100_cp313.conda + build_number: 100 + sha256: a6bdf48a245d70526b4e6a277a4b344ec3f7c787b358e5377d544ac9a303c111 + md5: 732a86d6786402b95e1dc68c32022500 + depends: + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-aarch64 >=2.36.1 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.5,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 33986700 + timestamp: 1770270924894 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.12-h20e6be0_100_cp313.conda + build_number: 100 + sha256: 9a4f16a64def0853f0a7b6a7beb40d498fd6b09bee10b90c3d6069b664156817 + md5: 179c0f5ae4f22bc3be567298ed0b17b9 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.5,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 12770674 + timestamp: 1770272314517 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.12-h09917c8_100_cp313.conda + build_number: 100 + sha256: da70aec20ff5a5ae18bbba9fdd1e18190b419605cafaafb3bdad8becf11ce94d + md5: 4440c24966d0aa0c8f1e1d5006dac2d6 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - python_abi 3.13.* *_cp313 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Python-2.0 + purls: [] + size: 16535316 + timestamp: 1770270322707 + python_site_packages_path: Lib/site-packages +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + build_number: 8 + sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 + md5: 94305520c52a4aa3f6c2b1ff6008d9f8 + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 7002 + timestamp: 1752805902938 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 + md5: d7d95fc8287ea7bf33e0e7116d2b95ec + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 345073 + timestamp: 1765813471974 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda + sha256: fe695f9d215e9a2e3dd0ca7f56435ab4df24f5504b83865e3d295df36e88d216 + md5: 3d49cad61f829f4f0e0611547a9cda12 + depends: + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 357597 + timestamp: 1765815673644 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 + md5: f8381319127120ce51e081dce4865cf4 + depends: + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 313930 + timestamp: 1765813902568 +- pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + name: requests + version: 2.32.5 + sha256: 2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 + requires_dist: + - charset-normalizer>=2,<4 + - idna>=2.5,<4 + - urllib3>=1.21.1,<3 + - certifi>=2017.4.17 + - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' + - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl + name: ruamel-yaml + version: 0.18.17 + sha256: 9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d + requires_dist: + - ruamel-yaml-clib>=0.2.15 ; python_full_version < '3.15' and platform_python_implementation == 'CPython' + - ruamel-yaml-jinja2>=0.2 ; extra == 'jinja2' + - ryd ; extra == 'docs' + - mercurial>5.7 ; extra == 'docs' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl + name: ruamel-yaml-clib + version: 0.2.15 + sha256: 45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: ruamel-yaml-clib + version: 0.2.15 + sha256: 3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl + name: ruamel-yaml-clib + version: 0.2.15 + sha256: 65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: ruamel-yaml-clib + version: 0.2.15 + sha256: 4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c6/13/b57ab75b0f60b5ee8cb8924bc01a5c419ed3221e00f8f11f8c059a707eb7/sqlglot-30.0.3-py3-none-any.whl + name: sqlglot + version: 30.0.3 + sha256: 5489cc98b5666f1fafc21e0304ca286e513e142aa054ee5760806a2139d07a05 + requires_dist: + - duckdb>=0.6 ; extra == 'dev' + - sqlglot-mypy>=1.19.1.post1 ; extra == 'dev' + - setuptools-scm ; extra == 'dev' + - pandas ; extra == 'dev' + - pandas-stubs ; extra == 'dev' + - python-dateutil ; extra == 'dev' + - pytz ; extra == 'dev' + - pdoc ; extra == 'dev' + - pre-commit ; extra == 'dev' + - ruff==0.15.6 ; extra == 'dev' + - types-python-dateutil ; extra == 'dev' + - types-pytz ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - pyperf ; extra == 'dev' + - sqlglotc==30.0.3 ; extra == 'c' + - sqlglotrs==0.13.0 ; extra == 'rs' + - sqlglotc==30.0.3 ; extra == 'rs' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac + md5: cffd3bdd58090148f4cfcd831f4b26ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3301196 + timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h0dc03b3_103.conda + sha256: e25c314b52764219f842b41aea2c98a059f06437392268f09b03561e4f6e5309 + md5: 7fc6affb9b01e567d2ef1d05b84aa6ed + depends: + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3368666 + timestamp: 1769464148928 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + sha256: 799cab4b6cde62f91f750149995d149bc9db525ec12595e8a1d91b9317f038b3 + md5: a9d86bc62f39b94c4661716624eb21b0 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3127137 + timestamp: 1769460817696 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + sha256: 0e79810fae28f3b69fe7391b0d43f5474d6bd91d451d5f2bde02f55ae481d5e3 + md5: 0481bfd9814bf525bd4b3ee4b51494c4 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: TCL + license_family: BSD + purls: [] + size: 3526350 + timestamp: 1769460339384 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda + sha256: 62940c563de45790ba0f076b9f2085a842a65662268b02dd136a8e9b1eaf47a8 + md5: 72e780e9aa2d0a3295f59b1874e3768b + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomli?source=compressed-mapping + size: 21453 + timestamp: 1768146676791 +- pypi: https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl + name: tomlkit + version: 0.13.3 + sha256: c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + name: typing-inspection + version: 0.4.2 + sha256: 4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 + requires_dist: + - typing-extensions>=4.12.0 + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 + md5: 0caa1af407ecff61170c9437a808404d + depends: + - python >=3.10 + - python + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/typing-extensions?source=hash-mapping + size: 51692 + timestamp: 1756220668932 +- pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl + name: tzdata + version: '2025.3' + sha256: 06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1 + requires_python: '>=2' +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + purls: [] + size: 119135 + timestamp: 1767016325805 +- pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + name: tzlocal + version: 5.3.1 + sha256: eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d + requires_dist: + - tzdata ; sys_platform == 'win32' + - pytest>=4.3 ; extra == 'devenv' + - pytest-mock>=3.3 ; extra == 'devenv' + - pytest-cov ; extra == 'devenv' + - check-manifest ; extra == 'devenv' + - zest-releaser ; extra == 'devenv' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 + md5: 71b24316859acd00bdb8b38f5e2ce328 + constrains: + - vc14_runtime >=14.29.30037 + - vs2015_runtime >=14.29.30037 + license: LicenseRef-MicrosoftWindowsSDK10 + purls: [] + size: 694692 + timestamp: 1756385147981 +- pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + name: urllib3 + version: 2.6.3 + sha256: bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + requires_dist: + - brotli>=1.2.0 ; platform_python_implementation == 'CPython' and extra == 'brotli' + - brotlicffi>=1.2.0.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' + - h2>=4,<5 ; extra == 'h2' + - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' + - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + sha256: 9dc40c2610a6e6727d635c62cced5ef30b7b30123f5ef67d6139e23d21744b3a + md5: 1e610f2416b6acdd231c5f573d754a0f + depends: + - vc14_runtime >=14.44.35208 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 19356 + timestamp: 1767320221521 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + sha256: 02732f953292cce179de9b633e74928037fa3741eb5ef91c3f8bae4f761d32a5 + md5: 37eb311485d2d8b2c419449582046a42 + depends: + - ucrt >=10.0.20348.0 + - vcomp14 14.44.35208 h818238b_34 + constrains: + - vs2015_runtime 14.44.35208.* *_34 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 683233 + timestamp: 1767320219644 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + sha256: 878d5d10318b119bd98ed3ed874bd467acbe21996e1d81597a1dbf8030ea0ce6 + md5: 242d9f25d2ae60c76b38a5e42858e51d + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.44.35208.* *_34 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 115235 + timestamp: 1767320173250 +- pypi: https://files.pythonhosted.org/packages/56/5c/8d6dc529595b5387f5727cd6c2c5b8615851d95fec5c599a61ef239cc1b3/whenever-0.9.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: whenever + version: 0.9.5 + sha256: b4056aaff273a579f0294e5397a0d198f52906bbaf7171da0a12ecd8cdf5026c + requires_dist: + - tzdata>=2020.1 ; sys_platform == 'win32' + - tzlocal>=4.0 ; sys_platform != 'darwin' and sys_platform != 'linux' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/81/b4/17d4bc76ca73c21eb5b7883d10d8bacb7ce7a30a8f36501db2373c63ffb3/whenever-0.9.5-cp313-cp313-win_amd64.whl + name: whenever + version: 0.9.5 + sha256: 16497a2b889aeeb0ee80a0d3b9ce14cdb63d7eb7d904e003aae3cd4ac67da1e8 + requires_dist: + - tzdata>=2020.1 ; sys_platform == 'win32' + - tzlocal>=4.0 ; sys_platform != 'darwin' and sys_platform != 'linux' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b5/dc/090732e6e75f15a6084700d3247db6aa1f885971b637531529c62c4ba1c6/whenever-0.9.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: whenever + version: 0.9.5 + sha256: 9ac83555db44e1fcfc032114f45c09af0ed9d641380672c8deb7f1131a0fd783 + requires_dist: + - tzdata>=2020.1 ; sys_platform == 'win32' + - tzlocal>=4.0 ; sys_platform != 'darwin' and sys_platform != 'linux' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ea/81/d59f0e226ef542fc4bc86567d7b9e2bf9016c353b1f83661ee3913a140a7/whenever-0.9.5-cp313-cp313-macosx_11_0_arm64.whl + name: whenever + version: 0.9.5 + sha256: e00bc8f93fa469c630aad9dfdc538587c28891d6a4dce2f0b08628d5a108a219 + requires_dist: + - tzdata>=2020.1 ; sys_platform == 'win32' + - tzlocal>=4.0 ; sys_platform != 'darwin' and sys_platform != 'linux' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 + md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 + depends: + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 601375 + timestamp: 1764777111296 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + sha256: 569990cf12e46f9df540275146da567d9c618c1e9c7a0bc9d9cfefadaed20b75 + md5: c3655f82dcea2aa179b291e7099c1fcc + depends: + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 614429 + timestamp: 1764777145593 diff --git a/rust/pixi.toml b/rust/pixi.toml new file mode 100644 index 0000000..315ea5c --- /dev/null +++ b/rust/pixi.toml @@ -0,0 +1,38 @@ +# Copyright (c) 2025-2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +# !!!! AUTO-GENERATED FILE. DO NOT EDIT. !!!! +# USE adbc-gen-workflow (see adbc-drivers/dev) TO UPDATE THIS FILE. + +[workspace] +authors = ["ADBC Drivers Contributors"] +channels = ["conda-forge"] +name = "build" +platforms = ["linux-64", "osx-arm64", "win-64", "linux-aarch64"] +version = "0.1.0" + +[tasks] +make = "adbc-make run build DRIVER=snowflake VERBOSE=true IMPL_LANG=rust" +test = "cargo test" +release = "adbc-release" +validate = "pytest -vvs --junit-xml=validation-report.xml -rfEsxX validation/tests/" +gendocs = "python -m validation.tests.generate_documentation" + +[dependencies] +python = ">=3.13.5,<3.14" +pytest-lazy-fixtures = ">=1.3.2,<2" + +[pypi-dependencies] +adbc-drivers-dev = { git = "https://github.com/adbc-drivers/dev" } +adbc-drivers-validation = { git = "https://github.com/adbc-drivers/validation" } diff --git a/rust/src/connection.rs b/rust/src/connection.rs new file mode 100644 index 0000000..06432fd --- /dev/null +++ b/rust/src/connection.rs @@ -0,0 +1,867 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// src/connection.rs + +/// Arrow library version reported via get_info(DriverArrowVersion). +/// Must be kept in sync with the `arrow-array` dependency version in Cargo.toml. +const ARROW_VERSION: &str = "v57.3.0"; + +use std::collections::HashSet; +use std::sync::Arc; + +use adbc_core::{ + Optionable, constants, + error::{Error, Result, Status}, + options::{InfoCode, ObjectDepth, OptionConnection, OptionValue}, + schemas, +}; +use arrow_array::{ + Array, ArrayRef, BooleanArray, Int64Array, RecordBatch, RecordBatchReader, StringArray, + UInt32Array, UnionArray, +}; +use arrow_buffer::ScalarBuffer; +use arrow_schema::{DataType, Field, Schema}; +use sf_core::apis::database_driver_v1::Handle; + +use crate::driver::{Inner, TimestampPrecision}; +use crate::statement::Statement; + +pub struct Connection { + pub(crate) inner: Arc, + pub(crate) conn_handle: Handle, + pub(crate) autocommit: bool, + pub(crate) active_transaction: bool, + pub(crate) use_high_precision: bool, + pub(crate) timestamp_precision: TimestampPrecision, +} + +impl Drop for Connection { + fn drop(&mut self) { + let _ = self.inner.sf.connection_release(self.conn_handle); + } +} + +pub(crate) struct SingleBatchReader { + batch: Option, + schema: std::sync::Arc, +} + +impl SingleBatchReader { + pub(crate) fn new(batch: RecordBatch) -> Self { + let schema = batch.schema(); + Self { + batch: Some(batch), + schema, + } + } +} + +impl Iterator for SingleBatchReader { + type Item = std::result::Result; + fn next(&mut self) -> Option { + Ok(self.batch.take()).transpose() + } +} + +impl RecordBatchReader for SingleBatchReader { + fn schema(&self) -> std::sync::Arc { + self.schema.clone() + } +} + +impl Connection { + pub(crate) fn execute_simple(&self, sql: &str) -> Result<()> { + let stmt_handle = self + .inner + .sf + .statement_new(self.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + let result = self.inner.runtime.block_on(async { + self.inner + .sf + .statement_set_sql_query(stmt_handle, sql.to_string()) + .await?; + self.inner + .sf + .statement_execute_query(stmt_handle, None) + .await + }); + let _ = self.inner.sf.statement_release(stmt_handle); + result + .map(|_| ()) + .map_err(crate::error::api_error_to_adbc_error) + } + + fn query_scalar(&self, sql: &str) -> Result { + let stmt_handle = self + .inner + .sf + .statement_new(self.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + let result = self.inner.runtime.block_on(async { + self.inner + .sf + .statement_set_sql_query(stmt_handle, sql.to_string()) + .await?; + self.inner + .sf + .statement_execute_query(stmt_handle, None) + .await + }); + let _ = self.inner.sf.statement_release(stmt_handle); + let exec_result = result.map_err(crate::error::api_error_to_adbc_error)?; + + let raw = + Box::into_raw(exec_result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let mut reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| { + // Safety: Arrow's C Data Interface specifies that on failure, from_raw + // does NOT call the stream's release callback, so reconstructing the + // Box here is the only release path — no double-free risk. + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; + + use arrow_array::cast::AsArray; + let batch = reader + .next() + .ok_or_else(|| { + Error::with_message_and_status("empty result from scalar query", Status::IO) + })? + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + Ok(batch.column(0).as_string::().value(0).to_string()) + } + + pub(crate) fn set_autocommit(&mut self, enabled: bool) -> Result<()> { + if enabled { + if self.active_transaction { + self.execute_simple("COMMIT")?; + self.active_transaction = false; + } + self.execute_simple("ALTER SESSION SET AUTOCOMMIT = true")?; + self.autocommit = true; + } else { + self.execute_simple("ALTER SESSION SET AUTOCOMMIT = false")?; + if !self.active_transaction { + self.execute_simple("BEGIN")?; + self.active_transaction = true; + } + self.autocommit = false; + } + Ok(()) + } +} + +impl Optionable for Connection { + type Option = OptionConnection; + + fn set_option(&mut self, key: Self::Option, value: OptionValue) -> Result<()> { + match key { + OptionConnection::AutoCommit => { + let enabled = match &value { + OptionValue::String(s) => s == "true" || s == "1", + _ => { + return Err(Error::with_message_and_status( + "autocommit value must be a string", + Status::InvalidArguments, + )); + } + }; + self.set_autocommit(enabled) + } + OptionConnection::CurrentCatalog => { + if let OptionValue::String(s) = &value { + self.execute_simple(&format!(r#"USE DATABASE "{}""#, s.replace('"', "\"\""))) + } else { + Err(Error::with_message_and_status( + "current_catalog value must be a string", + Status::InvalidArguments, + )) + } + } + OptionConnection::CurrentSchema => { + if let OptionValue::String(s) = &value { + self.execute_simple(&format!(r#"USE SCHEMA "{}""#, s.replace('"', "\"\""))) + } else { + Err(Error::with_message_and_status( + "current_schema value must be a string", + Status::InvalidArguments, + )) + } + } + _ => Err(Error::with_message_and_status( + format!("unsupported connection option: {}", key.as_ref()), + Status::NotFound, + )), + } + } + + fn get_option_string(&self, key: Self::Option) -> Result { + match key { + OptionConnection::AutoCommit => { + Ok(if self.autocommit { "true" } else { "false" }.to_string()) + } + OptionConnection::CurrentCatalog => self.query_scalar("SELECT CURRENT_DATABASE()"), + OptionConnection::CurrentSchema => self.query_scalar("SELECT CURRENT_SCHEMA()"), + _ => Err(Error::with_message_and_status( + format!("option not found: {}", key.as_ref()), + Status::NotFound, + )), + } + } + + fn get_option_bytes(&self, _key: Self::Option) -> Result> { + Err(Error::with_message_and_status( + "option not found", + Status::NotFound, + )) + } + + fn get_option_int(&self, _key: Self::Option) -> Result { + Err(Error::with_message_and_status( + "option not found", + Status::NotFound, + )) + } + + fn get_option_double(&self, _key: Self::Option) -> Result { + Err(Error::with_message_and_status( + "option not found", + Status::NotFound, + )) + } +} + +impl adbc_core::Connection for Connection { + type StatementType = Statement; + + fn new_statement(&mut self) -> Result { + let stmt_handle = self + .inner + .sf + .statement_new(self.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + Ok(Statement { + inner: self.inner.clone(), + stmt_handle, + conn_handle: self.conn_handle, + query: None, + target_table: None, + ingest_catalog: None, + ingest_schema: None, + ingest_mode: None, + query_tag: None, + use_high_precision: self.use_high_precision, + timestamp_precision: self.timestamp_precision, + bound_batches: vec![], + last_query_id: None, + }) + } + + fn cancel(&mut self) -> Result<()> { + Err(crate::error::not_implemented("cancel")) + } + + #[allow(refining_impl_trait)] + fn get_info( + &self, + codes: Option>, + ) -> Result> { + let need_vendor_version = codes + .as_ref() + .is_none_or(|s| s.contains(&InfoCode::VendorVersion)); + let vendor_version = if need_vendor_version { + self.query_scalar("SELECT CURRENT_VERSION()")? + } else { + String::new() + }; + + // (InfoCode, type_id, offset_within_arm_array) + let all_entries: &[(InfoCode, i8, i32)] = &[ + (InfoCode::VendorName, 0, 0), + (InfoCode::VendorSql, 1, 0), + (InfoCode::VendorSubstrait, 1, 1), + (InfoCode::DriverName, 0, 1), + (InfoCode::DriverVersion, 0, 2), + (InfoCode::DriverAdbcVersion, 2, 0), + (InfoCode::VendorVersion, 0, 3), + (InfoCode::DriverArrowVersion, 0, 4), + ]; + + let selected: Vec<_> = match &codes { + None => all_entries.iter().collect(), + Some(set) => all_entries + .iter() + .filter(|(c, _, _)| set.contains(c)) + .collect(), + }; + + if selected.is_empty() { + let batch = RecordBatch::new_empty(schemas::GET_INFO_SCHEMA.clone()); + return Ok(Box::new(SingleBatchReader::new(batch))); + } + + let name_vals: Vec = selected.iter().map(|(c, _, _)| u32::from(c)).collect(); + let type_ids: Vec = selected.iter().map(|(_, t, _)| *t).collect(); + let offsets: Vec = selected.iter().map(|(_, _, o)| *o).collect(); + + use arrow_schema::UnionFields; + + let string_values = Arc::new(StringArray::from(vec![ + "Snowflake", + "ADBC Snowflake Driver (Rust)", + env!("CARGO_PKG_VERSION"), + vendor_version.as_str(), + ARROW_VERSION, + ])) as ArrayRef; + let bool_values = Arc::new(BooleanArray::from(vec![true, false])) as ArrayRef; + let int64_values = + Arc::new(Int64Array::from(vec![constants::ADBC_VERSION_1_1_0 as i64])) as ArrayRef; + let int32_values = Arc::new(arrow_array::Int32Array::from(vec![0i32])) as ArrayRef; + let list_values = Arc::new(arrow_array::ListArray::new_null( + Arc::new(Field::new("item", DataType::Utf8, true)), + 0, + )) as ArrayRef; + // arm 5: int32_to_int32_list_map — proper empty MapArray to satisfy schema type check + // (This arm is never selected, but must have the right type for RecordBatch::try_new) + let empty_int32_list_inner = arrow_array::Int32Array::from(Vec::::new()); + let empty_int32_list = arrow_array::ListArray::new( + Arc::new(Field::new_list_field(DataType::Int32, true)), + arrow_buffer::OffsetBuffer::new(arrow_buffer::ScalarBuffer::from(vec![0i32])), + Arc::new(empty_int32_list_inner), + None, + ); + let empty_entries = arrow_array::StructArray::new( + arrow_schema::Fields::from(vec![ + Field::new("key", DataType::Int32, false), + Field::new_list("value", Field::new_list_field(DataType::Int32, true), true), + ]), + vec![ + Arc::new(arrow_array::Int32Array::from(Vec::::new())) as ArrayRef, + Arc::new(empty_int32_list) as ArrayRef, + ], + None, + ); + let map_values = Arc::new( + arrow_array::MapArray::try_new( + Arc::new(Field::new_struct( + "entries", + vec![ + Field::new("key", DataType::Int32, false), + Field::new_list( + "value", + Field::new_list_field(DataType::Int32, true), + true, + ), + ], + false, + )), + arrow_buffer::OffsetBuffer::new(arrow_buffer::ScalarBuffer::from(vec![0i32])), + empty_entries, + None, + false, + ) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal))?, + ) as ArrayRef; + + let union_array = UnionArray::try_new( + #[allow(deprecated)] + UnionFields::new( + [0i8, 1, 2, 3, 4, 5], + [ + Field::new("string_value", DataType::Utf8, true), + Field::new("bool_value", DataType::Boolean, true), + Field::new("int64_value", DataType::Int64, true), + Field::new("int32_bitmask", DataType::Int32, true), + Field::new_list( + "string_list", + Field::new_list_field(DataType::Utf8, true), + true, + ), + Field::new_map( + "int32_to_int32_list_map", + "entries", + Field::new("key", DataType::Int32, false), + Field::new_list( + "value", + Field::new_list_field(DataType::Int32, true), + true, + ), + false, + true, + ), + ], + ), + type_ids.into_iter().collect::>(), + Some(offsets.into_iter().collect::>()), + vec![ + string_values, + bool_values, + int64_values, + int32_values, + list_values, + map_values, + ], + ) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal))?; + + let batch = RecordBatch::try_new( + schemas::GET_INFO_SCHEMA.clone(), + vec![ + Arc::new(UInt32Array::from(name_vals)) as ArrayRef, + Arc::new(union_array) as ArrayRef, + ], + ) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal))?; + + Ok(Box::new(SingleBatchReader::new(batch))) + } + + #[allow(refining_impl_trait)] + fn get_objects( + &self, + depth: ObjectDepth, + catalog: Option<&str>, + db_schema: Option<&str>, + table_name: Option<&str>, + table_type: Option>, + column_name: Option<&str>, + ) -> Result> { + crate::get_objects::execute_get_objects( + self, + &depth, + catalog, + db_schema, + table_name, + table_type, + column_name, + ) + } + + fn get_table_schema( + &self, + catalog: Option<&str>, + db_schema: Option<&str>, + table_name: &str, + ) -> Result { + let quoted = |s: &str| format!(r#""{}""#, s.replace('"', "\"\"")); + let qualified = match (catalog, db_schema) { + (Some(c), Some(s)) => { + format!("{}.{}.{}", quoted(c), quoted(s), quoted(table_name)) + } + (None, Some(s)) => format!("{}.{}", quoted(s), quoted(table_name)), + (Some(c), None) => format!("{}.{}", quoted(c), quoted(table_name)), + (None, None) => quoted(table_name), + }; + let sql = format!("DESC TABLE {qualified}"); + let stmt_handle = self + .inner + .sf + .statement_new(self.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + let result = self.inner.runtime.block_on(async { + self.inner + .sf + .statement_set_sql_query(stmt_handle, sql) + .await?; + self.inner + .sf + .statement_execute_query(stmt_handle, None) + .await + }); + let _ = self.inner.sf.statement_release(stmt_handle); + let exec_result = result.map_err(crate::error::api_error_to_adbc_error)?; + + // Safety: exec_result.stream is a valid FFI stream from sf_core. We take ownership + // via Box::into_raw and transfer it to ArrowArrayStreamReader. The C ABI layout is + // stable across arrow versions per the Arrow C Data Interface specification. + let raw = + Box::into_raw(exec_result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| { + // Safety: Arrow's C Data Interface specifies that on failure, from_raw + // does NOT call the stream's release callback, so reconstructing the + // Box here is the only release path — no double-free risk. + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; + + let mut fields: Vec = Vec::new(); + for batch in reader { + let batch = + batch.map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + use arrow_array::cast::AsArray; + + // Resolve column indices by name (case-insensitive) so a future + // reordering of DESC TABLE columns doesn't silently shift the mapping. + // Known positional defaults from the current Snowflake DESC TABLE schema: + // 0=name, 1=type, 2=kind, 3=null?, 4=default, 5=primary key, + // 6=unique key, 7=check, 8=expression, 9=comment, … + let schema = batch.schema(); + let find = |name: &str, fallback: usize| { + schema + .fields() + .iter() + .position(|f| f.name().eq_ignore_ascii_case(name)) + .unwrap_or(fallback) + }; + let name_col = find("name", 0); + let type_col = find("type", 1); + let null_col = find("null?", 3); + let pk_col = find("primary key", 5); + let comment_col = find("comment", 9); + + if batch.num_columns() <= name_col + || batch.num_columns() <= type_col + || batch.num_columns() <= null_col + { + continue; + } + let names = batch.column(name_col).as_string::(); + let types = batch.column(type_col).as_string::(); + let nullables = batch.column(null_col).as_string::(); + // primary_key and comment are present only when the result has enough columns. + let primary_keys = + (batch.num_columns() > pk_col).then(|| batch.column(pk_col).as_string::()); + let comments = (batch.num_columns() > comment_col) + .then(|| batch.column(comment_col).as_string::()); + for i in 0..batch.num_rows() { + let type_str = types.value(i); + let arrow_type = snowflake_type_to_arrow( + type_str, + self.use_high_precision, + self.timestamp_precision.time_unit(), + ); + let mut md = std::collections::HashMap::new(); + md.insert("DATA_TYPE".to_string(), type_str.to_string()); + if let Some(pk) = &primary_keys { + md.insert("PRIMARY_KEY".to_string(), pk.value(i).to_string()); + } + if let Some(cm) = &comments + && !cm.is_null(i) + { + md.insert("COMMENT".to_string(), cm.value(i).to_string()); + } + fields.push( + Field::new(names.value(i), arrow_type, nullables.value(i) == "Y") + .with_metadata(md), + ); + } + } + Ok(Schema::new(fields)) + } + + #[allow(refining_impl_trait)] + fn get_table_types(&self) -> Result> { + let array = Arc::new(StringArray::from(vec!["TABLE", "VIEW"])); + let batch = RecordBatch::try_new(schemas::GET_TABLE_TYPES_SCHEMA.clone(), vec![array]) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal))?; + Ok(Box::new(SingleBatchReader::new(batch))) + } + + #[allow(refining_impl_trait)] + fn get_statistic_names(&self) -> Result> { + Err(crate::error::not_implemented("get_statistic_names")) + } + + #[allow(refining_impl_trait)] + fn get_statistics( + &self, + _catalog: Option<&str>, + _db_schema: Option<&str>, + _table_name: Option<&str>, + _approximate: bool, + ) -> Result> { + Err(crate::error::not_implemented("get_statistics")) + } + + fn commit(&mut self) -> Result<()> { + if self.autocommit { + return Err(Error::with_message_and_status( + "cannot commit: autocommit is enabled", + Status::InvalidState, + )); + } + self.execute_simple("COMMIT")?; + self.active_transaction = false; + self.execute_simple("BEGIN")?; + self.active_transaction = true; + Ok(()) + } + + fn rollback(&mut self) -> Result<()> { + if self.autocommit { + return Err(Error::with_message_and_status( + "cannot rollback: autocommit is enabled", + Status::InvalidState, + )); + } + self.execute_simple("ROLLBACK")?; + self.active_transaction = false; + self.execute_simple("BEGIN")?; + self.active_transaction = true; + Ok(()) + } + + #[allow(refining_impl_trait)] + fn read_partition( + &self, + _partition: impl AsRef<[u8]>, + ) -> Result> { + Err(crate::error::not_implemented("read_partition")) + } +} + +fn ts_scale_to_unit(scale: u32) -> arrow_schema::TimeUnit { + match scale { + 0 => arrow_schema::TimeUnit::Second, + 1..=3 => arrow_schema::TimeUnit::Millisecond, + 4..=6 => arrow_schema::TimeUnit::Microsecond, + _ => arrow_schema::TimeUnit::Nanosecond, + } +} + +fn min_time_unit(a: arrow_schema::TimeUnit, b: arrow_schema::TimeUnit) -> arrow_schema::TimeUnit { + use arrow_schema::TimeUnit::*; + let rank = |u| match u { + Second => 0u8, + Millisecond => 1, + Microsecond => 2, + Nanosecond => 3, + }; + if rank(a) <= rank(b) { a } else { b } +} + +fn snowflake_type_to_arrow( + type_str: &str, + high_precision: bool, + ts_unit: arrow_schema::TimeUnit, +) -> DataType { + let upper = type_str.to_uppercase(); + let base = upper.split('(').next().unwrap_or(&upper).trim(); + match base { + "FLOAT" | "DOUBLE" | "REAL" | "FLOAT4" | "FLOAT8" => DataType::Float64, + "BOOLEAN" => DataType::Boolean, + "DATE" => DataType::Date32, + "TIME" => DataType::Time64(arrow_schema::TimeUnit::Nanosecond), + "TEXT" | "STRING" | "VARCHAR" | "CHAR" | "CHARACTER" | "NCHAR" | "NVARCHAR" + | "NVARCHAR2" | "CHAR VARYING" | "NCHAR VARYING" => DataType::Utf8, + "BINARY" | "VARBINARY" => DataType::Binary, + "ARRAY" | "OBJECT" | "VARIANT" | "GEOGRAPHY" | "GEOMETRY" => DataType::Utf8, + "NUMBER" | "NUMERIC" | "DECIMAL" | "INT" | "INTEGER" | "BIGINT" | "SMALLINT" + | "TINYINT" | "BYTEINT" => { + if let Some(inner) = type_str + .find('(') + .and_then(|s| type_str.rfind(')').map(|e| &type_str[s + 1..e])) + { + let mut parts = inner.split(','); + let precision = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(38); + let scale = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + if scale == 0 { + DataType::Int64 + } else if high_precision { + DataType::Decimal128(precision, scale) + } else { + DataType::Float64 + } + } else { + DataType::Int64 + } + } + "TIMESTAMP" | "TIMESTAMP_NTZ" | "DATETIME" => { + let scale = type_str + .find('(') + .and_then(|s| type_str.rfind(')').map(|e| &type_str[s + 1..e])) + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(9); + let natural = ts_scale_to_unit(scale); + let unit = min_time_unit(natural, ts_unit); + DataType::Timestamp(unit, None) + } + "TIMESTAMP_LTZ" | "TIMESTAMP_TZ" => { + let scale = type_str + .find('(') + .and_then(|s| type_str.rfind(')').map(|e| &type_str[s + 1..e])) + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(9); + let natural = ts_scale_to_unit(scale); + let unit = min_time_unit(natural, ts_unit); + DataType::Timestamp(unit, Some("UTC".into())) + } + _ => DataType::Utf8, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_option_string_returns_not_found_for_unknown_key() { + let driver = crate::driver::Driver::default(); + let conn = Connection { + inner: driver.inner.clone(), + conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + autocommit: true, + active_transaction: false, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, + }; + let result = conn.get_option_string(OptionConnection::Other("unknown".into())); + assert_eq!(result.unwrap_err().status, Status::NotFound); + } + + #[test] + fn snowflake_type_number_no_scale_is_int64() { + assert_eq!( + snowflake_type_to_arrow("NUMBER(38,0)", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Int64 + ); + } + + #[test] + fn snowflake_type_number_with_scale_high_precision_is_decimal128() { + assert_eq!( + snowflake_type_to_arrow("NUMBER(10,2)", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Decimal128(10, 2) + ); + } + + #[test] + fn snowflake_type_number_with_scale_low_precision_is_float64() { + assert_eq!( + snowflake_type_to_arrow("NUMBER(10,2)", false, arrow_schema::TimeUnit::Nanosecond), + DataType::Float64 + ); + } + + #[test] + fn snowflake_type_text_is_utf8() { + assert_eq!( + snowflake_type_to_arrow("TEXT", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Utf8 + ); + assert_eq!( + snowflake_type_to_arrow( + "VARCHAR(16777216)", + true, + arrow_schema::TimeUnit::Nanosecond + ), + DataType::Utf8 + ); + } + + #[test] + fn snowflake_type_boolean_is_boolean() { + assert_eq!( + snowflake_type_to_arrow("BOOLEAN", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Boolean + ); + } + + #[test] + fn snowflake_type_timestamp_ntz_nanosecond() { + assert_eq!( + snowflake_type_to_arrow("TIMESTAMP_NTZ(9)", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Timestamp(arrow_schema::TimeUnit::Nanosecond, None) + ); + } + + #[test] + fn snowflake_type_timestamp_ntz_microsecond() { + assert_eq!( + snowflake_type_to_arrow( + "TIMESTAMP_NTZ(6)", + true, + arrow_schema::TimeUnit::Microsecond + ), + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, None) + ); + } + + #[test] + fn snowflake_type_timestamp_ntz_scale6_with_ns_unit_returns_us() { + assert_eq!( + snowflake_type_to_arrow("TIMESTAMP_NTZ(6)", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, None) + ); + } + + #[test] + fn snowflake_type_timestamp_ntz_scale9_capped_by_us_unit() { + assert_eq!( + snowflake_type_to_arrow( + "TIMESTAMP_NTZ(9)", + true, + arrow_schema::TimeUnit::Microsecond + ), + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, None) + ); + } + + #[test] + fn snowflake_type_timestamp_ltz_scale6_with_ns_unit_returns_us() { + assert_eq!( + snowflake_type_to_arrow("TIMESTAMP_LTZ(6)", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, Some("UTC".into())) + ); + } + + #[test] + fn get_table_types_returns_table_and_view() { + use adbc_core::Connection as _; + use arrow_array::cast::AsArray; + let driver = crate::driver::Driver::default(); + let conn = Connection { + inner: driver.inner.clone(), + conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + autocommit: true, + active_transaction: false, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, + }; + let mut reader = conn.get_table_types().unwrap(); + let batch = reader.next().unwrap().unwrap(); + let types: Vec<&str> = batch + .column(0) + .as_string::() + .iter() + .flatten() + .collect(); + assert_eq!(types, vec!["TABLE", "VIEW"]); + } + + #[test] + fn snowflake_type_timestamp_tz_scale6_with_ns_unit_returns_us() { + assert_eq!( + snowflake_type_to_arrow("TIMESTAMP_TZ(6)", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, Some("UTC".into())) + ); + } + + #[test] + fn snowflake_type_timestamp_ntz_no_parens_defaults_to_ns() { + assert_eq!( + snowflake_type_to_arrow("TIMESTAMP_NTZ", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Timestamp(arrow_schema::TimeUnit::Nanosecond, None) + ); + } +} diff --git a/rust/src/database.rs b/rust/src/database.rs new file mode 100644 index 0000000..efacaa0 --- /dev/null +++ b/rust/src/database.rs @@ -0,0 +1,764 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// src/database.rs +use std::collections::HashMap; +use std::sync::Arc; + +use adbc_core::{ + Optionable, + error::{Error, Result, Status}, + options::{OptionConnection, OptionDatabase, OptionValue}, +}; +use sf_core::apis::database_driver_v1::Handle; +use sf_core::config::param_registry::param_names; +use sf_core::config::settings::Setting; + +use crate::connection::Connection; +use crate::driver::{Inner, TimestampPrecision}; + +use percent_encoding::percent_decode_str; + +/// Convert an ADBC OptionDatabase key + OptionValue into an sf_core (param_name, Setting) pair. +/// Returns None for the "uri" key (handled by apply_uri separately). +/// Returns Err for keys with invalid values (e.g. non-numeric port). +fn adbc_db_opt_to_sf(key: &str, value: &OptionValue) -> Result> { + let setting = match value { + OptionValue::String(s) => Setting::String(s.clone()), + OptionValue::Int(i) => Setting::Int(*i), + OptionValue::Double(d) => Setting::Double(*d), + OptionValue::Bytes(b) => Setting::Bytes(b.clone()), + _ => { + return Err(Error::with_message_and_status( + "unsupported option value type", + Status::InvalidArguments, + )); + } + }; + + let param: String = match key { + "username" => param_names::USER.into(), + "password" => param_names::PASSWORD.into(), + "adbc.snowflake.sql.account" => param_names::ACCOUNT.into(), + "adbc.snowflake.sql.db" => param_names::DATABASE.into(), + "adbc.snowflake.sql.schema" => param_names::SCHEMA.into(), + "adbc.snowflake.sql.warehouse" => param_names::WAREHOUSE.into(), + "adbc.snowflake.sql.role" => param_names::ROLE.into(), + "adbc.snowflake.sql.uri.host" => param_names::HOST.into(), + "adbc.snowflake.sql.uri.protocol" => param_names::PROTOCOL.into(), + "adbc.snowflake.sql.auth_type" => param_names::AUTHENTICATOR.into(), + "adbc.snowflake.sql.client_option.application" => "client_app_id".to_string(), + "adbc.snowflake.sql.client_option.auth_token" => param_names::TOKEN.into(), + "adbc.snowflake.sql.client_option.jwt_private_key" => param_names::PRIVATE_KEY_FILE.into(), + "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_value" => { + param_names::PRIVATE_KEY.into() + } + "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_password" => { + param_names::PRIVATE_KEY_PASSWORD.into() + } + // Account geography + "adbc.snowflake.sql.region" => "region".to_string(), + // Auth extras + // The Okta authenticator URL is the authenticator value in sf_core. + "adbc.snowflake.sql.client_option.okta_url" => param_names::AUTHENTICATOR.into(), + "adbc.snowflake.sql.client_option.identity_provider" => "identity_provider".to_string(), + // Connection timeouts (stored as-is; sf_core will use them once supported) + "adbc.snowflake.sql.client_option.login_timeout" => "login_timeout".to_string(), + "adbc.snowflake.sql.client_option.request_timeout" => "request_timeout".to_string(), + "adbc.snowflake.sql.client_option.jwt_expire_timeout" => "jwt_expire_timeout".to_string(), + "adbc.snowflake.sql.client_option.client_timeout" => "client_timeout".to_string(), + // TLS — tls_skip_verify compound effect is applied separately in set_option + "adbc.snowflake.sql.client_option.tls_skip_verify" => "tls_skip_verify".to_string(), + "adbc.snowflake.sql.client_option.tls_root_cert" => { + param_names::CUSTOM_ROOT_STORE_PATH.into() + } + // OCSP — ocsp_fail_open_mode compound effect is applied separately in set_option + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode" => "ocsp_fail_open_mode".to_string(), + // Session behaviour + "adbc.snowflake.sql.client_option.keep_session_alive" => "keep_session_alive".to_string(), + "adbc.snowflake.sql.client_option.disable_telemetry" => "disable_telemetry".to_string(), + "adbc.snowflake.sql.client_option.cache_mfa_token" => "cache_mfa_token".to_string(), + "adbc.snowflake.sql.client_option.store_temp_creds" => "store_temp_creds".to_string(), + // Config / logging + "adbc.snowflake.sql.client_option.config_file" => "config_file".to_string(), + "adbc.snowflake.sql.client_option.tracing" => "log_level".to_string(), + "adbc.snowflake.sql.uri.port" => { + let port = match value { + OptionValue::String(s) => s.parse::().map_err(|_| { + Error::with_message_and_status( + format!("invalid port value: {s}"), + Status::InvalidArguments, + ) + })?, + OptionValue::Int(i) => *i, + _ => { + return Err(Error::with_message_and_status( + "port must be a string or int", + Status::InvalidArguments, + )); + } + }; + return Ok(Some((param_names::PORT.into(), Setting::Int(port)))); + } + "uri" => return Ok(None), + other => other.to_string(), + }; + + Ok(Some((param, setting))) +} + +pub struct Database { + pub(crate) inner: Arc, + pub(crate) db_handle: Handle, + /// Local copy of sf_core settings keyed by canonical param name. + /// Propagated to each new connection before connection_init. + pub(crate) sf_settings: HashMap, + /// Map NUMBER(p,s) with s>0 to Decimal128 instead of Float64. + pub(crate) use_high_precision: bool, + /// Arrow time unit used for TIMESTAMP columns. + pub(crate) timestamp_precision: TimestampPrecision, +} + +impl Drop for Database { + fn drop(&mut self) { + let _ = self.inner.sf.database_release(self.db_handle); + } +} + +impl Optionable for Database { + type Option = OptionDatabase; + + fn set_option(&mut self, key: Self::Option, value: OptionValue) -> Result<()> { + let key_str = key.as_ref(); + if key_str == "uri" { + if let OptionValue::String(uri) = &value { + return self.apply_uri(uri.clone()); + } + return Err(Error::with_message_and_status( + "uri option must be a string", + Status::InvalidArguments, + )); + } + if key_str == "adbc.snowflake.sql.client_option.use_high_precision" { + if let OptionValue::String(s) = &value { + self.use_high_precision = s == "enabled" || s == "true"; + } + return Ok(()); + } + if key_str == "adbc.snowflake.sql.client_option.max_timestamp_precision" { + if let OptionValue::String(s) = &value { + self.timestamp_precision = match s.as_str() { + "microseconds" => TimestampPrecision::Microseconds, + "nanoseconds_error_on_overflow" => { + TimestampPrecision::NanosecondsErrorOnOverflow + } + _ => TimestampPrecision::Nanoseconds, + }; + } + return Ok(()); + } + if let Some((param, setting)) = adbc_db_opt_to_sf(key_str, &value)? { + self.sf_settings.insert(param.clone(), setting.clone()); + self.inner + .runtime + .block_on( + self.inner + .sf + .database_set_option(self.db_handle, param, setting), + ) + .map_err(crate::error::api_error_to_adbc_error)?; + } + + // tls_skip_verify: also drive the underlying verify_certificates / verify_hostname + // params so sf_core skips certificate and hostname checks when enabled. + if key_str == "adbc.snowflake.sql.client_option.tls_skip_verify" { + let skip = matches!(&value, OptionValue::String(s) if s == "enabled"); + let verify = Setting::Bool(!skip); + self.sf_settings.insert( + param_names::VERIFY_CERTIFICATES.as_str().to_string(), + verify.clone(), + ); + self.sf_settings.insert( + param_names::VERIFY_HOSTNAME.as_str().to_string(), + verify.clone(), + ); + self.inner + .runtime + .block_on(async { + self.inner + .sf + .database_set_option( + self.db_handle, + param_names::VERIFY_CERTIFICATES.into(), + verify.clone(), + ) + .await?; + self.inner + .sf + .database_set_option( + self.db_handle, + param_names::VERIFY_HOSTNAME.into(), + verify, + ) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; + } + + // ocsp_fail_open_mode: map to sf_core's crl_check_mode + // enabled (fail-open / advisory) → ADVISORY; disabled (strict) → ENABLED. + if key_str == "adbc.snowflake.sql.client_option.ocsp_fail_open_mode" { + let fail_open = matches!(&value, OptionValue::String(s) if s == "enabled"); + let mode = Setting::String(if fail_open { "ADVISORY" } else { "ENABLED" }.to_string()); + self.sf_settings.insert( + param_names::CRL_CHECK_MODE.as_str().to_string(), + mode.clone(), + ); + self.inner + .runtime + .block_on(self.inner.sf.database_set_option( + self.db_handle, + param_names::CRL_CHECK_MODE.into(), + mode, + )) + .map_err(crate::error::api_error_to_adbc_error)?; + } + + Ok(()) + } + + fn get_option_string(&self, key: Self::Option) -> Result { + let key_str = key.as_ref(); + if key_str == "adbc.snowflake.sql.client_option.use_high_precision" { + return Ok(if self.use_high_precision { + "enabled".to_string() + } else { + "disabled".to_string() + }); + } + if key_str == "adbc.snowflake.sql.client_option.max_timestamp_precision" { + return Ok(match self.timestamp_precision { + TimestampPrecision::Microseconds => "microseconds", + TimestampPrecision::NanosecondsErrorOnOverflow => "nanoseconds_error_on_overflow", + TimestampPrecision::Nanoseconds => "nanoseconds", + } + .to_string()); + } + if let Ok(Some((param, _))) = + adbc_db_opt_to_sf(key_str, &OptionValue::String(String::new())) + && let Some(Setting::String(s)) = self.sf_settings.get(¶m) + { + return Ok(s.clone()); + } + Err(Error::with_message_and_status( + format!("option not found: {key_str}"), + Status::NotFound, + )) + } + + fn get_option_bytes(&self, key: Self::Option) -> Result> { + let key_str = key.as_ref(); + if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::Bytes(vec![])) + && let Some(Setting::Bytes(b)) = self.sf_settings.get(¶m) + { + return Ok(b.clone()); + } + Err(Error::with_message_and_status( + format!("option not found: {key_str}"), + Status::NotFound, + )) + } + + fn get_option_int(&self, key: Self::Option) -> Result { + let key_str = key.as_ref(); + if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::Int(0)) + && let Some(Setting::Int(i)) = self.sf_settings.get(¶m) + { + return Ok(*i); + } + Err(Error::with_message_and_status( + format!("option not found: {key_str}"), + Status::NotFound, + )) + } + + fn get_option_double(&self, key: Self::Option) -> Result { + let key_str = key.as_ref(); + if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::Double(0.0)) + && let Some(Setting::Double(d)) = self.sf_settings.get(¶m) + { + return Ok(*d); + } + Err(Error::with_message_and_status( + format!("option not found: {key_str}"), + Status::NotFound, + )) + } +} + +impl Database { + /// Parse a Snowflake URI and apply each component as an individual option. + /// Format: snowflake://[user[:password]@]account[/database[/schema]][?param=value&...] + /// Recognized query params: warehouse, role, host, port, protocol, authenticator + /// + /// Limitations: passwords containing `@` are not supported; use `set_option` for + /// Username/Password directly when credentials contain special characters. + /// Query parameter values are not URL-decoded. + fn apply_uri(&mut self, uri: String) -> Result<()> { + let stripped = uri.strip_prefix("snowflake://").unwrap_or(&uri).to_string(); + + let (user_info, rest) = if let Some(at) = stripped.find('@') { + ( + Some(stripped[..at].to_string()), + stripped[at + 1..].to_string(), + ) + } else { + (None, stripped) + }; + + if let Some(info) = user_info { + if let Some(colon) = info.find(':') { + let user = percent_decode_str(&info[..colon]) + .decode_utf8() + .map_err(|e| { + Error::with_message_and_status( + &format!("invalid UTF-8 in URI username: {e}"), + Status::InvalidArguments, + ) + })?; + let pass = percent_decode_str(&info[colon + 1..]) + .decode_utf8() + .map_err(|e| { + Error::with_message_and_status( + &format!("invalid UTF-8 in URI password: {e}"), + Status::InvalidArguments, + ) + })?; + if !user.is_empty() { + self.set_option( + OptionDatabase::Username, + OptionValue::String(user.into_owned()), + )?; + } + self.set_option( + OptionDatabase::Password, + OptionValue::String(pass.into_owned()), + )?; + } else if !info.is_empty() { + let user = percent_decode_str(&info).decode_utf8().map_err(|e| { + Error::with_message_and_status( + &format!("invalid UTF-8 in URI username: {e}"), + Status::InvalidArguments, + ) + })?; + self.set_option( + OptionDatabase::Username, + OptionValue::String(user.into_owned()), + )?; + } + } + + let (path, query) = if let Some(q) = rest.find('?') { + (rest[..q].to_string(), Some(rest[q + 1..].to_string())) + } else { + (rest, None) + }; + + let parts: Vec<&str> = path.splitn(3, '/').collect(); + if let Some(account) = parts.first().filter(|s| !s.is_empty()) { + self.set_option( + OptionDatabase::Other("adbc.snowflake.sql.account".into()), + OptionValue::String(account.to_string()), + )?; + } + if let Some(database) = parts.get(1).filter(|s| !s.is_empty()) { + self.set_option( + OptionDatabase::Other("adbc.snowflake.sql.db".into()), + OptionValue::String(database.to_string()), + )?; + } + if let Some(schema) = parts.get(2).filter(|s| !s.is_empty()) { + self.set_option( + OptionDatabase::Other("adbc.snowflake.sql.schema".into()), + OptionValue::String(schema.to_string()), + )?; + } + + if let Some(q) = query { + for pair in q.split('&') { + if let Some(eq) = pair.find('=') { + let k = &pair[..eq]; + let v = percent_decode_str(&pair[eq + 1..]) + .decode_utf8() + .map_err(|e| { + Error::with_message_and_status( + &format!("invalid UTF-8 in URI query parameter: {e}"), + Status::InvalidArguments, + ) + })?; + let adbc_key = match k { + "warehouse" => "adbc.snowflake.sql.warehouse", + "role" => "adbc.snowflake.sql.role", + "host" => "adbc.snowflake.sql.uri.host", + "port" => "adbc.snowflake.sql.uri.port", + "protocol" => "adbc.snowflake.sql.uri.protocol", + "authenticator" => "adbc.snowflake.sql.auth_type", + "private_key_file" => "adbc.snowflake.sql.client_option.jwt_private_key", + "private_key" => { + "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_value" + } + _ => continue, + }; + self.set_option( + OptionDatabase::Other(adbc_key.into()), + OptionValue::String(v.into_owned()), + )?; + } + } + } + Ok(()) + } +} + +impl adbc_core::Database for Database { + type ConnectionType = Connection; + + fn new_connection(&self) -> Result { + self.new_connection_with_opts(std::iter::empty()) + } + + fn new_connection_with_opts( + &self, + opts: impl IntoIterator, + ) -> Result { + let conn_handle = self.inner.sf.connection_new(); + + // Propagate all database-level settings to the connection + for (param, setting) in &self.sf_settings { + self.inner + .runtime + .block_on(self.inner.sf.connection_set_option( + conn_handle, + param.clone(), + setting.clone(), + )) + .map_err(crate::error::api_error_to_adbc_error)?; + } + + let mut post_autocommit: Option = None; + let mut post_catalog: Option = None; + let mut post_schema: Option = None; + + for (key, value) in opts { + match &key { + OptionConnection::AutoCommit => { + if let OptionValue::String(s) = &value { + post_autocommit = Some(s == "true" || s == "1"); + } + } + OptionConnection::CurrentCatalog => { + if let OptionValue::String(s) = &value { + post_catalog = Some(s.clone()); + } + } + OptionConnection::CurrentSchema => { + if let OptionValue::String(s) = &value { + post_schema = Some(s.clone()); + } + } + OptionConnection::Other(k) => { + let sf_setting = match &value { + OptionValue::String(s) => Setting::String(s.clone()), + OptionValue::Int(i) => Setting::Int(*i), + OptionValue::Double(d) => Setting::Double(*d), + OptionValue::Bytes(b) => Setting::Bytes(b.clone()), + _ => { + return Err(Error::with_message_and_status( + "unsupported option value type", + Status::InvalidArguments, + )); + } + }; + self.inner + .runtime + .block_on(self.inner.sf.connection_set_option( + conn_handle, + k.clone(), + sf_setting, + )) + .map_err(crate::error::api_error_to_adbc_error)?; + } + _ => {} + } + } + + // If neither host nor server_url was provided, derive host from account. + if !self.sf_settings.contains_key(param_names::HOST.as_str()) + && !self + .sf_settings + .contains_key(param_names::SERVER_URL.as_str()) + && let Some(Setting::String(account)) = + self.sf_settings.get(param_names::ACCOUNT.as_str()) + { + let host = format!("{}.snowflakecomputing.com", account); + self.inner + .runtime + .block_on(self.inner.sf.connection_set_option( + conn_handle, + param_names::HOST.into(), + Setting::String(host), + )) + .map_err(crate::error::api_error_to_adbc_error)?; + } + + // Authenticate + self.inner + .runtime + .block_on(self.inner.sf.connection_init(conn_handle, self.db_handle)) + .map_err(crate::error::api_error_to_adbc_error)?; + + let mut conn = Connection { + inner: self.inner.clone(), + conn_handle, + autocommit: true, + active_transaction: false, + use_high_precision: self.use_high_precision, + timestamp_precision: self.timestamp_precision, + }; + + if let Some(ac) = post_autocommit { + conn.set_autocommit(ac)?; + } + if let Some(cat) = post_catalog { + conn.execute_simple(&format!(r#"USE DATABASE "{}""#, cat.replace('"', "\"\"")))?; + } + if let Some(sch) = post_schema { + conn.execute_simple(&format!(r#"USE SCHEMA "{}""#, sch.replace('"', "\"\"")))?; + } + + Ok(conn) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use adbc_core::{ + Driver as _, + options::{OptionDatabase, OptionValue}, + }; + use sf_core::config::param_registry::param_names; + + fn make_db() -> Database { + let mut driver = crate::driver::Driver::default(); + driver.new_database().unwrap() + } + + #[test] + fn set_and_get_account_option() { + let mut db = make_db(); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.account".into()), + OptionValue::String("myaccount".into()), + ) + .unwrap(); + assert_eq!( + db.get_option_string(OptionDatabase::Other("adbc.snowflake.sql.account".into())) + .unwrap(), + "myaccount" + ); + } + + #[test] + fn set_port_option_as_string_converts_to_int() { + let mut db = make_db(); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.uri.port".into()), + OptionValue::String("443".into()), + ) + .unwrap(); + let setting = db.sf_settings.get(param_names::PORT.as_str()).unwrap(); + assert_eq!(*setting, sf_core::config::settings::Setting::Int(443)); + } + + #[test] + fn username_maps_to_user_param() { + let mut db = make_db(); + db.set_option( + OptionDatabase::Username, + OptionValue::String("alice".into()), + ) + .unwrap(); + let setting = db.sf_settings.get(param_names::USER.as_str()).unwrap(); + assert_eq!( + *setting, + sf_core::config::settings::Setting::String("alice".into()) + ); + } + + #[test] + fn tls_skip_verify_enabled_clears_verify_flags() { + use sf_core::config::settings::Setting; + let mut db = make_db(); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.tls_skip_verify".into()), + OptionValue::String("enabled".into()), + ) + .unwrap(); + // Round-trip + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.tls_skip_verify".into() + )) + .unwrap(), + "enabled" + ); + // Compound: verify_certificates and verify_hostname must be false + assert_eq!( + db.sf_settings + .get(param_names::VERIFY_CERTIFICATES.as_str()), + Some(&Setting::Bool(false)) + ); + assert_eq!( + db.sf_settings.get(param_names::VERIFY_HOSTNAME.as_str()), + Some(&Setting::Bool(false)) + ); + } + + #[test] + fn tls_skip_verify_disabled_restores_verify_flags() { + use sf_core::config::settings::Setting; + let mut db = make_db(); + // First enable, then disable + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.tls_skip_verify".into()), + OptionValue::String("enabled".into()), + ) + .unwrap(); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.tls_skip_verify".into()), + OptionValue::String("disabled".into()), + ) + .unwrap(); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.tls_skip_verify".into() + )) + .unwrap(), + "disabled" + ); + assert_eq!( + db.sf_settings + .get(param_names::VERIFY_CERTIFICATES.as_str()), + Some(&Setting::Bool(true)) + ); + assert_eq!( + db.sf_settings.get(param_names::VERIFY_HOSTNAME.as_str()), + Some(&Setting::Bool(true)) + ); + } + + #[test] + fn ocsp_fail_open_mode_enabled_maps_to_crl_advisory() { + use sf_core::config::settings::Setting; + let mut db = make_db(); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into()), + OptionValue::String("enabled".into()), + ) + .unwrap(); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into() + )) + .unwrap(), + "enabled" + ); + assert_eq!( + db.sf_settings.get(param_names::CRL_CHECK_MODE.as_str()), + Some(&Setting::String("ADVISORY".into())) + ); + } + + #[test] + fn ocsp_fail_open_mode_disabled_maps_to_crl_enabled() { + use sf_core::config::settings::Setting; + let mut db = make_db(); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into()), + OptionValue::String("disabled".into()), + ) + .unwrap(); + assert_eq!( + db.sf_settings.get(param_names::CRL_CHECK_MODE.as_str()), + Some(&Setting::String("ENABLED".into())) + ); + } + + #[test] + fn simple_option_round_trips() { + let mut db = make_db(); + let cases = [ + ("adbc.snowflake.sql.region", "us-east-1"), + ("adbc.snowflake.sql.client_option.login_timeout", "30s"), + ("adbc.snowflake.sql.client_option.request_timeout", "60s"), + ( + "adbc.snowflake.sql.client_option.keep_session_alive", + "enabled", + ), + ( + "adbc.snowflake.sql.client_option.disable_telemetry", + "enabled", + ), + ("adbc.snowflake.sql.client_option.tracing", "debug"), + ( + "adbc.snowflake.sql.client_option.config_file", + "/home/user/.snowflake/config.toml", + ), + ]; + for (key, val) in cases { + db.set_option( + OptionDatabase::Other(key.into()), + OptionValue::String(val.into()), + ) + .unwrap_or_else(|e| panic!("set_option({key}) failed: {e}")); + let got = db + .get_option_string(OptionDatabase::Other(key.into())) + .unwrap_or_else(|e| panic!("get_option_string({key}) failed: {e}")); + assert_eq!(got, val, "round-trip failed for {key}"); + } + } + + #[test] + fn uri_parses_account_user_database() { + let mut db = make_db(); + db.set_option( + OptionDatabase::Uri, + OptionValue::String("snowflake://alice:secret@myaccount/mydb/myschema".into()), + ) + .unwrap(); + assert_eq!( + db.sf_settings.get(param_names::ACCOUNT.as_str()).unwrap(), + &sf_core::config::settings::Setting::String("myaccount".into()) + ); + assert_eq!( + db.sf_settings.get(param_names::USER.as_str()).unwrap(), + &sf_core::config::settings::Setting::String("alice".into()) + ); + assert_eq!( + db.sf_settings.get(param_names::DATABASE.as_str()).unwrap(), + &sf_core::config::settings::Setting::String("mydb".into()) + ); + } +} diff --git a/rust/src/driver.rs b/rust/src/driver.rs new file mode 100644 index 0000000..90a1139 --- /dev/null +++ b/rust/src/driver.rs @@ -0,0 +1,126 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// src/driver.rs +use std::sync::Arc; + +use adbc_core::{ + Optionable, + error::{Error, Result, Status}, + options::{OptionDatabase, OptionValue}, +}; +use arrow_schema::TimeUnit; +use sf_core::apis::database_driver_v1::DatabaseDriverV1; +use tokio::runtime::Runtime; + +use crate::database::Database; + +/// Controls the Arrow time unit used for Snowflake TIMESTAMP columns. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub(crate) enum TimestampPrecision { + /// Nanosecond precision (default). May overflow for dates outside 1677–2262. + #[default] + Nanoseconds, + /// Microsecond precision. Safe for all Snowflake-representable dates. + Microseconds, + /// Nanosecond precision; returns an error when a value would overflow. + NanosecondsErrorOnOverflow, +} + +impl TimestampPrecision { + pub(crate) fn time_unit(self) -> TimeUnit { + match self { + Self::Microseconds => TimeUnit::Microsecond, + _ => TimeUnit::Nanosecond, + } + } +} + +pub(crate) struct Inner { + pub runtime: Runtime, + pub sf: DatabaseDriverV1, +} + +impl Inner { + fn new() -> Result { + let runtime = Runtime::new().map_err(|e| { + Error::with_message_and_status( + format!("Failed to create tokio runtime: {e}"), + Status::IO, + ) + })?; + Ok(Self { + runtime, + sf: DatabaseDriverV1::new(), + }) + } +} + +/// Snowflake ADBC Driver. +pub struct Driver { + pub(crate) inner: Arc, +} + +impl Default for Driver { + fn default() -> Self { + Self { + inner: Arc::new(Inner::new().expect("failed to initialize driver")), + } + } +} + +impl adbc_core::Driver for Driver { + type DatabaseType = Database; + + fn new_database(&mut self) -> Result { + self.new_database_with_opts(std::iter::empty()) + } + + fn new_database_with_opts( + &mut self, + opts: impl IntoIterator, + ) -> Result { + let db_handle = self.inner.sf.database_new(); + let sf_settings: std::collections::HashMap = + Default::default(); + let mut db = Database { + inner: self.inner.clone(), + db_handle, + sf_settings, + use_high_precision: true, + timestamp_precision: TimestampPrecision::default(), + }; + for (key, value) in opts { + db.set_option(key, value)?; + } + Ok(db) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use adbc_core::Driver as _; + + #[test] + fn driver_default_creates_successfully() { + let _driver = Driver::default(); + } + + #[test] + fn new_database_succeeds_with_no_options() { + let mut driver = Driver::default(); + let _db = driver.new_database().expect("new_database failed"); + } +} diff --git a/rust/src/error.rs b/rust/src/error.rs new file mode 100644 index 0000000..2e7587d --- /dev/null +++ b/rust/src/error.rs @@ -0,0 +1,73 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// src/error.rs +use adbc_core::error::{Error, Status}; +use sf_core::apis::database_driver_v1::ApiError; + +pub(crate) fn api_error_to_adbc_error(err: ApiError) -> Error { + let status = match &err { + ApiError::InvalidArgument { .. } => Status::InvalidArguments, + ApiError::Configuration { .. } => Status::InvalidArguments, + ApiError::ConnectionNotInitialized { .. } => Status::InvalidState, + ApiError::ConnectionLocking { .. } => Status::InvalidState, + ApiError::StatementLocking { .. } => Status::InvalidState, + ApiError::DatabaseLocking { .. } => Status::InvalidState, + ApiError::InvalidRefreshState { .. } => Status::InvalidState, + ApiError::Login { .. } => Status::Unauthenticated, + ApiError::SessionRefresh { .. } => Status::Unauthenticated, + ApiError::MasterTokenExpired { .. } => Status::Unauthenticated, + ApiError::TlsClientCreation { .. } => Status::IO, + ApiError::Query { .. } => Status::IO, + ApiError::QueryResponseProcessing { .. } => Status::IO, + ApiError::Statement { .. } => Status::IO, + ApiError::RuntimeCreation { .. } => Status::IO, + ApiError::GenericError { .. } => Status::IO, + ApiError::TokenCacheInitialization { .. } => Status::IO, + ApiError::ArrowParsing { .. } => Status::IO, + ApiError::ChunkFetch { .. } => Status::IO, + ApiError::Base64Decoding { .. } => Status::IO, + ApiError::HttpRequest { .. } => Status::IO, + ApiError::TokenRequest { .. } => Status::Unauthenticated, + }; + Error::with_message_and_status(err.to_string(), status) +} + +pub(crate) fn not_implemented(msg: &str) -> Error { + Error::with_message_and_status(msg, Status::NotImplemented) +} + +#[cfg(test)] +mod tests { + use super::*; + use adbc_core::error::Status; + + #[test] + fn invalid_argument_maps_to_invalid_arguments() { + use sf_core::apis::database_driver_v1::{DatabaseDriverV1, Handle}; + // Releasing a non-existent handle produces an InvalidArgument error + let driver = DatabaseDriverV1::new(); + let bogus_handle = Handle { id: 999, magic: 0 }; + let err = driver.database_init(bogus_handle).unwrap_err(); + let adbc_err = api_error_to_adbc_error(err); + assert_eq!(adbc_err.status, Status::InvalidArguments); + } + + #[test] + fn not_implemented_returns_correct_status() { + let err = not_implemented("foo"); + assert_eq!(err.status, adbc_core::error::Status::NotImplemented); + assert!(err.message.contains("foo")); + } +} diff --git a/rust/src/get_objects.rs b/rust/src/get_objects.rs new file mode 100644 index 0000000..2544a57 --- /dev/null +++ b/rust/src/get_objects.rs @@ -0,0 +1,807 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// src/get_objects.rs + +use std::sync::Arc; + +use adbc_core::{ + error::{Error, Result, Status}, + options::ObjectDepth, + schemas, +}; +use arrow_array::{ + Array, ArrayRef, BooleanArray, Int16Array, Int32Array, ListArray, RecordBatch, + RecordBatchReader, StringArray, StructArray, +}; +use arrow_buffer::{NullBuffer, OffsetBuffer, ScalarBuffer}; +use arrow_schema::{DataType, Field, Fields}; + +use crate::connection::{Connection, SingleBatchReader}; + +// ── SQL helpers ─────────────────────────────────────────────────────────────── + +fn sql_esc(s: &str) -> String { + s.replace('\'', "''") +} + +/// Converts an optional filter into a SQL ILIKE pattern. None → match-all. +fn ilike(p: Option<&str>) -> String { + match p { + None | Some("") | Some("%") | Some(".*") => "%".to_string(), + Some(s) => sql_esc(s), + } +} + +/// Returns an `information_schema.` prefix, optionally qualified by a specific +/// catalog when the filter is an exact name (not a wildcard). +fn info_prefix(catalog_filter: Option<&str>) -> String { + match catalog_filter { + Some(c) if !c.is_empty() && !c.contains('%') && !c.contains('_') => { + format!("\"{}\".information_schema.", c.replace('"', "\"\"")) + } + _ => "information_schema.".to_string(), + } +} + +/// Builds a table-type WHERE fragment, mapping ADBC 'TABLE' → Snowflake 'BASE TABLE'. +fn type_clause(filter: &Option>) -> String { + let Some(types) = filter else { + return String::new(); + }; + if types.is_empty() { + return String::new(); + } + let quoted: Vec = types + .iter() + .map(|t| { + let up = t.to_uppercase(); + let sf = if up == "TABLE" { "BASE TABLE" } else { t }; + format!("'{}'", sql_esc(sf)) + }) + .collect(); + format!(" AND table_type IN ({})", quoted.join(", ")) +} + +// ── query execution ─────────────────────────────────────────────────────────── + +/// Runs a SQL query and returns all rows as `Vec>>`. +fn run_query(conn: &Connection, sql: &str) -> Result>>> { + let stmt = conn + .inner + .sf + .statement_new(conn.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + + let result = conn.inner.runtime.block_on(async { + conn.inner + .sf + .statement_set_sql_query(stmt, sql.to_string()) + .await?; + conn.inner.sf.statement_execute_query(stmt, None).await + }); + let _ = conn.inner.sf.statement_release(stmt); + let exec = result.map_err(crate::error::api_error_to_adbc_error)?; + + let raw = Box::into_raw(exec.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| { + // Safety: Arrow's C Data Interface specifies that on failure, from_raw + // does NOT call the stream's release callback, so reconstructing the + // Box here is the only release path — no double-free risk. + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; + + let mut rows = Vec::new(); + for batch_res in reader { + let batch = + batch_res.map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + let ncols = batch.num_columns(); + for row_idx in 0..batch.num_rows() { + let row = (0..ncols) + .map(|c| cell_to_string(batch.column(c).as_ref(), row_idx)) + .collect(); + rows.push(row); + } + } + Ok(rows) +} + +fn cell_to_string(arr: &dyn Array, i: usize) -> Option { + if arr.is_null(i) { + return None; + } + if let Some(s) = arr.as_any().downcast_ref::() { + return Some(s.value(i).to_string()); + } + if let Some(s) = arr.as_any().downcast_ref::() { + return Some(s.value(i).to_string()); + } + if let Some(n) = arr.as_any().downcast_ref::() { + return Some(n.value(i).to_string()); + } + if let Some(n) = arr.as_any().downcast_ref::() { + return Some(n.value(i).to_string()); + } + if let Some(n) = arr.as_any().downcast_ref::() { + return Some(n.value(i).to_string()); + } + // Some NUMBER(p,0) columns (like ordinal_position) arrive as Float64 in the + // sf_core Arrow stream for metadata queries; truncate to integer string. + if let Some(n) = arr.as_any().downcast_ref::() { + let v = n.value(i); + return if v.is_finite() { + Some((v as i64).to_string()) + } else { + None + }; + } + if let Some(n) = arr.as_any().downcast_ref::() { + let v = n.value(i); + return if v.is_finite() { + Some((v as i64).to_string()) + } else { + None + }; + } + // Snowflake NUMBER(p,0) columns (like ordinal_position) arrive as Decimal128 + // when sf_core applies high-precision type mapping. Extract the integer part + // by dividing by 10^scale (scale is typically 0 for integer metadata columns). + if let Some(a) = arr.as_any().downcast_ref::() { + let scale = match a.data_type() { + DataType::Decimal128(_, s) => *s, + _ => 0i8, + }; + let raw = a.value(i); + let value = if scale > 0 && scale <= 38 { + raw / 10i128.pow(scale as u32) + } else { + // scale == 0: no adjustment; scale > 38: i128 exponent would overflow, + // return the raw representation rather than panicking. + raw + }; + return Some(value.to_string()); + } + None +} + +// ── data holders ───────────────────────────────────────────────────────────── + +struct ColEntry { + name: String, + ordinal_position: i32, + remarks: Option, + xdbc_type_name: Option, + xdbc_column_size: Option, + xdbc_char_octet_length: Option, + xdbc_decimal_digits: Option, + xdbc_num_prec_radix: Option, + xdbc_nullable: Option, + xdbc_is_nullable: Option, + xdbc_datetime_sub: Option, +} + +struct TableEntry { + name: String, + table_type: String, + columns: Vec, +} + +struct SchemaEntry { + name: String, + tables: Vec, +} + +struct CatalogEntry { + name: String, + schemas: Vec, +} + +// ── data collection ─────────────────────────────────────────────────────────── + +pub(crate) fn execute_get_objects( + conn: &Connection, + depth: &ObjectDepth, + catalog_filter: Option<&str>, + db_schema_filter: Option<&str>, + table_name_filter: Option<&str>, + table_type_filter: Option>, + column_name_filter: Option<&str>, +) -> Result> { + let entries = collect( + conn, + depth, + catalog_filter, + db_schema_filter, + table_name_filter, + table_type_filter, + column_name_filter, + )?; + let batch = build_batch(&entries, depth)?; + Ok(Box::new(SingleBatchReader::new(batch))) +} + +fn collect( + conn: &Connection, + depth: &ObjectDepth, + catalog_filter: Option<&str>, + db_schema_filter: Option<&str>, + table_name_filter: Option<&str>, + table_type_filter: Option>, + column_name_filter: Option<&str>, +) -> Result> { + let cat_pat = ilike(catalog_filter); + let sch_pat = ilike(db_schema_filter); + let tbl_pat = ilike(table_name_filter); + let col_pat = ilike(column_name_filter); + + // 1. Catalogs — always from information_schema.databases (account-wide) + let cat_rows = run_query( + conn, + &format!( + "SELECT database_name FROM information_schema.databases \ + WHERE database_name ILIKE '{}' ORDER BY database_name", + cat_pat + ), + )?; + let catalog_names: Vec = cat_rows + .into_iter() + .filter_map(|r| r.into_iter().next().flatten()) + .collect(); + + if catalog_names.is_empty() || matches!(depth, ObjectDepth::Catalogs) { + return Ok(catalog_names + .into_iter() + .map(|name| CatalogEntry { + name, + schemas: vec![], + }) + .collect()); + } + + // 2. Schemas — scoped to the catalog(s) in view + // When catalog_filter is a specific database we qualify the prefix; otherwise + // information_schema resolves to the current database. + let prefix = info_prefix(catalog_filter); + let sch_rows = run_query( + conn, + &format!( + "SELECT catalog_name, schema_name FROM {}schemata \ + WHERE catalog_name ILIKE '{}' AND schema_name ILIKE '{}' \ + ORDER BY catalog_name, schema_name", + prefix, cat_pat, sch_pat + ), + )?; + + // Group schemas by catalog + let mut schemas_by_cat: std::collections::BTreeMap> = + catalog_names.iter().map(|n| (n.clone(), vec![])).collect(); + for row in &sch_rows { + if row.len() < 2 { + continue; + } + if let (Some(cat), Some(sch)) = (&row[0], &row[1]) { + schemas_by_cat + .entry(cat.clone()) + .or_default() + .push(sch.clone()); + } + } + + if matches!(depth, ObjectDepth::Schemas) { + return Ok(catalog_names + .into_iter() + .map(|cat| { + let schemas = schemas_by_cat + .remove(&cat) + .unwrap_or_default() + .into_iter() + .map(|sch| SchemaEntry { + name: sch, + tables: vec![], + }) + .collect(); + CatalogEntry { name: cat, schemas } + }) + .collect()); + } + + // 3. Tables + let tc = type_clause(&table_type_filter); + let tbl_rows = run_query( + conn, + &format!( + "SELECT table_catalog, table_schema, table_name, \ + CASE table_type WHEN 'BASE TABLE' THEN 'TABLE' ELSE table_type END \ + FROM {}tables \ + WHERE table_catalog ILIKE '{}' AND table_schema ILIKE '{}' \ + AND table_name ILIKE '{}'{} \ + ORDER BY table_catalog, table_schema, table_name", + prefix, cat_pat, sch_pat, tbl_pat, tc + ), + )?; + + // Group tables by (catalog, schema) + let mut tables_by_key: std::collections::BTreeMap<(String, String), Vec> = + std::collections::BTreeMap::new(); + for row in &tbl_rows { + if row.len() < 4 { + continue; + } + if let (Some(cat), Some(sch), Some(tbl), Some(tt)) = (&row[0], &row[1], &row[2], &row[3]) { + tables_by_key + .entry((cat.clone(), sch.clone())) + .or_default() + .push(TableEntry { + name: tbl.clone(), + table_type: tt.clone(), + columns: vec![], + }); + } + } + + if matches!(depth, ObjectDepth::Tables) { + return Ok(assemble(catalog_names, schemas_by_cat, tables_by_key)); + } + + // 4. Columns (All / Columns depth) + let col_rows = run_query( + conn, + &format!( + "SELECT table_catalog, table_schema, table_name, column_name, \ + ordinal_position::INTEGER, comment, data_type, is_nullable, \ + COALESCE(character_maximum_length, numeric_precision)::INTEGER, \ + character_octet_length::INTEGER, numeric_scale::INTEGER, \ + numeric_precision_radix::INTEGER, datetime_precision::INTEGER \ + FROM {}columns \ + WHERE table_catalog ILIKE '{}' AND table_schema ILIKE '{}' \ + AND table_name ILIKE '{}' AND column_name ILIKE '{}' \ + ORDER BY table_catalog, table_schema, table_name, ordinal_position", + prefix, cat_pat, sch_pat, tbl_pat, col_pat + ), + )?; + + // Track 1-based column position per (catalog, schema, table). + // The query is already sorted by ordinal_position so the rows arrive in + // column order; we count them ourselves rather than trusting the database + // value, which varies across Snowflake environments. + let mut col_counter: std::collections::HashMap<(String, String, String), i32> = + Default::default(); + + for row in &col_rows { + if row.len() < 13 { + continue; + } + let (Some(cat), Some(sch), Some(tbl), Some(col_name)) = + (&row[0], &row[1], &row[2], &row[3]) + else { + continue; + }; + let key = (cat.clone(), sch.clone()); + if let Some(tables) = tables_by_key.get_mut(&key) + && let Some(table) = tables.iter_mut().find(|t| &t.name == tbl) + { + let nullable_str = row[7].clone(); + let nullable_int = nullable_str.as_deref().map(|s| { + if s.eq_ignore_ascii_case("YES") { + 1i16 + } else { + 0 + } + }); + let ordinal = { + let c = col_counter + .entry((cat.clone(), sch.clone(), tbl.clone())) + .or_insert(0); + *c += 1; + *c + }; + table.columns.push(ColEntry { + name: col_name.clone(), + ordinal_position: ordinal, + remarks: row[5].clone(), + xdbc_type_name: row[6].clone(), + xdbc_column_size: row[8].as_deref().and_then(|s| s.parse().ok()), + xdbc_char_octet_length: row[9].as_deref().and_then(|s| s.parse().ok()), + xdbc_decimal_digits: row[10].as_deref().and_then(|s| s.parse().ok()), + xdbc_num_prec_radix: row[11].as_deref().and_then(|s| s.parse().ok()), + xdbc_nullable: nullable_int, + xdbc_is_nullable: nullable_str, + xdbc_datetime_sub: row[12].as_deref().and_then(|s| s.parse().ok()), + }); + } + } + + Ok(assemble(catalog_names, schemas_by_cat, tables_by_key)) +} + +fn assemble( + catalog_names: Vec, + mut schemas_by_cat: std::collections::BTreeMap>, + mut tables_by_key: std::collections::BTreeMap<(String, String), Vec>, +) -> Vec { + catalog_names + .into_iter() + .map(|cat| { + let schema_names = schemas_by_cat.remove(&cat).unwrap_or_default(); + let schemas = schema_names + .into_iter() + .map(|sch| { + let tables = tables_by_key + .remove(&(cat.clone(), sch.clone())) + .unwrap_or_default(); + SchemaEntry { name: sch, tables } + }) + .collect(); + CatalogEntry { name: cat, schemas } + }) + .collect() +} + +// ── Arrow output construction ───────────────────────────────────────────────── + +fn build_batch(entries: &[CatalogEntry], depth: &ObjectDepth) -> Result { + let schemas_null = matches!(depth, ObjectDepth::Catalogs); + let tables_null = matches!(depth, ObjectDepth::Catalogs | ObjectDepth::Schemas); + let cols_null = !matches!(depth, ObjectDepth::All | ObjectDepth::Columns); + + // Flatten counts + let n_cats = entries.len(); + let n_schemas: usize = entries.iter().map(|c| c.schemas.len()).sum(); + let n_tables: usize = entries + .iter() + .flat_map(|c| &c.schemas) + .map(|s| s.tables.len()) + .sum(); + + // ── columns ─────────────────────────────────────────────────────────────── + let all_cols: Vec<&ColEntry> = entries + .iter() + .flat_map(|c| &c.schemas) + .flat_map(|s| &s.tables) + .flat_map(|t| &t.columns) + .collect(); + let col_struct = build_col_struct(&all_cols); + + // col offsets per table + let (col_offsets, col_null_buf) = if cols_null { + ( + vec![0i32; n_tables + 1], + Some(NullBuffer::new_null(n_tables)), + ) + } else { + let mut offs = Vec::with_capacity(n_tables + 1); + offs.push(0i32); + for c in entries + .iter() + .flat_map(|c| &c.schemas) + .flat_map(|s| &s.tables) + { + let last = *offs.last().unwrap(); + offs.push(last + c.columns.len() as i32); + } + (offs, None) + }; + let col_item_field = Arc::new(item_field_of(&schemas::COLUMN_SCHEMA)); + let col_list = make_list( + col_item_field, + col_offsets, + Arc::new(col_struct), + col_null_buf, + )?; + + // ── table_constraints (always null in our impl) ─────────────────────────── + let constraint_item_field = Arc::new(item_field_of(&schemas::CONSTRAINT_SCHEMA)); + let constraint_list = make_all_null_list(constraint_item_field, n_tables)?; + + // ── table struct ────────────────────────────────────────────────────────── + let tbl_struct = build_table_struct(entries, col_list, constraint_list)?; + + // table offsets per schema + let (tbl_offsets, tbl_null_buf) = if tables_null { + ( + vec![0i32; n_schemas + 1], + Some(NullBuffer::new_null(n_schemas)), + ) + } else { + let mut offs = Vec::with_capacity(n_schemas + 1); + offs.push(0i32); + for s in entries.iter().flat_map(|c| &c.schemas) { + let last = *offs.last().unwrap(); + offs.push(last + s.tables.len() as i32); + } + (offs, None) + }; + let tbl_item_field = Arc::new(item_field_of(&schemas::TABLE_SCHEMA)); + let tbl_list = make_list( + tbl_item_field, + tbl_offsets, + Arc::new(tbl_struct), + tbl_null_buf, + )?; + + // ── schema struct ───────────────────────────────────────────────────────── + let sch_struct = build_schema_struct(entries, tbl_list)?; + + // schema offsets per catalog + let (sch_offsets, sch_null_buf) = if schemas_null { + (vec![0i32; n_cats + 1], Some(NullBuffer::new_null(n_cats))) + } else { + let mut offs = Vec::with_capacity(n_cats + 1); + offs.push(0i32); + for c in entries { + let last = *offs.last().unwrap(); + offs.push(last + c.schemas.len() as i32); + } + (offs, None) + }; + let sch_item_field = Arc::new(item_field_of(&schemas::OBJECTS_DB_SCHEMA_SCHEMA)); + let sch_list = make_list( + sch_item_field, + sch_offsets, + Arc::new(sch_struct), + sch_null_buf, + )?; + + // ── top-level batch ─────────────────────────────────────────────────────── + let cat_names: Vec> = entries.iter().map(|c| Some(c.name.as_str())).collect(); + RecordBatch::try_new( + schemas::GET_OBJECTS_SCHEMA.clone(), + vec![ + Arc::new(StringArray::from(cat_names)) as ArrayRef, + Arc::new(sch_list) as ArrayRef, + ], + ) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal)) +} + +// ── struct array builders ───────────────────────────────────────────────────── + +fn build_col_struct(cols: &[&ColEntry]) -> StructArray { + let n = cols.len(); + let mut col_name = StringBuilder::with_capacity(n, n * 16); + let mut ordinal = Int32Builder::with_capacity(n); + let mut remarks = StringBuilder::with_capacity(n, n * 8); + let mut xdbc_data_type = Int16Builder::with_capacity(n); + let mut xdbc_type_name = StringBuilder::with_capacity(n, n * 8); + let mut xdbc_col_size = Int32Builder::with_capacity(n); + let mut xdbc_dec_digits = Int16Builder::with_capacity(n); + let mut xdbc_radix = Int16Builder::with_capacity(n); + let mut xdbc_nullable = Int16Builder::with_capacity(n); + let mut xdbc_col_def = StringBuilder::with_capacity(n, 0); + let mut xdbc_sql_dt = Int16Builder::with_capacity(n); + let mut xdbc_dt_sub = Int16Builder::with_capacity(n); + let mut xdbc_octet_len = Int32Builder::with_capacity(n); + let mut xdbc_is_nullable = StringBuilder::with_capacity(n, n * 4); + let mut xdbc_scope_cat = StringBuilder::with_capacity(n, 0); + let mut xdbc_scope_sch = StringBuilder::with_capacity(n, 0); + let mut xdbc_scope_tbl = StringBuilder::with_capacity(n, 0); + let mut xdbc_auto_inc = BooleanBuilder::with_capacity(n); + let mut xdbc_gen_col = BooleanBuilder::with_capacity(n); + + for c in cols { + col_name.append_value(&c.name); + ordinal.append_value(c.ordinal_position); + append_opt_str(&mut remarks, c.remarks.as_deref()); + xdbc_data_type.append_null(); // computed from Arrow type — not available here + append_opt_str(&mut xdbc_type_name, c.xdbc_type_name.as_deref()); + append_opt_i32(&mut xdbc_col_size, c.xdbc_column_size); + append_opt_i16(&mut xdbc_dec_digits, c.xdbc_decimal_digits); + append_opt_i16(&mut xdbc_radix, c.xdbc_num_prec_radix); + append_opt_i16(&mut xdbc_nullable, c.xdbc_nullable); + xdbc_col_def.append_null(); + xdbc_sql_dt.append_null(); + append_opt_i16(&mut xdbc_dt_sub, c.xdbc_datetime_sub); + append_opt_i32(&mut xdbc_octet_len, c.xdbc_char_octet_length); + append_opt_str(&mut xdbc_is_nullable, c.xdbc_is_nullable.as_deref()); + xdbc_scope_cat.append_null(); + xdbc_scope_sch.append_null(); + xdbc_scope_tbl.append_null(); + xdbc_auto_inc.append_null(); + xdbc_gen_col.append_null(); + } + + let fields = struct_fields(&schemas::COLUMN_SCHEMA); + StructArray::try_new( + fields, + vec![ + Arc::new(col_name.finish()) as ArrayRef, + Arc::new(ordinal.finish()), + Arc::new(remarks.finish()), + Arc::new(xdbc_data_type.finish()), + Arc::new(xdbc_type_name.finish()), + Arc::new(xdbc_col_size.finish()), + Arc::new(xdbc_dec_digits.finish()), + Arc::new(xdbc_radix.finish()), + Arc::new(xdbc_nullable.finish()), + Arc::new(xdbc_col_def.finish()), + Arc::new(xdbc_sql_dt.finish()), + Arc::new(xdbc_dt_sub.finish()), + Arc::new(xdbc_octet_len.finish()), + Arc::new(xdbc_is_nullable.finish()), + Arc::new(xdbc_scope_cat.finish()), + Arc::new(xdbc_scope_sch.finish()), + Arc::new(xdbc_scope_tbl.finish()), + Arc::new(xdbc_auto_inc.finish()), + Arc::new(xdbc_gen_col.finish()), + ], + None, + ) + .expect("column StructArray construction") +} + +fn build_table_struct( + entries: &[CatalogEntry], + col_list: ListArray, + constraint_list: ListArray, +) -> Result { + let n_tables: usize = entries + .iter() + .flat_map(|c| &c.schemas) + .map(|s| s.tables.len()) + .sum(); + + let mut tbl_name = StringBuilder::with_capacity(n_tables, n_tables * 16); + let mut tbl_type = StringBuilder::with_capacity(n_tables, n_tables * 8); + + for t in entries + .iter() + .flat_map(|c| &c.schemas) + .flat_map(|s| &s.tables) + { + tbl_name.append_value(&t.name); + tbl_type.append_value(&t.table_type); + } + + let fields = struct_fields(&schemas::TABLE_SCHEMA); + StructArray::try_new( + fields, + vec![ + Arc::new(tbl_name.finish()) as ArrayRef, + Arc::new(tbl_type.finish()), + Arc::new(col_list), + Arc::new(constraint_list), + ], + None, + ) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal)) +} + +fn build_schema_struct(entries: &[CatalogEntry], tbl_list: ListArray) -> Result { + let n_schemas: usize = entries.iter().map(|c| c.schemas.len()).sum(); + let mut sch_name = StringBuilder::with_capacity(n_schemas, n_schemas * 16); + + for s in entries.iter().flat_map(|c| &c.schemas) { + sch_name.append_value(&s.name); + } + + let fields = struct_fields(&schemas::OBJECTS_DB_SCHEMA_SCHEMA); + StructArray::try_new( + fields, + vec![Arc::new(sch_name.finish()) as ArrayRef, Arc::new(tbl_list)], + None, + ) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal)) +} + +// ── list array helpers ──────────────────────────────────────────────────────── + +fn make_list( + item_field: Arc, + offsets: Vec, + values: ArrayRef, + null_buf: Option, +) -> Result { + let offs = OffsetBuffer::new(ScalarBuffer::from(offsets)); + ListArray::try_new(item_field, offs, values, null_buf) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal)) +} + +/// Creates a ListArray where every entry is null (for table_constraints). +fn make_all_null_list(item_field: Arc, n: usize) -> Result { + let null_buf = if n > 0 { + Some(NullBuffer::new_null(n)) + } else { + None + }; + let offsets = vec![0i32; n + 1]; + // Empty values StructArray matching the item field's struct type + let values: ArrayRef = match item_field.data_type() { + DataType::Struct(fields) => { + let empty_arrays: Vec = fields + .iter() + .map(|f| empty_array_for(f.data_type())) + .collect(); + Arc::new( + StructArray::try_new(fields.clone(), empty_arrays, None) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal))?, + ) + } + _ => { + return Err(Error::with_message_and_status( + "expected struct item type", + Status::Internal, + )); + } + }; + make_list(item_field, offsets, values, null_buf) +} + +// ── type extraction helpers ─────────────────────────────────────────────────── + +/// Extracts the `Fields` from a DataType::Struct stored in an adbc_core schema static. +fn struct_fields(dt: &DataType) -> Fields { + match dt { + DataType::Struct(f) => f.clone(), + _ => panic!("expected DataType::Struct"), + } +} + +/// Returns a `Field { name: "item", data_type: , nullable: true }` +/// that matches what `DataType::new_list(dt, nullable)` produces internally. +fn item_field_of(dt: &DataType) -> Field { + Field::new("item", dt.clone(), true) +} + +/// Returns an empty (zero-length) array for the given DataType, used when +/// building all-null list arrays that still need a correctly-typed values array. +fn empty_array_for(dt: &DataType) -> ArrayRef { + match dt { + DataType::Utf8 => Arc::new(StringArray::from(Vec::>::new())), + DataType::Int16 => Arc::new(Int16Array::from(Vec::>::new())), + DataType::Int32 => Arc::new(Int32Array::from(Vec::>::new())), + DataType::Boolean => Arc::new(BooleanArray::from(Vec::>::new())), + DataType::Struct(fields) => { + let children: Vec = fields + .iter() + .map(|f| empty_array_for(f.data_type())) + .collect(); + Arc::new( + StructArray::try_new(fields.clone(), children, None).expect("empty struct array"), + ) + } + DataType::List(f) => { + let values = empty_array_for(f.data_type()); + let offs = OffsetBuffer::::new(ScalarBuffer::from(vec![0i32])); + Arc::new(ListArray::try_new(f.clone(), offs, values, None).expect("empty list array")) + } + _ => Arc::new(StringArray::from(Vec::>::new())), + } +} + +// ── builder shorthand ───────────────────────────────────────────────────────── + +use arrow_array::builder::{BooleanBuilder, Int16Builder, Int32Builder, StringBuilder}; + +fn append_opt_str(b: &mut StringBuilder, v: Option<&str>) { + match v { + Some(s) => b.append_value(s), + None => b.append_null(), + } +} + +fn append_opt_i16(b: &mut Int16Builder, v: Option) { + match v { + Some(n) => b.append_value(n), + None => b.append_null(), + } +} + +fn append_opt_i32(b: &mut Int32Builder, v: Option) { + match v { + Some(n) => b.append_value(n), + None => b.append_null(), + } +} diff --git a/rust/src/ingest.rs b/rust/src/ingest.rs new file mode 100644 index 0000000..22edc13 --- /dev/null +++ b/rust/src/ingest.rs @@ -0,0 +1,480 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// src/ingest.rs +// +// Bulk ingest implementation using a VALUES-based INSERT pipeline. +// For each bound batch: CREATE TABLE (if needed) then INSERT … VALUES. +// Rows are chunked so individual SQL statements stay well under 1 MB. + +use std::sync::Arc; + +use adbc_core::error::{Error, Result, Status}; +use arrow_array::{Array, RecordBatch}; +use arrow_schema::{DataType, Schema, TimeUnit}; + +use crate::driver::Inner; +use crate::statement::Statement; + +const INSERT_CHUNK_ROWS: usize = 500; + +// ── public entry point ──────────────────────────────────────────────────────── + +pub(crate) fn execute_ingest(stmt: &Statement) -> Result> { + if stmt.bound_batches.is_empty() { + return Err(Error::with_message_and_status( + "ingest requires bound data — call bind() or bind_stream() first", + Status::InvalidState, + )); + } + + let table = stmt.target_table.as_deref().ok_or_else(|| { + Error::with_message_and_status("target_table not set", Status::InvalidState) + })?; + let qname = qualified_name( + table, + stmt.ingest_catalog.as_deref(), + stmt.ingest_schema.as_deref(), + ); + + let mode = stmt + .ingest_mode + .as_deref() + .unwrap_or("adbc.ingest.mode.create"); + let schema = stmt.bound_batches[0].schema(); + + match mode { + "adbc.ingest.mode.create" => { + let ddl = build_create_sql(&qname, &schema, false)?; + run_sql(&stmt.inner, stmt.conn_handle, &ddl)?; + } + "adbc.ingest.mode.append" => { + // table must already exist — no DDL needed + } + "adbc.ingest.mode.replace" => { + run_sql( + &stmt.inner, + stmt.conn_handle, + &format!("DROP TABLE IF EXISTS {qname}"), + )?; + let ddl = build_create_sql(&qname, &schema, false)?; + run_sql(&stmt.inner, stmt.conn_handle, &ddl)?; + } + "adbc.ingest.mode.create_append" => { + let ddl = build_create_sql(&qname, &schema, true)?; + run_sql(&stmt.inner, stmt.conn_handle, &ddl)?; + } + other => { + return Err(Error::with_message_and_status( + format!("unknown ingest mode: {other}"), + Status::InvalidArguments, + )); + } + } + + let mut total = 0i64; + for batch in &stmt.bound_batches { + total += insert_batch(&stmt.inner, stmt.conn_handle, &qname, batch)?; + } + Ok(Some(total)) +} + +// ── DDL helpers ─────────────────────────────────────────────────────────────── + +/// Returns the fully-qualified, double-quoted table name. +fn qualified_name(table: &str, catalog: Option<&str>, schema: Option<&str>) -> String { + let q = |s: &str| format!("\"{}\"", s.replace('"', "\"\"")); + match (catalog, schema) { + (Some(c), Some(s)) => format!("{}.{}.{}", q(c), q(s), q(table)), + (None, Some(s)) => format!("{}.{}", q(s), q(table)), + (Some(c), None) => format!("{}.{}", q(c), q(table)), + (None, None) => q(table), + } +} + +/// Builds `CREATE [OR REPLACE] TABLE [IF NOT EXISTS] (cols…)`. +fn build_create_sql(qname: &str, schema: &Schema, if_not_exists: bool) -> Result { + let mut cols = Vec::with_capacity(schema.fields().len()); + for field in schema.fields() { + let sf_type = to_sf_ddl(field.data_type())?; + let null_clause = if field.is_nullable() { "" } else { " NOT NULL" }; + cols.push(format!( + "\"{}\" {sf_type}{null_clause}", + field.name().replace('"', "\"\"") + )); + } + let exists = if if_not_exists { " IF NOT EXISTS" } else { "" }; + Ok(format!( + "CREATE TABLE{exists} {qname} ({})", + cols.join(", ") + )) +} + +/// Maps an Arrow DataType to its Snowflake DDL type string. +/// Matches the Go driver's `toSnowflakeType` function. +fn to_sf_ddl(dt: &DataType) -> Result { + Ok(match dt { + DataType::Int8 + | DataType::Int16 + | DataType::Int32 + | DataType::Int64 + | DataType::UInt8 + | DataType::UInt16 + | DataType::UInt32 + | DataType::UInt64 => "integer".to_string(), + + DataType::Float16 | DataType::Float32 | DataType::Float64 => "double".to_string(), + + DataType::Decimal128(p, s) => format!("NUMERIC({p},{s})"), + + DataType::Utf8 | DataType::LargeUtf8 => "text".to_string(), + + DataType::Binary | DataType::LargeBinary => "binary".to_string(), + + DataType::FixedSizeBinary(n) => format!("binary({n})"), + + DataType::Boolean => "boolean".to_string(), + + DataType::Date32 | DataType::Date64 => "date".to_string(), + + DataType::Time32(u) => { + let prec = time_unit_prec(u); + format!("time({prec})") + } + DataType::Time64(u) => { + let prec = time_unit_prec(u); + format!("time({prec})") + } + + DataType::Timestamp(u, tz) => { + let prec = time_unit_prec(u); + if tz.is_some() { + format!("timestamp_ltz({prec})") + } else { + format!("timestamp_ntz({prec})") + } + } + + // List/Struct/Map are intentionally excluded: DDL generation would succeed + // but value_to_sql has no handler for these types, causing every data row + // to fail. Return NotImplemented so callers get a clear error up-front. + DataType::List(_) + | DataType::LargeList(_) + | DataType::FixedSizeList(_, _) + | DataType::Struct(_) + | DataType::Map(_, _) => { + return Err(Error::with_message_and_status( + format!("ingest of nested type {dt:?} is not yet supported"), + Status::NotImplemented, + )); + } + + other => { + return Err(Error::with_message_and_status( + format!("unsupported ingest type: {other:?}"), + Status::NotImplemented, + )); + } + }) +} + +fn time_unit_prec(u: &TimeUnit) -> u8 { + match u { + TimeUnit::Second => 0, + TimeUnit::Millisecond => 3, + TimeUnit::Microsecond => 6, + TimeUnit::Nanosecond => 9, + } +} + +// ── INSERT helpers ──────────────────────────────────────────────────────────── + +fn insert_batch( + inner: &Arc, + conn: sf_core::handle_manager::Handle, + qname: &str, + batch: &RecordBatch, +) -> Result { + if batch.num_rows() == 0 { + return Ok(0); + } + + let schema = batch.schema(); + let col_list: Vec = schema + .fields() + .iter() + .map(|f| format!("\"{}\"", f.name().replace('"', "\"\""))) + .collect(); + let col_clause = col_list.join(", "); + + let mut inserted = 0i64; + let mut row = 0usize; + + while row < batch.num_rows() { + let end = (row + INSERT_CHUNK_ROWS).min(batch.num_rows()); + let mut rows_sql = Vec::with_capacity(end - row); + + for r in row..end { + let mut vals = Vec::with_capacity(schema.fields().len()); + for (c, field) in schema.fields().iter().enumerate() { + vals.push(value_to_sql( + batch.column(c).as_ref(), + r, + field.data_type(), + )?); + } + rows_sql.push(format!("({})", vals.join(", "))); + } + + let sql = format!( + "INSERT INTO {qname} ({col_clause}) VALUES {}", + rows_sql.join(", ") + ); + run_sql(inner, conn, &sql)?; + inserted += (end - row) as i64; + row = end; + } + + Ok(inserted) +} + +// ── SQL execution helper ────────────────────────────────────────────────────── + +fn run_sql(inner: &Arc, conn: sf_core::handle_manager::Handle, sql: &str) -> Result<()> { + let tmp = inner + .sf + .statement_new(conn) + .map_err(crate::error::api_error_to_adbc_error)?; + let result = inner.runtime.block_on(async { + inner + .sf + .statement_set_sql_query(tmp, sql.to_string()) + .await?; + inner.sf.statement_execute_query(tmp, None).await + }); + let _ = inner.sf.statement_release(tmp); + result + .map(|_| ()) + .map_err(crate::error::api_error_to_adbc_error) +} + +// ── value formatting ────────────────────────────────────────────────────────── + +/// Formats a single Arrow column value as a Snowflake SQL literal. +fn value_to_sql(arr: &dyn Array, row: usize, dt: &DataType) -> Result { + if arr.is_null(row) { + return Ok("NULL".to_string()); + } + + use arrow_array::{ + BinaryArray, BooleanArray, Date32Array, Decimal128Array, FixedSizeBinaryArray, + Float32Array, Float64Array, Int8Array, Int16Array, Int32Array, Int64Array, + LargeBinaryArray, LargeStringArray, StringArray, Time32MillisecondArray, Time32SecondArray, + Time64MicrosecondArray, Time64NanosecondArray, TimestampMicrosecondArray, + TimestampMillisecondArray, TimestampNanosecondArray, TimestampSecondArray, UInt8Array, + UInt16Array, UInt32Array, UInt64Array, + }; + + macro_rules! num { + ($T:ty) => { + if let Some(a) = arr.as_any().downcast_ref::<$T>() { + return Ok(format!("{}", a.value(row))); + } + }; + } + + num!(Int8Array); + num!(Int16Array); + num!(Int32Array); + num!(Int64Array); + num!(UInt8Array); + num!(UInt16Array); + num!(UInt32Array); + num!(UInt64Array); + // Floats: use {:?} to always emit a decimal or exponent (avoids huge integer strings). + // NaN and ±Inf cannot be represented in SQL; they are mapped to NULL, which is a + // deliberate lossy conversion — callers should avoid ingesting non-finite values. + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return if v.is_finite() { + Ok(format!("{v:?}")) + } else { + Ok("NULL".to_string()) + }; + } + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return if v.is_finite() { + Ok(format!("{v:?}")) + } else { + Ok("NULL".to_string()) + }; + } + + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(if a.value(row) { "TRUE" } else { "FALSE" }.to_string()); + } + + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(sql_str(a.value(row))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(sql_str(a.value(row))); + } + + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(sql_binary(a.value(row))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(sql_binary(a.value(row))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(sql_binary(a.value(row))); + } + + if let Some(a) = arr.as_any().downcast_ref::() { + let days = a.value(row) as i64; + return Ok(format!("'{}'::DATE", days_to_date(days))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + // Date64 stores milliseconds since epoch; divide to get days. + let days = a.value(row).div_euclid(86_400_000); + return Ok(format!("'{}'::DATE", days_to_date(days))); + } + + // TIME + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(format!("'{}'::TIME(9)", ns_to_time(a.value(row)))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(format!("'{}'::TIME(6)", us_to_time(a.value(row)))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(format!("'{}'::TIME(3)", ms_to_time(a.value(row) as i64))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(format!("'{}'::TIME(0)", s_to_time(a.value(row) as i64))); + } + + // TIMESTAMP — use TO_TIMESTAMP_NTZ / TO_TIMESTAMP_LTZ with epoch + precision + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return Ok(match dt { + DataType::Timestamp(_, Some(_)) => format!("TO_TIMESTAMP_LTZ({v}, 9)"), + _ => format!("TO_TIMESTAMP_NTZ({v}, 9)"), + }); + } + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return Ok(match dt { + DataType::Timestamp(_, Some(_)) => format!("TO_TIMESTAMP_LTZ({v}, 6)"), + _ => format!("TO_TIMESTAMP_NTZ({v}, 6)"), + }); + } + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return Ok(match dt { + DataType::Timestamp(_, Some(_)) => format!("TO_TIMESTAMP_LTZ({v}, 3)"), + _ => format!("TO_TIMESTAMP_NTZ({v}, 3)"), + }); + } + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return Ok(match dt { + DataType::Timestamp(_, Some(_)) => format!("TO_TIMESTAMP_LTZ({v}, 0)"), + _ => format!("TO_TIMESTAMP_NTZ({v}, 0)"), + }); + } + + if let Some(a) = arr.as_any().downcast_ref::() + && let DataType::Decimal128(_, scale) = dt + { + return Ok(decimal128_to_str(a.value(row), *scale)); + } + + Err(Error::with_message_and_status( + format!("unsupported ingest value type: {dt:?}"), + Status::NotImplemented, + )) +} + +// ── value formatting helpers ────────────────────────────────────────────────── + +fn sql_str(s: &str) -> String { + // Double backslashes before doubling single quotes — see statement.rs sql_str_lit. + format!("'{}'", s.replace('\\', "\\\\").replace('\'', "''")) +} + +fn sql_binary(b: &[u8]) -> String { + let hex: String = b.iter().map(|byte| format!("{byte:02x}")).collect(); + format!("TO_BINARY('{hex}', 'HEX')") +} + +/// Thin wrapper so ingest.rs shares the single implementation in statement.rs. +fn days_to_date(days: i64) -> String { + crate::statement::days_since_epoch_to_date_str(days) +} + +fn ns_to_time(ns: i64) -> String { + let h = ns / 3_600_000_000_000; + let r = ns % 3_600_000_000_000; + let m = r / 60_000_000_000; + let r = r % 60_000_000_000; + let s = r / 1_000_000_000; + let f = r % 1_000_000_000; + format!("{h:02}:{m:02}:{s:02}.{f:09}") +} + +fn us_to_time(us: i64) -> String { + let h = us / 3_600_000_000; + let r = us % 3_600_000_000; + let m = r / 60_000_000; + let r = r % 60_000_000; + let s = r / 1_000_000; + let f = r % 1_000_000; + format!("{h:02}:{m:02}:{s:02}.{f:06}") +} + +fn ms_to_time(ms: i64) -> String { + let h = ms / 3_600_000; + let r = ms % 3_600_000; + let m = r / 60_000; + let r = r % 60_000; + let s = r / 1_000; + let f = r % 1_000; + format!("{h:02}:{m:02}:{s:02}.{f:03}") +} + +fn s_to_time(s: i64) -> String { + let h = s / 3600; + let r = s % 3600; + let m = r / 60; + let s = r % 60; + format!("{h:02}:{m:02}:{s:02}") +} + +fn decimal128_to_str(raw: i128, scale: i8) -> String { + if scale <= 0 { + // Negative scale means multiply by 10^(-scale); just format as integer + // (Arrow guarantees the stored value is already the scaled integer). + return format!("{raw}"); + } + let scale = scale as usize; + let neg = raw < 0; + let abs = raw.unsigned_abs(); + let s = format!("{abs:0>width$}", width = scale + 1); + let int_part = &s[..s.len() - scale]; + let frac_part = &s[s.len() - scale..]; + format!("{}{int_part}.{frac_part}", if neg { "-" } else { "" }) +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..f38c2cf --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,33 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// src/lib.rs +mod error; + +mod driver; +pub use driver::Driver; + +mod database; +pub use database::Database; + +mod connection; +pub use connection::Connection; + +mod get_objects; +mod ingest; + +mod statement; +pub use statement::Statement; + +adbc_ffi::export_driver!(AdbcDriverSnowflakeInit, Driver); diff --git a/rust/src/statement.rs b/rust/src/statement.rs new file mode 100644 index 0000000..d8928f1 --- /dev/null +++ b/rust/src/statement.rs @@ -0,0 +1,2385 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// src/statement.rs +use std::sync::Arc; + +use adbc_core::{ + Optionable, PartitionedResult, + error::{Error, Result, Status}, + options::{OptionStatement, OptionValue}, +}; +use arrow_array::{Array, ArrayRef, RecordBatch, RecordBatchReader}; +use arrow_schema::{DataType, Field, Schema, TimeUnit}; +use sf_core::apis::database_driver_v1::{BindingType, DataPtr, Handle}; + +use crate::driver::{Inner, TimestampPrecision}; + +pub struct Statement { + pub(crate) inner: Arc, + pub(crate) stmt_handle: Handle, + pub(crate) conn_handle: Handle, + pub(crate) query: Option, + pub(crate) target_table: Option, + pub(crate) ingest_catalog: Option, + pub(crate) ingest_schema: Option, + pub(crate) ingest_mode: Option, + pub(crate) query_tag: Option, + pub(crate) use_high_precision: bool, + pub(crate) timestamp_precision: TimestampPrecision, + /// Parameter batches stored by bind() / bind_stream(). Each row is one execution. + pub(crate) bound_batches: Vec, + pub(crate) last_query_id: Option, +} + +impl Drop for Statement { + fn drop(&mut self) { + let _ = self.inner.sf.statement_release(self.stmt_handle); + } +} + +impl Optionable for Statement { + type Option = OptionStatement; + + fn set_option(&mut self, key: Self::Option, value: OptionValue) -> Result<()> { + match key { + OptionStatement::TargetTable => { + if let OptionValue::String(s) = value { + self.query = None; + self.target_table = Some(s); + Ok(()) + } else { + Err(Error::with_message_and_status( + "target_table must be a string", + Status::InvalidArguments, + )) + } + } + OptionStatement::IngestMode => { + if let OptionValue::String(s) = value { + self.ingest_mode = Some(s); + Ok(()) + } else { + Err(Error::with_message_and_status( + "ingest_mode must be a string", + Status::InvalidArguments, + )) + } + } + OptionStatement::Other(ref k) + if k == "adbc.snowflake.sql.client_option.use_high_precision" => + { + if let OptionValue::String(s) = value { + self.use_high_precision = s == "enabled" || s == "true"; + } + Ok(()) + } + OptionStatement::Other(ref k) + if k == "adbc.snowflake.sql.client_option.max_timestamp_precision" => + { + if let OptionValue::String(s) = value { + self.timestamp_precision = match s.as_str() { + "microseconds" => TimestampPrecision::Microseconds, + "nanoseconds_error_on_overflow" => { + TimestampPrecision::NanosecondsErrorOnOverflow + } + _ => TimestampPrecision::Nanoseconds, + }; + } + Ok(()) + } + OptionStatement::Temporary => { + // Accepted silently; used to select CREATE TEMPORARY TABLE during ingest. + Ok(()) + } + OptionStatement::TargetCatalog => { + if let OptionValue::String(s) = value { + self.ingest_catalog = Some(s); + } + Ok(()) + } + OptionStatement::TargetDbSchema => { + if let OptionValue::String(s) = value { + self.ingest_schema = Some(s); + } + Ok(()) + } + OptionStatement::Other(ref k) if k == "adbc.snowflake.statement.query_tag" => { + if let OptionValue::String(s) = value { + self.query_tag = Some(s); + Ok(()) + } else { + Err(Error::with_message_and_status( + "query_tag must be a string", + Status::InvalidArguments, + )) + } + } + _ => Err(Error::with_message_and_status( + format!("unknown statement option: {}", key.as_ref()), + Status::NotFound, + )), + } + } + + fn get_option_string(&self, key: Self::Option) -> Result { + match key { + OptionStatement::Other(ref k) if k == "adbc.snowflake.statement.query_tag" => { + Ok(self.query_tag.clone().unwrap_or_default()) + } + OptionStatement::Other(ref k) if k == "adbc.snowflake.sql.query_id" => { + self.last_query_id.clone().ok_or_else(|| { + Error::with_message_and_status( + "no query has been executed yet", + Status::NotFound, + ) + }) + } + _ => Err(Error::with_message_and_status( + format!("option not found: {}", key.as_ref()), + Status::NotFound, + )), + } + } + + fn get_option_bytes(&self, _key: Self::Option) -> Result> { + Err(Error::with_message_and_status( + "option not found", + Status::NotFound, + )) + } + + fn get_option_int(&self, _key: Self::Option) -> Result { + Err(Error::with_message_and_status( + "option not found", + Status::NotFound, + )) + } + + fn get_option_double(&self, _key: Self::Option) -> Result { + Err(Error::with_message_and_status( + "option not found", + Status::NotFound, + )) + } +} + +impl Statement { + /// Execute a parameterized query with all bound rows sent as JSON bindings + /// in a single round-trip via sf_core's `BindingType::Json` API. + fn execute_bound( + &mut self, + query: String, + ) -> Result> { + let json_bytes = arrow_batches_to_json_bindings(&self.bound_batches)?; + let data_ptr = DataPtr::new(json_bytes.as_ptr(), json_bytes.len() as i64); + let binding = BindingType::Json(data_ptr); + + let result = self + .inner + .runtime + .block_on(async { + self.inner + .sf + .statement_set_sql_query(self.stmt_handle, query) + .await?; + self.inner + .sf + .statement_execute_query(self.stmt_handle, Some(binding)) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; + + self.last_query_id = Some(result.query_id.clone()); + + // Safety: result.stream is a valid FFI stream from sf_core. Ownership is transferred + // to ArrowArrayStreamReader. The C ABI layout is stable per the Arrow C Data Interface. + let raw = + Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| { + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; + Ok(Box::new(ConvertingReader::new( + reader, + self.use_high_precision, + self.timestamp_precision.time_unit(), + self.timestamp_precision, + ))) + } + + fn apply_query_tag(&self) -> Result<()> { + if let Some(ref tag) = self.query_tag { + let escaped = tag.replace('\'', "''"); + let set_sql = format!("ALTER SESSION SET QUERY_TAG = '{escaped}'"); + let tmp_handle = self + .inner + .sf + .statement_new(self.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + let set_result = self.inner.runtime.block_on(async { + self.inner + .sf + .statement_set_sql_query(tmp_handle, set_sql) + .await?; + self.inner + .sf + .statement_execute_query(tmp_handle, None) + .await + }); + let _ = self.inner.sf.statement_release(tmp_handle); + set_result.map_err(crate::error::api_error_to_adbc_error)?; + } + Ok(()) + } +} + +impl adbc_core::Statement for Statement { + fn bind(&mut self, batch: RecordBatch) -> Result<()> { + self.bound_batches = vec![batch]; + Ok(()) + } + + fn bind_stream(&mut self, reader: Box) -> Result<()> { + let mut batches = Vec::new(); + for batch_result in reader { + let batch = batch_result + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + batches.push(batch); + } + self.bound_batches = batches; + Ok(()) + } + + #[allow(refining_impl_trait)] + fn execute(&mut self) -> Result> { + if self.target_table.is_some() { + // Ingest via execute() — run the ingest and return an empty reader. + let result = crate::ingest::execute_ingest(self); + self.bound_batches.clear(); + result?; + let batch = + arrow_array::RecordBatch::new_empty(Arc::new(arrow_schema::Schema::empty())); + return Ok(Box::new(crate::connection::SingleBatchReader::new(batch))); + } + let query = self.query.clone().ok_or_else(|| { + Error::with_message_and_status("cannot execute without a query", Status::InvalidState) + })?; + + self.apply_query_tag()?; + + if !self.bound_batches.is_empty() { + return self.execute_bound(query); + } + + let result = self + .inner + .runtime + .block_on(async { + self.inner + .sf + .statement_set_sql_query(self.stmt_handle, query) + .await?; + self.inner + .sf + .statement_execute_query(self.stmt_handle, None) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; + + self.last_query_id = Some(result.query_id.clone()); + + // Safety: result.stream is a valid FFI stream from sf_core. Ownership is transferred + // to ArrowArrayStreamReader. The C ABI layout is stable per the Arrow C Data Interface. + let raw = + Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| { + // Safety: on failure, from_raw does NOT call the stream's release callback. + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; + Ok(Box::new(ConvertingReader::new( + reader, + self.use_high_precision, + self.timestamp_precision.time_unit(), + self.timestamp_precision, + ))) + } + + fn execute_update(&mut self) -> Result> { + if self.target_table.is_some() { + let result = crate::ingest::execute_ingest(self); + self.bound_batches.clear(); + return result; + } + let query = self.query.clone().ok_or_else(|| { + Error::with_message_and_status("cannot execute without a query", Status::InvalidState) + })?; + + self.apply_query_tag()?; + + // Parameterised DML: execute once with JSON bindings. + if !self.bound_batches.is_empty() { + let json_bytes = arrow_batches_to_json_bindings(&self.bound_batches)?; + let data_ptr = DataPtr::new(json_bytes.as_ptr(), json_bytes.len() as i64); + let binding = BindingType::Json(data_ptr); + + let result = self + .inner + .runtime + .block_on(async { + self.inner + .sf + .statement_set_sql_query(self.stmt_handle, query) + .await?; + self.inner + .sf + .statement_execute_query(self.stmt_handle, Some(binding)) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; + + self.last_query_id = Some(result.query_id.clone()); + + let rows = if is_ddl(self.query.as_deref().unwrap_or("")) { + None + } else { + result.rows_affected + }; + return Ok(rows); + } + + let result = self + .inner + .runtime + .block_on(async { + self.inner + .sf + .statement_set_sql_query(self.stmt_handle, query) + .await?; + self.inner + .sf + .statement_execute_query(self.stmt_handle, None) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; + + self.last_query_id = Some(result.query_id.clone()); + + // DDL statements (CREATE, DROP, ALTER, TRUNCATE) return a non-meaningful row + // count from Snowflake (typically 1 for "success"). Per the ADBC convention, + // return None (-1 in Python) for DDL so callers can distinguish it from DML. + let rows = if is_ddl(self.query.as_deref().unwrap_or("")) { + None + } else { + result.rows_affected + }; + Ok(rows) + } + + fn execute_schema(&mut self) -> Result { + let query = self.query.clone().ok_or_else(|| { + Error::with_message_and_status("cannot execute without a query", Status::InvalidState) + })?; + + self.apply_query_tag()?; + + let result = self + .inner + .runtime + .block_on(async { + self.inner + .sf + .statement_set_sql_query(self.stmt_handle, query) + .await?; + self.inner + .sf + .statement_execute_query(self.stmt_handle, None) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; + + self.last_query_id = Some(result.query_id.clone()); + + // Safety: result.stream is a valid FFI stream from sf_core. Ownership is transferred + // to ArrowArrayStreamReader. The C ABI layout is stable per the Arrow C Data Interface. + let raw = + Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| { + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; + Ok(adjust_schema( + &reader.schema(), + self.use_high_precision, + self.timestamp_precision.time_unit(), + ) + .as_ref() + .clone()) + } + + fn execute_partitions(&mut self) -> Result { + Err(crate::error::not_implemented("execute_partitions")) + } + + fn get_parameter_schema(&self) -> Result { + Err(crate::error::not_implemented("get_parameter_schema")) + } + + fn prepare(&mut self) -> Result<()> { + if self.query.is_none() { + return Err(Error::with_message_and_status( + "cannot prepare statement with no query", + Status::InvalidState, + )); + } + Ok(()) // No-op: Snowflake has no server-side prepare + } + + fn set_sql_query(&mut self, query: impl AsRef) -> Result<()> { + self.query = Some(query.as_ref().to_string()); + self.target_table = None; + self.bound_batches.clear(); + Ok(()) + } + + fn set_substrait_plan(&mut self, _plan: impl AsRef<[u8]>) -> Result<()> { + Err(crate::error::not_implemented( + "Snowflake does not support Substrait plans", + )) + } + + fn cancel(&mut self) -> Result<()> { + Err(crate::error::not_implemented("cancel")) + } +} + +// ── ConcatReader: chains multiple RecordBatches into a single reader ────────── + +#[cfg(test)] +struct ConcatReader { + batches: std::vec::IntoIter, + schema: Arc, +} + +#[cfg(test)] +impl Iterator for ConcatReader { + type Item = std::result::Result; + fn next(&mut self) -> Option { + self.batches.next().map(Ok) + } +} + +#[cfg(test)] +impl RecordBatchReader for ConcatReader { + fn schema(&self) -> Arc { + self.schema.clone() + } +} + +// ── Schema adjustment and type conversions ──────────────────────────────── + +fn scale_to_time_unit(scale: u32) -> TimeUnit { + match scale { + 0 => TimeUnit::Second, + 1..=3 => TimeUnit::Millisecond, + 4..=6 => TimeUnit::Microsecond, + _ => TimeUnit::Nanosecond, + } +} + +fn timestamp_target_unit(scale: i64, ts_unit: TimeUnit) -> TimeUnit { + let natural = scale_to_time_unit(scale as u32); + let natural_rank = time_unit_rank(natural); + let max_rank = time_unit_rank(ts_unit); + if natural_rank <= max_rank { + natural + } else { + ts_unit + } +} + +fn time_unit_rank(unit: TimeUnit) -> u32 { + match unit { + TimeUnit::Second => 0, + TimeUnit::Millisecond => 1, + TimeUnit::Microsecond => 2, + TimeUnit::Nanosecond => 3, + } +} + +pub(crate) fn adjust_schema( + schema: &Schema, + use_high_precision: bool, + ts_unit: TimeUnit, +) -> Arc { + let adjusted_fields: Vec = schema + .fields() + .iter() + .map(|f| { + let target = compute_target_type(f, use_high_precision, ts_unit); + Field::new(f.name(), target, f.is_nullable()) + }) + .collect(); + Arc::new(Schema::new_with_metadata( + adjusted_fields, + schema.metadata().clone(), + )) +} + +fn compute_target_type(field: &Field, use_high_precision: bool, ts_unit: TimeUnit) -> DataType { + let logical_type = field + .metadata() + .get("logicalType") + .map(|s| s.as_str()) + .unwrap_or(""); + let scale: i64 = field + .metadata() + .get("scale") + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + let precision: u8 = field + .metadata() + .get("precision") + .and_then(|s| s.parse().ok()) + .unwrap_or(38) + .min(38); + + match logical_type { + "FIXED" => { + if use_high_precision && scale > 0 { + DataType::Decimal128(precision, scale as i8) + } else if scale == 0 { + DataType::Int64 + } else { + DataType::Float64 + } + } + "TIME" => { + let unit = scale_to_time_unit(scale as u32); + if scale < 6 { + DataType::Time32(unit) + } else { + DataType::Time64(unit) + } + } + "TIMESTAMP_NTZ" => { + let unit = timestamp_target_unit(scale, ts_unit); + DataType::Timestamp(unit, None) + } + "REAL" => DataType::Float64, + "TIMESTAMP_LTZ" => { + let unit = timestamp_target_unit(scale, ts_unit); + DataType::Timestamp(unit, Some(Arc::from("UTC"))) + } + "TIMESTAMP_TZ" => { + let unit = timestamp_target_unit(scale, ts_unit); + DataType::Timestamp(unit, Some(Arc::from("UTC"))) + } + _ => match field.data_type() { + DataType::Int8 | DataType::Int16 | DataType::Int32 => DataType::Int64, + other => other.clone(), + }, + } +} + +#[cfg(test)] +pub(crate) fn adjust_schema_integers(schema: &Schema) -> Arc { + let adjusted_fields: Vec = schema + .fields() + .iter() + .map(|f| match f.data_type() { + DataType::Int8 | DataType::Int16 | DataType::Int32 => { + Field::new(f.name(), DataType::Int64, f.is_nullable()) + } + _ => f.as_ref().clone(), + }) + .collect(); + Arc::new(Schema::new_with_metadata( + adjusted_fields, + schema.metadata().clone(), + )) +} + +pub(crate) struct ConvertingReader { + inner: R, + schema: Arc, + use_high_precision: bool, + ts_unit: TimeUnit, + check_overflow: bool, + logical_types: Vec, + scales: Vec, +} + +impl ConvertingReader { + pub(crate) fn new( + inner: R, + use_high_precision: bool, + ts_unit: TimeUnit, + ts_precision: TimestampPrecision, + ) -> Self { + let orig_schema = inner.schema(); + let logical_types: Vec = orig_schema + .fields() + .iter() + .map(|f| f.metadata().get("logicalType").cloned().unwrap_or_default()) + .collect(); + let scales: Vec = orig_schema + .fields() + .iter() + .map(|f| { + f.metadata() + .get("scale") + .and_then(|s| s.parse().ok()) + .unwrap_or(0) + }) + .collect(); + let schema = adjust_schema(&orig_schema, use_high_precision, ts_unit); + + Self { + inner, + schema, + use_high_precision, + ts_unit, + check_overflow: ts_precision == TimestampPrecision::NanosecondsErrorOnOverflow, + logical_types, + scales, + } + } + + fn convert_column( + col: &ArrayRef, + logical_type: &str, + scale: i64, + target_type: &DataType, + use_high_precision: bool, + ts_unit: TimeUnit, + check_overflow: bool, + ) -> std::result::Result { + match logical_type { + "FIXED" => { + if scale == 0 { + match col.data_type() { + DataType::Int64 => Ok(col.clone()), + _ => arrow_cast::cast(col.as_ref(), &DataType::Int64), + } + } else if use_high_precision { + match col.data_type() { + // Source is already Decimal128 — cast to ensure target_type metadata is applied exactly + DataType::Decimal128(_, _) => arrow_cast::cast(col.as_ref(), target_type), + // Source is Int64 (scaled integer) — convert to Decimal128(precision, scale) + // following Go's integerToDecimal128: cast Int64 → Decimal128(20,0), + // then reinterpret as Decimal128(precision, scale). + _ => { + let intermediate = + arrow_cast::cast(col.as_ref(), &DataType::Decimal128(20, 0))?; + let data = intermediate.to_data(); + // Safety: Decimal128 and Decimal128(20,0) share identical 128-bit + // buffer layouts, so this reinterpret is memory-safe. + // Values that exceed target_type's precision are not validated here + // and will silently overflow the declared precision. + let retyped = unsafe { + data.into_builder() + .data_type(target_type.clone()) + .build_unchecked() + }; + Ok(arrow_array::make_array(retyped)) + } + } + } else { + match col.data_type() { + DataType::Decimal128(_, _) => { + arrow_cast::cast(col.as_ref(), &DataType::Float64) + } + _ => { + let casted = arrow_cast::cast(col.as_ref(), &DataType::Float64)?; + let divisor = 10f64.powi(scale as i32); + let float_arr = casted + .as_any() + .downcast_ref::() + .ok_or_else(|| { + arrow_schema::ArrowError::CastError( + "expected Float64Array after cast".into(), + ) + })?; + let divided: arrow_array::Float64Array = + float_arr.iter().map(|v| v.map(|x| x / divisor)).collect(); + Ok(Arc::new(divided) as ArrayRef) + } + } + } + } + "TIME" => arrow_cast::cast(col.as_ref(), target_type), + "REAL" => arrow_cast::cast(col.as_ref(), &DataType::Float64), + "TIMESTAMP_NTZ" => convert_timestamp_ntz(col, scale, ts_unit, check_overflow, None), + "TIMESTAMP_LTZ" => { + convert_timestamp_ntz(col, scale, ts_unit, check_overflow, Some(Arc::from("UTC"))) + } + "TIMESTAMP_TZ" => convert_timestamp_tz(col, scale, ts_unit, check_overflow), + _ => match col.data_type() { + DataType::Int8 | DataType::Int16 | DataType::Int32 => { + arrow_cast::cast(col.as_ref(), &DataType::Int64) + } + _ => Ok(col.clone()), + }, + } + } +} + +fn convert_timestamp_ntz( + col: &ArrayRef, + scale: i64, + ts_unit: TimeUnit, + check_overflow: bool, + tz_str: Option>, +) -> std::result::Result { + let unit = timestamp_target_unit(scale, ts_unit); + let target = DataType::Timestamp(unit, tz_str.clone()); + + match col.data_type() { + DataType::Int64 => { + let natural_unit = scale_to_time_unit(scale.max(0) as u32); + if natural_unit == unit { + arrow_cast::cast(col.as_ref(), &target) + } else { + let natural_target = DataType::Timestamp(natural_unit, tz_str); + let intermediate = arrow_cast::cast(col.as_ref(), &natural_target)?; + arrow_cast::cast(intermediate.as_ref(), &target) + } + } + DataType::Struct(_) => { + use arrow_array::{Int32Array, Int64Array, StructArray}; + let struct_arr = col.as_any().downcast_ref::().ok_or_else(|| { + arrow_schema::ArrowError::CastError("expected StructArray".into()) + })?; + let epoch = struct_arr + .column(0) + .as_any() + .downcast_ref::() + .ok_or_else(|| { + arrow_schema::ArrowError::CastError("expected Int64 epoch".into()) + })?; + let fraction = struct_arr + .column(1) + .as_any() + .downcast_ref::() + .ok_or_else(|| { + arrow_schema::ArrowError::CastError("expected Int32 fraction".into()) + })?; + + build_timestamp_from_epoch_fraction( + epoch, + fraction, + struct_arr, + scale, + check_overflow, + target, + ) + } + _ => arrow_cast::cast(col.as_ref(), &target), + } +} + +fn convert_timestamp_tz( + col: &ArrayRef, + scale: i64, + ts_unit: TimeUnit, + check_overflow: bool, +) -> std::result::Result { + use arrow_array::{Int32Array, Int64Array, StructArray}; + + let unit = timestamp_target_unit(scale, ts_unit); + let target = DataType::Timestamp(unit, Some(Arc::from("UTC"))); + + let struct_arr = col.as_any().downcast_ref::().ok_or_else(|| { + arrow_schema::ArrowError::CastError("expected StructArray for TIMESTAMP_TZ".into()) + })?; + + let num_fields = struct_arr.num_columns(); + let epoch = struct_arr + .column(0) + .as_any() + .downcast_ref::() + .ok_or_else(|| arrow_schema::ArrowError::CastError("expected Int64 epoch".into()))?; + + if num_fields == 2 { + let tzoffset = struct_arr + .column(1) + .as_any() + .downcast_ref::() + .ok_or_else(|| arrow_schema::ArrowError::CastError("expected Int32 timezone".into()))?; + + build_timestamp_tz_2field(epoch, tzoffset, struct_arr, scale, check_overflow, target) + } else { + let fraction = struct_arr + .column(1) + .as_any() + .downcast_ref::() + .ok_or_else(|| arrow_schema::ArrowError::CastError("expected Int32 fraction".into()))?; + let tzoffset = struct_arr + .column(2) + .as_any() + .downcast_ref::() + .ok_or_else(|| arrow_schema::ArrowError::CastError("expected Int32 timezone".into()))?; + + build_timestamp_tz_3field( + epoch, + fraction, + tzoffset, + struct_arr, + scale, + check_overflow, + target, + ) + } +} + +fn check_ns_overflow(ns: i128) -> std::result::Result<(), arrow_schema::ArrowError> { + if ns > i64::MAX as i128 || ns < i64::MIN as i128 { + Err(arrow_schema::ArrowError::CastError(format!( + "timestamp value {ns} nanoseconds overflows i64" + ))) + } else { + Ok(()) + } +} + +fn ns_to_unit(ns: i128, unit: TimeUnit) -> i64 { + let val = match unit { + TimeUnit::Second => ns / 1_000_000_000, + TimeUnit::Millisecond => ns / 1_000_000, + TimeUnit::Microsecond => ns / 1_000, + // Intentional truncation: ns outside i64::MIN..=i64::MAX wraps. Use check_overflow=true in the caller to reject out-of-range values *before* this cast is reached. + TimeUnit::Nanosecond => ns, + }; + val as i64 +} + +fn build_timestamp_from_epoch_fraction( + epoch: &arrow_array::Int64Array, + fraction: &arrow_array::Int32Array, + struct_arr: &arrow_array::StructArray, + scale: i64, + check_overflow: bool, + target: DataType, +) -> std::result::Result { + let unit = match &target { + DataType::Timestamp(u, _) => *u, + _ => unreachable!("target must be Timestamp"), + }; + + let len = epoch.len(); + let scale = scale.clamp(0, 9); + let frac_to_ns: i128 = 10i128.pow((9 - scale) as u32); + + let mut values: Vec> = Vec::with_capacity(len); + for i in 0..len { + if struct_arr.is_null(i) { + values.push(None); + } else { + let ns: i128 = + epoch.value(i) as i128 * 1_000_000_000 + fraction.value(i) as i128 * frac_to_ns; + if check_overflow { + check_ns_overflow(ns)?; + } + values.push(Some(ns_to_unit(ns, unit))); + } + } + let int_arr = arrow_array::Int64Array::from(values); + let data = unsafe { + int_arr + .into_data() + .into_builder() + .data_type(target.clone()) + .build_unchecked() + }; + Ok(Arc::new(arrow_array::make_array(data)) as ArrayRef) +} + +fn build_timestamp_tz_2field( + epoch: &arrow_array::Int64Array, + tzoffset: &arrow_array::Int32Array, + struct_arr: &arrow_array::StructArray, + scale: i64, + check_overflow: bool, + target: DataType, +) -> std::result::Result { + let unit = match &target { + DataType::Timestamp(u, _) => *u, + _ => unreachable!("target must be Timestamp"), + }; + + let len = epoch.len(); + let mut values: Vec> = Vec::with_capacity(len); + + for i in 0..len { + if struct_arr.is_null(i) { + values.push(None); + } else { + let tz_offset_minutes: i128 = (tzoffset.value(i) as i128) - 1440; + let tz_offset_ns: i128 = tz_offset_minutes * 60 * 1_000_000_000; + + let epoch_ns: i128 = epoch.value(i) as i128 + * 10i128.pow((9u32).saturating_sub(scale.clamp(0, 9) as u32)); + + let utc_ns = epoch_ns - tz_offset_ns; + if check_overflow { + check_ns_overflow(utc_ns)?; + } + values.push(Some(ns_to_unit(utc_ns, unit))); + } + } + let int_arr = arrow_array::Int64Array::from(values); + let data = unsafe { + int_arr + .into_data() + .into_builder() + .data_type(target.clone()) + .build_unchecked() + }; + Ok(Arc::new(arrow_array::make_array(data)) as ArrayRef) +} + +#[allow(clippy::too_many_arguments)] +fn build_timestamp_tz_3field( + epoch: &arrow_array::Int64Array, + fraction: &arrow_array::Int32Array, + tzoffset: &arrow_array::Int32Array, + struct_arr: &arrow_array::StructArray, + scale: i64, + check_overflow: bool, + target: DataType, +) -> std::result::Result { + let unit = match &target { + DataType::Timestamp(u, _) => *u, + _ => unreachable!("target must be Timestamp"), + }; + + let len = epoch.len(); + let scale = scale.clamp(0, 9); + let frac_to_ns: i128 = 10i128.pow((9 - scale) as u32); + let mut values: Vec> = Vec::with_capacity(len); + + for i in 0..len { + if struct_arr.is_null(i) { + values.push(None); + } else { + let tz_offset_minutes: i128 = (tzoffset.value(i) as i128) - 1440; + let tz_offset_ns: i128 = tz_offset_minutes * 60 * 1_000_000_000; + + let epoch_ns: i128 = + epoch.value(i) as i128 * 1_000_000_000 + fraction.value(i) as i128 * frac_to_ns; + + let utc_ns = epoch_ns - tz_offset_ns; + if check_overflow { + check_ns_overflow(utc_ns)?; + } + values.push(Some(ns_to_unit(utc_ns, unit))); + } + } + let int_arr = arrow_array::Int64Array::from(values); + let data = unsafe { + int_arr + .into_data() + .into_builder() + .data_type(target.clone()) + .build_unchecked() + }; + Ok(Arc::new(arrow_array::make_array(data)) as ArrayRef) +} + +impl Iterator for ConvertingReader { + type Item = std::result::Result; + + fn next(&mut self) -> Option { + let batch = match self.inner.next()? { + Ok(b) => b, + Err(e) => return Some(Err(e)), + }; + + let check_overflow = self.check_overflow; + + let adjusted_columns: std::result::Result, arrow_schema::ArrowError> = batch + .columns() + .iter() + .enumerate() + .map(|(i, col)| { + let logical_type = &self.logical_types[i]; + let scale = self.scales[i]; + let target_type = self.schema.field(i).data_type(); + ConvertingReader::::convert_column( + col, + logical_type, + scale, + target_type, + self.use_high_precision, + self.ts_unit, + check_overflow, + ) + }) + .collect(); + + Some(RecordBatch::try_new( + self.schema.clone(), + match adjusted_columns { + Ok(cols) => cols, + Err(e) => return Some(Err(e)), + }, + )) + } +} + +impl RecordBatchReader for ConvertingReader { + fn schema(&self) -> Arc { + self.schema.clone() + } +} + +// ── JSON parameter bindings ─────────────────────────────────────────────────── + +fn snowflake_type_name(dt: &DataType) -> Result<&'static str> { + match dt { + DataType::Int8 + | DataType::Int16 + | DataType::Int32 + | DataType::Int64 + | DataType::UInt8 + | DataType::UInt16 + | DataType::UInt32 + | DataType::UInt64 + | DataType::Decimal128(_, _) => Ok("FIXED"), + DataType::Float32 | DataType::Float64 => Ok("REAL"), + DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => Ok("TEXT"), + DataType::Boolean => Ok("BOOLEAN"), + DataType::Date32 | DataType::Date64 => Ok("DATE"), + DataType::Binary + | DataType::LargeBinary + | DataType::FixedSizeBinary(_) + | DataType::BinaryView => Ok("BINARY"), + DataType::Time32(_) | DataType::Time64(_) => Ok("TIME"), + DataType::Timestamp(_, None) => Ok("TIMESTAMP_NTZ"), + DataType::Timestamp(_, Some(_)) => Ok("TIMESTAMP_LTZ"), + _ => Err(Error::with_message_and_status( + format!("unsupported bind parameter type: {dt:?}"), + Status::NotImplemented, + )), + } +} + +fn escape_json_string(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + c if (c as u32) < 0x20 => { + out.push_str(&format!("\\u{:04x}", c as u32)); + } + c => out.push(c), + } + } + out +} + +fn arrow_batches_to_json_bindings(batches: &[RecordBatch]) -> Result { + if batches.is_empty() { + return Ok("{}".to_string()); + } + + let num_cols = batches[0].num_columns(); + let schema = batches[0].schema(); + + let mut col_types: Vec<&'static str> = Vec::with_capacity(num_cols); + for i in 0..num_cols { + col_types.push(snowflake_type_name(schema.field(i).data_type())?); + } + + let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum(); + + let mut col_values: Vec>> = vec![Vec::with_capacity(total_rows); num_cols]; + + for batch in batches { + for col_idx in 0..num_cols { + let col = batch.column(col_idx); + let dt = col.data_type(); + for row in 0..batch.num_rows() { + if col.is_null(row) { + col_values[col_idx].push(None); + continue; + } + let val = format_arrow_value(col.as_ref(), row, dt)?; + col_values[col_idx].push(val); + } + } + } + + let mut json = String::from("{"); + for col_idx in 0..num_cols { + if col_idx > 0 { + json.push(','); + } + let key = col_idx + 1; + json.push_str(&format!( + "\"{}\":{{\"type\":\"{}\",\"value\":", + key, col_types[col_idx] + )); + + if total_rows == 1 { + match &col_values[col_idx][0] { + None => json.push_str("null"), + Some(v) => { + json.push('"'); + json.push_str(&escape_json_string(v)); + json.push('"'); + } + } + } else { + json.push('['); + for (i, v) in col_values[col_idx].iter().enumerate() { + if i > 0 { + json.push(','); + } + match v { + None => json.push_str("null"), + Some(s) => { + json.push('"'); + json.push_str(&escape_json_string(s)); + json.push('"'); + } + } + } + json.push(']'); + } + json.push('}'); + } + json.push('}'); + Ok(json) +} + +fn format_arrow_value(arr: &dyn Array, row: usize, dt: &DataType) -> Result> { + use arrow_array::*; + match dt { + DataType::Int8 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Int16 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Int32 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Int64 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::UInt8 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::UInt16 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::UInt32 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::UInt64 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Float32 => { + let v = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + if v.is_finite() { + Ok(Some(format!("{v:?}"))) + } else { + Ok(None) + } + } + DataType::Float64 => { + let v = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + if v.is_finite() { + Ok(Some(format!("{v:?}"))) + } else { + Ok(None) + } + } + DataType::Utf8 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::LargeUtf8 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Boolean => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Date32 => { + let days = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row) as i64; + Ok(Some((days * 86_400_000).to_string())) + } + DataType::Date64 => { + let ms = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(ms.to_string())) + } + DataType::Decimal128(_, scale) => { + let val = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(decimal128_to_string(val, *scale))) + } + DataType::Binary => { + let bytes = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(bytes.iter().map(|b| format!("{b:02x}")).collect())) + } + DataType::LargeBinary => { + let bytes = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(bytes.iter().map(|b| format!("{b:02x}")).collect())) + } + DataType::FixedSizeBinary(_) => { + let bytes = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(bytes.iter().map(|b| format!("{b:02x}")).collect())) + } + DataType::BinaryView => { + let bytes = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(bytes.iter().map(|b| format!("{b:02x}")).collect())) + } + DataType::Utf8View => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Time32(TimeUnit::Second) => { + let v = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row) as i64; + Ok(Some((v * 1_000_000_000).to_string())) + } + DataType::Time32(TimeUnit::Millisecond) => { + let v = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row) as i64; + Ok(Some((v * 1_000_000).to_string())) + } + DataType::Time32(_) => { + unreachable!("Time32 only supports Second and Millisecond units") + } + DataType::Time64(TimeUnit::Microsecond) => { + let v = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some((v * 1_000).to_string())) + } + DataType::Time64(TimeUnit::Nanosecond) => { + let v = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(v.to_string())) + } + DataType::Time64(_) => { + unreachable!("Time64 only supports Microsecond and Nanosecond units") + } + DataType::Timestamp(unit, _) => { + let (v, multiplier) = match unit { + TimeUnit::Second => ( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row), + 1_000_000_000i64, + ), + TimeUnit::Millisecond => ( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row), + 1_000_000i64, + ), + TimeUnit::Microsecond => ( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row), + 1_000i64, + ), + TimeUnit::Nanosecond => ( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row), + 1i64, + ), + }; + Ok(Some((v as i128 * multiplier as i128).to_string())) + } + _ => Err(Error::with_message_and_status( + format!("unsupported bind parameter type: {dt:?}"), + Status::NotImplemented, + )), + } +} + +fn decimal128_to_string(value: i128, scale: i8) -> String { + if scale <= 0 { + return value.to_string(); + } + let sign = if value < 0 { "-" } else { "" }; + let abs = value.unsigned_abs(); + let divisor = 10u128.pow(scale as u32); + let integer_part = abs / divisor; + let fractional_part = abs % divisor; + format!( + "{sign}{integer_part}.{fractional_part:0>width$}", + width = scale as usize + ) +} + +/// Converts days since Unix epoch (1970-01-01) to a YYYY-MM-DD string. +pub(crate) fn days_since_epoch_to_date_str(days: i64) -> String { + // Algorithm: civil date from days (Gregorian proleptic) + let z = days + 719468; + let era = z.div_euclid(146097); + let doe = z.rem_euclid(146097); + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = if mp < 10 { mp + 3 } else { mp - 9 }; + let y = if m <= 2 { y + 1 } else { y }; + format!("{:04}-{:02}-{:02}", y, m, d) +} + +/// Strips leading SQL whitespace, line comments (`--…`), and block +/// comments (`/*…*/`) from `query`, returning the remaining slice. +fn strip_sql_comments(query: &str) -> &str { + let mut s = query.trim_start(); + loop { + if s.starts_with("--") { + s = s[s.find('\n').map(|i| i + 1).unwrap_or(s.len())..].trim_start(); + } else if s.starts_with("/*") { + if let Some(end) = s.find("*/") { + s = s[end + 2..].trim_start(); + } else { + break; + } + } else { + break; + } + } + s +} + +fn is_ddl(query: &str) -> bool { + let upper = strip_sql_comments(query).to_uppercase(); + upper.starts_with("CREATE ") + || upper.starts_with("DROP ") + || upper.starts_with("ALTER ") + || upper.starts_with("TRUNCATE ") + || upper.starts_with("RENAME ") + || upper.starts_with("COMMENT ") + || upper.starts_with("GRANT ") + || upper.starts_with("REVOKE ") + || upper.starts_with("SHOW ") + || upper.starts_with("USE ") + || upper.starts_with("DESCRIBE ") + || upper.starts_with("DESC ") +} + +#[cfg(test)] +mod tests { + use super::*; + use adbc_core::Statement as _; + + fn make_stmt() -> Statement { + let driver = crate::driver::Driver::default(); + Statement { + inner: driver.inner.clone(), + stmt_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + query: None, + target_table: None, + ingest_catalog: None, + ingest_schema: None, + ingest_mode: None, + query_tag: None, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, + bound_batches: vec![], + last_query_id: None, + } + } + + #[test] + fn set_sql_query_stores_query() { + let mut stmt = make_stmt(); + stmt.set_sql_query("SELECT 1").unwrap(); + assert_eq!(stmt.query.as_deref(), Some("SELECT 1")); + } + + #[test] + fn execute_without_query_returns_invalid_state() { + let mut stmt = make_stmt(); + match stmt.execute() { + Err(err) => assert_eq!(err.status, adbc_core::error::Status::InvalidState), + Ok(_) => panic!("execute should have returned an error"), + } + } + + #[test] + fn execute_schema_without_query_returns_invalid_state() { + let mut stmt = make_stmt(); + match stmt.execute_schema() { + Err(err) => assert_eq!(err.status, adbc_core::error::Status::InvalidState), + Ok(_) => panic!("execute_schema should have returned an error"), + } + } + + #[test] + fn execute_with_target_table_no_data_returns_invalid_state() { + let driver = crate::driver::Driver::default(); + let mut stmt = Statement { + inner: driver.inner.clone(), + stmt_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + query: None, + target_table: Some("mytable".into()), + ingest_catalog: None, + ingest_schema: None, + ingest_mode: None, + query_tag: None, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, + bound_batches: vec![], + last_query_id: None, + }; + match stmt.execute() { + Err(err) => assert_eq!(err.status, adbc_core::error::Status::InvalidState), + Ok(_) => panic!("execute should have returned an error"), + } + } + + #[test] + fn set_query_clears_target_table() { + let driver = crate::driver::Driver::default(); + let mut stmt = Statement { + inner: driver.inner.clone(), + stmt_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + query: None, + target_table: Some("mytable".into()), + ingest_catalog: None, + ingest_schema: None, + ingest_mode: None, + query_tag: None, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, + bound_batches: vec![], + last_query_id: None, + }; + stmt.set_sql_query("SELECT 1").unwrap(); + assert!(stmt.target_table.is_none()); + } + + #[test] + fn prepare_without_query_returns_invalid_state() { + let mut stmt = make_stmt(); + let err = stmt.prepare().unwrap_err(); + assert_eq!(err.status, adbc_core::error::Status::InvalidState); + } + + #[test] + fn prepare_with_query_is_noop() { + let mut stmt = make_stmt(); + stmt.set_sql_query("SELECT 1").unwrap(); + stmt.prepare().unwrap(); + } + + #[test] + fn set_target_table_option() { + let mut stmt = make_stmt(); + stmt.set_option( + OptionStatement::TargetTable, + OptionValue::String("mytable".into()), + ) + .unwrap(); + assert_eq!(stmt.target_table.as_deref(), Some("mytable")); + } + + #[test] + fn unknown_option_returns_not_found() { + let mut stmt = make_stmt(); + let err = stmt + .set_option( + OptionStatement::Other("unknown.option".into()), + OptionValue::String("val".into()), + ) + .unwrap_err(); + assert_eq!(err.status, adbc_core::error::Status::NotFound); + } + + #[test] + fn set_query_tag_stored_and_readable() { + let mut stmt = make_stmt(); + stmt.set_option( + OptionStatement::Other("adbc.snowflake.statement.query_tag".into()), + OptionValue::String("my_tag".into()), + ) + .unwrap(); + assert_eq!( + stmt.get_option_string(OptionStatement::Other( + "adbc.snowflake.statement.query_tag".into() + )) + .unwrap(), + "my_tag" + ); + // Verify conn_handle is present on the struct (compile-time check) + let _ = stmt.conn_handle; + } + + #[test] + fn test_adjust_schema_int8_to_int64() { + let schema = Schema::new(vec![Field::new("a", DataType::Int8, false)]); + let result = adjust_schema_integers(&schema); + assert_eq!(result.field(0).data_type(), &DataType::Int64); + } + + #[test] + fn test_adjust_schema_int16_to_int64() { + let schema = Schema::new(vec![Field::new("b", DataType::Int16, true)]); + let result = adjust_schema_integers(&schema); + assert_eq!(result.field(0).data_type(), &DataType::Int64); + } + + #[test] + fn test_adjust_schema_int32_to_int64() { + let schema = Schema::new(vec![Field::new("c", DataType::Int32, false)]); + let result = adjust_schema_integers(&schema); + assert_eq!(result.field(0).data_type(), &DataType::Int64); + } + + #[test] + fn test_adjust_schema_int64_passthrough() { + let schema = Schema::new(vec![Field::new("d", DataType::Int64, false)]); + let result = adjust_schema_integers(&schema); + assert_eq!(result.field(0).data_type(), &DataType::Int64); + } + + #[test] + fn test_adjust_schema_mixed() { + use arrow_schema::DataType; + let schema = Schema::new(vec![ + Field::new("i8", DataType::Int8, false), + Field::new("s", DataType::Utf8, false), + Field::new("i32", DataType::Int32, true), + Field::new("f64", DataType::Float64, false), + ]); + let result = adjust_schema_integers(&schema); + assert_eq!(result.field(0).data_type(), &DataType::Int64); + assert_eq!(result.field(1).data_type(), &DataType::Utf8); + assert_eq!(result.field(2).data_type(), &DataType::Int64); + assert_eq!(result.field(3).data_type(), &DataType::Float64); + } + + #[test] + fn test_converting_reader_int8_values() { + use arrow_array::Int8Array; + let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int8, false)])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(Int8Array::from(vec![1i8, 2, 3]))], + ) + .unwrap(); + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); + let out = cr.next().unwrap().unwrap(); + assert_eq!(out.schema().field(0).data_type(), &DataType::Int64); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(col.values(), &[1i64, 2, 3]); + } + + #[test] + fn test_converting_reader_null_values() { + use arrow_array::Int16Array; + let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int16, true)])); + let arr = Int16Array::from(vec![Some(10i16), None, Some(30)]); + let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(arr)]).unwrap(); + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); + let out = cr.next().unwrap().unwrap(); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(col.value(0), 10i64); + assert!(col.is_null(1)); + assert_eq!(col.value(2), 30i64); + } + + #[test] + fn test_converting_reader_empty_batch() { + let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int32, false)])); + let batch = RecordBatch::new_empty(schema.clone()); + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); + let out = cr.next().unwrap().unwrap(); + assert_eq!(out.schema().field(0).data_type(), &DataType::Int64); + assert_eq!(out.num_rows(), 0); + } + + #[test] + fn test_converting_reader_multiple_batches_different_widths() { + use arrow_array::{Int8Array, Int32Array}; + struct TwoBatchReader { + batches: std::vec::IntoIter, + schema: Arc, + } + impl Iterator for TwoBatchReader { + type Item = std::result::Result; + fn next(&mut self) -> Option { + self.batches.next().map(Ok) + } + } + impl RecordBatchReader for TwoBatchReader { + fn schema(&self) -> Arc { + self.schema.clone() + } + } + + let declared_schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int64, false)])); + + let schema_i8 = Arc::new(Schema::new(vec![Field::new("v", DataType::Int8, false)])); + let schema_i32 = Arc::new(Schema::new(vec![Field::new("v", DataType::Int32, false)])); + let batch1 = + RecordBatch::try_new(schema_i8, vec![Arc::new(Int8Array::from(vec![1i8, 2]))]).unwrap(); + let batch2 = RecordBatch::try_new( + schema_i32, + vec![Arc::new(Int32Array::from(vec![100i32, 200]))], + ) + .unwrap(); + + let reader = TwoBatchReader { + batches: vec![batch1, batch2].into_iter(), + schema: declared_schema, + }; + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); + + let out1 = cr.next().unwrap().unwrap(); + assert_eq!(out1.column(0).data_type(), &DataType::Int64); + let col1 = out1 + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(col1.values(), &[1i64, 2]); + + let out2 = cr.next().unwrap().unwrap(); + assert_eq!(out2.column(0).data_type(), &DataType::Int64); + let col2 = out2 + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(col2.values(), &[100i64, 200]); + } + fn make_field_with_meta(name: &str, dt: DataType, logical_type: &str, scale: &str) -> Field { + make_field_with_precision(name, dt, logical_type, scale, "38") + } + + fn make_field_with_precision( + name: &str, + dt: DataType, + logical_type: &str, + scale: &str, + precision: &str, + ) -> Field { + let mut md = std::collections::HashMap::new(); + md.insert("logicalType".to_string(), logical_type.to_string()); + md.insert("scale".to_string(), scale.to_string()); + md.insert("precision".to_string(), precision.to_string()); + Field::new(name, dt, true).with_metadata(md) + } + + #[test] + fn test_adjust_schema_time_scale3_is_time32_millisecond() { + let f = make_field_with_meta("t", DataType::Int32, "TIME", "3"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Nanosecond); + assert_eq!( + result.field(0).data_type(), + &DataType::Time32(TimeUnit::Millisecond) + ); + } + + #[test] + fn test_adjust_schema_time_scale9_is_time64_nanosecond() { + let f = make_field_with_meta("t", DataType::Int64, "TIME", "9"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Nanosecond); + assert_eq!( + result.field(0).data_type(), + &DataType::Time64(TimeUnit::Nanosecond) + ); + } + + #[test] + fn test_adjust_schema_fixed_scale2_is_float64() { + let f = make_field_with_meta("x", DataType::Int64, "FIXED", "2"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Nanosecond); + assert_eq!(result.field(0).data_type(), &DataType::Float64); + } + + #[test] + fn test_adjust_schema_fixed_scale2_high_precision_is_decimal128() { + let f = make_field_with_precision("x", DataType::Int64, "FIXED", "2", "10"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, true, TimeUnit::Nanosecond); + assert_eq!(result.field(0).data_type(), &DataType::Decimal128(10, 2)); + } + + #[test] + fn test_adjust_schema_timestamp_ntz_int64_to_timestamp_ns() { + let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_NTZ", "9"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Nanosecond); + assert_eq!( + result.field(0).data_type(), + &DataType::Timestamp(TimeUnit::Nanosecond, None) + ); + } + + #[test] + fn test_adjust_schema_timestamp_ltz_is_utc() { + let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_LTZ", "6"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Microsecond); + assert_eq!( + result.field(0).data_type(), + &DataType::Timestamp(TimeUnit::Microsecond, Some(Arc::from("UTC"))) + ); + } + + #[test] + fn test_converting_reader_fixed_scale2_produces_float64() { + let f = make_field_with_meta("x", DataType::Int64, "FIXED", "2"); + let schema = Arc::new(Schema::new(vec![f])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(arrow_array::Int64Array::from(vec![12345i64, 255]))], + ) + .unwrap(); + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; + let mut cr = ConvertingReader::new( + reader, + false, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); + let out = cr.next().unwrap().unwrap(); + assert_eq!(out.schema().field(0).data_type(), &DataType::Float64); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert!((col.value(0) - 123.45).abs() < 1e-9); + assert!((col.value(1) - 2.55).abs() < 1e-9); + } + + #[test] + fn test_converting_reader_fixed_scale2_high_precision_produces_decimal128() { + let f = make_field_with_precision("x", DataType::Int64, "FIXED", "2", "10"); + let schema = Arc::new(Schema::new(vec![f])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(arrow_array::Int64Array::from(vec![ + 12345i64, -255, + ]))], + ) + .unwrap(); + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); + let out = cr.next().unwrap().unwrap(); + assert_eq!( + out.schema().field(0).data_type(), + &DataType::Decimal128(10, 2) + ); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + // 12345 with scale 2 = 123.45 + assert_eq!(col.value(0), 12345i128); + // -255 with scale 2 = -2.55 + assert_eq!(col.value(1), -255i128); + } + + #[test] + fn test_converting_reader_fixed_scale0_high_precision_stays_int64() { + let f = make_field_with_precision("x", DataType::Int64, "FIXED", "0", "10"); + let schema = Arc::new(Schema::new(vec![f])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(arrow_array::Int64Array::from(vec![42i64]))], + ) + .unwrap(); + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); + let out = cr.next().unwrap().unwrap(); + assert_eq!(out.schema().field(0).data_type(), &DataType::Int64); + } + + #[test] + fn test_converting_reader_decimal128_source_high_precision_identity() { + let f = make_field_with_precision("x", DataType::Decimal128(10, 2), "FIXED", "2", "10"); + let schema = Arc::new(Schema::new(vec![f])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new( + arrow_array::Decimal128Array::from(vec![12345i128, -255]) + .with_precision_and_scale(10, 2) + .unwrap(), + )], + ) + .unwrap(); + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); + let out = cr.next().unwrap().unwrap(); + assert_eq!( + out.schema().field(0).data_type(), + &DataType::Decimal128(10, 2) + ); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(col.value(0), 12345i128); + assert_eq!(col.value(1), -255i128); + } + + #[test] + fn test_converting_reader_decimal128_source_no_high_precision_casts_to_float64() { + let f = make_field_with_precision("x", DataType::Decimal128(10, 2), "FIXED", "2", "10"); + let schema = Arc::new(Schema::new(vec![f])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new( + arrow_array::Decimal128Array::from(vec![12345i128]) + .with_precision_and_scale(10, 2) + .unwrap(), + )], + ) + .unwrap(); + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; + let mut cr = ConvertingReader::new( + reader, + false, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); + let out = cr.next().unwrap().unwrap(); + assert_eq!(out.schema().field(0).data_type(), &DataType::Float64); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert!((col.value(0) - 123.45).abs() < 1e-9); + } + + #[test] + fn test_converting_reader_timestamp_ntz_int64_cast() { + let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_NTZ", "9"); + let schema = Arc::new(Schema::new(vec![f])); + let epoch_ns: i64 = 1_000_000_000; + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(arrow_array::Int64Array::from(vec![epoch_ns]))], + ) + .unwrap(); + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; + let mut cr = ConvertingReader::new( + reader, + false, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); + let out = cr.next().unwrap().unwrap(); + assert_eq!( + out.schema().field(0).data_type(), + &DataType::Timestamp(TimeUnit::Nanosecond, None) + ); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(col.value(0), epoch_ns); + } + + #[test] + fn test_adjust_schema_timestamp_ntz_capped_by_ts_unit() { + let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_NTZ", "9"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Microsecond); + assert_eq!( + result.field(0).data_type(), + &DataType::Timestamp(TimeUnit::Microsecond, None) + ); + } + + #[test] + fn test_build_timestamp_tz_2field_year9999() { + use arrow_array::{Int32Array, Int64Array, StructArray}; + use arrow_schema::Field as SchemaField; + + let epoch_us: i64 = 253402300799000000; // 9999-12-31T23:59:59Z in microseconds + let epoch_arr = Int64Array::from(vec![epoch_us]); + let tz_arr = Int32Array::from(vec![1440i32]); // UTC + + let fields = vec![ + Arc::new(SchemaField::new("epoch", DataType::Int64, false)), + Arc::new(SchemaField::new("tz", DataType::Int32, false)), + ]; + let struct_arr = StructArray::try_new( + fields.into(), + vec![ + Arc::new(epoch_arr) as ArrayRef, + Arc::new(tz_arr) as ArrayRef, + ], + None, + ) + .unwrap(); + + let epoch_col = struct_arr + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + let tz_col = struct_arr + .column(1) + .as_any() + .downcast_ref::() + .unwrap(); + + let result = build_timestamp_tz_2field( + epoch_col, + tz_col, + &struct_arr, + 6, + false, + DataType::Timestamp(TimeUnit::Microsecond, Some(Arc::from("UTC"))), + ) + .unwrap(); + + let ts = result + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!( + ts.value(0), + epoch_us, + "year 9999 should round-trip as microseconds" + ); + } + + #[test] + fn test_build_timestamp_from_epoch_fraction_negative_scale_clamps() { + let epoch = arrow_array::Int64Array::from(vec![1i64]); + let fraction = arrow_array::Int32Array::from(vec![5i32]); + let fields = vec![ + Field::new("epoch", DataType::Int64, true), + Field::new("fraction", DataType::Int32, true), + ]; + let struct_arr = arrow_array::StructArray::from(vec![ + ( + Arc::new(fields[0].clone()), + Arc::new(epoch.clone()) as ArrayRef, + ), + ( + Arc::new(fields[1].clone()), + Arc::new(fraction.clone()) as ArrayRef, + ), + ]); + let result = build_timestamp_from_epoch_fraction( + &epoch, + &fraction, + &struct_arr, + -1, + false, + DataType::Timestamp(TimeUnit::Nanosecond, None), + ) + .expect("should not panic"); + let ts = result + .as_any() + .downcast_ref::() + .unwrap(); + // scale -1 clamps to 0. frac_to_ns = 10^9. + // ns = 1 * 10^9 + 5 * 10^9 = 6000000000. + assert_eq!(ts.value(0), 6_000_000_000); + } + + #[test] + fn test_build_timestamp_from_epoch_fraction_oversized_scale_clamps() { + let epoch = arrow_array::Int64Array::from(vec![1i64]); + let fraction = arrow_array::Int32Array::from(vec![5i32]); + let fields = vec![ + Field::new("epoch", DataType::Int64, true), + Field::new("fraction", DataType::Int32, true), + ]; + let struct_arr = arrow_array::StructArray::from(vec![ + ( + Arc::new(fields[0].clone()), + Arc::new(epoch.clone()) as ArrayRef, + ), + ( + Arc::new(fields[1].clone()), + Arc::new(fraction.clone()) as ArrayRef, + ), + ]); + let result = build_timestamp_from_epoch_fraction( + &epoch, + &fraction, + &struct_arr, + 10, + false, + DataType::Timestamp(TimeUnit::Nanosecond, None), + ) + .expect("should not panic"); + let ts = result + .as_any() + .downcast_ref::() + .unwrap(); + // scale 10 clamps to 9. frac_to_ns = 10^0 = 1. + // ns = 1 * 10^9 + 5 * 1 = 1000000005. + assert_eq!(ts.value(0), 1_000_000_005); + } + + #[test] + fn test_build_timestamp_tz_3field_negative_scale_clamps() { + let epoch = arrow_array::Int64Array::from(vec![1i64]); + let fraction = arrow_array::Int32Array::from(vec![5i32]); + let tzoffset = arrow_array::Int32Array::from(vec![1440i32]); + let fields = vec![ + Field::new("epoch", DataType::Int64, true), + Field::new("fraction", DataType::Int32, true), + Field::new("tzoffset", DataType::Int32, true), + ]; + let struct_arr = arrow_array::StructArray::from(vec![ + ( + Arc::new(fields[0].clone()), + Arc::new(epoch.clone()) as ArrayRef, + ), + ( + Arc::new(fields[1].clone()), + Arc::new(fraction.clone()) as ArrayRef, + ), + ( + Arc::new(fields[2].clone()), + Arc::new(tzoffset.clone()) as ArrayRef, + ), + ]); + let result = build_timestamp_tz_3field( + &epoch, + &fraction, + &tzoffset, + &struct_arr, + -1, + false, + DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("UTC"))), + ) + .expect("should not panic"); + let ts = result + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(ts.value(0), 6_000_000_000); + } + + #[test] + fn test_build_timestamp_tz_3field_oversized_scale_clamps() { + let epoch = arrow_array::Int64Array::from(vec![1i64]); + let fraction = arrow_array::Int32Array::from(vec![5i32]); + let tzoffset = arrow_array::Int32Array::from(vec![1440i32]); + let fields = vec![ + Field::new("epoch", DataType::Int64, true), + Field::new("fraction", DataType::Int32, true), + Field::new("tzoffset", DataType::Int32, true), + ]; + let struct_arr = arrow_array::StructArray::from(vec![ + ( + Arc::new(fields[0].clone()), + Arc::new(epoch.clone()) as ArrayRef, + ), + ( + Arc::new(fields[1].clone()), + Arc::new(fraction.clone()) as ArrayRef, + ), + ( + Arc::new(fields[2].clone()), + Arc::new(tzoffset.clone()) as ArrayRef, + ), + ]); + let result = build_timestamp_tz_3field( + &epoch, + &fraction, + &tzoffset, + &struct_arr, + 10, + false, + DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("UTC"))), + ) + .expect("should not panic"); + let ts = result + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(ts.value(0), 1_000_000_005); + } + + #[test] + fn test_arrow_batches_to_json_bindings_basic() { + let schema = Arc::new(Schema::new(vec![ + Field::new("id", DataType::Int64, false), + Field::new("name", DataType::Utf8, false), + ])); + let batch = RecordBatch::try_new( + schema, + vec![ + Arc::new(arrow_array::Int64Array::from(vec![123, 456])) as ArrayRef, + Arc::new(arrow_array::StringArray::from(vec!["hello", "world"])) as ArrayRef, + ], + ) + .unwrap(); + + let json = arrow_batches_to_json_bindings(&[batch]).unwrap(); + assert!(json.contains("\"1\":{\"type\":\"FIXED\",\"value\":[\"123\",\"456\"]}")); + assert!(json.contains("\"2\":{\"type\":\"TEXT\",\"value\":[\"hello\",\"world\"]}")); + } + + #[test] + fn test_arrow_batches_to_json_bindings_with_nulls() { + let schema = Arc::new(Schema::new(vec![ + Field::new("id", DataType::Int64, true), + Field::new("name", DataType::Utf8, true), + ])); + let batch = RecordBatch::try_new( + schema, + vec![ + Arc::new(arrow_array::Int64Array::from(vec![Some(1), None, Some(3)])) as ArrayRef, + Arc::new(arrow_array::StringArray::from(vec![ + Some("a"), + Some("b"), + None, + ])) as ArrayRef, + ], + ) + .unwrap(); + + let json = arrow_batches_to_json_bindings(&[batch]).unwrap(); + assert!(json.contains("\"1\":{\"type\":\"FIXED\",\"value\":[\"1\",null,\"3\"]}")); + assert!(json.contains("\"2\":{\"type\":\"TEXT\",\"value\":[\"a\",\"b\",null]}")); + } + + #[test] + fn test_arrow_batches_to_json_bindings_float_precision() { + let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Float64, false)])); + let batch = RecordBatch::try_new( + schema, + vec![Arc::new(arrow_array::Float64Array::from(vec![3.141592653589793])) as ArrayRef], + ) + .unwrap(); + + let json = arrow_batches_to_json_bindings(&[batch]).unwrap(); + assert!( + json.contains("3.141592653589793"), + "should preserve full f64 precision, got: {json}" + ); + } + + #[test] + fn test_decimal128_to_string_negative_fractional() { + // Values in (-1, 0) must preserve the negative sign + assert_eq!(decimal128_to_string(-50, 2), "-0.50"); + assert_eq!(decimal128_to_string(-1, 1), "-0.1"); + assert_eq!(decimal128_to_string(-999, 3), "-0.999"); + // Normal negative values + assert_eq!(decimal128_to_string(-150, 2), "-1.50"); + // Positive values unchanged + assert_eq!(decimal128_to_string(50, 2), "0.50"); + assert_eq!(decimal128_to_string(150, 2), "1.50"); + // Zero + assert_eq!(decimal128_to_string(0, 2), "0.00"); + // No scale + assert_eq!(decimal128_to_string(-42, 0), "-42"); + } +} diff --git a/rust/tests/integration.rs b/rust/tests/integration.rs new file mode 100644 index 0000000..04bfcf0 --- /dev/null +++ b/rust/tests/integration.rs @@ -0,0 +1,701 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// tests/integration.rs +use adbc_core::{ + Connection as _, Database as _, Driver as _, Optionable, Statement as _, + options::{OptionConnection, OptionDatabase, OptionValue}, +}; +use adbc_driver_snowflake::{Database, Driver}; +use arrow_array::Array; +use arrow_array::cast::AsArray; +use arrow_schema::{DataType, TimeUnit}; + +fn get_env(key: &str) -> Option { + std::env::var(key).ok().filter(|s| !s.is_empty()) +} + +/// Build a configured Database without opening a connection. +/// Callers can set extra options before calling `.new_connection()`. +fn make_db() -> Option { + let uri = get_env("SNOWFLAKE_URI")?; + let mut driver = Driver::default(); + let mut db = driver.new_database().expect("new_database"); + db.set_option(OptionDatabase::Uri, OptionValue::String(uri)) + .expect("set uri"); + if let Some(database) = get_env("SNOWFLAKE_DATABASE") { + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.db".into()), + OptionValue::String(database), + ) + .expect("set database"); + } + if let Some(schema) = get_env("SNOWFLAKE_SCHEMA") { + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.schema".into()), + OptionValue::String(schema), + ) + .expect("set schema"); + } + Some(db) +} + +fn make_connection() -> Option { + Some(make_db()?.new_connection().expect("new_connection")) +} + +#[test] +fn test_private_key_simple_query() { + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + let mut stmt = conn.new_statement().expect("new_statement"); + stmt.set_sql_query("SELECT CURRENT_USER(), CURRENT_WAREHOUSE(), CURRENT_ROLE()") + .expect("set_sql_query"); + let mut reader = stmt.execute().expect("execute"); + let batch = reader.next().expect("no batch").expect("batch error"); + + assert_eq!(batch.num_rows(), 1); + assert_eq!(batch.num_columns(), 3); + assert!( + !batch.column(0).as_string::().value(0).is_empty(), + "CURRENT_USER() is empty" + ); + assert!( + !batch.column(1).as_string::().value(0).is_empty(), + "CURRENT_WAREHOUSE() is empty" + ); + assert!( + !batch.column(2).as_string::().value(0).is_empty(), + "CURRENT_ROLE() is empty" + ); +} + +#[test] +fn test_select_one() { + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + let mut stmt = conn.new_statement().expect("new_statement"); + stmt.set_sql_query("SELECT 1 AS n").expect("set_sql_query"); + let mut reader = stmt.execute().expect("execute"); + let batch = reader.next().expect("no batch").expect("batch error"); + assert_eq!(batch.num_rows(), 1); + assert_eq!(batch.num_columns(), 1); +} + +#[test] +fn test_get_table_types() { + let Some(conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + let mut reader = conn.get_table_types().expect("get_table_types"); + let batch = reader.next().expect("no batch").expect("batch error"); + let types: Vec<&str> = batch + .column(0) + .as_string::() + .iter() + .flatten() + .collect(); + assert!(types.contains(&"TABLE")); + assert!(types.contains(&"VIEW")); +} + +#[test] +fn test_get_info_no_codes() { + let Some(conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + let mut reader = conn.get_info(None).expect("get_info"); + let batch = reader.next().expect("no batch").expect("batch error"); + assert!(batch.num_rows() > 0, "expected at least one info row"); +} + +#[test] +fn test_get_info_vendor_version() { + use adbc_core::options::InfoCode; + use std::collections::HashSet; + + let Some(conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + let mut reader = conn + .get_info(Some(HashSet::from([InfoCode::VendorVersion]))) + .expect("get_info"); + let batch = reader.next().expect("no batch").expect("batch error"); + assert_eq!(batch.num_rows(), 1); + + // VendorVersion is a string value — type_id 0 in the union + use arrow_array::cast::AsArray; + let type_ids = batch + .column(1) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!( + type_ids.type_id(0), + 0, + "VendorVersion should be a string union arm" + ); + let version_str = type_ids.value(0); + let v = version_str.as_string::().value(0); + assert!(!v.is_empty(), "VendorVersion should not be empty"); + // Snowflake versions look like "8.x.x" — just check it contains a dot + assert!( + v.contains('.'), + "VendorVersion should look like a version string, got: {v}" + ); +} + +#[test] +fn test_get_option_string_current_catalog_and_schema() { + let Some(conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + let catalog = conn + .get_option_string(OptionConnection::CurrentCatalog) + .expect("get CurrentCatalog"); + let schema = conn + .get_option_string(OptionConnection::CurrentSchema) + .expect("get CurrentSchema"); + + assert!(!catalog.is_empty(), "current catalog should not be empty"); + assert!(!schema.is_empty(), "current schema should not be empty"); +} + +#[test] +fn test_get_option_string_autocommit() { + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + let ac = conn + .get_option_string(OptionConnection::AutoCommit) + .expect("get AutoCommit"); + assert_eq!(ac, "true", "default autocommit should be true"); + + conn.set_option( + OptionConnection::AutoCommit, + OptionValue::String("false".into()), + ) + .expect("disable autocommit"); + let ac = conn + .get_option_string(OptionConnection::AutoCommit) + .expect("get AutoCommit after disable"); + assert_eq!(ac, "false"); +} + +#[test] +fn test_execute_ddl_and_dml() { + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("CREATE OR REPLACE TEMP TABLE adbc_rust_test (id INTEGER, name TEXT)") + .unwrap(); + stmt.execute_update().expect("create table"); + } + + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("INSERT INTO adbc_rust_test VALUES (1, 'hello')") + .unwrap(); + let rows = stmt.execute_update().expect("insert"); + assert_eq!(rows, Some(1)); + } + + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("SELECT * FROM adbc_rust_test").unwrap(); + let mut reader = stmt.execute().expect("select"); + let batch = reader.next().expect("no batch").expect("batch error"); + assert_eq!(batch.num_rows(), 1); + } + + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("DROP TABLE IF EXISTS adbc_rust_test") + .unwrap(); + stmt.execute_update().expect("drop table"); + } +} + +// ── precision option tests ──────────────────────────────────────────────────── + +/// Verify that the precision options round-trip correctly on Database +/// (no live query required — purely option set/get). +#[test] +fn test_precision_options_defaults_and_round_trip() { + let Some(mut db) = make_db() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + // Defaults + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.use_high_precision".into() + )) + .unwrap(), + "enabled" + ); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.max_timestamp_precision".into() + )) + .unwrap(), + "nanoseconds" + ); + + // Round-trip: disable high precision + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.use_high_precision".into()), + OptionValue::String("disabled".into()), + ) + .unwrap(); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.use_high_precision".into() + )) + .unwrap(), + "disabled" + ); + + // Round-trip: microsecond timestamps + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.max_timestamp_precision".into()), + OptionValue::String("microseconds".into()), + ) + .unwrap(); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.max_timestamp_precision".into() + )) + .unwrap(), + "microseconds" + ); + + // Round-trip: nanoseconds_error_on_overflow + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.max_timestamp_precision".into()), + OptionValue::String("nanoseconds_error_on_overflow".into()), + ) + .unwrap(); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.max_timestamp_precision".into() + )) + .unwrap(), + "nanoseconds_error_on_overflow" + ); +} + +/// Mirrors Go's TestUseHighPrecision / TestSchemaWithLowPrecision. +/// +/// With high precision (default): NUMBER(10,0) → Int64, NUMBER(15,2) → Decimal128(15,2). +/// With high precision disabled: NUMBER(10,0) → Int64, NUMBER(15,2) → Float64. +#[test] +fn test_high_precision_get_table_schema() { + let table_name = format!( + "ADBC_RUST_PRECISION_TEST_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_micros() + ); + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + // Create a permanent table so a second connection can also DESC it. + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query(&format!( + "CREATE OR REPLACE TABLE {} (INT_COL NUMBER(10,0), DEC_COL NUMBER(15,2))", + table_name + )) + .unwrap(); + stmt.execute_update().expect("create precision test table"); + } + + // Snowflake folds unquoted identifiers to uppercase. + // ── high precision (default: enabled) ──────────────────────────────────── + let schema_hp = conn + .get_table_schema(None, None, &table_name) + .expect("get_table_schema high precision"); + + assert_eq!( + schema_hp.field_with_name("INT_COL").unwrap().data_type(), + &DataType::Int64, + "INT_COL: expected Int64 (scale=0)" + ); + assert_eq!( + schema_hp.field_with_name("DEC_COL").unwrap().data_type(), + &DataType::Decimal128(15, 2), + "DEC_COL: expected Decimal128(15,2) with high precision" + ); + + // ── low precision (disabled) ────────────────────────────────────────────── + let Some(mut db_lp) = make_db() else { return }; + db_lp + .set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.use_high_precision".into()), + OptionValue::String("disabled".into()), + ) + .unwrap(); + let conn_lp = db_lp.new_connection().expect("low-precision connection"); + + let schema_lp = conn_lp + .get_table_schema(None, None, &table_name) + .expect("get_table_schema low precision"); + + assert_eq!( + schema_lp.field_with_name("INT_COL").unwrap().data_type(), + &DataType::Int64, + "INT_COL: expected Int64 (scale=0)" + ); + assert_eq!( + schema_lp.field_with_name("DEC_COL").unwrap().data_type(), + &DataType::Float64, + "DEC_COL: expected Float64 with low precision" + ); + + // Cleanup + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query(&format!("DROP TABLE IF EXISTS {}", table_name)) + .unwrap(); + stmt.execute_update().expect("drop precision test table"); + } +} + +/// Mirrors Go's TestTimestampPrecision. +/// +/// With nanoseconds (default): TIMESTAMP_NTZ → Timestamp(Nanosecond, None), +/// TIMESTAMP_TZ → Timestamp(Nanosecond, UTC). +/// With microseconds: TIMESTAMP_NTZ → Timestamp(Microsecond, None), +/// TIMESTAMP_TZ → Timestamp(Microsecond, UTC). +#[test] +fn test_timestamp_precision_get_table_schema() { + let table_name = format!( + "ADBC_RUST_TS_PRECISION_TEST_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_micros() + ); + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query(&format!( + "CREATE OR REPLACE TABLE {} (NTZ_COL TIMESTAMP_NTZ, TZ_COL TIMESTAMP_TZ)", + table_name + )) + .unwrap(); + stmt.execute_update() + .expect("create ts precision test table"); + } + + // Snowflake folds unquoted identifiers to uppercase. + // ── nanoseconds (default) ───────────────────────────────────────────────── + let schema_ns = conn + .get_table_schema(None, None, &table_name) + .expect("get_table_schema nanoseconds"); + + assert_eq!( + schema_ns.field_with_name("NTZ_COL").unwrap().data_type(), + &DataType::Timestamp(TimeUnit::Nanosecond, None), + "NTZ_COL: expected Timestamp(Nanosecond, None)" + ); + assert_eq!( + schema_ns.field_with_name("TZ_COL").unwrap().data_type(), + &DataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into())), + "TZ_COL: expected Timestamp(Nanosecond, UTC)" + ); + + // ── microseconds ────────────────────────────────────────────────────────── + let Some(mut db_us) = make_db() else { return }; + db_us + .set_option( + OptionDatabase::Other( + "adbc.snowflake.sql.client_option.max_timestamp_precision".into(), + ), + OptionValue::String("microseconds".into()), + ) + .unwrap(); + let conn_us = db_us.new_connection().expect("microsecond connection"); + + let schema_us = conn_us + .get_table_schema(None, None, &table_name) + .expect("get_table_schema microseconds"); + + assert_eq!( + schema_us.field_with_name("NTZ_COL").unwrap().data_type(), + &DataType::Timestamp(TimeUnit::Microsecond, None), + "NTZ_COL: expected Timestamp(Microsecond, None)" + ); + assert_eq!( + schema_us.field_with_name("TZ_COL").unwrap().data_type(), + &DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())), + "TZ_COL: expected Timestamp(Microsecond, UTC)" + ); + + // Cleanup + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query(&format!("DROP TABLE IF EXISTS {}", table_name)) + .unwrap(); + stmt.execute_update().expect("drop ts precision test table"); + } +} + +// ── missing database options ────────────────────────────────────────────────── + +/// Verify simple 1:1 option round-trips through the public Database API. +/// No live connection required — make_db() only needs SNOWFLAKE_URI for the env +/// var check; no actual network call is made until new_connection(). +#[test] +fn test_database_options_round_trip() { + let Some(mut db) = make_db() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + let cases: &[(&str, &str)] = &[ + ("adbc.snowflake.sql.region", "us-east-1"), + ("adbc.snowflake.sql.client_option.login_timeout", "30s"), + ("adbc.snowflake.sql.client_option.request_timeout", "60s"), + ("adbc.snowflake.sql.client_option.jwt_expire_timeout", "90s"), + ("adbc.snowflake.sql.client_option.client_timeout", "120s"), + ( + "adbc.snowflake.sql.client_option.keep_session_alive", + "enabled", + ), + ( + "adbc.snowflake.sql.client_option.disable_telemetry", + "enabled", + ), + ( + "adbc.snowflake.sql.client_option.cache_mfa_token", + "enabled", + ), + ( + "adbc.snowflake.sql.client_option.store_temp_creds", + "enabled", + ), + ("adbc.snowflake.sql.client_option.tracing", "debug"), + ( + "adbc.snowflake.sql.client_option.identity_provider", + "azure", + ), + ]; + + for (key, val) in cases { + db.set_option( + OptionDatabase::Other((*key).into()), + OptionValue::String((*val).into()), + ) + .unwrap_or_else(|e| panic!("set_option({key}) failed: {e}")); + + let got = db + .get_option_string(OptionDatabase::Other((*key).into())) + .unwrap_or_else(|e| panic!("get_option_string({key}) failed: {e}")); + + assert_eq!(got, *val, "round-trip mismatch for {key}"); + } +} + +/// Verify tls_skip_verify round-trips and that the compound TLS flags are +/// readable back through their own option keys. +#[test] +fn test_tls_skip_verify_option() { + let Some(mut db) = make_db() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + // enabled → skip verification + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.tls_skip_verify".into()), + OptionValue::String("enabled".into()), + ) + .expect("set tls_skip_verify enabled"); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.tls_skip_verify".into() + )) + .unwrap(), + "enabled" + ); + + // disabled → restore verification + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.tls_skip_verify".into()), + OptionValue::String("disabled".into()), + ) + .expect("set tls_skip_verify disabled"); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.tls_skip_verify".into() + )) + .unwrap(), + "disabled" + ); +} + +/// Verify ocsp_fail_open_mode round-trips correctly. +#[test] +fn test_ocsp_fail_open_mode_option() { + let Some(mut db) = make_db() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into()), + OptionValue::String("enabled".into()), + ) + .expect("set ocsp_fail_open_mode enabled"); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into() + )) + .unwrap(), + "enabled" + ); + + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into()), + OptionValue::String("disabled".into()), + ) + .expect("set ocsp_fail_open_mode disabled"); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into() + )) + .unwrap(), + "disabled" + ); +} + +#[test] +fn test_execute_schema() { + use arrow_schema::{DataType, TimeUnit}; + + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + // Schema from a plain SELECT + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("SELECT 1::INTEGER AS n, 'hello'::VARCHAR AS s") + .unwrap(); + let schema = stmt.execute_schema().expect("execute_schema"); + assert_eq!(schema.fields().len(), 2); + } + + // Schema from a table select matches the table definition + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query( + "CREATE OR REPLACE TEMP TABLE adbc_rust_schema_test \ + (id NUMBER(10,0), val VARCHAR, ts TIMESTAMP_NTZ)", + ) + .unwrap(); + stmt.execute_update().expect("create table"); + + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("SELECT * FROM ADBC_RUST_SCHEMA_TEST") + .unwrap(); + let schema = stmt.execute_schema().expect("execute_schema on table"); + assert_eq!(schema.fields().len(), 3); + assert_eq!(schema.field(0).name(), "ID"); + assert_eq!(schema.field(1).name(), "VAL"); + assert_eq!(schema.field(2).name(), "TS"); + assert_eq!(schema.field(0).data_type(), &DataType::Int64); + assert_eq!(schema.field(1).data_type(), &DataType::Utf8); + assert_eq!( + schema.field(2).data_type(), + &DataType::Timestamp(TimeUnit::Nanosecond, None) + ); + } +} + +#[test] +fn test_float64_select_precision() { + let Some(mut conn) = make_connection() else { + return; + }; + let mut stmt = conn.new_statement().expect("new_statement"); + + stmt.set_sql_query( + "SELECT 3.14159265358979::FLOAT as pi, \ + 0.0::FLOAT as zero, \ + -1.7976931348623157e308::FLOAT as neg_max, \ + 1.7976931348623157e308::FLOAT as pos_max, \ + 2.2250738585072014e-308::FLOAT as min_pos, \ + NULL::FLOAT as null_val", + ) + .expect("set_sql_query"); + + let mut reader = stmt.execute().expect("execute"); + let batch = reader.next().expect("batch").expect("ok"); + + let check = |col_idx: usize, name: &str, expected_bits: u64| { + let arr = batch + .column(col_idx) + .as_primitive::(); + let v = arr.value(0); + assert_eq!( + v.to_bits(), + expected_bits, + "{name}: got {v} (bits={:#018x}), expected bits={:#018x}", + v.to_bits(), + expected_bits + ); + }; + + check(0, "pi", 0x400921fb54442d11); + check(1, "zero", 0x0000000000000000); + check(2, "neg_max", 0xffefffffffffffff); + check(3, "pos_max", 0x7fefffffffffffff); + check(4, "min_pos", 0x0010000000000000); + + let null_arr = batch + .column(5) + .as_primitive::(); + assert!(null_arr.is_null(0), "null_val should be null"); +} diff --git a/rust/validation/README.md b/rust/validation/README.md new file mode 100644 index 0000000..252bfe2 --- /dev/null +++ b/rust/validation/README.md @@ -0,0 +1,49 @@ + + +# Validation Suite Setup + +The following must be set up. + +- Snowflake Account and credentials +- A database, stored in `SNOWFLAKE_DATABASE` +- A schema (dataset), stored in `SNOWFLAKE_SCHEMA` + +## Authentication + +You must provide Snowflake credentials by setting the following environment variables: + +```bash +export SNOWFLAKE_URI="snowflake://user:password@account.snowflakecomputing.com/database/schema?warehouse=warehouse" +export SNOWFLAKE_DATABASE="your_database" +export SNOWFLAKE_SCHEMA="your_schema" +``` + +Example: +```bash +export SNOWFLAKE_URI="snowflake://testuser:mypassword@myorg-account1/testdb/public?warehouse=compute_wh&role=myrole" +export SNOWFLAKE_DATABASE="testdb" +export SNOWFLAKE_SCHEMA="public" +``` + +## Running Tests + +Once configured, run the validation suite with: + +```bash +cd rust +pixi run validate +``` diff --git a/rust/validation/README_sf_core_limitations.md b/rust/validation/README_sf_core_limitations.md new file mode 100644 index 0000000..0c058a1 --- /dev/null +++ b/rust/validation/README_sf_core_limitations.md @@ -0,0 +1,68 @@ + + +# Snowflake and sf_core Limitations in the Rust ADBC Driver + +During the implementation and validation of the Rust ADBC Snowflake driver, we encountered several architectural limitations in both the Snowflake backend and the `sf_core` library. The driver currently employs workarounds for these issues. + +This document serves as a reference for future improvements that should ideally be made upstream in `sf_core` or Snowflake itself. + +## 1. Arrow IPC Format and `client_app_id` +**Issue:** Snowflake's backend determines the default `queryResultFormat` (Arrow IPC vs JSON rowset) based on the `CLIENT_APP_ID` field sent during the login request. Unrecognized client IDs fall back to JSON rowset. +**Impact:** JSON rowset format truncates `FLOAT`/`REAL` values to ~10 significant digits (losing IEEE 754 double precision) and converts `DBL_MAX` to `inf`. +**Workaround in Driver:** The driver currently omits overriding `client_app_id` (leaving it as `sf_core`'s default `"PythonConnector"`). This tricks Snowflake into recognizing the client and returning true Arrow IPC payloads with full-precision floats. +**Ideal Fix:** +- **Snowflake backend:** Honor `ALTER SESSION SET QUERY_RESULT_FORMAT = 'ARROW_FORCE'` uniformly, regardless of `CLIENT_APP_ID`. +- **sf_core:** Expose a native way to request Arrow format reliably without depending on a specific `CLIENT_APP_ID`, or ensure that custom client IDs can explicitly opt into Arrow format. + +## 2. Float Bind Parameter Precision +**Issue:** When binding `Float64` parameters, Snowflake relies on string representation if using standard substitution. The Go driver (`gosnowflake`) formats float64 bind parameters with `FormatFloat(..., 32)` (float32 precision, ~7 significant digits), permanently truncating `3.141592653589793` to `"3.1415927"`. +**Impact:** Test expectations written for the Go driver expect truncated bind values. +**Workaround in Driver:** The Rust driver uses `format!("{v:?}")` for `Float64` (preserving full precision). For `Float32`, it avoids casting to `f64` first to prevent precision expansion (e.g. `3.14159` → `3.141590118408203`), preserving exact float32 semantics. +**Ideal Fix:** +- **Go Driver / gosnowflake:** Stop truncating float64 bind parameters to 32-bit precision. +- **sf_core:** Implement native Arrow batch binding (via the Arrow IPC streaming endpoint) rather than relying on SQL string substitution for bind parameters. + +## 3. Timestamp Decoding (Epoch + Fraction) +**Issue:** `sf_core` decodes `TIMESTAMP_NTZ`/`TIMESTAMP_TZ` columns as a struct containing `epoch` (seconds, Int64) and `fraction` (nanoseconds, Int32/Int64). For pre-1970 timestamps (negative epochs), Snowflake uses floor division (e.g., `-9223372037` seconds + `145224192` nanoseconds), meaning the fraction is always positive. +**Impact:** Calculating total nanoseconds requires careful sign handling. If the fraction is subtracted when the epoch is negative (standard C-style truncation logic), the resulting date is off by ~1 second. +**Workaround in Driver:** The `ConvertingReader` must explicitly add the fraction regardless of the epoch's sign: `epoch * 1_000_000_000 + fraction * frac_to_ns`. +**Ideal Fix:** +- **sf_core:** When requested, natively map `TIMESTAMP_*` columns directly to Arrow `Timestamp` primitive arrays in the returned FFI stream, hiding the epoch/fraction struct complexity from the consumer. + +## 4. Scaled Integers (`FIXED` type) +**Issue:** `sf_core` returns `FIXED` (e.g., `NUMBER(10,3)`) columns as raw `Int64` buffers representing the scaled integer (e.g., `12345` for `12.345`). The Arrow FFI stream schema only reports `Int64` with custom metadata (`logicalType: "FIXED"`, `scale: "3"`). +**Impact:** The ADBC driver has to implement a complex `ConvertingReader` to interpret this metadata and manually cast `Int64` to `Float64` (dividing by `10^scale`) or `Decimal128`. +**Workaround in Driver:** We intercept the Arrow FFI stream, read the metadata, and perform a secondary pass using `arrow_cast::cast` + manual division (for `Float64`) or unchecked precision reinterpretation via `build_unchecked` (values that exceed `target_type`'s precision are not rejected) for `Decimal128`. +**Ideal Fix:** +- **sf_core:** Automatically apply the scale. If `sf_core` detects a `FIXED` type with `scale > 0`, it should export the FFI stream as `Decimal128` natively. Arrow already supports `Decimal128Type`, which perfectly represents Snowflake's scaled integers without needing consumer-side interpretation. + +## 5. FFI Error Path Memory Leak +**Issue:** If `ArrowArrayStreamReader::from_raw` fails on an FFI stream exported by `sf_core`, the Arrow C Data Interface specification states that the release callback is *not* called. +**Impact:** If the driver doesn't handle the error, the `Box::into_raw` pointer leaks. +**Workaround in Driver:** We catch the error in `map_err`, reconstruct the `Box` using `Box::from_raw`, and explicitly `drop` it. +**Ideal Fix:** +- **Arrow Crate:** Provide a safer abstraction for FFI stream consumption that guarantees cleanup on failure. + +## 6. Timezone Handling in Arrow IPC +**Issue:** Snowflake's Arrow IPC responses include named timezones like `"UTC"` in the metadata. The Rust `arrow` crate requires the `"chrono-tz"` feature to parse these named timezones; otherwise, decoding fails. +**Impact:** Consumers of the ADBC driver (like `pyarrow`) receive invalid timezone errors. +**Workaround in Driver:** The driver enables the `"chrono-tz"` feature on its `arrow-array` dependency. +**Ideal Fix:** +- **sf_core:** Normalize timezone strings to standard offsets (e.g., `"+00:00"`) before exporting the FFI stream, removing the requirement for consumers to bundle heavy timezone databases just to parse `"UTC"`. + +## Summary +The current Rust ADBC driver performs significant "last-mile" data conversion (scaled integers, timestamp reconstruction, schema adjustment). If `sf_core` were to export canonical Arrow types (`Decimal128`, `Timestamp(unit, tz)`) directly, the `ConvertingReader` in the ADBC driver could be completely eliminated, resulting in a zero-copy, highly performant passthrough. diff --git a/rust/validation/pytest.ini b/rust/validation/pytest.ini new file mode 100644 index 0000000..7ee1426 --- /dev/null +++ b/rust/validation/pytest.ini @@ -0,0 +1,21 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +[pytest] +junit_suite_name = validation +junit_duration_report = call +xfail_strict = true + +markers = + feature: test for a driver-specific feature diff --git a/rust/validation/queries/ingest/binary.txtcase b/rust/validation/queries/ingest/binary.txtcase new file mode 100644 index 0000000..d8f7d03 --- /dev/null +++ b/rust/validation/queries/ingest/binary.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "BINARY" diff --git a/rust/validation/queries/ingest/binary_view.toml b/rust/validation/queries/ingest/binary_view.toml new file mode 100644 index 0000000..9d1b344 --- /dev/null +++ b/rust/validation/queries/ingest/binary_view.toml @@ -0,0 +1,16 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +# Skip binary_view tests - not supported by Snowflake driver +skip = "BINARY_VIEW not supported by Snowflake driver" diff --git a/rust/validation/queries/ingest/decimal.input.json b/rust/validation/queries/ingest/decimal.input.json new file mode 100644 index 0000000..bd5930c --- /dev/null +++ b/rust/validation/queries/ingest/decimal.input.json @@ -0,0 +1,5 @@ +{"idx": 0, "value": 0.00} +{"idx": 1, "value": 123.45} +{"idx": 2, "value": -123.45} +{"idx": 3, "value": 9999999.99} +{"idx": 4, "value": -9999999.99} diff --git a/rust/validation/queries/ingest/decimal.input.schema.json b/rust/validation/queries/ingest/decimal.input.schema.json new file mode 100644 index 0000000..0b7a6ac --- /dev/null +++ b/rust/validation/queries/ingest/decimal.input.schema.json @@ -0,0 +1,15 @@ +{ + "format": "+s", + "children": [ + { + "name": "idx", + "format": "l", + "flags": ["nullable"] + }, + { + "name": "value", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/ingest/fixed_size_binary.txtcase b/rust/validation/queries/ingest/fixed_size_binary.txtcase new file mode 100644 index 0000000..4579f8d --- /dev/null +++ b/rust/validation/queries/ingest/fixed_size_binary.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "BINARY(n)" diff --git a/rust/validation/queries/ingest/float32.input.schema.json b/rust/validation/queries/ingest/float32.input.schema.json new file mode 100644 index 0000000..0b7a6ac --- /dev/null +++ b/rust/validation/queries/ingest/float32.input.schema.json @@ -0,0 +1,15 @@ +{ + "format": "+s", + "children": [ + { + "name": "idx", + "format": "l", + "flags": ["nullable"] + }, + { + "name": "value", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/ingest/int16.input.schema.json b/rust/validation/queries/ingest/int16.input.schema.json new file mode 100644 index 0000000..f11d2f8 --- /dev/null +++ b/rust/validation/queries/ingest/int16.input.schema.json @@ -0,0 +1,15 @@ +{ + "format": "+s", + "children": [ + { + "name": "idx", + "format": "l", + "flags": ["nullable"] + }, + { + "name": "value", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/ingest/int32.input.schema.json b/rust/validation/queries/ingest/int32.input.schema.json new file mode 100644 index 0000000..f11d2f8 --- /dev/null +++ b/rust/validation/queries/ingest/int32.input.schema.json @@ -0,0 +1,15 @@ +{ + "format": "+s", + "children": [ + { + "name": "idx", + "format": "l", + "flags": ["nullable"] + }, + { + "name": "value", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/ingest/large_binary.txtcase b/rust/validation/queries/ingest/large_binary.txtcase new file mode 100644 index 0000000..d8f7d03 --- /dev/null +++ b/rust/validation/queries/ingest/large_binary.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "BINARY" diff --git a/rust/validation/queries/ingest/large_string.txtcase b/rust/validation/queries/ingest/large_string.txtcase new file mode 100644 index 0000000..c290338 --- /dev/null +++ b/rust/validation/queries/ingest/large_string.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "STRING" diff --git a/rust/validation/queries/ingest/string.txtcase b/rust/validation/queries/ingest/string.txtcase new file mode 100644 index 0000000..c290338 --- /dev/null +++ b/rust/validation/queries/ingest/string.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "STRING" diff --git a/rust/validation/queries/ingest/string_view.toml b/rust/validation/queries/ingest/string_view.toml new file mode 100644 index 0000000..1d7693c --- /dev/null +++ b/rust/validation/queries/ingest/string_view.toml @@ -0,0 +1,16 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +# Skip string_view tests - not supported by Snowflake driver +skip = "string view not supported by Snowflake driver" diff --git a/rust/validation/queries/ingest/time_ms.txtcase b/rust/validation/queries/ingest/time_ms.txtcase new file mode 100644 index 0000000..eda3e18 --- /dev/null +++ b/rust/validation/queries/ingest/time_ms.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "TIME(3)" diff --git a/rust/validation/queries/ingest/time_ns.txtcase b/rust/validation/queries/ingest/time_ns.txtcase new file mode 100644 index 0000000..c66d02b --- /dev/null +++ b/rust/validation/queries/ingest/time_ns.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "TIME(9)" diff --git a/rust/validation/queries/ingest/time_s.txtcase b/rust/validation/queries/ingest/time_s.txtcase new file mode 100644 index 0000000..966300c --- /dev/null +++ b/rust/validation/queries/ingest/time_s.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "TIME(0)" diff --git a/rust/validation/queries/ingest/time_us.txtcase b/rust/validation/queries/ingest/time_us.txtcase new file mode 100644 index 0000000..bfcfbcf --- /dev/null +++ b/rust/validation/queries/ingest/time_us.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "TIME(6)" diff --git a/rust/validation/queries/ingest/timestamp_ms.txtcase b/rust/validation/queries/ingest/timestamp_ms.txtcase new file mode 100644 index 0000000..8df59ca --- /dev/null +++ b/rust/validation/queries/ingest/timestamp_ms.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_NTZ(3)" diff --git a/rust/validation/queries/ingest/timestamp_ns.txtcase b/rust/validation/queries/ingest/timestamp_ns.txtcase new file mode 100644 index 0000000..e86fe48 --- /dev/null +++ b/rust/validation/queries/ingest/timestamp_ns.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_NTZ(9)" diff --git a/rust/validation/queries/ingest/timestamp_s.txtcase b/rust/validation/queries/ingest/timestamp_s.txtcase new file mode 100644 index 0000000..df7ba91 --- /dev/null +++ b/rust/validation/queries/ingest/timestamp_s.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_NTZ(0)" diff --git a/rust/validation/queries/ingest/timestamp_us.txtcase b/rust/validation/queries/ingest/timestamp_us.txtcase new file mode 100644 index 0000000..2fb6cb0 --- /dev/null +++ b/rust/validation/queries/ingest/timestamp_us.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_NTZ(6)" diff --git a/rust/validation/queries/ingest/timestamptz_ms.txtcase b/rust/validation/queries/ingest/timestamptz_ms.txtcase new file mode 100644 index 0000000..aaa6b8a --- /dev/null +++ b/rust/validation/queries/ingest/timestamptz_ms.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_LTZ(3)" diff --git a/rust/validation/queries/ingest/timestamptz_ns.txtcase b/rust/validation/queries/ingest/timestamptz_ns.txtcase new file mode 100644 index 0000000..924df80 --- /dev/null +++ b/rust/validation/queries/ingest/timestamptz_ns.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_LTZ(9)" diff --git a/rust/validation/queries/ingest/timestamptz_s.txtcase b/rust/validation/queries/ingest/timestamptz_s.txtcase new file mode 100644 index 0000000..184631b --- /dev/null +++ b/rust/validation/queries/ingest/timestamptz_s.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_LTZ(0)" diff --git a/rust/validation/queries/ingest/timestamptz_us.txtcase b/rust/validation/queries/ingest/timestamptz_us.txtcase new file mode 100644 index 0000000..899313c --- /dev/null +++ b/rust/validation/queries/ingest/timestamptz_us.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_LTZ(6)" diff --git a/rust/validation/queries/type/bind/decimal.txtcase b/rust/validation/queries/type/bind/decimal.txtcase new file mode 100644 index 0000000..ee57a69 --- /dev/null +++ b/rust/validation/queries/type/bind/decimal.txtcase @@ -0,0 +1,45 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +sort-keys = [["res", "ascending"]] + +[setup] +drop = "test_decimal" + +[tags] +sql-type-name = "NUMERIC" +caveats = ["With use_high_precision=false, Snowflake returns NUMERIC as double"] + +// part: expected_schema + +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "g", + "flags": ["nullable"] + } + ] +} + +// part: expected + +{"res": null} +{"res": -999.99} +{"res": 0.0} +{"res": 123.45} +{"res": 9999999.99} diff --git a/rust/validation/queries/type/bind/float16.txtcase b/rust/validation/queries/type/bind/float16.txtcase new file mode 100644 index 0000000..61690fc --- /dev/null +++ b/rust/validation/queries/type/bind/float16.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata +skip = "float16 parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/float32.schema.json b/rust/validation/queries/type/bind/float32.schema.json new file mode 100644 index 0000000..48ee373 --- /dev/null +++ b/rust/validation/queries/type/bind/float32.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/bind/float64.bind.json b/rust/validation/queries/type/bind/float64.bind.json new file mode 100644 index 0000000..75775dd --- /dev/null +++ b/rust/validation/queries/type/bind/float64.bind.json @@ -0,0 +1,6 @@ +{"res": 3.141592653589793} +{"res": -2.5} +{"res": 0.0} +{"res": 1.797693e+38} +{"res": -1.797693e+38} +{"res": null} diff --git a/rust/validation/queries/type/bind/float64.json b/rust/validation/queries/type/bind/float64.json new file mode 100644 index 0000000..7d0f7f1 --- /dev/null +++ b/rust/validation/queries/type/bind/float64.json @@ -0,0 +1,6 @@ +{"res": null} +{"res": -1.797693e+38} +{"res": -2.5} +{"res": 0.0} +{"res": 3.141592653589793} +{"res": 1.797693e+38} diff --git a/rust/validation/queries/type/bind/int16.schema.json b/rust/validation/queries/type/bind/int16.schema.json new file mode 100644 index 0000000..89e94b6 --- /dev/null +++ b/rust/validation/queries/type/bind/int16.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/bind/int32.schema.json b/rust/validation/queries/type/bind/int32.schema.json new file mode 100644 index 0000000..89e94b6 --- /dev/null +++ b/rust/validation/queries/type/bind/int32.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/bind/time_ms.txtcase b/rust/validation/queries/type/bind/time_ms.txtcase new file mode 100644 index 0000000..e27121e --- /dev/null +++ b/rust/validation/queries/type/bind/time_ms.txtcase @@ -0,0 +1,33 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[setup] +drop = "test_time" + +[tags] +sql-type-name = "TIME" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_time" ("idx" INT, "res" TIME(3)); + +// part: bind_query + +INSERT INTO "test_time" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_time" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/time_ns.txtcase b/rust/validation/queries/type/bind/time_ns.txtcase new file mode 100644 index 0000000..7550272 --- /dev/null +++ b/rust/validation/queries/type/bind/time_ns.txtcase @@ -0,0 +1,33 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[setup] +drop = "test_time" + +[tags] +sql-type-name = "TIME" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_time" ("idx" INT, "res" TIME(9)); + +// part: bind_query + +INSERT INTO "test_time" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_time" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/time_s.txtcase b/rust/validation/queries/type/bind/time_s.txtcase new file mode 100644 index 0000000..198689e --- /dev/null +++ b/rust/validation/queries/type/bind/time_s.txtcase @@ -0,0 +1,33 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[setup] +drop = "test_time" + +[tags] +sql-type-name = "TIME" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_time" ("idx" INT, "res" TIME(0)); + +// part: bind_query + +INSERT INTO "test_time" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_time" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/time_us.txtcase b/rust/validation/queries/type/bind/time_us.txtcase new file mode 100644 index 0000000..3822942 --- /dev/null +++ b/rust/validation/queries/type/bind/time_us.txtcase @@ -0,0 +1,33 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[setup] +drop = "test_time" + +[tags] +sql-type-name = "TIME" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_time" ("idx" INT, "res" TIME(6)); + +// part: bind_query + +INSERT INTO "test_time" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_time" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamp_ms.txtcase b/rust/validation/queries/type/bind/timestamp_ms.txtcase new file mode 100644 index 0000000..a625b1a --- /dev/null +++ b/rust/validation/queries/type/bind/timestamp_ms.txtcase @@ -0,0 +1,33 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[setup] +drop = "test_timestamp" + +[tags] +sql-type-name = "TIMESTAMP_NTZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamp" ("idx" INT, "res" TIMESTAMP(3)); + +// part: bind_query + +INSERT INTO "test_timestamp" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_timestamp" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamp_ns.txtcase b/rust/validation/queries/type/bind/timestamp_ns.txtcase new file mode 100644 index 0000000..9fa9651 --- /dev/null +++ b/rust/validation/queries/type/bind/timestamp_ns.txtcase @@ -0,0 +1,33 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[setup] +drop = "test_timestamp" + +[tags] +sql-type-name = "TIMESTAMP_NTZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamp" ("idx" INT, "res" TIMESTAMP(9)); + +// part: bind_query + +INSERT INTO "test_timestamp" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_timestamp" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamp_s.txtcase b/rust/validation/queries/type/bind/timestamp_s.txtcase new file mode 100644 index 0000000..0e6a19a --- /dev/null +++ b/rust/validation/queries/type/bind/timestamp_s.txtcase @@ -0,0 +1,33 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[setup] +drop = "test_timestamp" + +[tags] +sql-type-name = "TIMESTAMP_NTZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamp" ("idx" INT, "res" TIMESTAMP(0)); + +// part: bind_query + +INSERT INTO "test_timestamp" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_timestamp" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamp_us.txtcase b/rust/validation/queries/type/bind/timestamp_us.txtcase new file mode 100644 index 0000000..fd1464e --- /dev/null +++ b/rust/validation/queries/type/bind/timestamp_us.txtcase @@ -0,0 +1,33 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[setup] +drop = "test_timestamp" + +[tags] +sql-type-name = "TIMESTAMP_NTZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamp" ("idx" INT, "res" TIMESTAMP(6)); + +// part: bind_query + +INSERT INTO "test_timestamp" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_timestamp" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamptz_ms.txtcase b/rust/validation/queries/type/bind/timestamptz_ms.txtcase new file mode 100644 index 0000000..eec6e24 --- /dev/null +++ b/rust/validation/queries/type/bind/timestamptz_ms.txtcase @@ -0,0 +1,33 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[setup] +drop = "test_timestamptz" + +[tags] +sql-type-name = "TIMESTAMP_TZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamptz" ("idx" INT, "res" TIMESTAMPLTZ(3), "res2" TIMESTAMPLTZ(3)); + +// part: bind_query + +INSERT INTO "test_timestamptz" VALUES ($1, $2, $3) + +// part: query + +SELECT "res", "res2" FROM "test_timestamptz" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamptz_ns.txtcase b/rust/validation/queries/type/bind/timestamptz_ns.txtcase new file mode 100644 index 0000000..2b0bc30 --- /dev/null +++ b/rust/validation/queries/type/bind/timestamptz_ns.txtcase @@ -0,0 +1,33 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[setup] +drop = "test_timestamptz" + +[tags] +sql-type-name = "TIMESTAMP_TZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamptz" ("idx" INT, "res" TIMESTAMPLTZ(9), "res2" TIMESTAMPLTZ(9)); + +// part: bind_query + +INSERT INTO "test_timestamptz" VALUES ($1, $2, $3) + +// part: query + +SELECT "res", "res2" FROM "test_timestamptz" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamptz_s.txtcase b/rust/validation/queries/type/bind/timestamptz_s.txtcase new file mode 100644 index 0000000..1da5e02 --- /dev/null +++ b/rust/validation/queries/type/bind/timestamptz_s.txtcase @@ -0,0 +1,33 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[setup] +drop = "test_timestamptz" + +[tags] +sql-type-name = "TIMESTAMP_TZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamptz" ("idx" INT, "res" TIMESTAMPLTZ(0), "res2" TIMESTAMPLTZ(0)); + +// part: bind_query + +INSERT INTO "test_timestamptz" VALUES ($1, $2, $3) + +// part: query + +SELECT "res", "res2" FROM "test_timestamptz" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamptz_us.txtcase b/rust/validation/queries/type/bind/timestamptz_us.txtcase new file mode 100644 index 0000000..ab5a447 --- /dev/null +++ b/rust/validation/queries/type/bind/timestamptz_us.txtcase @@ -0,0 +1,33 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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. + +// part: metadata + +[setup] +drop = "test_timestamptz" + +[tags] +sql-type-name = "TIMESTAMP_TZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamptz" ("idx" INT, "res" TIMESTAMPLTZ(6), "res2" TIMESTAMPLTZ(6)); + +// part: bind_query + +INSERT INTO "test_timestamptz" VALUES ($1, $2, $3) + +// part: query + +SELECT "res", "res2" FROM "test_timestamptz" ORDER BY "idx" diff --git a/rust/validation/queries/type/literal/binary.sql b/rust/validation/queries/type/literal/binary.sql new file mode 100644 index 0000000..7e3bae5 --- /dev/null +++ b/rust/validation/queries/type/literal/binary.sql @@ -0,0 +1 @@ +SELECT X'e38193e38293e381abe381a1e381afe38081e4b896e7958cefbc81' AS "res" diff --git a/rust/validation/queries/type/literal/decimal.json b/rust/validation/queries/type/literal/decimal.json new file mode 100644 index 0000000..5a01358 --- /dev/null +++ b/rust/validation/queries/type/literal/decimal.json @@ -0,0 +1 @@ +{"res": 123.45} diff --git a/rust/validation/queries/type/literal/decimal.schema.json b/rust/validation/queries/type/literal/decimal.schema.json new file mode 100644 index 0000000..48ee373 --- /dev/null +++ b/rust/validation/queries/type/literal/decimal.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/literal/float32.schema.json b/rust/validation/queries/type/literal/float32.schema.json new file mode 100644 index 0000000..48ee373 --- /dev/null +++ b/rust/validation/queries/type/literal/float32.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/literal/int16.schema.json b/rust/validation/queries/type/literal/int16.schema.json new file mode 100644 index 0000000..89e94b6 --- /dev/null +++ b/rust/validation/queries/type/literal/int16.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/literal/int32.schema.json b/rust/validation/queries/type/literal/int32.schema.json new file mode 100644 index 0000000..89e94b6 --- /dev/null +++ b/rust/validation/queries/type/literal/int32.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/literal/time.json b/rust/validation/queries/type/literal/time.json new file mode 100644 index 0000000..1833ba5 --- /dev/null +++ b/rust/validation/queries/type/literal/time.json @@ -0,0 +1 @@ +{"res": 49531123456000} diff --git a/rust/validation/queries/type/literal/time.schema.json b/rust/validation/queries/type/literal/time.schema.json new file mode 100644 index 0000000..8cd1a21 --- /dev/null +++ b/rust/validation/queries/type/literal/time.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "ttn", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/literal/timestamp.sql b/rust/validation/queries/type/literal/timestamp.sql new file mode 100644 index 0000000..5991af9 --- /dev/null +++ b/rust/validation/queries/type/literal/timestamp.sql @@ -0,0 +1 @@ +SELECT TIMESTAMP '2023-05-15 13:45:30'::TIMESTAMP(6) AS res diff --git a/rust/validation/queries/type/literal/timestamptz.sql b/rust/validation/queries/type/literal/timestamptz.sql new file mode 100644 index 0000000..70a8c70 --- /dev/null +++ b/rust/validation/queries/type/literal/timestamptz.sql @@ -0,0 +1 @@ +SELECT '2023-05-15 13:45:30+00:00'::TIMESTAMP_TZ(6) AS res diff --git a/rust/validation/queries/type/select/binary.setup.sql b/rust/validation/queries/type/select/binary.setup.sql new file mode 100644 index 0000000..ba89c9f --- /dev/null +++ b/rust/validation/queries/type/select/binary.setup.sql @@ -0,0 +1,10 @@ +CREATE TABLE test_binary ( + idx INTEGER, + res VARBINARY(1000) +); + +INSERT INTO test_binary (idx, res) VALUES (1, X'e38193e38293e381abe381a1e381afe38081e4b896e7958cefbc81'); -- 'こんにちは、世界!' in UTF-8 +INSERT INTO test_binary (idx, res) VALUES (2, X'00'); +INSERT INTO test_binary (idx, res) VALUES (3, X'deadbeef'); +INSERT INTO test_binary (idx, res) VALUES (4, X''); +INSERT INTO test_binary (idx, res) VALUES (5, NULL); diff --git a/rust/validation/queries/type/select/decimal.json b/rust/validation/queries/type/select/decimal.json new file mode 100644 index 0000000..371aae5 --- /dev/null +++ b/rust/validation/queries/type/select/decimal.json @@ -0,0 +1,5 @@ +{"res": 123.45} +{"res": 0.00} +{"res": -999.99} +{"res": 9999999.99} +{"res": null} diff --git a/rust/validation/queries/type/select/decimal.schema.json b/rust/validation/queries/type/select/decimal.schema.json new file mode 100644 index 0000000..48ee373 --- /dev/null +++ b/rust/validation/queries/type/select/decimal.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/select/float32.schema.json b/rust/validation/queries/type/select/float32.schema.json new file mode 100644 index 0000000..48ee373 --- /dev/null +++ b/rust/validation/queries/type/select/float32.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/select/int16.schema.json b/rust/validation/queries/type/select/int16.schema.json new file mode 100644 index 0000000..89e94b6 --- /dev/null +++ b/rust/validation/queries/type/select/int16.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/select/int32.schema.json b/rust/validation/queries/type/select/int32.schema.json new file mode 100644 index 0000000..89e94b6 --- /dev/null +++ b/rust/validation/queries/type/select/int32.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/select/string.setup.sql b/rust/validation/queries/type/select/string.setup.sql new file mode 100644 index 0000000..d096207 --- /dev/null +++ b/rust/validation/queries/type/select/string.setup.sql @@ -0,0 +1,10 @@ +CREATE TABLE test_string ( + idx INTEGER, + res VARCHAR(1000) +); + +INSERT INTO test_string ("idx", "res") VALUES (1, 'hello'); +INSERT INTO test_string ("idx", "res") VALUES (2, ''); +INSERT INTO test_string ("idx", "res") VALUES (3, 'Special chars: !@#$%^&*()_+{}|:"<>?~`-=[]\\;'',./'); +INSERT INTO test_string ("idx", "res") VALUES (4, 'Unicode: 你好, Привет, こんにちは, สวัสดี'); +INSERT INTO test_string ("idx", "res") VALUES (5, NULL); diff --git a/rust/validation/queries/type/select/time.json b/rust/validation/queries/type/select/time.json new file mode 100644 index 0000000..b881096 --- /dev/null +++ b/rust/validation/queries/type/select/time.json @@ -0,0 +1,5 @@ +{"res": 49531123456000} +{"res": 0} +{"res": 86399999999000} +{"res": 45045500000000} +{"res": null} diff --git a/rust/validation/queries/type/select/time.schema.json b/rust/validation/queries/type/select/time.schema.json new file mode 100644 index 0000000..8cd1a21 --- /dev/null +++ b/rust/validation/queries/type/select/time.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "ttn", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/select/timestamp.setup.sql b/rust/validation/queries/type/select/timestamp.setup.sql new file mode 100644 index 0000000..cc07255 --- /dev/null +++ b/rust/validation/queries/type/select/timestamp.setup.sql @@ -0,0 +1,10 @@ +CREATE TABLE test_timestamp ( + idx INTEGER, + res TIMESTAMP(6) +); + +INSERT INTO test_timestamp (idx, res) VALUES (1, TIMESTAMP '2023-05-15 13:45:30'); +INSERT INTO test_timestamp (idx, res) VALUES (2, TIMESTAMP '2000-01-01 00:00:00'); +INSERT INTO test_timestamp (idx, res) VALUES (3, TIMESTAMP '1969-07-20 20:17:40'); +INSERT INTO test_timestamp (idx, res) VALUES (4, TIMESTAMP '9999-12-31 23:59:59'); +INSERT INTO test_timestamp (idx, res) VALUES (5, NULL); diff --git a/rust/validation/queries/type/select/timestamptz.setup.sql b/rust/validation/queries/type/select/timestamptz.setup.sql new file mode 100644 index 0000000..4aa9412 --- /dev/null +++ b/rust/validation/queries/type/select/timestamptz.setup.sql @@ -0,0 +1,10 @@ +CREATE TABLE test_timestamptz ( + idx INTEGER, + res TIMESTAMP_TZ(6) +); + +INSERT INTO test_timestamptz (idx, res) VALUES (1, '2023-05-15 13:45:30+00:00'::TIMESTAMP_TZ); +INSERT INTO test_timestamptz (idx, res) VALUES (2, '2000-01-01 00:00:00+00:00'::TIMESTAMP_TZ); +INSERT INTO test_timestamptz (idx, res) VALUES (3, '1969-07-20 20:17:40+00:00'::TIMESTAMP_TZ); +INSERT INTO test_timestamptz (idx, res) VALUES (4, '9999-12-31 23:59:59+00:00'::TIMESTAMP_TZ); +INSERT INTO test_timestamptz (idx, res) VALUES (5, NULL); diff --git a/rust/validation/tests/.gitignore b/rust/validation/tests/.gitignore new file mode 100644 index 0000000..0b1d246 --- /dev/null +++ b/rust/validation/tests/.gitignore @@ -0,0 +1,13 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. diff --git a/rust/validation/tests/__init__.py b/rust/validation/tests/__init__.py new file mode 100644 index 0000000..2940c8a --- /dev/null +++ b/rust/validation/tests/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +"""Snowflake ADBC driver validation tests.""" diff --git a/rust/validation/tests/conftest.py b/rust/validation/tests/conftest.py new file mode 100644 index 0000000..03bd06c --- /dev/null +++ b/rust/validation/tests/conftest.py @@ -0,0 +1,52 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +import sys +from pathlib import Path + +import adbc_drivers_validation.model +import adbc_drivers_validation.tests.conftest +import pytest +from adbc_drivers_validation.tests.conftest import ( # noqa: F401 + conn, + conn_factory, + manual_test, + pytest_collection_modifyitems, +) + +from . import snowflake + + +def pytest_addoption(parser): + adbc_drivers_validation.tests.conftest.pytest_addoption(parser) + parser.addoption("--vendor-version", action="store", default="10") + + +@pytest.fixture(scope="session") +def driver(request, pytestconfig) -> adbc_drivers_validation.model.DriverQuirks: + driver = request.param + assert driver.startswith("snowflake") + return snowflake.get_quirks(pytestconfig.getoption("vendor_version")) + + +@pytest.fixture(scope="session") +def driver_path(driver: adbc_drivers_validation.model.DriverQuirks) -> str: + ext = { + "win32": "dll", + "darwin": "dylib", + }.get(sys.platform, "so") + return str( + Path(__file__).parent.parent.parent + / f"build/libadbc_driver_{driver.name}.{ext}" + ) diff --git a/rust/validation/tests/generate_documentation.py b/rust/validation/tests/generate_documentation.py new file mode 100644 index 0000000..b083b41 --- /dev/null +++ b/rust/validation/tests/generate_documentation.py @@ -0,0 +1,36 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +import argparse +from pathlib import Path + +import adbc_drivers_validation.generate_documentation as generate_documentation + +from . import snowflake + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + + template = Path(__file__).parent.parent.parent / "docs/snowflake.md" + template = template.resolve() + + reports = [report.resolve() for report in Path(".").glob("validation-report*.xml")] + generate_documentation.generate( + snowflake.get_quirks, + reports, + template, + args.output.resolve(), + ) diff --git a/rust/validation/tests/snowflake.py b/rust/validation/tests/snowflake.py new file mode 100644 index 0000000..fb1d929 --- /dev/null +++ b/rust/validation/tests/snowflake.py @@ -0,0 +1,100 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +import functools +import re +from pathlib import Path + +from adbc_drivers_validation import model, quirks + + +class SnowflakeQuirks(model.DriverQuirks): + name = "snowflake" + driver = "adbc_driver_snowflake" + driver_name = "ADBC Snowflake Driver (Rust)" + vendor_name = "Snowflake" + vendor_version = re.compile(r"\d+\.[0-9]+\.[0-9]+") + short_version = "10" + features = model.DriverFeatures( + connection_get_table_schema=True, + connection_set_current_catalog=True, + connection_set_current_schema=True, + connection_transactions=True, + get_objects=True, + get_objects_constraints_foreign=False, + get_objects_constraints_primary=False, + get_objects_constraints_unique=False, + statement_bind=True, + statement_bulk_ingest=True, + statement_bulk_ingest_catalog=True, + statement_bulk_ingest_schema=True, + statement_bulk_ingest_temporary=False, + statement_execute_schema=True, + statement_get_parameter_schema=False, + statement_prepare=True, + statement_rows_affected=True, + statement_rows_affected_ddl=False, + current_catalog=model.FromEnv("SNOWFLAKE_DATABASE"), + current_schema=model.FromEnv("SNOWFLAKE_SCHEMA"), + secondary_schema=model.FromEnv("SNOWFLAKE_SCHEMA_SECONDARY"), + secondary_catalog=model.FromEnv("SNOWFLAKE_DATABASE_SECONDARY"), + secondary_catalog_schema=model.FromEnv("SNOWFLAKE_DATABASE_SECONDARY_SCHEMA"), + supported_xdbc_fields=[], + ) + setup = model.DriverSetup( + database={ + "uri": model.FromEnv("SNOWFLAKE_URI"), + "adbc.snowflake.sql.db": model.FromEnv("SNOWFLAKE_DATABASE"), + "adbc.snowflake.sql.schema": model.FromEnv("SNOWFLAKE_SCHEMA"), + "adbc.snowflake.sql.client_option.use_high_precision": "false", + "timezone": "UTC", + }, + connection={}, + statement={}, + ) + + @property + def queries_paths(self) -> tuple[Path]: + return (Path(__file__).parent.parent / "queries",) + + def is_table_not_found(self, table_name: str | None, error: Exception) -> bool: + error_msg = str(error).lower() + + # Snowflake returns "Object does not exist, or operation cannot be performed." + # Error codes 002043 or 002003 for table/object not found errors + # Snowflake doesn't include the table name in the error message + return ( + "002043" in error_msg + or "002003" in error_msg + or "object does not exist" in error_msg + or "does not exist or not authorized" in error_msg + ) + + def quote_one_identifier(self, identifier: str) -> str: + identifier = identifier.replace('"', '""') + return f'"{identifier}"' + + def bind_parameter(self, index: int) -> str: + return f"${index}" + + def split_statement(self, statement: str) -> list[str]: + return quirks.split_statement(statement, dialect=self.name) + + +@functools.cache +def get_quirks(version: str) -> SnowflakeQuirks: + quirks = SnowflakeQuirks() + if version != quirks.short_version: + raise ValueError(f"Unsupported Snowflake version: {version}") + return quirks diff --git a/rust/validation/tests/test_connection.py b/rust/validation/tests/test_connection.py new file mode 100644 index 0000000..1a66e50 --- /dev/null +++ b/rust/validation/tests/test_connection.py @@ -0,0 +1,25 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +from adbc_drivers_validation.tests.connection import ( + TestConnection, # noqa: F401 + generate_tests, +) + +from . import snowflake + + +def pytest_generate_tests(metafunc) -> None: + quirks = [snowflake.get_quirks(metafunc.config.getoption("vendor_version"))] + return generate_tests(quirks, metafunc) diff --git a/rust/validation/tests/test_ingest.py b/rust/validation/tests/test_ingest.py new file mode 100644 index 0000000..f57427e --- /dev/null +++ b/rust/validation/tests/test_ingest.py @@ -0,0 +1,27 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + + +import adbc_drivers_validation.tests.ingest as ingest_tests + +from . import snowflake + + +def pytest_generate_tests(metafunc) -> None: + quirks = [snowflake.get_quirks(metafunc.config.getoption("vendor_version"))] + return ingest_tests.generate_tests(quirks, metafunc) + + +class TestIngest(ingest_tests.TestIngest): + pass diff --git a/rust/validation/tests/test_query.py b/rust/validation/tests/test_query.py new file mode 100644 index 0000000..3352e0e --- /dev/null +++ b/rust/validation/tests/test_query.py @@ -0,0 +1,113 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + + +import functools +import re +from pathlib import Path + +import adbc_drivers_validation.model as model +import adbc_drivers_validation.tests.query as query_tests + +from . import snowflake + +# Store the original query function +_original_query = model.query + + +@functools.cache +def _snowflake_query(path: Path) -> str: + """ + Snowflake-specific query function that quotes table names and column names in SQL. + + This is required because snowflake turns the table and column names uppercase if not quoted. + """ + sql = _original_query(path) + + # Pattern to match table names like test_boolean, test_int32, etc. + table_pattern = r"\b(test_\w+)\b" + + def quote_identifier(match): + identifier = match.group(1) + return f'"{identifier}"' + + # Replace unquoted table references with quoted ones + quoted_sql = re.sub(table_pattern, quote_identifier, sql) + + # Quote column names in CREATE TABLE statements + # Pattern to find CREATE TABLE (...) and quote unquoted column names inside + def quote_create_table_columns(match): + before_paren = match.group(1) # "CREATE TABLE table_name (" + columns_part = match.group(2) # column definitions + after_paren = match.group(3) # ");" + + # Quote common column names like idx, res, etc. + column_names = r"\b(idx|res|value|name)\b(?=\s)" + quoted_columns = re.sub(column_names, lambda m: f'"{m.group(1)}"', columns_part) + + return before_paren + quoted_columns + after_paren + + # Apply column quoting to CREATE TABLE statements + create_table_pattern = r"(CREATE\s+TABLE\s+[^(]+\s*\()(.*?)(\);?)" + quoted_sql = re.sub( + create_table_pattern, + quote_create_table_columns, + quoted_sql, + flags=re.DOTALL | re.IGNORECASE, + ) + + # Quote column names in SELECT statements + column_pattern = r"\b(res\w*|idx\w*)\b(?=\s|$|,)" + quoted_sql = re.sub(column_pattern, quote_identifier, quoted_sql) + + # Quote column names in INSERT statements + # Pattern to match INSERT INTO table_name (columns) VALUES + def quote_insert_columns(match): + before_cols = match.group(1) # "INSERT INTO table_name (" + columns_list = match.group(2) # column list + after_cols = match.group(3) # ") VALUES" + + # Split by comma and quote each unquoted column name + columns = [col.strip() for col in columns_list.split(",")] + quoted_columns = [] + + for col in columns: + # If already quoted, keep as is; otherwise quote it + if col.startswith('"') and col.endswith('"'): + quoted_columns.append(col) + else: + quoted_columns.append(f'"{col}"') + + return before_cols + ", ".join(quoted_columns) + after_cols + + # Apply column quoting to INSERT statements + insert_pattern = r"(INSERT\s+INTO\s+[^(]+\s*\()([^)]+)(\)\s+VALUES)" + quoted_sql = re.sub( + insert_pattern, quote_insert_columns, quoted_sql, flags=re.IGNORECASE + ) + + return quoted_sql + + +# Monkey patch the query function for Snowflake tests +model.query = _snowflake_query + + +def pytest_generate_tests(metafunc) -> None: + quirks = [snowflake.get_quirks(metafunc.config.getoption("vendor_version"))] + return query_tests.generate_tests(quirks, metafunc) + + +class TestQuery(query_tests.TestQuery): + pass diff --git a/rust/validation/tests/test_statement.py b/rust/validation/tests/test_statement.py new file mode 100644 index 0000000..ad2a160 --- /dev/null +++ b/rust/validation/tests/test_statement.py @@ -0,0 +1,27 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + + +import adbc_drivers_validation.tests.statement as statement_tests + +from . import snowflake + + +def pytest_generate_tests(metafunc) -> None: + quirks = [snowflake.get_quirks(metafunc.config.getoption("vendor_version"))] + return statement_tests.generate_tests(quirks, metafunc) + + +class TestStatement(statement_tests.TestStatement): + pass