diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index 3e0f65f..3b66722 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -40,18 +40,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: cache-dependency-path: go/go.sum check-latest: true go-version-file: go/go.mod - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.x" @@ -59,7 +59,7 @@ jobs: run: pip install pre-commit - name: pre-commit (cache) - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/pre-commit key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} @@ -69,6 +69,6 @@ jobs: pre-commit run --all-files --color=always --show-diff-on-failure --verbose - name: Analyze workflows with zizmor - uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 with: advanced-security: false diff --git a/.github/workflows/dev_pr.yaml b/.github/workflows/dev_pr.yaml index 5c8910b..4b73abc 100644 --- a/.github/workflows/dev_pr.yaml +++ b/.github/workflows/dev_pr.yaml @@ -44,7 +44,7 @@ jobs: runs-on: ubuntu-slim steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 persist-credentials: false @@ -54,6 +54,6 @@ jobs: PR_TITLE: ${{ github.event.pull_request.title }} run: | git clone --depth 1 https://github.com/adbc-drivers/dev - python dev/adbc_drivers_dev/title_check.py $(pwd) "$PR_TITLE" + python dev/adbc_drivers_dev/title_check.py "$(pwd)" "$PR_TITLE" # TODO: assign milestone diff --git a/.github/workflows/generate.toml b/.github/workflows/generate.toml index 3228c57..cf1d5d2 100644 --- a/.github/workflows/generate.toml +++ b/.github/workflows/generate.toml @@ -13,21 +13,29 @@ # limitations under the License. driver = "snowflake" +environment = "Snowflake CI" private = false +[lang.rust] + [lang.go.build] additional-make-args = ["BUILD_TAGS=minicore_disabled"] [[lang.go.validation.configs]] -environment = "Snowflake CI" [secrets] SNOWFLAKE_DATABASE = { secret = "SNOWFLAKE_DATABASE", contexts = ["validate"] } SNOWFLAKE_SCHEMA = { secret = "SNOWFLAKE_SCHEMA", contexts = ["validate"] } SNOWFLAKE_URI = { secret = "SNOWFLAKE_URI", contexts = ["validate"] } -SNOWFLAKE_SCHEMA_SECONDARY = { secret = "SNOWFLAKE_SCHEMA_SECONDARY", contexts = ["validate"] } -SNOWFLAKE_DATABASE_SECONDARY = { secret = "SNOWFLAKE_DATABASE_SECONDARY", contexts = ["validate"] } -SNOWFLAKE_DATABASE_SECONDARY_SCHEMA = { secret = "SNOWFLAKE_DATABASE_SECONDARY_SCHEMA", contexts = ["validate"] } +SNOWFLAKE_SCHEMA_SECONDARY = { secret = "SNOWFLAKE_SCHEMA_SECONDARY", contexts = [ + "validate", +] } +SNOWFLAKE_DATABASE_SECONDARY = { secret = "SNOWFLAKE_DATABASE_SECONDARY", contexts = [ + "validate", +] } +SNOWFLAKE_DATABASE_SECONDARY_SCHEMA = { secret = "SNOWFLAKE_DATABASE_SECONDARY_SCHEMA", contexts = [ + "validate", +] } [validation] [validation.extra-dependencies] diff --git a/.github/workflows/go_release.yaml b/.github/workflows/go_release.yaml index fa1b430..9eb0011 100644 --- a/.github/workflows/go_release.yaml +++ b/.github/workflows/go_release.yaml @@ -63,25 +63,24 @@ jobs: # https://github.com/actions/runner-images/issues/2840 sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: cache: false cache-dependency-path: go/go.sum check-latest: true go-version-file: go/go.mod - - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 + - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 with: pixi-version: v0.72.0 run-install: false - - name: Build working-directory: go run: | @@ -153,13 +152,27 @@ jobs: validate: name: "Validate snowflake ${{ matrix.vendor_version }}/${{ matrix.platform }}_${{ matrix.arch }}" runs-on: ${{ matrix.runner }} + # Go and Rust validation use fixed table names in the same live schema. + concurrency: + group: snowflake-live-validation + cancel-in-progress: false strategy: fail-fast: true max-parallel: 4 matrix: include: # I think we only need to test one platform, but we can change that later - - { platform: linux, arch: amd64, runner: ubuntu-latest, service_name: "test-service", vendor_version: "latest", environment: "Snowflake CI", aws: false, azure: false, gcloud: false } + - { + platform: linux, + arch: amd64, + runner: ubuntu-latest, + service_name: "test-service", + vendor_version: "latest", + environment: "Snowflake CI", + aws: false, + azure: false, + gcloud: false, + } environment: ${{ matrix.environment }} permissions: contents: read @@ -173,25 +186,24 @@ jobs: # https://github.com/actions/runner-images/issues/2840 sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: cache: false cache-dependency-path: go/go.sum check-latest: true go-version-file: go/go.mod - - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 + - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 with: pixi-version: v0.72.0 run-install: false - - name: Log in to ghcr.io if: runner.os == 'Linux' env: @@ -214,7 +226,7 @@ jobs: source .env.test fi set +a - pixi run adbc-make build DEBUG=true VERBOSE=true BUILD_TAGS=minicore_disabled + pixi run adbc-make build DEBUG=true VERBOSE=true DRIVER=snowflake IMPL_LANG=go 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 @@ -274,8 +286,7 @@ jobs: cp validation-report.xml validation-report-${{ matrix.vendor_version }}.xml - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: validation-report-${{ matrix.vendor_version }} path: "go/validation-report-${{ matrix.vendor_version }}.xml" @@ -286,7 +297,7 @@ jobs: run: | pixi run gendocs --output generated - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs-${{ matrix.vendor_version }} path: "go/generated/snowflake.md" @@ -309,20 +320,20 @@ jobs: packages: read steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: cache: false cache-dependency-path: go/go.sum check-latest: true go-version-file: go/go.mod - - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 + - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 with: pixi-version: v0.72.0 run-install: false @@ -365,12 +376,12 @@ jobs: source .env.release fi set +a - pixi run adbc-make check CI=true VERBOSE=true BUILD_TAGS=minicore_disabled RELEASE=true + pixi run adbc-make check CI=true VERBOSE=true DRIVER=snowflake IMPL_LANG=go BUILD_TAGS=minicore_disabled RELEASE=true if [[ -f ci/scripts/post-build.sh ]]; then ./ci/scripts/post-build.sh release ${{ matrix.platform }} ${{ matrix.arch }} fi - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: drivers-${{ matrix.platform }}-${{ matrix.arch }} path: "go/build/libadbc_driver_snowflake.*" @@ -386,24 +397,24 @@ jobs: contents: read steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: cache: false check-latest: true go-version: "stable" - - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 + - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 with: pixi-version: v0.72.0 run-install: false - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: "drivers-*" path: "~/drivers" @@ -432,13 +443,13 @@ jobs: ls ~/packages - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: all-packages path: ~/packages retention-days: 7 - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: "validation-report-*" path: "~/validation-report" @@ -452,7 +463,7 @@ jobs: ls *.xml pixi run gendocs --output generated - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs path: "go/generated/snowflake.md" @@ -472,15 +483,15 @@ jobs: - { platform: windows, arch: amd64, runner: windows-latest } steps: # for now, install dbc from main - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: cache: false - go-version: 'stable' - - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 + go-version: "stable" + - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 with: pixi-version: v0.72.0 run-install: false - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: "all-packages" path: "~/packages" @@ -497,11 +508,11 @@ jobs: echo "Installing ${driver_pkg}" ./dbc install --no-verify "${driver_pkg}" echo "Installed ${driver_pkg}" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - name: load package working-directory: go run: | @@ -531,23 +542,23 @@ jobs: contents: write steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 + - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 with: pixi-version: v0.72.0 run-install: false - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: "all-packages" path: "~/packages" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: "docs" path: "~/packages" diff --git a/.github/workflows/go_test.yaml b/.github/workflows/go_test.yaml index 9c58a25..4e852e8 100644 --- a/.github/workflows/go_test.yaml +++ b/.github/workflows/go_test.yaml @@ -94,36 +94,35 @@ jobs: # https://github.com/actions/runner-images/issues/2840 sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name != 'workflow_dispatch' with: fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - name: "checkout remote" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name == 'workflow_dispatch' with: repository: ${{ inputs.repository }} ref: ${{ inputs.ref }} fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: cache: true cache-dependency-path: go/go.sum check-latest: true go-version-file: go/go.mod - - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 + - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 with: pixi-version: v0.72.0 run-install: false - - name: Build working-directory: go run: | @@ -195,13 +194,27 @@ jobs: validate: name: "Validate snowflake ${{ matrix.vendor_version }}/${{ matrix.platform }}_${{ matrix.arch }}" runs-on: ${{ matrix.runner }} + # Go and Rust validation use fixed table names in the same live schema. + concurrency: + group: snowflake-live-validation + cancel-in-progress: false strategy: fail-fast: true max-parallel: 4 matrix: include: # I think we only need to test one platform, but we can change that later - - { platform: linux, arch: amd64, runner: ubuntu-latest, service_name: "test-service", vendor_version: "latest", environment: "Snowflake CI", aws: false, azure: false, gcloud: false } + - { + platform: linux, + arch: amd64, + runner: ubuntu-latest, + service_name: "test-service", + vendor_version: "latest", + environment: "Snowflake CI", + aws: false, + azure: false, + gcloud: false, + } environment: ${{ matrix.environment }} permissions: contents: read @@ -215,36 +228,35 @@ jobs: # https://github.com/actions/runner-images/issues/2840 sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name != 'workflow_dispatch' with: fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - name: "checkout remote" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name == 'workflow_dispatch' with: repository: ${{ inputs.repository }} ref: ${{ inputs.ref }} fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: cache: true cache-dependency-path: go/go.sum check-latest: true go-version-file: go/go.mod - - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 + - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 with: pixi-version: v0.72.0 run-install: false - - name: Log in to ghcr.io if: runner.os == 'Linux' env: @@ -267,7 +279,7 @@ jobs: source .env.test fi set +a - pixi run adbc-make build DEBUG=true VERBOSE=true BUILD_TAGS=minicore_disabled + pixi run adbc-make build DEBUG=true VERBOSE=true DRIVER=snowflake IMPL_LANG=go 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 @@ -327,8 +339,7 @@ jobs: cp validation-report.xml validation-report-${{ matrix.vendor_version }}.xml - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: validation-report-${{ matrix.vendor_version }} path: "go/validation-report-${{ matrix.vendor_version }}.xml" @@ -339,7 +350,7 @@ jobs: run: | pixi run gendocs --output generated - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs-${{ matrix.vendor_version }} path: "go/generated/snowflake.md" @@ -362,31 +373,31 @@ jobs: packages: read steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name != 'workflow_dispatch' with: fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - name: "checkout remote" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name == 'workflow_dispatch' with: repository: ${{ inputs.repository }} ref: ${{ inputs.ref }} fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: cache: true cache-dependency-path: go/go.sum check-latest: true go-version-file: go/go.mod - - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 + - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 with: pixi-version: v0.72.0 run-install: false @@ -429,12 +440,12 @@ jobs: source .env.release fi set +a - pixi run adbc-make check CI=true VERBOSE=true BUILD_TAGS=minicore_disabled + pixi run adbc-make check CI=true VERBOSE=true DRIVER=snowflake IMPL_LANG=go BUILD_TAGS=minicore_disabled if [[ -f ci/scripts/post-build.sh ]]; then ./ci/scripts/post-build.sh release ${{ matrix.platform }} ${{ matrix.arch }} fi - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: drivers-${{ matrix.platform }}-${{ matrix.arch }} path: "go/build/libadbc_driver_snowflake.*" @@ -450,35 +461,35 @@ jobs: contents: read steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name != 'workflow_dispatch' with: fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - name: "checkout remote" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name == 'workflow_dispatch' with: repository: ${{ inputs.repository }} ref: ${{ inputs.ref }} fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: cache: true check-latest: true go-version: "stable" - - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 + - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 with: pixi-version: v0.72.0 run-install: false - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: "drivers-*" path: "~/drivers" @@ -507,13 +518,13 @@ jobs: ls ~/packages - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: all-packages path: ~/packages retention-days: 7 - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: "validation-report-*" path: "~/validation-report" @@ -527,7 +538,7 @@ jobs: ls *.xml pixi run gendocs --output generated - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs path: "go/generated/snowflake.md" @@ -547,15 +558,15 @@ jobs: - { platform: windows, arch: amd64, runner: windows-latest } steps: # for now, install dbc from main - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: cache: true - go-version: 'stable' - - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 + go-version: "stable" + - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 with: pixi-version: v0.72.0 run-install: false - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: "all-packages" path: "~/packages" @@ -572,21 +583,21 @@ jobs: echo "Installing ${driver_pkg}" ./dbc install --no-verify "${driver_pkg}" echo "Installed ${driver_pkg}" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name != 'workflow_dispatch' with: fetch-depth: 1 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - name: "checkout remote" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name == 'workflow_dispatch' with: repository: ${{ inputs.repository }} ref: ${{ inputs.ref }} fetch-depth: 1 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - name: load package working-directory: go run: | @@ -616,34 +627,34 @@ jobs: contents: read steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name != 'workflow_dispatch' with: fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - name: "checkout remote" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name == 'workflow_dispatch' with: repository: ${{ inputs.repository }} ref: ${{ inputs.ref }} fetch-depth: 0 persist-credentials: false - submodules: 'recursive' + submodules: "recursive" - - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 + - uses: prefix-dev/setup-pixi@f00437f565399d418b0acc85936d12c1fb668347 # v0.10.1 with: pixi-version: v0.72.0 run-install: false - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: "all-packages" path: "~/packages" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: "docs" path: "~/packages" diff --git a/.github/workflows/go_test_pr.yaml b/.github/workflows/go_test_pr.yaml index f0dbc76..76d1881 100644 --- a/.github/workflows/go_test_pr.yaml +++ b/.github/workflows/go_test_pr.yaml @@ -111,7 +111,7 @@ jobs: echo message=":x: **Test failed:** " | tee -a $GITHUB_OUTPUT fi - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUTS: ${{ toJSON(inputs) }} MESSAGE: ${{ steps.get_run.outputs.message }} diff --git a/.github/workflows/rust_test.yaml b/.github/workflows/rust_test.yaml new file mode 100644 index 0000000..57253c3 --- /dev/null +++ b/.github/workflows/rust_test.yaml @@ -0,0 +1,617 @@ +# 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 + +permissions: + contents: read + +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@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + 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 }} + # Go and Rust validation use fixed table names in the same live schema. + concurrency: + group: snowflake-live-validation + cancel-in-progress: false + 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@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + 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@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + 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@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + 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" \ + -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" + find ~/packages -type f \ + \( -name '*.tar.gz' -o -name 'manifest.yaml' -o -name '*.md' \) \ + -print0 | xargs -0 echo gh release upload "$tag" diff --git a/.gitignore b/.gitignore index a69b167..bc101c0 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,10 @@ go.work.sum # Editor/IDE # .idea/ -# .vscode/ +.vscode/ + +# Local agent runtime state +.pi/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9acbb00..c74a9a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,60 +13,62 @@ # limitations under the License. repos: -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: 0671d8ab202c4ac093b78433ae5baf74f3fc7246 # frozen: v0.15.15 - hooks: - - id: ruff-check - name: "ruff check (isort)" - args: ["--select", "I", "--fix"] - - id: ruff-check - args: ["--fix"] - - id: ruff-format + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 0671d8ab202c4ac093b78433ae5baf74f3fc7246 # frozen: v0.15.15 + hooks: + - id: ruff-check + name: "ruff check (isort)" + args: ["--select", "I", "--fix"] + - id: ruff-check + args: ["--fix"] + - id: ruff-format -- repo: https://github.com/codespell-project/codespell - rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2 - hooks: - - id: codespell - exclude: > - (?x)^( - .*go\.sum - )$ + - repo: https://github.com/codespell-project/codespell + rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2 + hooks: + - id: codespell + exclude: > + (?x)^( + .*go\.sum + )$ -- repo: https://github.com/golangci/golangci-lint - # XXX: don't update this as it seems newer versions break nolint directives - rev: 5d1e709b7be35cb2025444e19de266b056b7b7ee # frozen: v2.10.1 - hooks: - - id: golangci-lint - name: lint - entry: bash -c 'cd go && golangci-lint run --fix --timeout 5m' - types_or: [go, go-mod] + - repo: https://github.com/golangci/golangci-lint + # XXX: don't update this as it seems newer versions break nolint directives + rev: 5d1e709b7be35cb2025444e19de266b056b7b7ee # frozen: v2.10.1 + hooks: + - id: golangci-lint + name: lint + entry: bash -c 'cd go && golangci-lint run --fix --timeout 5m' + types_or: [go, go-mod] -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 - hooks: - - id: check-added-large-files - args: ["--enforce-all"] - - id: check-case-conflict - - id: check-executables-have-shebangs - - id: check-yaml - - id: detect-private-key - - id: end-of-file-fixer - exclude: '\.sln$' - - id: mixed-line-ending - args: [--fix=lf] - exclude: '\.(bat|sln)$' - - id: trailing-whitespace + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 + hooks: + - id: check-added-large-files + args: ["--enforce-all"] + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-yaml + # These are Jinja/Mako templates, not standalone YAML documents. + exclude: '^vendor/adbc-drivers-dev/adbc_drivers_dev/templates/.*\.yaml$' + - id: detect-private-key + - id: end-of-file-fixer + exclude: '\.sln$' + - id: mixed-line-ending + args: [--fix=lf] + exclude: '\.(bat|sln)$' + - id: trailing-whitespace -- repo: https://github.com/adbc-drivers/dev - rev: efa57f642e794eb191cdd43a1de72413a849ba79 - hooks: - - id: rat + - repo: https://github.com/adbc-drivers/dev + rev: efa57f642e794eb191cdd43a1de72413a849ba79 + hooks: + - id: rat -- repo: local - hooks: - - id: gofix - name: go fix - language: system - types: [go] - entry: bash -c 'cd go && go fix' - pass_filenames: false + - repo: local + hooks: + - id: gofix + name: go fix + language: system + types: [go] + entry: bash -c 'cd go && go fix' + pass_filenames: false diff --git a/.rat-apache b/.rat-apache index 86b6c9b..769a7b5 100644 --- a/.rat-apache +++ b/.rat-apache @@ -13,6 +13,8 @@ # limitations under the License. .github/workflows/dev_issues.yaml +vendor/adbc-drivers-dev/adbc_drivers_dev/templates/dev_issues.yaml +vendor/adbc-drivers-dev/adbc_drivers_dev/title_check.py csharp/Directory.Build.props csharp/src/Interop/Build-SnowflakeDriver.ps1 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/go/pixi.lock b/go/pixi.lock index 8c27c3a..b30a65b 100644 --- a/go/pixi.lock +++ b/go/pixi.lock @@ -59,7 +59,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-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: git+https://github.com/adbc-drivers/dev#e97f2b6b3440252b50e47ce7d3775a304eac058f + - pypi: ../vendor/adbc-drivers-dev - pypi: git+https://github.com/adbc-drivers/validation#7654fca2d7725bcbd857925e3fce78d3bea5bd08 - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl @@ -120,7 +120,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-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: git+https://github.com/adbc-drivers/dev#e97f2b6b3440252b50e47ce7d3775a304eac058f + - pypi: ../vendor/adbc-drivers-dev - pypi: git+https://github.com/adbc-drivers/validation#7654fca2d7725bcbd857925e3fce78d3bea5bd08 - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/19/6c/4aabac7ed4d5944f544b7cf7881d50fe4b34eac908605291b633a53be875/adbc_driver_manager-1.11.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl @@ -175,7 +175,7 @@ environments: - 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/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - - pypi: git+https://github.com/adbc-drivers/dev#e97f2b6b3440252b50e47ce7d3775a304eac058f + - pypi: ../vendor/adbc-drivers-dev - pypi: git+https://github.com/adbc-drivers/validation#7654fca2d7725bcbd857925e3fce78d3bea5bd08 - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/b2/b9dc7624b105d414585b8530451c1162c0b4750c0be9be2e497bb47a8a9b/duckdb-1.5.4-cp314-cp314-macosx_11_0_arm64.whl @@ -231,7 +231,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_39.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_39.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - - pypi: git+https://github.com/adbc-drivers/dev#e97f2b6b3440252b50e47ce7d3775a304eac058f + - pypi: ../vendor/adbc-drivers-dev - pypi: git+https://github.com/adbc-drivers/validation#7654fca2d7725bcbd857925e3fce78d3bea5bd08 - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl @@ -1236,9 +1236,8 @@ packages: purls: [] size: 388453 timestamp: 1764777142545 -- pypi: git+https://github.com/adbc-drivers/dev#e97f2b6b3440252b50e47ce7d3775a304eac058f +- pypi: ../vendor/adbc-drivers-dev name: adbc-drivers-dev - version: '0.1' requires_dist: - doit - jinja2 @@ -1249,7 +1248,6 @@ packages: - requests - ruamel-yaml>=0.18.11,<0.19 - tomlkit>=0.13.2,<0.14 - requires_python: '>=3.14' - pypi: git+https://github.com/adbc-drivers/validation#7654fca2d7725bcbd857925e3fce78d3bea5bd08 name: adbc-drivers-validation version: '0.1' diff --git a/go/pixi.toml b/go/pixi.toml index 7bcc4b7..d668282 100644 --- a/go/pixi.toml +++ b/go/pixi.toml @@ -23,7 +23,7 @@ platforms = ["linux-64", "osx-arm64", "win-64", "linux-aarch64"] version = "0.1.0" [tasks] -make = "adbc-make run build VERBOSE=true" +make = "adbc-make run build DRIVER=snowflake VERBOSE=true" test = "go test -tags assert -v ./..." release = "adbc-release" validate = "pytest -vvs --junit-xml=validation-report.xml -rfEsxX validation/tests/" @@ -34,5 +34,6 @@ python = ">=3.14.3,<3.15" pytest-lazy-fixtures = ">=1.3.2,<2" [pypi-dependencies] -adbc-drivers-dev = { git = "https://github.com/adbc-drivers/dev" } +# LOCAL DEVIATION: Keep the vendored SemVer repair instead of the generated Git source. +adbc-drivers-dev = { path = "../vendor/adbc-drivers-dev", editable = true } adbc-drivers-validation = { git = "https://github.com/adbc-drivers/validation" } 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..be59d5d --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,5988 @@ +# 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", + "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", + "snafu 0.8.9", + "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 = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.9.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[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 = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[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 = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[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 = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[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 = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + +[[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 = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[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=f9175f07e7784c45907dffca9d0738fccbaf37d1#f9175f07e7784c45907dffca9d0738fccbaf37d1" +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=f9175f07e7784c45907dffca9d0738fccbaf37d1#f9175f07e7784c45907dffca9d0738fccbaf37d1" +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 = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[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.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[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", + "httpdate", + "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-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 = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "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 = "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 = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[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 = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[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 = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.17", + "http 1.4.0", + "rand 0.8.7", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[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 0.9.2", + "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 = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "os_info" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b" +dependencies = [ + "android_system_properties", + "log", + "nix", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + +[[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 = "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 = "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=f9175f07e7784c45907dffca9d0738fccbaf37d1#f9175f07e7784c45907dffca9d0738fccbaf37d1" +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=f9175f07e7784c45907dffca9d0738fccbaf37d1#f9175f07e7784c45907dffca9d0738fccbaf37d1" + +[[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 0.9.2", + "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.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[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_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-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "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 = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[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 = "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_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[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=f9175f07e7784c45907dffca9d0738fccbaf37d1#f9175f07e7784c45907dffca9d0738fccbaf37d1" +dependencies = [ + "arrow", + "arrow-ipc", + "aws-config", + "aws-credential-types", + "aws-lc-rs", + "aws-runtime", + "aws-sdk-s3", + "aws-sdk-sts", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "axum", + "base64 0.22.1", + "bytes", + "chrono", + "const-oid", + "der 0.7.10", + "der-parser", + "dirs", + "error_trace", + "flate2", + "fs2", + "futures", + "glob", + "hex", + "hmac", + "html-escape", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "infer", + "jwt", + "keyring", + "libc", + "lru 0.12.5", + "memchr", + "num-traits", + "oauth2", + "once_cell", + "openssl", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "os_info", + "pin-project-lite", + "prost 0.14.3", + "proto_generator", + "proto_utils", + "rand 0.9.2", + "reqwest", + "rust-ini", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-pemfile", + "rustls-webpki 0.102.8", + "serde", + "serde_json", + "sf_params_spec", + "sha2", + "signature 2.2.0", + "snafu 0.8.9", + "sonic-rs", + "spki 0.7.3", + "tempfile", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "toml", + "tracing", + "tracing-appender", + "tracing-core", + "tracing-opentelemetry", + "tracing-subscriber", + "url", + "urlencoding", + "uuid", + "webbrowser", + "x509-cert", + "x509-parser", + "zeroize", +] + +[[package]] +name = "sf_params_spec" +version = "0.0.0" +source = "git+https://github.com/snowflakedb/universal-driver?rev=f9175f07e7784c45907dffca9d0738fccbaf37d1#f9175f07e7784c45907dffca9d0738fccbaf37d1" + +[[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 = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[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 = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + +[[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", + "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-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-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror 2.0.18", + "time", + "tracing-subscriber", +] + +[[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", + "serde_derive", +] + +[[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 = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[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 = "webbrowser" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94" +dependencies = [ + "jni", + "log", + "ndk-context", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "url", + "web-sys", +] + +[[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" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.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..989efb0 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,50 @@ +# 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", rev = "f9175f07e7784c45907dffca9d0738fccbaf37d1" } +arrow-array = { version = "58.1.0", default-features = false, features = [ + "ffi", + "chrono-tz", +] } +arrow-sf-core = { package = "arrow", version = "=56.2.0", default-features = false, features = [ + "ffi", +] } +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" +snafu = "0.8.9" +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..26e5180 --- /dev/null +++ b/rust/about.toml @@ -0,0 +1,61 @@ +# 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" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[error_trace_derive.clarify] +license = "Apache-2.0" +[[error_trace_derive.clarify.files]] +path = "../LICENSE" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[proto_generator.clarify] +license = "Apache-2.0" +[[proto_generator.clarify.files]] +path = "../LICENSE" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[proto_utils.clarify] +license = "Apache-2.0" +[[proto_utils.clarify.files]] +path = "../LICENSE" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[sf_core.clarify] +license = "Apache-2.0" +[[sf_core.clarify.files]] +path = "../LICENSE" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[sf_params_spec.clarify] +license = "Apache-2.0" +[[sf_params_spec.clarify.files]] +path = "../LICENSE" +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..3254762 --- /dev/null +++ b/rust/docs/snowflake.md @@ -0,0 +1,118 @@ +--- +# 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) + +## Statement Options + +- `adbc.snowflake.statement.query_timeout`: per-statement query timeout in seconds. Accepts a non-negative integer or a string such as `"30"` or `"30s"`. Set it to `0` to clear the override and use the Snowflake session/default timeout. + +## 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..81de8d8 --- /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: ../vendor/adbc-drivers-dev + - 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: ../vendor/adbc-drivers-dev + - 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: ../vendor/adbc-drivers-dev + - 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: ../vendor/adbc-drivers-dev + - 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: ../vendor/adbc-drivers-dev + 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..ba6f721 --- /dev/null +++ b/rust/pixi.toml @@ -0,0 +1,39 @@ +# 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] +# LOCAL DEVIATION: Keep the vendored SemVer repair instead of the generated Git source. +adbc-drivers-dev = { path = "../vendor/adbc-drivers-dev", editable = true } +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..86d5614 --- /dev/null +++ b/rust/src/connection.rs @@ -0,0 +1,732 @@ +// 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 = "v58.1.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::{ + 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::{ConnectionInfo, Handle}; + +use crate::driver::{Inner, TimestampPrecision}; +use crate::statement::{Statement, adjust_schema}; + +pub struct Connection { + pub(crate) inner: Arc, + pub(crate) conn_handle: Handle, + pub(crate) autocommit: bool, + pub(crate) use_high_precision: bool, + pub(crate) timestamp_precision: TimestampPrecision, +} + +type CleanupTask = Box; + +fn attempt_connection_cleanup( + close: impl FnOnce() + Send + 'static, + release: impl FnOnce(), + spawn: impl FnOnce(CleanupTask) -> std::io::Result>, +) { + // Runtime::block_on panics when entered from some Tokio runtime contexts. + // Always run close on a dedicated OS thread, wait for sf_core's bounded + // close behavior, then release the handle. Drop remains infallible even if + // spawning, closing, joining, or releasing fails or panics. + let close = Box::new(move || { + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(close)); + }); + let spawned = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| spawn(close))); + if let Ok(Ok(thread)) = spawned { + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _ = thread.join(); + })); + } + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(release)); +} + +pub(crate) fn cleanup_connection_handle(inner: &Arc, conn_handle: Handle) { + let close_inner = Arc::clone(inner); + attempt_connection_cleanup( + move || { + let _ = close_inner + .runtime + .block_on(close_inner.sf.connection_close(conn_handle)); + }, + || { + let _ = inner.sf.connection_release(conn_handle); + }, + |close| { + std::thread::Builder::new() + .name("snowflake-connection-close".into()) + .spawn(close) + }, + ); +} + +impl Drop for Connection { + fn drop(&mut self) { + cleanup_connection_handle(&self.inner, self.conn_handle); + } +} + +pub(crate) struct SingleBatchReader { + batch: Option, + schema: std::sync::Arc, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SessionIdentifierKind { + Database, + Schema, +} + +/// sf_core f9175f07 trims identifiers before quoting them. Keep the standard +/// core path unless trimming would change the identifier, in which case an +/// exactly quoted USE statement is required to preserve Snowflake semantics. +fn exact_identifier_sql(kind: SessionIdentifierKind, name: &str) -> Option { + if name == name.trim() { + return None; + } + let keyword = match kind { + SessionIdentifierKind::Database => "DATABASE", + SessionIdentifierKind::Schema => "SCHEMA", + }; + Some(format!("USE {keyword} \"{}\"", name.replace('"', "\"\""))) +} + +fn quote_identifier(identifier: &str) -> String { + format!("\"{}\"", identifier.replace('"', "\"\"")) +} + +fn qualified_table_identifier( + catalog: Option<&str>, + db_schema: Option<&str>, + table_name: &str, +) -> String { + let table_name = quote_identifier(table_name); + match (catalog, db_schema) { + (Some(catalog), Some(db_schema)) => format!( + "{}.{}.{}", + quote_identifier(catalog), + quote_identifier(db_schema), + table_name + ), + (None, Some(db_schema)) => format!("{}.{}", quote_identifier(db_schema), table_name), + (Some(catalog), None) => format!("{}..{}", quote_identifier(catalog), table_name), + (None, None) => table_name, + } +} + +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 { + fn context_names(&self) -> Result<(Option, Option)> { + // Deliberately extract only cached context names. ConnectionInfo also carries + // sensitive tokens, which must never be surfaced through ADBC options. + let ConnectionInfo { + database, schema, .. + } = self + .inner + .runtime + .block_on(self.inner.sf.connection_get_info(self.conn_handle)) + .map_err(crate::error::api_error_to_adbc_error)?; + Ok((database, schema)) + } + + pub(crate) fn set_autocommit(&mut self, enabled: bool) -> Result<()> { + self.inner + .runtime + .block_on( + self.inner + .sf + .connection_set_autocommit(self.conn_handle, enabled), + ) + .map_err(crate::error::api_error_to_adbc_error)?; + self.autocommit = enabled; + Ok(()) + } + + fn set_current_identifier(&self, kind: SessionIdentifierKind, name: &str) -> Result<()> { + if let Some(sql) = exact_identifier_sql(kind, name) { + return self + .inner + .execute_temporary_statement(self.conn_handle, sql, None); + } + let result = match kind { + SessionIdentifierKind::Database => self.inner.runtime.block_on( + self.inner + .sf + .connection_use_database(self.conn_handle, name), + ), + SessionIdentifierKind::Schema => self + .inner + .runtime + .block_on(self.inner.sf.connection_use_schema(self.conn_handle, name)), + }; + result.map_err(crate::error::api_error_to_adbc_error) + } +} + +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.set_current_identifier(SessionIdentifierKind::Database, s) + } 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.set_current_identifier(SessionIdentifierKind::Schema, s) + } 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.context_names()?.0.ok_or_else(|| { + Error::with_message_and_status("current catalog is not set", Status::NotFound) + }), + OptionConnection::CurrentSchema => self.context_names()?.1.ok_or_else(|| { + Error::with_message_and_status("current schema is not set", 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 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, + query_timeout_seconds: None, + use_high_precision: self.use_high_precision, + timestamp_precision: self.timestamp_precision, + bound_batches: vec![], + binding_supplied: false, + last_query_id: None, + prepared_parameter_schema: None, + prepared_result_schema: 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.inner + .runtime + .block_on( + self.inner + .sf + .connection_get_server_version(self.conn_handle), + ) + .map_err(crate::error::api_error_to_adbc_error)? + .unwrap_or_default() + } 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 qualified = qualified_table_identifier(catalog, db_schema, table_name); + let stmt_handle = self + .inner + .sf + .statement_new(self.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + + let result = (|| { + let prepared = self + .inner + .prepare_statement(stmt_handle, format!("SELECT * FROM {qualified}"))?; + let schema = prepared.reader.schema(); + if schema.fields().is_empty() { + return Err(Error::with_message_and_status( + format!("sf_core prepared an empty schema for table {qualified}"), + Status::Internal, + )); + } + Ok(adjust_schema( + &schema, + self.use_high_precision, + self.timestamp_precision.time_unit(), + ) + .as_ref() + .clone()) + })(); + let release = self.inner.sf.statement_release(stmt_handle); + match result { + Ok(schema) => { + release.map_err(crate::error::api_error_to_adbc_error)?; + Ok(schema) + } + Err(error) => { + let _ = release; + Err(error) + } + } + } + + #[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.inner + .runtime + .block_on(self.inner.sf.connection_commit(self.conn_handle)) + .map_err(crate::error::api_error_to_adbc_error) + } + + fn rollback(&mut self) -> Result<()> { + if self.autocommit { + return Err(Error::with_message_and_status( + "cannot rollback: autocommit is enabled", + Status::InvalidState, + )); + } + self.inner + .runtime + .block_on(self.inner.sf.connection_rollback(self.conn_handle)) + .map_err(crate::error::api_error_to_adbc_error) + } + + #[allow(refining_impl_trait)] + fn read_partition( + &self, + _partition: impl AsRef<[u8]>, + ) -> Result> { + Err(crate::error::not_implemented("read_partition")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn connection_cleanup_waits_for_close_before_release_after_close_panic() { + use std::sync::Mutex; + + let calls = Arc::new(Mutex::new(Vec::new())); + let close_calls = Arc::clone(&calls); + let release_calls = Arc::clone(&calls); + attempt_connection_cleanup( + move || { + close_calls.lock().unwrap().push("close"); + panic!("simulated close failure"); + }, + move || release_calls.lock().unwrap().push("release"), + |close| std::thread::Builder::new().spawn(close), + ); + + assert_eq!(&*calls.lock().unwrap(), &["close", "release"]); + } + + #[test] + fn connection_cleanup_releases_when_thread_spawn_fails() { + use std::sync::atomic::{AtomicBool, Ordering}; + + let released = Arc::new(AtomicBool::new(false)); + let released_by_cleanup = Arc::clone(&released); + attempt_connection_cleanup( + || panic!("close must not run without its dedicated thread"), + move || released_by_cleanup.store(true, Ordering::SeqCst), + |_close| Err(std::io::Error::other("simulated spawn failure")), + ); + + assert!(released.load(Ordering::SeqCst)); + } + + #[test] + fn exact_identifier_fallback_is_narrow_and_quotes_without_trimming() { + assert_eq!( + exact_identifier_sql(SessionIdentifierKind::Database, " db\" name "), + Some("USE DATABASE \" db\"\" name \"".into()) + ); + assert_eq!( + exact_identifier_sql(SessionIdentifierKind::Schema, " schema "), + Some("USE SCHEMA \" schema \"".into()) + ); + assert_eq!( + exact_identifier_sql(SessionIdentifierKind::Database, "normal_name"), + None + ); + } + + #[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, + 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 table_identifier_quoting_preserves_all_components() { + assert_eq!( + qualified_table_identifier(Some("db\"name"), Some("schema"), "table"), + "\"db\"\"name\".\"schema\".\"table\"" + ); + assert_eq!( + qualified_table_identifier(Some("db"), None, "table"), + "\"db\"..\"table\"" + ); + assert_eq!( + qualified_table_identifier(None, Some("schema"), "table"), + "\"schema\".\"table\"" + ); + assert_eq!( + qualified_table_identifier(None, None, " table "), + "\" table \"" + ); + } + + #[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, + 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"]); + } +} diff --git a/rust/src/database.rs b/rust/src/database.rs new file mode 100644 index 0000000..fcefe62 --- /dev/null +++ b/rust/src/database.rs @@ -0,0 +1,1267 @@ +// 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, HashSet}; +use std::sync::Arc; + +use adbc_core::{ + Optionable, + error::{Error, Result, Status}, + options::{OptionConnection, OptionDatabase, OptionValue}, +}; +use sf_core::apis::database_driver_v1::{ + Handle, ValidationIssue, ValidationSeverity, connection::WrapperIdentity, +}; +use sf_core::config::param_registry::{param_names, registry}; +use sf_core::config::settings::Setting; + +use crate::connection::{Connection, cleanup_connection_handle}; +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" => "application".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() + } + // 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" => { + param_names::WORKLOAD_IDENTITY_PROVIDER.into() + } + // Connection timeouts (normalized to sf_core integer seconds below) + "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 + "adbc.snowflake.sql.client_option.tls_skip_verify" => param_names::TLS_SKIP_VERIFY.into(), + "adbc.snowflake.sql.client_option.tls_root_cert" => { + param_names::CUSTOM_ROOT_STORE_PATH.into() + } + // Session behaviour + "adbc.snowflake.sql.client_option.keep_session_alive" => { + param_names::CLIENT_SESSION_KEEP_ALIVE.into() + } + "adbc.snowflake.sql.client_option.disable_telemetry" => { + param_names::CLIENT_TELEMETRY_ENABLED.into() + } + // sf_core's temporary-credential setting explicitly controls MFA-token + // caching. The Go/ADBC store_temp_creds option instead controls ID-token + // storage, so retain that raw name and let sf_core report it as unknown. + "adbc.snowflake.sql.client_option.cache_mfa_token" => { + param_names::CLIENT_STORE_TEMPORARY_CREDENTIAL.into() + } + "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(), + }; + + let setting = match key { + "adbc.snowflake.sql.client_option.login_timeout" + | "adbc.snowflake.sql.client_option.request_timeout" => { + let seconds = match value { + OptionValue::Int(value) => *value, + OptionValue::String(value) => { + let value = value.strip_suffix('s').unwrap_or(value); + if value.is_empty() { + 0 + } else { + value.parse::().map_err(|_| { + Error::with_message_and_status( + format!("invalid timeout value: {value}"), + Status::InvalidArguments, + ) + })? + } + } + _ => { + return Err(Error::with_message_and_status( + "timeout must be a string or int", + Status::InvalidArguments, + )); + } + }; + Setting::Int(seconds) + } + "adbc.snowflake.sql.client_option.tls_skip_verify" + | "adbc.snowflake.sql.client_option.keep_session_alive" + | "adbc.snowflake.sql.client_option.cache_mfa_token" + | "adbc.snowflake.sql.client_option.store_temp_creds" => { + Setting::Bool(adbc_option_enabled(value)?) + } + "adbc.snowflake.sql.client_option.disable_telemetry" => { + Setting::Bool(!adbc_option_enabled(value)?) + } + _ => setting, + }; + + Ok(Some((param, setting))) +} + +fn adbc_option_enabled(value: &OptionValue) -> Result { + match value { + OptionValue::String(value) => match value.to_ascii_lowercase().as_str() { + "enabled" | "true" | "1" => Ok(true), + "disabled" | "false" | "0" => Ok(false), + _ => Err(Error::with_message_and_status( + "boolean option must be enabled, true, 1, disabled, false, or 0", + Status::InvalidArguments, + )), + }, + OptionValue::Int(value) => Ok(*value != 0), + _ => Err(Error::with_message_and_status( + "boolean option must be a string or int", + Status::InvalidArguments, + )), + } +} + +pub struct Database { + pub(crate) inner: Arc, + pub(crate) db_handle: Handle, + /// Local copy of sf_core settings, using canonical parameter names when an + /// equivalent exists and preserving original raw names otherwise. + /// Propagated to each new connection before connection_init. + pub(crate) sf_settings: HashMap, + /// Database-level warning messages already surfaced to the application logger. + pub(crate) surfaced_warnings: HashSet, + /// 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 Database { + fn set_sf_options(&mut self, options: HashMap) -> Result<()> { + let warnings = self + .inner + .runtime + .block_on(self.inner.sf.database_set_options(self.db_handle, options)) + .map_err(crate::error::api_error_to_adbc_error)?; + for warning in warnings { + let rendered = warning.to_string(); + if self.surfaced_warnings.insert(rendered.clone()) { + log::warn!("Snowflake database option warning: {rendered}"); + } + } + Ok(()) + } +} + +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.set_sf_options(HashMap::from([(param.clone(), setting.clone())]))?; + self.sf_settings.insert(param, setting); + } + + 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()); + } + let lookup_value = if matches!( + key_str, + "adbc.snowflake.sql.client_option.tls_skip_verify" + | "adbc.snowflake.sql.client_option.keep_session_alive" + | "adbc.snowflake.sql.client_option.cache_mfa_token" + | "adbc.snowflake.sql.client_option.store_temp_creds" + | "adbc.snowflake.sql.client_option.disable_telemetry" + ) { + OptionValue::String("disabled".into()) + } else { + OptionValue::String(String::new()) + }; + if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &lookup_value) + && let Some(setting) = self.sf_settings.get(¶m) + { + return match setting { + Setting::String(value) => Ok(value.clone()), + Setting::Int(value) + if matches!( + key_str, + "adbc.snowflake.sql.client_option.login_timeout" + | "adbc.snowflake.sql.client_option.request_timeout" + ) => + { + Ok(format!("{value}s")) + } + Setting::Int(value) => Ok(value.to_string()), + Setting::Double(value) => Ok(value.to_string()), + Setting::Bool(value) + if key_str == "adbc.snowflake.sql.client_option.disable_telemetry" => + { + Ok(if *value { "disabled" } else { "enabled" }.into()) + } + Setting::Bool(value) => Ok(if *value { "enabled" } else { "disabled" }.into()), + Setting::Bytes(_) => Err(Error::with_message_and_status( + format!("option is not a string: {key_str}"), + Status::InvalidArguments, + )), + }; + } + 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]@]host[/database[/schema]][?param=value&...] + /// Recognized query params: account, warehouse, role, host, port, protocol, authenticator + /// + /// Limitations: passwords containing `@` are not supported; use `set_option` for + /// Username/Password directly when credentials contain special characters. + 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], Some(&rest[q + 1..])) + } else { + (rest.as_str(), None) + }; + let explicit_account = query.is_some_and(|query| { + query.split('&').any(|pair| { + pair.split_once('=') + .is_some_and(|(key, _)| key == "account") + }) + }); + + let parts: Vec<&str> = path.splitn(3, '/').collect(); + if let Some(authority) = parts.first().filter(|s| !s.is_empty()) { + if explicit_account { + let (host, port) = authority + .rsplit_once(':') + .map_or((*authority, None), |(host, port)| (host, Some(port))); + self.set_option( + OptionDatabase::Other("adbc.snowflake.sql.uri.host".into()), + OptionValue::String(host.to_string()), + )?; + if let Some(port) = port { + self.set_option( + OptionDatabase::Other("adbc.snowflake.sql.uri.port".into()), + OptionValue::String(port.to_string()), + )?; + } + } else { + self.set_option( + OptionDatabase::Other("adbc.snowflake.sql.account".into()), + OptionValue::String(authority.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((key, encoded_value)) = pair.split_once('=') { + let value = percent_decode_str(encoded_value) + .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 key { + "account" => "adbc.snowflake.sql.account", + "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(value.into_owned()), + )?; + } + } + } + Ok(()) + } +} + +fn connection_option_to_setting(value: &OptionValue) -> Result { + match value { + OptionValue::String(value) => Ok(Setting::String(value.clone())), + OptionValue::Int(value) => Ok(Setting::Int(*value)), + OptionValue::Double(value) => Ok(Setting::Double(*value)), + OptionValue::Bytes(value) => Ok(Setting::Bytes(value.clone())), + _ => Err(Error::with_message_and_status( + "unsupported option value type", + Status::InvalidArguments, + )), + } +} + +struct AccumulatedConnectionOptions { + sf_options: HashMap, + post_connect_options: Vec<(OptionConnection, OptionValue)>, + no_connection_details: bool, +} + +fn accumulate_connection_options( + database_options: &HashMap, + opts: impl IntoIterator, +) -> Result { + let mut sf_options = database_options.clone(); + // This mirrors sf_core peers' raw-kwargs contract: only a call with no + // database settings and no raw connection options is a bare connection. + let mut no_connection_details = database_options.is_empty(); + let mut post_connect_options = Vec::new(); + + for (key, value) in opts { + no_connection_details = false; + if let OptionConnection::Other(name) = &key { + // Canonicalize only known registry parameters so a connection-level + // alias/case variant replaces the database-level canonical value. + // Unknown wrapper/vendor options retain their original spelling. + let name = registry() + .resolve(name) + .map(|param| param.canonical_name.to_owned()) + .unwrap_or_else(|| name.clone()); + sf_options.insert(name, connection_option_to_setting(&value)?); + } else { + post_connect_options.push((key, value)); + } + } + + Ok(AccumulatedConnectionOptions { + sf_options, + post_connect_options: order_post_connect_options(post_connect_options), + no_connection_details, + }) +} + +fn wrapper_identity() -> WrapperIdentity { + WrapperIdentity { + driver_name: "ADBC Snowflake Driver (Rust)".into(), + driver_version: env!("CARGO_PKG_VERSION").into(), + language_runtime: "Rust".into(), + language_version: String::new(), + language_compiler: None, + release_type: None, + } +} + +fn process_validation_issues( + issues: impl IntoIterator, + seen_warnings: &mut HashSet, + database_warnings: &HashSet, +) -> Result<()> { + let mut errors = Vec::new(); + for issue in issues { + match issue.severity { + ValidationSeverity::Error => errors.push(issue.to_string()), + ValidationSeverity::Warning => { + let rendered = issue.to_string(); + if seen_warnings.insert(rendered.clone()) && !database_warnings.contains(&rendered) + { + log::warn!("Snowflake connection option warning: {rendered}"); + } + } + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(Error::with_message_and_status( + format!( + "invalid Snowflake connection options: {}", + errors.join("; ") + ), + Status::InvalidArguments, + )) + } +} + +fn order_post_connect_options( + mut options: Vec<(OptionConnection, OptionValue)>, +) -> Vec<(OptionConnection, OptionValue)> { + // Stable sorting preserves caller order among duplicate options while making + // the database/schema dependency deterministic. Autocommit and any future + // standard options are still applied exactly once after both context names. + options.sort_by_key(|(key, _)| match key { + OptionConnection::CurrentCatalog => 0, + OptionConnection::CurrentSchema => 1, + _ => 2, + }); + options +} + +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 AccumulatedConnectionOptions { + mut sf_options, + post_connect_options, + no_connection_details, + } = accumulate_connection_options(&self.sf_settings, opts)?; + // Snowflake currently gates Arrow result transport on this protocol ID. + // Keep the visible application and wrapper telemetry driver-specific. + sf_options.insert( + "client_app_id".to_string(), + Setting::String("PythonConnector".to_string()), + ); + sf_options + .entry("application".to_string()) + .or_insert_with(|| Setting::String("ADBC Snowflake Driver (Rust)".to_string())); + let conn_handle = self.inner.sf.connection_new(); + + let setup = (|| -> Result<()> { + // Batch all pre-connect options in one validation/resolution pass. + let warnings = self + .inner + .runtime + .block_on(self.inner.sf.connection_set_options( + conn_handle, + sf_options, + no_connection_details, + )) + .map_err(crate::error::api_error_to_adbc_error)?; + let mut seen_warnings = HashSet::new(); + process_validation_issues(warnings, &mut seen_warnings, &self.surfaced_warnings)?; + + self.inner + .runtime + .block_on( + self.inner + .sf + .set_wrapper_identity(conn_handle, wrapper_identity()), + ) + .map_err(crate::error::api_error_to_adbc_error)?; + + // Seed Arrow format in the login payload; the backend requires this in + // addition to the post-login ARROW_FORCE switch below. + self.inner + .runtime + .block_on(self.inner.sf.connection_set_session_parameters( + conn_handle, + HashMap::from([( + "PYTHON_CONNECTOR_QUERY_RESULT_FORMAT".to_string(), + "ARROW".to_string(), + )]), + )) + .map_err(crate::error::api_error_to_adbc_error)?; + + let validation_issues = self + .inner + .runtime + .block_on(self.inner.sf.connection_validate_options(conn_handle)) + .map_err(crate::error::api_error_to_adbc_error)?; + process_validation_issues( + validation_issues, + &mut seen_warnings, + &self.surfaced_warnings, + )?; + + self.inner + .runtime + .block_on( + self.inner + .sf + .connection_init(None, conn_handle, self.db_handle), + ) + .map_err(crate::error::api_error_to_adbc_error)?; + + // Complete the backend's Arrow handshake after login. Without the + // connector-specific forced value, query responses remain JSON. + self.inner.execute_temporary_statement( + conn_handle, + "ALTER SESSION SET PYTHON_CONNECTOR_QUERY_RESULT_FORMAT = 'ARROW_FORCE'", + None, + ) + })(); + + if let Err(error) = setup { + // Initialization may have established a partial session. Attempt + // close before release, while still guaranteeing release on error. + cleanup_connection_handle(&self.inner, conn_handle); + return Err(error); + } + + let mut conn = Connection { + inner: self.inner.clone(), + conn_handle, + autocommit: true, + use_high_precision: self.use_high_precision, + timestamp_precision: self.timestamp_precision, + }; + // From this point Connection::drop owns cleanup on every error path. + for (key, value) in post_connect_options { + conn.set_option(key, value)?; + } + + 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 connection_options_accumulate_once_with_connection_values_winning() { + let database_options = HashMap::from([ + ("account".to_string(), Setting::String("db-account".into())), + ( + "warehouse".to_string(), + Setting::String("db-warehouse".into()), + ), + ]); + let accumulated = accumulate_connection_options( + &database_options, + [ + ( + OptionConnection::Other("account".into()), + OptionValue::String("connection-account".into()), + ), + ( + OptionConnection::CurrentSchema, + OptionValue::String("schema".into()), + ), + ( + OptionConnection::AutoCommit, + OptionValue::String("false".into()), + ), + ], + ) + .unwrap(); + + assert_eq!( + accumulated.sf_options.get("account"), + Some(&Setting::String("connection-account".into())) + ); + assert_eq!( + accumulated.sf_options.get("warehouse"), + Some(&Setting::String("db-warehouse".into())) + ); + assert!(!accumulated.no_connection_details); + assert!(matches!( + accumulated.post_connect_options.as_slice(), + [ + (OptionConnection::CurrentSchema, _), + (OptionConnection::AutoCommit, _) + ] + )); + } + + #[test] + fn only_no_database_settings_and_no_raw_options_is_bare() { + let bare = accumulate_connection_options(&HashMap::new(), []).unwrap(); + assert!(bare.no_connection_details); + + let database_options = HashMap::from([( + param_names::ACCOUNT.to_string(), + Setting::String("account".into()), + )]); + let with_database_setting = accumulate_connection_options(&database_options, []).unwrap(); + assert!(!with_database_setting.no_connection_details); + } + + #[test] + fn any_raw_option_makes_the_connection_non_bare() { + let accumulated = accumulate_connection_options( + &HashMap::new(), + [( + OptionConnection::AutoCommit, + OptionValue::String("false".into()), + )], + ) + .unwrap(); + assert!(!accumulated.no_connection_details); + assert!(accumulated.sf_options.is_empty()); + } + + #[test] + fn known_connection_alias_overrides_database_canonical_option() { + let database_options = HashMap::from([( + param_names::USER.to_string(), + Setting::String("database-user".into()), + )]); + let accumulated = accumulate_connection_options( + &database_options, + [( + OptionConnection::Other("uId".into()), + OptionValue::String("connection-user".into()), + )], + ) + .unwrap(); + + assert_eq!( + accumulated.sf_options, + HashMap::from([( + param_names::USER.to_string(), + Setting::String("connection-user".into()), + )]) + ); + } + + #[test] + fn unknown_connection_option_preserves_original_key() { + let accumulated = accumulate_connection_options( + &HashMap::new(), + [( + OptionConnection::Other("Vendor.CustomOption".into()), + OptionValue::String("value".into()), + )], + ) + .unwrap(); + + assert_eq!( + accumulated.sf_options.get("Vendor.CustomOption"), + Some(&Setting::String("value".into())) + ); + } + + #[test] + fn post_connect_options_always_apply_catalog_before_schema() { + let options = vec![ + ( + OptionConnection::CurrentSchema, + OptionValue::String("schema_one".into()), + ), + ( + OptionConnection::AutoCommit, + OptionValue::String("false".into()), + ), + ( + OptionConnection::CurrentCatalog, + OptionValue::String("database".into()), + ), + ( + OptionConnection::CurrentSchema, + OptionValue::String("schema_two".into()), + ), + ]; + let ordered = order_post_connect_options(options); + let keys = ordered + .iter() + .map(|(key, _)| key.clone()) + .collect::>(); + assert_eq!( + keys, + vec![ + OptionConnection::CurrentCatalog, + OptionConnection::CurrentSchema, + OptionConnection::CurrentSchema, + OptionConnection::AutoCommit, + ] + ); + assert!(matches!( + &ordered[3].1, + OptionValue::String(value) if value == "false" + )); + } + + #[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_passes_only_the_canonical_bool() { + for (value, expected) in [ + (OptionValue::String("enabled".into()), true), + (OptionValue::String("disabled".into()), false), + (OptionValue::Int(-1), true), + ] { + let mut db = make_db(); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.tls_skip_verify".into()), + value, + ) + .unwrap(); + assert_eq!( + db.sf_settings, + HashMap::from([( + param_names::TLS_SKIP_VERIFY.to_string(), + Setting::Bool(expected), + )]) + ); + } + } + + #[test] + fn boolean_option_parser_accepts_documented_strings_and_integers() { + for (value, expected) in [ + (OptionValue::String("enabled".into()), true), + (OptionValue::String("TRUE".into()), true), + (OptionValue::String("1".into()), true), + (OptionValue::String("DISABLED".into()), false), + (OptionValue::String("false".into()), false), + (OptionValue::String("0".into()), false), + (OptionValue::Int(-1), true), + (OptionValue::Int(0), false), + (OptionValue::Int(2), true), + ] { + assert_eq!(adbc_option_enabled(&value).unwrap(), expected); + } + } + + #[test] + fn boolean_options_reject_invalid_values_without_changing_state() { + for (key, param, enabled_setting) in [ + ( + "adbc.snowflake.sql.client_option.tls_skip_verify", + param_names::TLS_SKIP_VERIFY.as_str(), + true, + ), + ( + "adbc.snowflake.sql.client_option.keep_session_alive", + param_names::CLIENT_SESSION_KEEP_ALIVE.as_str(), + true, + ), + ( + "adbc.snowflake.sql.client_option.cache_mfa_token", + param_names::CLIENT_STORE_TEMPORARY_CREDENTIAL.as_str(), + true, + ), + ( + "adbc.snowflake.sql.client_option.store_temp_creds", + "store_temp_creds", + true, + ), + ( + "adbc.snowflake.sql.client_option.disable_telemetry", + param_names::CLIENT_TELEMETRY_ENABLED.as_str(), + false, + ), + ] { + let mut db = make_db(); + let option = OptionDatabase::Other(key.into()); + db.set_option(option.clone(), OptionValue::String("enabled".into())) + .unwrap(); + + for invalid in [ + OptionValue::String("ambiguous".into()), + OptionValue::Double(1.0), + OptionValue::Bytes(vec![1]), + ] { + let error = db.set_option(option.clone(), invalid).unwrap_err(); + assert_eq!(error.status, Status::InvalidArguments); + assert_eq!( + db.sf_settings.get(param), + Some(&Setting::Bool(enabled_setting)), + "invalid value changed {key}" + ); + } + } + } + + #[test] + fn legacy_options_use_equivalent_core_parameters_or_original_raw_names() { + let cases = [ + ( + "adbc.snowflake.sql.client_option.keep_session_alive", + param_names::CLIENT_SESSION_KEEP_ALIVE.as_str(), + Setting::Bool(true), + ), + ( + "adbc.snowflake.sql.client_option.cache_mfa_token", + param_names::CLIENT_STORE_TEMPORARY_CREDENTIAL.as_str(), + Setting::Bool(true), + ), + // sf_core has no ID-token-storage equivalent. Preserving the Go + // option name keeps it distinct and produces an explicit unknown- + // parameter validation warning rather than changing its meaning. + ( + "adbc.snowflake.sql.client_option.store_temp_creds", + "store_temp_creds", + Setting::Bool(true), + ), + ( + "adbc.snowflake.sql.client_option.identity_provider", + param_names::WORKLOAD_IDENTITY_PROVIDER.as_str(), + Setting::String("AWS".into()), + ), + ( + "adbc.snowflake.sql.client_option.disable_telemetry", + param_names::CLIENT_TELEMETRY_ENABLED.as_str(), + Setting::Bool(false), + ), + ]; + for (key, expected_name, expected_setting) in cases { + let value = if key.ends_with("identity_provider") { + OptionValue::String("AWS".into()) + } else { + OptionValue::String("enabled".into()) + }; + let (name, setting) = adbc_db_opt_to_sf(key, &value).unwrap().unwrap(); + assert_eq!(name, expected_name); + assert_eq!(setting, expected_setting); + } + } + + #[test] + fn cache_mfa_token_and_store_temp_creds_are_independent() { + let cache_mfa_token = + OptionDatabase::Other("adbc.snowflake.sql.client_option.cache_mfa_token".into()); + let store_temp_creds = + OptionDatabase::Other("adbc.snowflake.sql.client_option.store_temp_creds".into()); + let mut db = make_db(); + + db.set_option( + cache_mfa_token.clone(), + OptionValue::String("enabled".into()), + ) + .unwrap(); + db.set_option( + store_temp_creds.clone(), + OptionValue::String("disabled".into()), + ) + .unwrap(); + + assert_eq!( + db.sf_settings, + HashMap::from([ + ( + param_names::CLIENT_STORE_TEMPORARY_CREDENTIAL.to_string(), + Setting::Bool(true), + ), + ("store_temp_creds".to_string(), Setting::Bool(false)), + ]) + ); + assert_eq!( + db.get_option_string(cache_mfa_token.clone()).unwrap(), + "enabled" + ); + assert_eq!( + db.get_option_string(store_temp_creds.clone()).unwrap(), + "disabled" + ); + + db.set_option(cache_mfa_token.clone(), OptionValue::Int(0)) + .unwrap(); + db.set_option(store_temp_creds.clone(), OptionValue::Int(1)) + .unwrap(); + + assert_eq!( + db.sf_settings + .get(param_names::CLIENT_STORE_TEMPORARY_CREDENTIAL.as_str()), + Some(&Setting::Bool(false)) + ); + assert_eq!( + db.sf_settings.get("store_temp_creds"), + Some(&Setting::Bool(true)) + ); + assert_eq!(db.get_option_string(cache_mfa_token).unwrap(), "disabled"); + assert_eq!(db.get_option_string(store_temp_creds).unwrap(), "enabled"); + } + + #[test] + fn region_and_ocsp_are_not_translated_to_non_equivalent_core_options() { + for key in [ + "adbc.snowflake.sql.region", + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode", + ] { + let (name, _) = adbc_db_opt_to_sf(key, &OptionValue::String("enabled".into())) + .unwrap() + .unwrap(); + assert_eq!(name, key); + } + } + + #[test] + fn wrapper_identity_is_stable_and_does_not_claim_a_rust_version() { + let identity = wrapper_identity(); + assert_eq!(identity.driver_name, "ADBC Snowflake Driver (Rust)"); + assert_eq!(identity.driver_version, env!("CARGO_PKG_VERSION")); + assert_eq!(identity.language_runtime, "Rust"); + assert!(identity.language_version.is_empty()); + assert!(identity.language_compiler.is_none()); + } + + #[test] + fn validation_errors_are_invalid_arguments_and_warnings_are_deduplicated() { + use sf_core::apis::database_driver_v1::ValidationCode; + + let warning = ValidationIssue { + severity: ValidationSeverity::Warning, + parameter: "unknown".into(), + message: "warning".into(), + code: ValidationCode::UnknownParameter, + }; + let mut seen = HashSet::new(); + process_validation_issues([warning.clone(), warning], &mut seen, &HashSet::new()).unwrap(); + assert_eq!(seen.len(), 1); + + let error = process_validation_issues( + [ValidationIssue { + severity: ValidationSeverity::Error, + parameter: "account".into(), + message: "missing".into(), + code: ValidationCode::MissingRequired, + }], + &mut seen, + &HashSet::new(), + ) + .unwrap_err(); + assert_eq!(error.status, Status::InvalidArguments); + assert!(error.message.contains("account")); + } + + #[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_full_hostname_parses_host_port_and_explicit_account() { + let mut db = make_db(); + db.set_option( + OptionDatabase::Uri, + OptionValue::String( + "snowflake://sys_admin@private.network.com:443/OPS_MONITOR/DBA?account=vpc-id-1234" + .into(), + ), + ) + .unwrap(); + assert_eq!( + db.sf_settings.get(param_names::HOST.as_str()).unwrap(), + &Setting::String("private.network.com".into()) + ); + assert_eq!( + db.sf_settings.get(param_names::PORT.as_str()).unwrap(), + &Setting::Int(443) + ); + assert_eq!( + db.sf_settings.get(param_names::ACCOUNT.as_str()).unwrap(), + &Setting::String("vpc-id-1234".into()) + ); + } + + #[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..3fa2343 --- /dev/null +++ b/rust/src/driver.rs @@ -0,0 +1,303 @@ +// 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_array::RecordBatchReader; +use arrow_schema::TimeUnit; +use sf_core::apis::database_driver_v1::{ + ApiError, BindingType, ColumnMetadata, DatabaseDriverV1, ExecuteQueryResult, Handle, + ResultSetDescriptor, ResultSetInfo, +}; +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, +} + +pub(crate) struct QueryResult { + pub reader: Box, + pub descriptor: ResultSetDescriptor, +} + +pub(crate) struct PreparedResult { + pub reader: Box, + pub query_id: String, + pub number_of_binds: i32, + pub binds: Vec, +} + +/// Bridge sf_core's Arrow 56 reader to the Arrow 58 types exposed by this +/// driver through the stable Arrow C Stream interface. +pub(crate) fn bridge_arrow_reader( + reader: Box, +) -> Result> { + let stream = Box::new(arrow_sf_core::ffi_stream::FFI_ArrowArrayStream::new(reader)); + let raw = Box::into_raw(stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|error| { + // from_raw does not invoke release on failure, so reclaim the stream. + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(error.to_string(), Status::IO) + })?; + Ok(Box::new(reader)) +} + +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(), + }) + } + + /// Execute SQL using a temporary sf_core statement and release the + /// statement on every execution path. Execution errors take precedence over + /// cleanup errors; a release error is returned only after successful execution. + pub(crate) fn execute_temporary_statement( + &self, + connection: Handle, + query: impl Into, + timeout_seconds: Option, + ) -> Result<()> { + let statement = self + .sf + .statement_new(connection) + .map_err(crate::error::api_error_to_adbc_error)?; + let result = + self.execute_query_with_timeout(statement, query.into(), None, timeout_seconds); + let release = self.sf.statement_release(statement); + match result { + Ok(_) => release + .map_err(crate::error::api_error_to_adbc_error) + .map(|_| ()), + Err(error) => { + let _ = release; + Err(error) + } + } + } + + /// Execute a query while forwarding a per-statement timeout to sf_core. + /// Metadata and session helpers use [`Self::execute_query`] and therefore + /// retain the core/session default. + pub fn execute_query_with_timeout<'a>( + &self, + statement: Handle, + query: String, + bindings: Option>, + timeout_seconds: Option, + ) -> Result { + let result = self + .execute_query_raw_with_timeout(statement, query, bindings, timeout_seconds) + .map_err(crate::error::api_error_to_adbc_error)?; + self.acquire_execute_result(result) + } + + /// Execute through sf_core without erasing its typed error. Callers must + /// pass successful results to [`Self::acquire_execute_result`] so result + /// handles are released on every stream-acquisition path. + pub(crate) fn execute_query_raw_with_timeout<'a>( + &self, + statement: Handle, + query: String, + bindings: Option>, + timeout_seconds: Option, + ) -> std::result::Result { + self.runtime + .block_on(self.sf.statement_set_sql_query(statement, query))?; + self.runtime.block_on( + self.sf + .statement_execute_query(statement, bindings, timeout_seconds), + ) + } + + pub(crate) fn acquire_execute_result(&self, result: ExecuteQueryResult) -> Result { + let ExecuteQueryResult::Single(result) = result else { + return Err(Error::with_message_and_status( + "multi-statement query results are not supported", + Status::NotImplemented, + )); + }; + self.acquire_result(result) + } + + /// Synchronize SQL to sf_core and issue its describe-only prepare request. + pub(crate) fn prepare_statement( + &self, + statement: Handle, + query: String, + ) -> Result { + self.runtime + .block_on(self.sf.statement_set_sql_query(statement, query)) + .map_err(crate::error::api_error_to_adbc_error)?; + let result = self + .runtime + .block_on(self.sf.statement_prepare(statement)) + .map_err(crate::error::api_error_to_adbc_error)?; + let reader = bridge_arrow_reader(result.stream)?; + Ok(PreparedResult { + reader, + query_id: result.query_id, + number_of_binds: result.number_of_binds, + binds: result.binds, + }) + } + + /// Acquire a core result stream, release its handle on every path, and bridge + /// sf_core's Arrow version to the ADBC-facing Arrow version. + pub fn acquire_result(&self, result: ResultSetInfo) -> Result { + let stream = self + .runtime + .block_on(self.sf.result_set_get_stream(result.handle)); + let release = self.sf.result_set_release(result.handle); + + let reader = match stream { + Ok(reader) => { + release.map_err(crate::error::api_error_to_adbc_error)?; + reader + } + Err(error) => { + // The release was attempted above even though stream acquisition failed. + let _ = release; + return Err(crate::error::api_error_to_adbc_error(error)); + } + }; + + let reader = bridge_arrow_reader(reader)?; + + Ok(QueryResult { + reader, + descriptor: result.descriptor, + }) + } +} + +/// 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, + surfaced_warnings: Default::default(), + 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"); + } + + #[test] + fn arrow_reader_bridge_preserves_schema_and_owns_stream() { + let schema = Arc::new(arrow_sf_core::datatypes::Schema::new(vec![ + arrow_sf_core::datatypes::Field::new( + "value", + arrow_sf_core::datatypes::DataType::Int64, + true, + ), + ])); + let batch = arrow_sf_core::record_batch::RecordBatch::new_empty(schema.clone()); + let reader = Box::new(arrow_sf_core::record_batch::RecordBatchIterator::new( + std::iter::once(Ok(batch)), + schema, + )) as Box; + let reader = bridge_arrow_reader(reader).unwrap(); + + assert_eq!(reader.schema().field(0).name(), "value"); + assert_eq!( + reader.schema().field(0).data_type(), + &arrow_schema::DataType::Int64 + ); + } +} diff --git a/rust/src/error.rs b/rust/src/error.rs new file mode 100644 index 0000000..014cf9b --- /dev/null +++ b/rust/src/error.rs @@ -0,0 +1,231 @@ +// 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 std::os::raw::c_char; + +use adbc_core::error::{Error, Status}; +use sf_core::apis::database_driver_v1::ApiError; +use sf_core::rest::snowflake::{QUERY_CANCELED, RestError}; + +fn sqlstate_to_adbc(sqlstate: &str) -> Option<[c_char; 5]> { + let bytes: [u8; 5] = sqlstate.as_bytes().try_into().ok()?; + Some(bytes.map(|byte| byte as c_char)) +} + +fn is_snowflake_statement_timeout(message: &str) -> bool { + let message = message.to_ascii_lowercase(); + message.contains("statement or warehouse timeout") || message.contains("statement timeout") +} + +pub(crate) fn api_error_to_adbc_error(err: ApiError) -> Error { + let mut vendor_code = 0; + let mut sqlstate = [0; 5]; + let status = match &err { + ApiError::InvalidArgument { .. } => Status::InvalidArguments, + ApiError::Configuration { .. } => Status::InvalidArguments, + ApiError::ConnectionNotInitialized { .. } => Status::InvalidState, + ApiError::ConnectionClosed { .. } => Status::InvalidState, + ApiError::ConnectionLock { .. } => 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 { source, .. } => { + if let RestError::QueryFailed { + message, + code, + sql_state, + .. + } = source.as_ref() + { + vendor_code = code.unwrap_or_default(); + if let Some(value) = sql_state.as_deref().and_then(sqlstate_to_adbc) { + sqlstate = value; + } + + let server_cancel = + *code == Some(QUERY_CANCELED) || sql_state.as_deref() == Some("57014"); + if server_cancel && is_snowflake_statement_timeout(message) { + Status::Timeout + } else if server_cancel { + Status::Cancelled + } else { + Status::IO + } + } else { + Status::IO + } + } + ApiError::QueryResponseProcess { .. } => Status::IO, + ApiError::Statement { .. } => Status::IO, + ApiError::RuntimeCreation { .. } => Status::IO, + ApiError::GenericError { .. } => Status::IO, + ApiError::TokenCacheInitialization { .. } => Status::IO, + ApiError::ArrowParse { .. } => Status::IO, + ApiError::ChunkFetch { .. } => Status::IO, + ApiError::Base64Decode { .. } => Status::IO, + ApiError::HttpRequest { .. } => Status::IO, + ApiError::TokenRequest { .. } => Status::Unauthenticated, + ApiError::QueryTimeout { .. } | ApiError::CancelTimeout { .. } => Status::Timeout, + ApiError::Cancelled { .. } => Status::Cancelled, + _ => Status::IO, + }; + let mut error = Error::with_message_and_status(err.to_string(), status); + error.vendor_code = vendor_code; + error.sqlstate = sqlstate; + error +} + +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 cancellation_and_lifecycle_errors_map_to_specific_statuses() { + use std::time::Duration; + + let cases = [ + ( + ApiError::ConnectionClosed { + location: snafu::Location::default(), + }, + Status::InvalidState, + ), + ( + ApiError::QueryTimeout { + budget: Duration::from_secs(30), + location: snafu::Location::default(), + }, + Status::Timeout, + ), + ( + ApiError::CancelTimeout { + timeout: Duration::from_secs(5), + location: snafu::Location::default(), + }, + Status::Timeout, + ), + ( + ApiError::Cancelled { + location: snafu::Location::default(), + }, + Status::Cancelled, + ), + ]; + + for (error, expected) in cases { + assert_eq!(api_error_to_adbc_error(error).status, expected); + } + } + + fn query_failed(message: &str, code: Option, sql_state: Option<&str>) -> ApiError { + ApiError::Query { + source: Box::new(RestError::QueryFailed { + message: message.to_owned(), + code, + sql_state: sql_state.map(str::to_owned), + query_id: None, + request_id: None, + location: snafu::Location::default(), + }), + location: snafu::Location::default(), + } + } + + #[test] + fn server_cancellation_preserves_vendor_code_and_sqlstate() { + let error = api_error_to_adbc_error(query_failed( + "SQL execution canceled", + Some(QUERY_CANCELED), + Some("57014"), + )); + + assert_eq!(error.status, Status::Cancelled); + assert_eq!(error.vendor_code, QUERY_CANCELED); + assert_eq!(error.sqlstate, sqlstate_to_adbc("57014").unwrap()); + } + + #[test] + fn cancellation_sqlstate_is_sufficient_without_vendor_code() { + let error = api_error_to_adbc_error(query_failed( + "query canceled by request", + None, + Some("57014"), + )); + + assert_eq!(error.status, Status::Cancelled); + assert_eq!(error.vendor_code, 0); + assert_eq!(error.sqlstate, sqlstate_to_adbc("57014").unwrap()); + } + + #[test] + fn snowflake_statement_timeout_is_distinguished_from_other_cancellation() { + let error = api_error_to_adbc_error(query_failed( + "SQL execution canceled: Statement reached its statement or warehouse timeout of 1 second(s) and was canceled.", + Some(QUERY_CANCELED), + Some("57014"), + )); + + assert_eq!(error.status, Status::Timeout); + assert_eq!(error.vendor_code, QUERY_CANCELED); + assert_eq!(error.sqlstate, sqlstate_to_adbc("57014").unwrap()); + } + + #[test] + fn other_server_query_failure_keeps_io_status_and_diagnostics() { + let error = api_error_to_adbc_error(query_failed( + "object does not exist", + Some(2003), + Some("42S02"), + )); + + assert_eq!(error.status, Status::IO); + assert_eq!(error.vendor_code, 2003); + assert_eq!(error.sqlstate, sqlstate_to_adbc("42S02").unwrap()); + } + + #[test] + fn malformed_server_sqlstate_is_not_forwarded() { + let error = api_error_to_adbc_error(query_failed("failure", Some(1), Some("TOO-LONG"))); + + assert_eq!(error.sqlstate, [0; 5]); + } + + #[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..1b02f95 --- /dev/null +++ b/rust/src/get_objects.rs @@ -0,0 +1,876 @@ +// 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. + +use std::collections::BTreeMap; +use std::ops::Range; +use std::sync::Arc; + +use adbc_core::{ + error::{Error, Result as AdbcResult, Status}, + options::ObjectDepth, + schemas, +}; +use arrow_array::{ + Array, ArrayRef, BooleanArray, Int16Array, Int32Array, Int64Array, LargeListArray, ListArray, + RecordBatch, RecordBatchReader, StringArray, StructArray, new_empty_array, new_null_array, +}; +use arrow_buffer::{NullBuffer, OffsetBuffer, ScalarBuffer}; +use arrow_schema::{ArrowError, DataType, Field, Fields}; +use sf_core::apis::database_driver_v1::{ + DEPTH_CATALOGS, DEPTH_COLUMNS, DEPTH_DB_SCHEMAS, DEPTH_TABLES, GetObjectsRequest, +}; + +use crate::connection::{Connection, SingleBatchReader}; + +#[derive(Default)] +struct ObjectTree { + catalogs: BTreeMap, +} + +#[derive(Default)] +struct CatalogEntry { + schemas: BTreeMap, +} + +#[derive(Default)] +struct SchemaEntry { + tables: BTreeMap, +} + +#[derive(Default)] +struct TableEntry { + table_type: String, + columns: Vec, +} + +struct ColumnEntry { + name: String, + ordinal_position: i32, + remarks: Option, + xdbc_type_name: Option, + xdbc_column_size: Option, + xdbc_decimal_digits: Option, + xdbc_num_prec_radix: Option, + xdbc_nullable: Option, + column_default: Option, + xdbc_char_octet_length: Option, + xdbc_is_nullable: Option, +} + +pub(crate) fn execute_get_objects( + connection: &Connection, + depth: &ObjectDepth, + catalog: Option<&str>, + db_schema: Option<&str>, + table_name: Option<&str>, + table_type: Option>, + column_name: Option<&str>, +) -> AdbcResult> { + let table_types = table_type + .unwrap_or_default() + .into_iter() + .map(str::to_owned) + .collect::>(); + let mut objects = ObjectTree::default(); + + let catalogs = fetch_core_objects(connection, DEPTH_CATALOGS, catalog, None, None, &[], None)?; + merge_catalogs(&mut objects, catalogs); + + if !matches!(depth, ObjectDepth::Catalogs) { + let db_schemas = fetch_core_objects( + connection, + DEPTH_DB_SCHEMAS, + catalog, + db_schema, + None, + &[], + None, + )?; + merge_schemas(&mut objects, db_schemas); + } + + if matches!( + depth, + ObjectDepth::Tables | ObjectDepth::Columns | ObjectDepth::All + ) { + // DEPTH_TABLES is the authoritative source for table identity and type. + // In sf_core f9175f07 DEPTH_COLUMNS neither applies table_type nor fills + // table_type, so columns are only merged into this filtered table set. + let tables = fetch_core_objects( + connection, + DEPTH_TABLES, + catalog, + db_schema, + table_name, + &table_types, + None, + )?; + merge_tables(&mut objects, tables); + } + + if matches!(depth, ObjectDepth::Columns | ObjectDepth::All) { + let columns = fetch_core_objects( + connection, + DEPTH_COLUMNS, + catalog, + db_schema, + table_name, + &[], + column_name, + )?; + merge_columns(&mut objects, columns); + } + + let batch = build_batch(&objects, depth).map_err(arrow_to_adbc_error)?; + Ok(Box::new(SingleBatchReader::new(batch))) +} + +#[allow(clippy::too_many_arguments)] +fn fetch_core_objects( + connection: &Connection, + depth: i32, + catalog: Option<&str>, + db_schema: Option<&str>, + table_name: Option<&str>, + table_type: &[String], + column_name: Option<&str>, +) -> AdbcResult { + let request = GetObjectsRequest { + conn_handle: connection.conn_handle, + depth, + catalog: catalog.map(str::to_owned), + db_schema: db_schema.map(str::to_owned), + table_name: table_name.map(str::to_owned), + table_type: table_type.to_vec(), + column_name: column_name.map(str::to_owned), + }; + let result = connection + .inner + .runtime + .block_on(connection.inner.sf.connection_get_objects(request)) + .map_err(crate::error::api_error_to_adbc_error)?; + let mut reader = connection.inner.acquire_result(result)?.reader; + let mut objects = ObjectTree::default(); + for batch in &mut reader { + let batch = batch.map_err(arrow_to_adbc_error)?; + merge_all( + &mut objects, + parse_core_batch(&batch).map_err(arrow_to_adbc_error)?, + ); + } + Ok(objects) +} + +fn merge_catalogs(target: &mut ObjectTree, source: ObjectTree) { + for name in source.catalogs.into_keys() { + target.catalogs.entry(name).or_default(); + } +} + +fn merge_schemas(target: &mut ObjectTree, source: ObjectTree) { + for (catalog_name, catalog) in source.catalogs { + let target_catalog = target.catalogs.entry(catalog_name).or_default(); + for schema_name in catalog.schemas.into_keys() { + target_catalog.schemas.entry(schema_name).or_default(); + } + } +} + +fn merge_tables(target: &mut ObjectTree, source: ObjectTree) { + for (catalog_name, catalog) in source.catalogs { + let target_catalog = target.catalogs.entry(catalog_name).or_default(); + for (schema_name, schema) in catalog.schemas { + let target_schema = target_catalog.schemas.entry(schema_name).or_default(); + for (table_name, table) in schema.tables { + target_schema.tables.insert( + table_name, + TableEntry { + table_type: table.table_type, + columns: Vec::new(), + }, + ); + } + } + } +} + +fn merge_columns(target: &mut ObjectTree, source: ObjectTree) { + for (catalog_name, catalog) in source.catalogs { + let Some(target_catalog) = target.catalogs.get_mut(&catalog_name) else { + continue; + }; + for (schema_name, schema) in catalog.schemas { + let Some(target_schema) = target_catalog.schemas.get_mut(&schema_name) else { + continue; + }; + for (table_name, table) in schema.tables { + if let Some(target_table) = target_schema.tables.get_mut(&table_name) { + target_table.columns = table.columns; + } + } + } + } +} + +fn merge_all(target: &mut ObjectTree, source: ObjectTree) { + for (catalog_name, catalog) in source.catalogs { + let target_catalog = target.catalogs.entry(catalog_name).or_default(); + for (schema_name, schema) in catalog.schemas { + let target_schema = target_catalog.schemas.entry(schema_name).or_default(); + for (table_name, table) in schema.tables { + target_schema.tables.insert(table_name, table); + } + } + } +} + +fn parse_core_batch(batch: &RecordBatch) -> Result { + if batch.num_columns() != 2 { + return Err(schema_error(format!( + "sf_core get_objects returned {} top-level columns; expected 2", + batch.num_columns() + ))); + } + let catalog_names = expect_array::(batch.column(0), "catalog_name")?; + let schema_lists = expect_array::(batch.column(1), "catalog_db_schemas")?; + let schema_values = expect_array::(schema_lists.values(), "schema list values")?; + if schema_values.num_columns() != 2 { + return Err(schema_error("sf_core schema struct must have 2 fields")); + } + let schema_names = expect_array::(schema_values.column(0), "db_schema_name")?; + let table_lists = expect_array::(schema_values.column(1), "db_schema_tables")?; + let table_values = expect_array::(table_lists.values(), "table list values")?; + if table_values.num_columns() != 4 { + return Err(schema_error("sf_core table struct must have 4 fields")); + } + let table_names = expect_array::(table_values.column(0), "table_name")?; + let table_types = expect_array::(table_values.column(1), "table_type")?; + let column_lists = expect_array::(table_values.column(2), "table_columns")?; + let column_values = expect_array::(column_lists.values(), "column list values")?; + let column_arrays = CoreColumnArrays::try_new(column_values)?; + + let mut objects = ObjectTree::default(); + for catalog_index in 0..batch.num_rows() { + let catalog_name = required_string(catalog_names, catalog_index, "catalog_name")?; + let catalog = objects.catalogs.entry(catalog_name.to_owned()).or_default(); + let Some(schema_range) = list_range(schema_lists, catalog_index, "catalog_db_schemas")? + else { + continue; + }; + for schema_index in schema_range { + let schema_name = required_string(schema_names, schema_index, "db_schema_name")?; + let schema = catalog.schemas.entry(schema_name.to_owned()).or_default(); + let Some(table_range) = list_range(table_lists, schema_index, "db_schema_tables")? + else { + continue; + }; + for table_index in table_range { + let table_name = required_string(table_names, table_index, "table_name")?; + let table_type = if table_types.is_null(table_index) { + String::new() + } else { + table_types.value(table_index).to_owned() + }; + let mut table = TableEntry { + table_type, + columns: Vec::new(), + }; + if let Some(column_range) = list_range(column_lists, table_index, "table_columns")? + { + table.columns.reserve(column_range.len()); + for column_index in column_range { + table.columns.push(column_arrays.column(column_index)?); + } + } + schema.tables.insert(table_name.to_owned(), table); + } + } + } + Ok(objects) +} + +struct CoreColumnArrays<'a> { + names: &'a StringArray, + ordinals: &'a Int32Array, + logical_types: &'a StringArray, + precisions: &'a Int32Array, + scales: &'a Int32Array, + char_lengths: &'a Int64Array, + byte_lengths: &'a Int64Array, + nullables: &'a BooleanArray, + defaults: &'a StringArray, + remarks: &'a StringArray, +} + +impl<'a> CoreColumnArrays<'a> { + fn try_new(values: &'a StructArray) -> Result { + if values.num_columns() != 10 { + return Err(schema_error("sf_core column struct must have 10 fields")); + } + Ok(Self { + names: expect_array::(values.column(0), "column_name")?, + ordinals: expect_array::(values.column(1), "ordinal_position")?, + logical_types: expect_array::(values.column(2), "logical_type")?, + precisions: expect_array::(values.column(3), "precision")?, + scales: expect_array::(values.column(4), "scale")?, + char_lengths: expect_array::(values.column(5), "char_length")?, + byte_lengths: expect_array::(values.column(6), "byte_length")?, + nullables: expect_array::(values.column(7), "nullable")?, + defaults: expect_array::(values.column(8), "column_def")?, + remarks: expect_array::(values.column(9), "remarks")?, + }) + } + + fn column(&self, index: usize) -> Result { + let logical_type = optional_string(self.logical_types, index); + let nullable = (!self.nullables.is_null(index)).then(|| self.nullables.value(index)); + Ok(ColumnEntry { + name: required_string(self.names, index, "column_name")?.to_owned(), + ordinal_position: if self.ordinals.is_null(index) { + return Err(schema_error("sf_core ordinal_position is null")); + } else { + self.ordinals.value(index) + }, + remarks: optional_string(self.remarks, index).map(str::to_owned), + xdbc_type_name: logical_type.map(public_snowflake_type_name), + xdbc_column_size: if !self.char_lengths.is_null(index) { + Some(checked_i64_to_i32( + self.char_lengths.value(index), + "char_length", + )?) + } else { + optional_i32(self.precisions, index) + }, + xdbc_decimal_digits: xdbc_decimal_digits( + logical_type, + optional_i32(self.scales, index), + )?, + xdbc_num_prec_radix: logical_type.and_then(|logical_type| match logical_type { + "FIXED" => Some(10), + "REAL" => Some(2), + _ => None, + }), + xdbc_nullable: nullable.map(|value| if value { 1 } else { 0 }), + column_default: optional_string(self.defaults, index).map(str::to_owned), + xdbc_char_octet_length: optional_i64(self.byte_lengths, index) + .map(|value| checked_i64_to_i32(value, "byte_length")) + .transpose()?, + xdbc_is_nullable: nullable.map(|value| if value { "YES" } else { "NO" }.to_owned()), + }) + } +} + +fn xdbc_decimal_digits( + logical_type: Option<&str>, + scale: Option, +) -> Result, ArrowError> { + if !matches!(logical_type, Some("FIXED" | "REAL")) { + return Ok(None); + } + + scale + .map(|value| { + i16::try_from(value).map_err(|_| schema_error("sf_core scale does not fit in Int16")) + }) + .transpose() +} + +fn public_snowflake_type_name(logical_type: &str) -> String { + match logical_type.to_ascii_uppercase().as_str() { + "FIXED" => "NUMBER".to_owned(), + "REAL" => "DOUBLE".to_owned(), + "TEXT" => "VARCHAR".to_owned(), + "BINARY" => "BINARY".to_owned(), + "BOOLEAN" => "BOOLEAN".to_owned(), + "DATE" => "DATE".to_owned(), + "TIME" => "TIME".to_owned(), + "TIMESTAMP_NTZ" => "TIMESTAMP_NTZ".to_owned(), + "TIMESTAMP_LTZ" => "TIMESTAMP_LTZ".to_owned(), + "TIMESTAMP_TZ" => "TIMESTAMP_TZ".to_owned(), + "TIMESTAMP" => "TIMESTAMP_NTZ".to_owned(), + _ => logical_type.to_owned(), + } +} + +fn build_batch(objects: &ObjectTree, depth: &ObjectDepth) -> Result { + let schemas_null = matches!(depth, ObjectDepth::Catalogs); + let tables_null = matches!(depth, ObjectDepth::Catalogs | ObjectDepth::Schemas); + let columns_null = !matches!(depth, ObjectDepth::Columns | ObjectDepth::All); + + let catalogs = objects.catalogs.iter().collect::>(); + let db_schemas = catalogs + .iter() + .flat_map(|(_, catalog)| catalog.schemas.iter()) + .collect::>(); + let tables = db_schemas + .iter() + .flat_map(|(_, schema)| schema.tables.iter()) + .collect::>(); + let columns = tables + .iter() + .flat_map(|(_, table)| table.columns.iter()) + .collect::>(); + + let column_fields = struct_fields(&schemas::COLUMN_SCHEMA)?; + let column_values = StructArray::try_new( + column_fields.clone(), + vec![ + Arc::new(StringArray::from( + columns + .iter() + .map(|column| column.name.as_str()) + .collect::>(), + )) as ArrayRef, + Arc::new(Int32Array::from( + columns + .iter() + .map(|column| column.ordinal_position) + .collect::>(), + )), + Arc::new(StringArray::from( + columns + .iter() + .map(|column| column.remarks.as_deref()) + .collect::>(), + )), + new_null_array(column_fields[3].data_type(), columns.len()), + Arc::new(StringArray::from( + columns + .iter() + .map(|column| column.xdbc_type_name.as_deref()) + .collect::>(), + )), + Arc::new(Int32Array::from( + columns + .iter() + .map(|column| column.xdbc_column_size) + .collect::>(), + )), + Arc::new(Int16Array::from( + columns + .iter() + .map(|column| column.xdbc_decimal_digits) + .collect::>(), + )), + Arc::new(Int16Array::from( + columns + .iter() + .map(|column| column.xdbc_num_prec_radix) + .collect::>(), + )), + Arc::new(Int16Array::from( + columns + .iter() + .map(|column| column.xdbc_nullable) + .collect::>(), + )), + Arc::new(StringArray::from( + columns + .iter() + .map(|column| column.column_default.as_deref()) + .collect::>(), + )), + new_null_array(column_fields[10].data_type(), columns.len()), + new_null_array(column_fields[11].data_type(), columns.len()), + Arc::new(Int32Array::from( + columns + .iter() + .map(|column| column.xdbc_char_octet_length) + .collect::>(), + )), + Arc::new(StringArray::from( + columns + .iter() + .map(|column| column.xdbc_is_nullable.as_deref()) + .collect::>(), + )), + new_null_array(column_fields[14].data_type(), columns.len()), + new_null_array(column_fields[15].data_type(), columns.len()), + new_null_array(column_fields[16].data_type(), columns.len()), + new_null_array(column_fields[17].data_type(), columns.len()), + new_null_array(column_fields[18].data_type(), columns.len()), + ], + None, + )?; + + let column_item = list_item_from_type(&schemas::TABLE_SCHEMA, 2)?; + let column_list = if columns_null { + all_null_list(column_item, tables.len()) + } else { + let offsets = offsets_from_counts( + tables.iter().map(|(_, table)| table.columns.len()), + "table_columns", + )?; + ListArray::try_new( + column_item, + OffsetBuffer::new(ScalarBuffer::from(offsets)), + Arc::new(column_values), + None, + )? + }; + let constraint_list = all_null_list( + list_item_from_type(&schemas::TABLE_SCHEMA, 3)?, + tables.len(), + ); + + let table_values = StructArray::try_new( + struct_fields(&schemas::TABLE_SCHEMA)?, + vec![ + Arc::new(StringArray::from( + tables + .iter() + .map(|(name, _)| name.as_str()) + .collect::>(), + )) as ArrayRef, + Arc::new(StringArray::from( + tables + .iter() + .map(|(_, table)| table.table_type.as_str()) + .collect::>(), + )), + Arc::new(column_list), + Arc::new(constraint_list), + ], + None, + )?; + + let table_item = list_item_from_type(&schemas::OBJECTS_DB_SCHEMA_SCHEMA, 1)?; + let table_list = if tables_null { + all_null_list(table_item, db_schemas.len()) + } else { + let offsets = offsets_from_counts( + db_schemas.iter().map(|(_, schema)| schema.tables.len()), + "db_schema_tables", + )?; + ListArray::try_new( + table_item, + OffsetBuffer::new(ScalarBuffer::from(offsets)), + Arc::new(table_values), + None, + )? + }; + + let schema_values = StructArray::try_new( + struct_fields(&schemas::OBJECTS_DB_SCHEMA_SCHEMA)?, + vec![ + Arc::new(StringArray::from( + db_schemas + .iter() + .map(|(name, _)| name.as_str()) + .collect::>(), + )) as ArrayRef, + Arc::new(table_list), + ], + None, + )?; + + let schema_item = list_item(schemas::GET_OBJECTS_SCHEMA.field(1).data_type())?; + let schema_list = if schemas_null { + all_null_list(schema_item, catalogs.len()) + } else { + let offsets = offsets_from_counts( + catalogs.iter().map(|(_, catalog)| catalog.schemas.len()), + "catalog_db_schemas", + )?; + ListArray::try_new( + schema_item, + OffsetBuffer::new(ScalarBuffer::from(offsets)), + Arc::new(schema_values), + None, + )? + }; + + RecordBatch::try_new( + schemas::GET_OBJECTS_SCHEMA.clone(), + vec![ + Arc::new(StringArray::from( + catalogs + .iter() + .map(|(name, _)| name.as_str()) + .collect::>(), + )) as ArrayRef, + Arc::new(schema_list), + ], + ) +} + +fn offsets_from_counts( + counts: impl IntoIterator, + field: &str, +) -> Result, ArrowError> { + let mut offsets = vec![0i32]; + for count in counts { + let count = i32::try_from(count).map_err(|_| { + schema_error(format!( + "get_objects {field} count {count} exceeds ADBC List capacity" + )) + })?; + let next = offsets + .last() + .copied() + .and_then(|offset| offset.checked_add(count)) + .ok_or_else(|| schema_error(format!("get_objects {field} offset overflow")))?; + offsets.push(next); + } + Ok(offsets) +} + +fn list_range( + list: &LargeListArray, + index: usize, + field: &str, +) -> Result>, ArrowError> { + if list.is_null(index) { + return Ok(None); + } + let offsets = list.value_offsets(); + let start = usize::try_from(offsets[index]) + .map_err(|_| schema_error(format!("sf_core {field} has a negative offset")))?; + let end = usize::try_from(offsets[index + 1]) + .map_err(|_| schema_error(format!("sf_core {field} has a negative offset")))?; + if start > end || end > list.values().len() { + return Err(schema_error(format!( + "sf_core {field} offsets are out of bounds" + ))); + } + Ok(Some(start..end)) +} + +fn all_null_list(item: Arc, len: usize) -> ListArray { + ListArray::new( + item.clone(), + OffsetBuffer::new(ScalarBuffer::from(vec![0i32; len + 1])), + new_empty_array(item.data_type()), + (len > 0).then(|| NullBuffer::new_null(len)), + ) +} + +fn required_string<'a>( + array: &'a StringArray, + index: usize, + field: &str, +) -> Result<&'a str, ArrowError> { + if array.is_null(index) { + Err(schema_error(format!("sf_core {field} is null"))) + } else { + Ok(array.value(index)) + } +} + +fn optional_string(array: &StringArray, index: usize) -> Option<&str> { + (!array.is_null(index)).then(|| array.value(index)) +} + +fn optional_i32(array: &Int32Array, index: usize) -> Option { + (!array.is_null(index)).then(|| array.value(index)) +} + +fn optional_i64(array: &Int64Array, index: usize) -> Option { + (!array.is_null(index)).then(|| array.value(index)) +} + +fn checked_i64_to_i32(value: i64, field: &str) -> Result { + i32::try_from(value) + .map_err(|_| schema_error(format!("sf_core {field} value {value} exceeds Int32"))) +} + +fn expect_array<'a, T: 'static>(array: &'a dyn Array, field: &str) -> Result<&'a T, ArrowError> { + array + .as_any() + .downcast_ref::() + .ok_or_else(|| schema_error(format!("sf_core get_objects field {field} has wrong type"))) +} + +fn struct_fields(data_type: &DataType) -> Result { + match data_type { + DataType::Struct(fields) => Ok(fields.clone()), + _ => Err(schema_error("expected ADBC struct type")), + } +} + +fn list_item(data_type: &DataType) -> Result, ArrowError> { + match data_type { + DataType::List(item) => Ok(item.clone()), + _ => Err(schema_error("expected ADBC List type")), + } +} + +fn list_item_from_type(data_type: &DataType, field_index: usize) -> Result, ArrowError> { + let fields = struct_fields(data_type)?; + list_item(fields[field_index].data_type()) +} + +fn schema_error(message: impl Into) -> ArrowError { + ArrowError::SchemaError(message.into()) +} + +fn arrow_to_adbc_error(error: ArrowError) -> Error { + Error::with_message_and_status(error.to_string(), Status::Internal) +} + +#[cfg(test)] +mod tests { + use super::*; + use arrow_array::cast::AsArray; + + fn parent_tree() -> ObjectTree { + let mut objects = ObjectTree::default(); + objects + .catalogs + .entry("database".into()) + .or_default() + .schemas + .entry("public".into()) + .or_default(); + objects + } + + fn column(name: &str, logical_type: &str) -> ColumnEntry { + ColumnEntry { + name: name.into(), + ordinal_position: 1, + remarks: None, + xdbc_type_name: Some(public_snowflake_type_name(logical_type)), + xdbc_column_size: Some(38), + xdbc_decimal_digits: Some(0), + xdbc_num_prec_radix: Some(10), + xdbc_nullable: Some(1), + column_default: None, + xdbc_char_octet_length: None, + xdbc_is_nullable: Some("YES".into()), + } + } + + #[test] + fn public_xdbc_type_names_use_snowflake_sql_names() { + assert_eq!(public_snowflake_type_name("FIXED"), "NUMBER"); + assert_eq!(public_snowflake_type_name("TEXT"), "VARCHAR"); + assert_eq!(public_snowflake_type_name("REAL"), "DOUBLE"); + assert_eq!(public_snowflake_type_name("TIMESTAMP_LTZ"), "TIMESTAMP_LTZ"); + } + + #[test] + fn decimal_digits_only_use_numeric_scales() { + assert_eq!( + xdbc_decimal_digits(Some("FIXED"), Some(2)).unwrap(), + Some(2) + ); + assert_eq!(xdbc_decimal_digits(Some("REAL"), Some(7)).unwrap(), Some(7)); + assert_eq!(xdbc_decimal_digits(Some("TIME"), Some(9)).unwrap(), None); + assert_eq!( + xdbc_decimal_digits(Some("TIMESTAMP_NTZ"), Some(9)).unwrap(), + None + ); + assert_eq!(xdbc_decimal_digits(Some("TEXT"), Some(0)).unwrap(), None); + } + + #[test] + fn column_depth_keeps_filtered_table_types_and_ignores_other_column_tables() { + let mut objects = parent_tree(); + let mut filtered_tables = parent_tree(); + filtered_tables + .catalogs + .get_mut("database") + .unwrap() + .schemas + .get_mut("public") + .unwrap() + .tables + .insert( + "wanted_view".into(), + TableEntry { + table_type: "VIEW".into(), + columns: Vec::new(), + }, + ); + merge_tables(&mut objects, filtered_tables); + + let mut column_results = parent_tree(); + let tables = &mut column_results + .catalogs + .get_mut("database") + .unwrap() + .schemas + .get_mut("public") + .unwrap() + .tables; + tables.insert( + "wanted_view".into(), + TableEntry { + table_type: String::new(), + columns: vec![column("amount", "FIXED"), column("label", "TEXT")], + }, + ); + tables.insert( + "filtered_out_table".into(), + TableEntry { + table_type: String::new(), + columns: vec![column("hidden", "FIXED")], + }, + ); + merge_columns(&mut objects, column_results); + + let batch = build_batch(&objects, &ObjectDepth::Columns).unwrap(); + assert_eq!(batch.schema(), schemas::GET_OBJECTS_SCHEMA.clone()); + let schemas = batch.column(1).as_list::(); + let schema_values = schemas.values().as_struct(); + let table_lists = schema_values.column(1).as_list::(); + assert_eq!(table_lists.value_length(0), 1); + let table_values = table_lists.values().as_struct(); + assert_eq!( + table_values.column(0).as_string::().value(0), + "wanted_view" + ); + assert_eq!(table_values.column(1).as_string::().value(0), "VIEW"); + let column_values = table_values.column(2).as_list::().values().as_struct(); + let type_names = column_values.column(4).as_string::(); + assert_eq!(type_names.value(0), "NUMBER"); + assert_eq!(type_names.value(1), "VARCHAR"); + assert!(table_values.column(3).as_list::().is_null(0)); + } + + #[test] + fn deeper_filter_with_no_matches_retains_catalog_and_schema_parents() { + let objects = parent_tree(); + let batch = build_batch(&objects, &ObjectDepth::Columns).unwrap(); + let schema_lists = batch.column(1).as_list::(); + assert!(!schema_lists.is_null(0)); + assert_eq!(schema_lists.value_length(0), 1); + let schema_values = schema_lists.values().as_struct(); + assert_eq!( + schema_values.column(0).as_string::().value(0), + "public" + ); + let table_lists = schema_values.column(1).as_list::(); + assert!(!table_lists.is_null(0)); + assert_eq!(table_lists.value_length(0), 0); + } + + #[test] + fn shallower_depths_use_null_child_lists() { + let objects = parent_tree(); + let catalogs = build_batch(&objects, &ObjectDepth::Catalogs).unwrap(); + assert!(catalogs.column(1).as_list::().is_null(0)); + + let db_schemas = build_batch(&objects, &ObjectDepth::Schemas).unwrap(); + let schema_values = db_schemas.column(1).as_list::().values().as_struct(); + assert!(schema_values.column(1).as_list::().is_null(0)); + } + + #[test] + fn list_offset_overflow_is_rejected() { + let error = offsets_from_counts([i32::MAX as usize, 1], "test").unwrap_err(); + assert!(error.to_string().contains("offset overflow")); + } +} diff --git a/rust/src/ingest.rs b/rust/src/ingest.rs new file mode 100644 index 0000000..911d227 --- /dev/null +++ b/rust/src/ingest.rs @@ -0,0 +1,668 @@ +// 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. + +use adbc_core::error::{Error, Result, Status}; +use arrow_array::RecordBatch; +use arrow_schema::{DataType, Schema, TimeUnit}; +use sf_core::apis::database_driver_v1::{ApiError, BindingType, DataPtr}; + +use crate::statement::{Statement, arrow_batches_to_json_bindings, format_arrow_value_for_csv}; + +const INGEST_CHUNK_ROWS: usize = 500; +const INGEST_CHUNK_BYTES: usize = 900_000; +const DEFAULT_STAGE_ARRAY_BINDING_THRESHOLD: u64 = 65_280; +const STAGE_ARRAY_BINDING_THRESHOLD_PARAMETER: &str = "CLIENT_STAGE_ARRAY_BINDING_THRESHOLD"; + +struct EncodedBindingChunk { + rows: usize, + json: String, +} + +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 schema = stmt.bound_batches[0].schema(); + + match stmt + .ingest_mode + .as_deref() + .unwrap_or("adbc.ingest.mode.create") + { + "adbc.ingest.mode.create" => { + stmt.inner.execute_temporary_statement( + stmt.conn_handle, + build_create_sql(&qname, &schema, false)?, + stmt.query_timeout_seconds, + )?; + } + "adbc.ingest.mode.append" => {} + "adbc.ingest.mode.replace" => { + stmt.inner.execute_temporary_statement( + stmt.conn_handle, + format!("DROP TABLE IF EXISTS {qname}"), + stmt.query_timeout_seconds, + )?; + stmt.inner.execute_temporary_statement( + stmt.conn_handle, + build_create_sql(&qname, &schema, false)?, + stmt.query_timeout_seconds, + )?; + } + "adbc.ingest.mode.create_append" => { + stmt.inner.execute_temporary_statement( + stmt.conn_handle, + build_create_sql(&qname, &schema, true)?, + stmt.query_timeout_seconds, + )?; + } + other => { + return Err(Error::with_message_and_status( + format!("unknown ingest mode: {other}"), + Status::InvalidArguments, + )); + } + } + + let insert_sql = build_insert_sql(&qname, &schema); + let threshold = stage_binding_threshold(stmt)?; + if let Some(csv) = encode_csv_stage_binding(&stmt.bound_batches, threshold)? { + let csv_len = i64::try_from(csv.len()).map_err(|_| { + Error::with_message_and_status( + "CSV stage binding is too large", + Status::InvalidArguments, + ) + })?; + let binding = BindingType::Csv(DataPtr::new(csv.as_ptr(), csv_len)); + // Keep the CSV owner in scope until sf_core finishes uploading it. A + // StageBinding error occurs before the INSERT is submitted, so it is + // the only safe error on which to retry with inline JSON bindings. + match stmt.inner.execute_query_raw_with_timeout( + stmt.stmt_handle, + insert_sql.clone(), + Some(binding), + stmt.query_timeout_seconds, + ) { + Ok(result) => { + let result = stmt.inner.acquire_execute_result(result)?; + return Ok(result.descriptor.rows_affected); + } + Err(error) if should_fallback_from_stage_binding(&error) => {} + Err(error) => return Err(crate::error::api_error_to_adbc_error(error)), + } + } + + let mut total = Some(0i64); + for batch in &stmt.bound_batches { + for chunk in encode_binding_chunks(batch)? { + debug_assert!((1..=INGEST_CHUNK_ROWS).contains(&chunk.rows)); + let json_len = i64::try_from(chunk.json.len()).map_err(|_| { + Error::with_message_and_status( + "JSON binding is too large", + Status::InvalidArguments, + ) + })?; + let binding = BindingType::Json(DataPtr::new(chunk.json.as_ptr(), json_len)); + // The encoded String stays in scope until execute_query's block_on completes. + let result = stmt.execute_query(insert_sql.clone(), Some(binding))?; + + total = match (total, result.descriptor.rows_affected) { + (Some(acc), Some(rows)) => Some(acc.checked_add(rows).ok_or_else(|| { + Error::with_message_and_status("ingest row count overflow", Status::Internal) + })?), + _ => None, + }; + } + } + Ok(total) +} + +fn should_fallback_from_stage_binding(error: &ApiError) -> bool { + matches!(error, ApiError::StageBinding { .. }) +} + +fn parse_stage_binding_threshold(raw: Option<&str>) -> u64 { + raw.and_then(|value| value.parse::().ok()) + .map(u64::from) + .unwrap_or(DEFAULT_STAGE_ARRAY_BINDING_THRESHOLD) +} + +fn stage_binding_threshold(stmt: &Statement) -> Result { + let raw = stmt + .inner + .runtime + .block_on(stmt.inner.sf.connection_get_parameter( + stmt.conn_handle, + STAGE_ARRAY_BINDING_THRESHOLD_PARAMETER.to_string(), + )) + .map_err(crate::error::api_error_to_adbc_error)?; + Ok(parse_stage_binding_threshold(raw.as_deref())) +} + +fn effective_binding_cells(batches: &[RecordBatch]) -> Option { + batches.iter().try_fold(0u64, |total, batch| { + let rows = u64::try_from(batch.num_rows()).ok()?; + let columns = u64::try_from(batch.num_columns()).ok()?; + total.checked_add(rows.checked_mul(columns)?) + }) +} + +fn csv_stage_type_supported(data_type: &DataType) -> bool { + matches!( + data_type, + DataType::Int8 + | DataType::Int16 + | DataType::Int32 + | DataType::Int64 + | DataType::UInt8 + | DataType::UInt16 + | DataType::UInt32 + | DataType::UInt64 + | DataType::Float32 + | DataType::Float64 + | DataType::Decimal128(_, _) + | DataType::Utf8 + | DataType::LargeUtf8 + | DataType::Boolean + | DataType::Date32 + | DataType::Date64 + | DataType::Binary + | DataType::LargeBinary + | DataType::FixedSizeBinary(_) + | DataType::Time32(_) + | DataType::Time64(_) + | DataType::Timestamp(_, _) + ) +} + +fn append_csv_cell(output: &mut String, value: &str) { + // sf_core's stage COPY uses FIELD_OPTIONALLY_ENCLOSED_BY='"'. Quoting every + // non-NULL cell is RFC-4180-compatible and preserves NULL (bare empty) vs + // empty string (quoted empty); embedded quotes are doubled. + output.push('"'); + for character in value.chars() { + if character == '"' { + output.push('"'); + } + output.push(character); + } + output.push('"'); +} + +/// Build one row-major CSV payload for stage binding. Returning `None` is an +/// intentional signal to retain the existing chunked JSON path. +fn encode_csv_stage_binding(batches: &[RecordBatch], threshold: u64) -> Result> { + let Some(effective_cells) = effective_binding_cells(batches) else { + return Ok(None); + }; + if threshold == 0 || effective_cells == 0 || effective_cells < threshold { + return Ok(None); + } + + let Some(first) = batches.first() else { + return Ok(None); + }; + let schema = first.schema(); + if schema + .fields() + .iter() + .any(|field| !csv_stage_type_supported(field.data_type())) + || batches.iter().any(|batch| batch.schema() != schema) + { + return Ok(None); + } + + let mut output = String::new(); + for batch in batches { + for row in 0..batch.num_rows() { + for column in 0..batch.num_columns() { + if column != 0 { + output.push(','); + } + let array = batch.column(column); + if array.is_null(row) { + continue; + } + let value = format_arrow_value_for_csv( + array.as_ref(), + row, + schema.field(column).data_type(), + )?; + let Some(value) = value else { + // Non-finite floating point values follow the JSON path's + // NULL behavior but remain explicitly unsupported for CSV. + return Ok(None); + }; + if matches!( + schema.field(column).data_type(), + DataType::Utf8 | DataType::LargeUtf8 + ) && value == r"\N" + { + // sf_core COPY uses NULL_IF=('\\N'); quoting does not make + // this literal distinguishable from NULL on that path. + return Ok(None); + } + append_csv_cell(&mut output, &value); + } + output.push('\n'); + } + } + Ok(Some(output)) +} + +fn encode_binding_chunks(batch: &RecordBatch) -> Result> { + let mut chunks = Vec::new(); + let mut offset = 0; + while offset < batch.num_rows() { + let rows = INGEST_CHUNK_ROWS.min(batch.num_rows() - offset); + encode_binding_range(batch, offset, rows, &mut chunks)?; + offset += rows; + } + Ok(chunks) +} + +fn encode_binding_range( + batch: &RecordBatch, + offset: usize, + rows: usize, + output: &mut Vec, +) -> Result<()> { + let slice = batch.slice(offset, rows); + let json = arrow_batches_to_json_bindings(std::slice::from_ref(&slice))?; + if json.len() <= INGEST_CHUNK_BYTES { + output.push(EncodedBindingChunk { rows, json }); + return Ok(()); + } + if rows == 1 { + return Err(Error::with_message_and_status( + format!("single-row JSON binding exceeds the {INGEST_CHUNK_BYTES}-byte ingest limit"), + Status::InvalidArguments, + )); + } + + let first_rows = rows / 2; + encode_binding_range(batch, offset, first_rows, output)?; + encode_binding_range(batch, offset + first_rows, rows - first_rows, output) +} + +fn qualified_name(table: &str, catalog: Option<&str>, schema: Option<&str>) -> String { + let quote = |s: &str| format!("\"{}\"", s.replace('"', "\"\"")); + match (catalog, schema) { + (Some(c), Some(s)) => format!("{}.{}.{}", quote(c), quote(s), quote(table)), + (None, Some(s)) => format!("{}.{}", quote(s), quote(table)), + (Some(c), None) => format!("{}..{}", quote(c), quote(table)), + (None, None) => quote(table), + } +} + +fn build_create_sql(qname: &str, schema: &Schema, if_not_exists: bool) -> Result { + let columns = schema + .fields() + .iter() + .map(|field| { + let sf_type = to_sf_ddl(field.data_type())?; + let null_clause = if field.is_nullable() { "" } else { " NOT NULL" }; + Ok(format!( + "\"{}\" {sf_type}{null_clause}", + field.name().replace('"', "\"\"") + )) + }) + .collect::>>()?; + let exists = if if_not_exists { " IF NOT EXISTS" } else { "" }; + Ok(format!( + "CREATE TABLE{exists} {qname} ({})", + columns.join(", ") + )) +} + +fn build_insert_sql(qname: &str, schema: &Schema) -> String { + let columns = schema + .fields() + .iter() + .map(|field| format!("\"{}\"", field.name().replace('"', "\"\""))) + .collect::>(); + let placeholders = vec!["?"; columns.len()]; + format!( + "INSERT INTO {qname} ({}) VALUES ({})", + columns.join(", "), + placeholders.join(", ") + ) +} + +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(precision, scale) => format!("NUMERIC({precision},{scale})"), + DataType::Utf8 | DataType::LargeUtf8 => "text".to_string(), + DataType::Binary | DataType::LargeBinary => "binary".to_string(), + DataType::FixedSizeBinary(size) => format!("binary({size})"), + DataType::Boolean => "boolean".to_string(), + DataType::Date32 | DataType::Date64 => "date".to_string(), + DataType::Time32(unit) | DataType::Time64(unit) => { + format!("time({})", time_unit_precision(unit)) + } + DataType::Timestamp(unit, timezone) => { + let kind = if timezone.is_some() { + "timestamp_ltz" + } else { + "timestamp_ntz" + }; + format!("{kind}({})", time_unit_precision(unit)) + } + 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_precision(unit: &TimeUnit) -> u8 { + match unit { + TimeUnit::Second => 0, + TimeUnit::Millisecond => 3, + TimeUnit::Microsecond => 6, + TimeUnit::Nanosecond => 9, + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use arrow_array::{ + ArrayRef, BinaryArray, Date32Array, Decimal128Array, Int64Array, NullArray, RecordBatch, + StringArray, Time64NanosecondArray, TimestampMicrosecondArray, + }; + use arrow_schema::Field; + + #[test] + fn catalog_without_schema_uses_snowflake_double_dot_qualification() { + assert_eq!( + qualified_name("table", Some("catalog"), None), + "\"catalog\"..\"table\"" + ); + } + + #[test] + fn insert_sql_uses_quoted_columns_and_placeholders_only() { + let schema = Schema::new(vec![ + Field::new("id", DataType::Int64, false), + Field::new("odd\"name", DataType::Utf8, true), + ]); + assert_eq!( + build_insert_sql("\"db\".\"table\"", &schema), + "INSERT INTO \"db\".\"table\" (\"id\", \"odd\"\"name\") VALUES (?, ?)" + ); + } + + #[test] + fn ingest_binding_reuses_arrow_json_encoder_for_values_and_nulls() { + let schema = Arc::new(Schema::new(vec![ + Field::new("id", DataType::Int64, false), + Field::new("name", DataType::Utf8, true), + ])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(Int64Array::from(vec![1, 2])) as ArrayRef, + Arc::new(StringArray::from(vec![Some("not SQL ' text"), None])) as ArrayRef, + ], + ) + .unwrap(); + + let sql = build_insert_sql("\"target\"", &schema); + let json = arrow_batches_to_json_bindings(&[batch]).unwrap(); + assert_eq!( + sql, + "INSERT INTO \"target\" (\"id\", \"name\") VALUES (?, ?)" + ); + assert!(!sql.contains("not SQL")); + assert!(json.contains("\"value\":[\"not SQL ' text\",null]")); + } + + #[test] + fn stage_threshold_defaults_and_preserves_explicit_zero() { + assert_eq!(parse_stage_binding_threshold(None), 65_280); + assert_eq!(parse_stage_binding_threshold(Some("invalid")), 65_280); + assert_eq!(parse_stage_binding_threshold(Some("-1")), 65_280); + assert_eq!(parse_stage_binding_threshold(Some("4294967296")), 65_280); + assert_eq!(parse_stage_binding_threshold(Some("0")), 0); + assert_eq!(parse_stage_binding_threshold(Some("20")), 20); + } + + #[test] + fn csv_selection_uses_effective_cells_at_exact_threshold() { + let schema = Arc::new(Schema::new(vec![ + Field::new("left", DataType::Int64, false), + Field::new("right", DataType::Int64, false), + ])); + let batch = RecordBatch::try_new( + schema, + vec![ + Arc::new(Int64Array::from_iter_values(0..10)) as ArrayRef, + Arc::new(Int64Array::from_iter_values(10..20)) as ArrayRef, + ], + ) + .unwrap(); + + assert!( + encode_csv_stage_binding(std::slice::from_ref(&batch), 20) + .unwrap() + .is_some() + ); + assert!(encode_csv_stage_binding(&[batch], 21).unwrap().is_none()); + } + + #[test] + fn csv_distinguishes_null_and_empty_and_escapes_rfc4180_hazards() { + let schema = Arc::new(Schema::new(vec![Field::new("value", DataType::Utf8, true)])); + let batch = RecordBatch::try_new( + schema, + vec![Arc::new(StringArray::from(vec![ + None, + Some(""), + Some("comma, quote \" and\nnewline"), + ])) as ArrayRef], + ) + .unwrap(); + + let csv = encode_csv_stage_binding(&[batch], 1).unwrap().unwrap(); + assert_eq!(csv, "\n\"\"\n\"comma, quote \"\" and\nnewline\"\n"); + } + + #[test] + fn csv_literal_backslash_n_text_falls_back_to_json() { + let schema = Arc::new(Schema::new(vec![Field::new( + "value", + DataType::Utf8, + false, + )])); + let batch = RecordBatch::try_new( + schema, + vec![Arc::new(StringArray::from(vec![r"\N"])) as ArrayRef], + ) + .unwrap(); + + assert!(encode_csv_stage_binding(&[batch], 1).unwrap().is_none()); + } + + #[test] + fn only_typed_stage_binding_errors_allow_json_retry() { + use sf_core::stage_binding::StageBindingError; + + let stage_error = ApiError::StageBinding { + source: Box::new(StageBindingError::Disabled { + location: snafu::Location::default(), + }), + location: snafu::Location::default(), + }; + let submitted_query_error = ApiError::Cancelled { + location: snafu::Location::default(), + }; + + assert!(should_fallback_from_stage_binding(&stage_error)); + assert!(!should_fallback_from_stage_binding(&submitted_query_error)); + } + + #[test] + fn csv_preserves_decimal_binary_and_temporal_wire_values() { + let decimal = Decimal128Array::from(vec![12345i128]) + .with_precision_and_scale(10, 2) + .unwrap(); + let schema = Arc::new(Schema::new(vec![ + Field::new("decimal", DataType::Decimal128(10, 2), false), + Field::new("binary", DataType::Binary, false), + Field::new("date", DataType::Date32, false), + Field::new("time", DataType::Time64(TimeUnit::Nanosecond), false), + Field::new( + "timestamp", + DataType::Timestamp(TimeUnit::Microsecond, None), + false, + ), + ])); + let batch = RecordBatch::try_new( + schema, + vec![ + Arc::new(decimal) as ArrayRef, + Arc::new(BinaryArray::from(vec![&b"\x00\xff"[..]])) as ArrayRef, + Arc::new(Date32Array::from(vec![365_000])) as ArrayRef, + Arc::new(Time64NanosecondArray::from(vec![123])) as ArrayRef, + Arc::new(TimestampMicrosecondArray::from(vec![1_234_567])) as ArrayRef, + ], + ) + .unwrap(); + + assert_eq!( + encode_csv_stage_binding(&[batch], 1).unwrap().unwrap(), + "\"123.45\",\"00ff\",\"31536000000000000000\",\"123\",\"1234567000\"\n" + ); + } + + #[test] + fn csv_combines_all_compatible_batches_into_one_payload() { + let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)])); + let first = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(Int64Array::from(vec![1, 2])) as ArrayRef], + ) + .unwrap(); + let second = RecordBatch::try_new( + schema, + vec![Arc::new(Int64Array::from(vec![3])) as ArrayRef], + ) + .unwrap(); + + assert_eq!( + encode_csv_stage_binding(&[first, second], 3) + .unwrap() + .unwrap(), + "\"1\"\n\"2\"\n\"3\"\n" + ); + } + + #[test] + fn unsupported_csv_type_deliberately_falls_back_to_json_path() { + let schema = Arc::new(Schema::new(vec![Field::new( + "unsupported", + DataType::Null, + true, + )])); + let batch = + RecordBatch::try_new(schema, vec![Arc::new(NullArray::new(1)) as ArrayRef]).unwrap(); + + assert!(encode_csv_stage_binding(&[batch], 1).unwrap().is_none()); + } + + #[test] + fn binding_batches_are_chunked_to_at_most_five_hundred_rows() { + let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)])); + let batch = RecordBatch::try_new( + schema, + vec![Arc::new(Int64Array::from_iter_values(0..1_201)) as ArrayRef], + ) + .unwrap(); + + let chunks = encode_binding_chunks(&batch).unwrap(); + assert_eq!( + chunks.iter().map(|chunk| chunk.rows).collect::>(), + vec![500, 500, 201] + ); + assert!(chunks.iter().all(|chunk| chunk.rows <= INGEST_CHUNK_ROWS)); + } + + #[test] + fn large_json_chunks_are_split_below_the_byte_ceiling() { + let schema = Arc::new(Schema::new(vec![Field::new( + "value", + DataType::Utf8, + false, + )])); + let values = (0..10).map(|_| "x".repeat(100_000)).collect::>(); + let batch = RecordBatch::try_new( + schema, + vec![Arc::new(StringArray::from(values)) as ArrayRef], + ) + .unwrap(); + + let chunks = encode_binding_chunks(&batch).unwrap(); + assert!(chunks.len() > 1); + assert_eq!(chunks.iter().map(|chunk| chunk.rows).sum::(), 10); + assert!( + chunks + .iter() + .all(|chunk| chunk.json.len() <= INGEST_CHUNK_BYTES) + ); + } + + #[test] + fn empty_schema_still_generates_a_parameterized_shape() { + assert_eq!( + build_insert_sql("\"target\"", &Schema::empty()), + "INSERT INTO \"target\" () VALUES ()" + ); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..cac593e --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,36 @@ +// 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 +// sf_core's query future exceeds rustc's default type-layout query depth in codegen. +#![recursion_limit = "256"] + +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..e126be5 --- /dev/null +++ b/rust/src/statement.rs @@ -0,0 +1,3130 @@ +// 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::{collections::VecDeque, 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, ColumnMetadata, DataPtr, Handle}; +use sf_core::config::{param_registry::param_names, settings::Setting}; +use sf_core::rest::snowflake::AbortOutcome; + +use crate::driver::{Inner, QueryResult, TimestampPrecision}; + +/// Per-statement Snowflake query timeout, in seconds. A value of zero clears +/// the override and lets the session/core default apply. +const QUERY_TIMEOUT_OPTION: &str = "adbc.snowflake.statement.query_timeout"; + +fn parse_query_timeout(value: &OptionValue) -> Result> { + let seconds = match value { + OptionValue::Int(value) => u32::try_from(*value).map_err(|_| { + Error::with_message_and_status( + "query_timeout must be between 0 and 4294967295 seconds", + Status::InvalidArguments, + ) + })?, + OptionValue::String(value) => { + let raw = value.strip_suffix('s').unwrap_or(value); + raw.parse::().map_err(|_| { + Error::with_message_and_status( + format!("invalid query_timeout value: {value}"), + Status::InvalidArguments, + ) + })? + } + _ => { + return Err(Error::with_message_and_status( + "query_timeout must be a string or int", + Status::InvalidArguments, + )); + } + }; + Ok((seconds != 0).then_some(seconds)) +} + +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) query_timeout_seconds: 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, + /// Whether bind() / bind_stream() was called, including with an empty stream. + pub(crate) binding_supplied: bool, + pub(crate) last_query_id: Option, + /// Cached only when sf_core's prepare bind count and metadata agree. + pub(crate) prepared_parameter_schema: Option, + /// Truthful result schema returned by Snowflake's describe-only prepare. + pub(crate) prepared_result_schema: 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); + self.prepared_parameter_schema = None; + self.prepared_result_schema = None; + 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); + self.prepared_parameter_schema = None; + self.prepared_result_schema = None; + 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"; + } + self.prepared_parameter_schema = None; + self.prepared_result_schema = None; + 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, + }; + } + self.prepared_parameter_schema = None; + self.prepared_result_schema = None; + Ok(()) + } + OptionStatement::Temporary => match value { + OptionValue::String(value) => match value.as_str() { + "false" | "0" | "disabled" => Ok(()), + "true" | "1" | "enabled" => Err(Error::with_message_and_status( + "temporary ingestion is not supported", + Status::NotImplemented, + )), + _ => Err(Error::with_message_and_status( + "temporary ingestion must be enabled or disabled", + Status::InvalidArguments, + )), + }, + _ => Err(Error::with_message_and_status( + "temporary ingestion must be a string", + Status::InvalidArguments, + )), + }, + OptionStatement::TargetCatalog => { + if let OptionValue::String(s) = value { + self.ingest_catalog = Some(s); + } + self.prepared_parameter_schema = None; + self.prepared_result_schema = None; + Ok(()) + } + OptionStatement::TargetDbSchema => { + if let OptionValue::String(s) = value { + self.ingest_schema = Some(s); + } + self.prepared_parameter_schema = None; + self.prepared_result_schema = None; + Ok(()) + } + OptionStatement::Other(ref k) if k == QUERY_TIMEOUT_OPTION => { + self.query_timeout_seconds = parse_query_timeout(&value)?; + Ok(()) + } + OptionStatement::Other(ref k) if k == "adbc.snowflake.statement.query_tag" => { + if let OptionValue::String(s) = value { + self.inner + .runtime + .block_on(self.inner.sf.statement_set_option( + self.stmt_handle, + param_names::QUERY_TAG.into(), + Setting::String(s.clone()), + )) + .map_err(crate::error::api_error_to_adbc_error)?; + // Update ADBC-visible storage only after sf_core accepts the option. + 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 == QUERY_TIMEOUT_OPTION => { + Ok(self.query_timeout_seconds.unwrap_or(0).to_string()) + } + 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 { + match key { + OptionStatement::Other(ref k) if k == QUERY_TIMEOUT_OPTION => { + Ok(i64::from(self.query_timeout_seconds.unwrap_or(0))) + } + _ => 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 { + fn execution_timeout_seconds(&self) -> Option { + self.query_timeout_seconds + } + + /// Route every user-statement execution through the statement timeout. + /// The binding owner must remain alive until this synchronous call returns. + pub(crate) fn execute_query<'a>( + &self, + query: String, + bindings: Option>, + ) -> Result { + self.inner.execute_query_with_timeout( + self.stmt_handle, + query, + bindings, + self.execution_timeout_seconds(), + ) + } + + /// Execute a parameterized query once per bound row and concatenate results. + fn execute_bound( + &mut self, + query: String, + ) -> Result> { + let row_count: usize = self.bound_batches.iter().map(RecordBatch::num_rows).sum(); + if row_count == 0 { + // ADBC defines this as zero executions. Describe-only prepare provides + // the result schema without accidentally running the SQL unbound. + self.last_query_id = None; + let schema = if let Some(schema) = &self.prepared_result_schema { + Arc::new(schema.clone()) + } else { + let prepared = self.inner.prepare_statement(self.stmt_handle, query)?; + self.prepared_parameter_schema = parameter_schema_from_prepare( + prepared.number_of_binds, + &prepared.binds, + self.use_high_precision, + self.timestamp_precision.time_unit(), + ); + let schema = adjust_schema( + &prepared.reader.schema(), + self.use_high_precision, + self.timestamp_precision.time_unit(), + ); + self.prepared_result_schema = Some(schema.as_ref().clone()); + schema + }; + return execute_bound_rows(&self.bound_batches, schema, |_| { + unreachable!("zero-row bindings must not execute") + }); + } + + let inner = self.inner.clone(); + let stmt_handle = self.stmt_handle; + let timeout_seconds = self.execution_timeout_seconds(); + let use_high_precision = self.use_high_precision; + let timestamp_precision = self.timestamp_precision; + let mut last_query_id = self.last_query_id.clone(); + let result = execute_bound_rows(&self.bound_batches, Arc::new(Schema::empty()), |row| { + let json_bytes = arrow_batches_to_json_bindings(&[row])?; + let data_ptr = DataPtr::new(json_bytes.as_ptr(), json_bytes.len() as i64); + let result = inner.execute_query_with_timeout( + stmt_handle, + query.clone(), + Some(BindingType::Json(data_ptr)), + timeout_seconds, + )?; + last_query_id = Some(result.descriptor.query_id.clone()); + Ok(Box::new(ConvertingReader::new( + result.reader, + use_high_precision, + timestamp_precision.time_unit(), + timestamp_precision, + ))) + }); + // On a later-row failure, retain the ID of the last successful execution. + self.last_query_id = last_query_id; + result + } +} + +impl adbc_core::Statement for Statement { + fn bind(&mut self, batch: RecordBatch) -> Result<()> { + self.bound_batches = vec![batch]; + self.binding_supplied = true; + 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; + self.binding_supplied = true; + 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(); + self.binding_supplied = false; + 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) + })?; + + if self.binding_supplied { + return self.execute_bound(query); + } + + let result = self.execute_query(query, None)?; + + self.last_query_id = Some(result.descriptor.query_id.clone()); + let reader = result.reader; + 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(); + self.binding_supplied = false; + return result; + } + let query = self.query.clone().ok_or_else(|| { + Error::with_message_and_status("cannot execute without a query", Status::InvalidState) + })?; + + // A supplied binding with no rows represents zero executions, so do not + // run the statement unbound or send an empty binding. + if self.binding_supplied { + let row_count: usize = self.bound_batches.iter().map(RecordBatch::num_rows).sum(); + if row_count == 0 { + self.last_query_id = None; + return Ok(Some(0)); + } + } + + // Parameterised DML: execute once with JSON bindings. + if self.binding_supplied { + 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.execute_query(query, Some(binding))?; + + self.last_query_id = Some(result.descriptor.query_id.clone()); + + return Ok(result.descriptor.rows_affected); + } + + let result = self.execute_query(query, None)?; + + self.last_query_id = Some(result.descriptor.query_id.clone()); + + Ok(result.descriptor.rows_affected) + } + + 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.prepared_parameter_schema = None; + self.prepared_result_schema = None; + let prepared = self.inner.prepare_statement(self.stmt_handle, query)?; + self.last_query_id = Some(prepared.query_id.clone()); + self.prepared_parameter_schema = parameter_schema_from_prepare( + prepared.number_of_binds, + &prepared.binds, + self.use_high_precision, + self.timestamp_precision.time_unit(), + ); + let schema = adjust_schema( + &prepared.reader.schema(), + self.use_high_precision, + self.timestamp_precision.time_unit(), + ) + .as_ref() + .clone(); + self.prepared_result_schema = Some(schema.clone()); + Ok(schema) + } + + fn execute_partitions(&mut self) -> Result { + Err(crate::error::not_implemented("execute_partitions")) + } + + fn get_parameter_schema(&self) -> Result { + self.prepared_parameter_schema.clone().ok_or_else(|| { + Error::with_message_and_status( + "parameter metadata is unavailable; prepare the statement first", + Status::NotImplemented, + ) + }) + } + + fn prepare(&mut self) -> Result<()> { + let query = self.query.clone().ok_or_else(|| { + Error::with_message_and_status( + "cannot prepare statement with no query", + Status::InvalidState, + ) + })?; + self.prepared_parameter_schema = None; + self.prepared_result_schema = None; + let prepared = self.inner.prepare_statement(self.stmt_handle, query)?; + self.last_query_id = Some(prepared.query_id); + self.prepared_parameter_schema = parameter_schema_from_prepare( + prepared.number_of_binds, + &prepared.binds, + self.use_high_precision, + self.timestamp_precision.time_unit(), + ); + self.prepared_result_schema = Some( + adjust_schema( + &prepared.reader.schema(), + self.use_high_precision, + self.timestamp_precision.time_unit(), + ) + .as_ref() + .clone(), + ); + Ok(()) + } + + 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(); + self.binding_supplied = false; + self.prepared_parameter_schema = None; + self.prepared_result_schema = None; + 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<()> { + match self + .inner + .runtime + .block_on(self.inner.sf.statement_cancel(self.stmt_handle)) + .map_err(crate::error::api_error_to_adbc_error)? + { + AbortOutcome::Aborted | AbortOutcome::NotRunning => Ok(()), + } + } +} + +// ── Bound query result concatenation ───────────────────────────────────────── + +struct ChainedReader { + readers: VecDeque>, + schema: Arc, +} + +impl Iterator for ChainedReader { + type Item = std::result::Result; + + fn next(&mut self) -> Option { + loop { + let reader = self.readers.front_mut()?; + if let Some(batch) = reader.next() { + return Some(batch); + } + self.readers.pop_front(); + } + } +} + +impl RecordBatchReader for ChainedReader { + fn schema(&self) -> Arc { + self.schema.clone() + } +} + +fn execute_bound_rows( + batches: &[RecordBatch], + empty_schema: Arc, + mut execute: F, +) -> Result> +where + F: FnMut(RecordBatch) -> Result>, +{ + let mut readers = VecDeque::new(); + let mut result_schema = None; + + for batch in batches { + for row_index in 0..batch.num_rows() { + let reader = execute(batch.slice(row_index, 1))?; + let schema = reader.schema(); + if let Some(expected) = &result_schema { + if schema.as_ref() != expected { + return Err(Error::with_message_and_status( + format!( + "bound query result schema mismatch: expected {expected:?}, got {schema:?}" + ), + Status::InvalidData, + )); + } + } else { + result_schema = Some(schema.as_ref().clone()); + } + readers.push_back(reader); + } + } + + Ok(Box::new(ChainedReader { + readers, + schema: Arc::new(result_schema.unwrap_or_else(|| empty_schema.as_ref().clone())), + })) +} + +// Test reader for constructing deterministic Arrow streams. +#[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 parameter_schema_from_prepare( + number_of_binds: i32, + binds: &[ColumnMetadata], + use_high_precision: bool, + ts_unit: TimeUnit, +) -> Option { + let bind_count = usize::try_from(number_of_binds).ok()?; + if bind_count != binds.len() || binds.iter().any(|bind| bind.r#type.trim().is_empty()) { + return None; + } + + let fields = binds + .iter() + .map(|bind| { + let logical_type = bind.r#type.to_ascii_uppercase(); + let scale = bind.scale.unwrap_or(0); + let physical_type = match logical_type.as_str() { + "FIXED" => DataType::Int64, + "REAL" => DataType::Float64, + "BOOLEAN" => DataType::Boolean, + "DATE" => DataType::Date32, + "TIME" if scale < 6 => DataType::Int32, + "TIME" => DataType::Int64, + "TIMESTAMP_NTZ" | "TIMESTAMP_LTZ" | "TIMESTAMP_TZ" => DataType::Int64, + "BINARY" => DataType::Binary, + "TEXT" | "ARRAY" | "OBJECT" | "VARIANT" | "GEOGRAPHY" | "GEOMETRY" => { + DataType::Utf8 + } + _ => DataType::Null, + }; + let mut metadata = std::collections::HashMap::from([ + ("logicalType".to_string(), logical_type), + ("nullable".to_string(), bind.nullable.to_string()), + ]); + if let Some(precision) = bind.precision { + metadata.insert("precision".into(), precision.to_string()); + } + if let Some(scale) = bind.scale { + metadata.insert("scale".into(), scale.to_string()); + } + Field::new(&bind.name, physical_type, bind.nullable).with_metadata(metadata) + }) + .collect::>(); + Some( + adjust_schema(&Schema::new(fields), use_high_precision, ts_unit) + .as_ref() + .clone(), + ) +} + +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 { + // Snowflake stores two-field TIMESTAMP_TZ epochs in physical unit groups: + // seconds for scale 0, milliseconds for 1-3, microseconds for 4-6, + // and nanoseconds for 7-9. The timezone offset is display metadata; the + // epoch already identifies the UTC instant. + let epoch_to_ns = match scale.clamp(0, 9) { + 0 => 1_000_000_000, + 1..=3 => 1_000_000, + 4..=6 => 1_000, + _ => 1, + }; + let utc_ns = epoch.value(i) as i128 * epoch_to_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 { + // The epoch/fraction pair is already a UTC instant. The timezone offset + // only preserves the original display offset and must not shift the value. + let utc_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(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 +} + +pub(crate) 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, values) in col_values.iter_mut().enumerate() { + let col = batch.column(col_idx); + let dt = col.data_type(); + for row in 0..batch.num_rows() { + if col.is_null(row) { + values.push(None); + continue; + } + let val = format_arrow_value(col.as_ref(), row, dt)?; + values.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, + )), + } +} + +/// Format a value for sf_core's CSV/stage binding path. Snowflake bulk DATE +/// binding uses epoch milliseconds until that representation reaches the +/// server overflow boundary, then switches to epoch nanoseconds, matching the +/// universal-driver Python serializer. +pub(crate) fn format_arrow_value_for_csv( + arr: &dyn Array, + row: usize, + dt: &DataType, +) -> Result> { + use arrow_array::{Date32Array, Date64Array}; + + const DATE_BULK_INSERTION_MS_OVERFLOW: i128 = 31_536_000_000_000; + let epoch_ms = match dt { + DataType::Date32 => { + let array = arr.as_any().downcast_ref::().ok_or_else(|| { + Error::with_message_and_status( + "Date32 data type is not backed by Date32Array", + Status::InvalidArguments, + ) + })?; + Some(i128::from(array.value(row)) * 86_400_000) + } + DataType::Date64 => { + let array = arr.as_any().downcast_ref::().ok_or_else(|| { + Error::with_message_and_status( + "Date64 data type is not backed by Date64Array", + Status::InvalidArguments, + ) + })?; + Some(i128::from(array.value(row))) + } + _ => None, + }; + + if let Some(epoch_ms) = epoch_ms { + return Ok(Some( + if epoch_ms < DATE_BULK_INSERTION_MS_OVERFLOW { + epoch_ms + } else { + epoch_ms * 1_000_000 + } + .to_string(), + )); + } + format_arrow_value(arr, row, dt) +} + +fn decimal128_to_string(value: i128, scale: i8) -> String { + if scale < 0 { + return format!("{value}{}", "0".repeat(scale.unsigned_abs() as usize)); + } + 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 + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use adbc_core::Statement as _; + + fn bind_metadata( + name: &str, + logical_type: &str, + precision: Option, + scale: Option, + ) -> ColumnMetadata { + ColumnMetadata { + name: name.into(), + r#type: logical_type.into(), + precision, + scale, + length: None, + byte_length: None, + nullable: true, + dimension: None, + fixed: false, + column_src_database: String::new(), + column_src_schema: String::new(), + column_src_table: String::new(), + is_auto_increment: false, + ext_col_type_name: String::new(), + udt_output_type: String::new(), + } + } + + 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, + query_timeout_seconds: None, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, + bound_batches: vec![], + binding_supplied: false, + last_query_id: None, + prepared_parameter_schema: None, + prepared_result_schema: 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 query_timeout_parses_round_trips_and_clears() { + let mut stmt = make_stmt(); + let key = OptionStatement::Other(QUERY_TIMEOUT_OPTION.into()); + + stmt.set_option(key.clone(), OptionValue::String("30".into())) + .unwrap(); + assert_eq!(stmt.execution_timeout_seconds(), Some(30)); + stmt.set_option(key.clone(), OptionValue::String("30s".into())) + .unwrap(); + assert_eq!(stmt.execution_timeout_seconds(), Some(30)); + assert_eq!(stmt.get_option_string(key.clone()).unwrap(), "30"); + assert_eq!(stmt.get_option_int(key.clone()).unwrap(), 30); + + stmt.set_option(key.clone(), OptionValue::Int(7)).unwrap(); + assert_eq!(stmt.execution_timeout_seconds(), Some(7)); + + stmt.set_option(key.clone(), OptionValue::String("0".into())) + .unwrap(); + assert_eq!(stmt.execution_timeout_seconds(), None); + assert_eq!(stmt.get_option_int(key).unwrap(), 0); + } + + #[test] + fn query_timeout_rejects_invalid_values_without_changing_state() { + let mut stmt = make_stmt(); + let key = OptionStatement::Other(QUERY_TIMEOUT_OPTION.into()); + stmt.set_option(key.clone(), OptionValue::Int(5)).unwrap(); + + for value in [ + OptionValue::Int(-1), + OptionValue::String(String::new()), + OptionValue::String("30ms".into()), + OptionValue::Double(30.0), + ] { + let error = stmt.set_option(key.clone(), value).unwrap_err(); + assert_eq!(error.status, Status::InvalidArguments); + assert_eq!(stmt.execution_timeout_seconds(), Some(5)); + } + } + + #[test] + fn cancel_without_inflight_query_succeeds_on_valid_core_statement() { + let mut stmt = make_stmt(); + let inner = stmt.inner.clone(); + let conn_handle = inner.sf.connection_new(); + stmt.conn_handle = conn_handle; + stmt.stmt_handle = inner.sf.statement_new(conn_handle).unwrap(); + + stmt.cancel().unwrap(); + + drop(stmt); + // Statement::drop released the statement; release the core connection. + inner.sf.connection_release(conn_handle).unwrap(); + } + + #[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 test_zero_row_query_binding() { + let mut stmt = make_stmt(); + stmt.set_sql_query("SELECT ?").unwrap(); + let schema = Arc::new(Schema::new(vec![Field::new( + "VALUE", + DataType::Int64, + true, + )])); + stmt.prepared_result_schema = Some(schema.as_ref().clone()); + stmt.last_query_id = Some("previous-query-id".into()); + stmt.bind(RecordBatch::new_empty(Arc::new(Schema::new(vec![ + Field::new("parameter", DataType::Int64, false), + ])))) + .unwrap(); + + let mut reader = stmt.execute().expect("zero rows should not execute SQL"); + assert_eq!(reader.schema(), schema); + assert!(reader.next().is_none()); + assert_eq!(stmt.last_query_id, None); + } + + #[test] + fn execute_update_with_empty_bind_stream_returns_zero_rows() { + let mut stmt = make_stmt(); + stmt.set_sql_query("DELETE FROM example").unwrap(); + + let reader = ConcatReader { + batches: vec![].into_iter(), + schema: Arc::new(Schema::new(vec![Field::new( + "value", + DataType::Int64, + false, + )])), + }; + stmt.bind_stream(Box::new(reader)).unwrap(); + + assert_eq!(stmt.execute_update().unwrap(), Some(0)); + assert_eq!(stmt.last_query_id, None); + } + + #[test] + fn execute_update_with_zero_row_batch_returns_zero_rows() { + let mut stmt = make_stmt(); + stmt.set_sql_query("DELETE FROM example WHERE id = ?") + .unwrap(); + let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)])); + stmt.bind(RecordBatch::new_empty(schema)).unwrap(); + + assert_eq!(stmt.execute_update().unwrap(), Some(0)); + assert_eq!(stmt.last_query_id, None); + } + + #[test] + fn execute_update_with_zero_rows_clears_previous_query_id_without_execution() { + let mut stmt = make_stmt(); + stmt.set_sql_query("DELETE FROM example WHERE id = ?") + .unwrap(); + stmt.query_tag = Some("should_not_be_applied".into()); + stmt.last_query_id = Some("previous-query-id".into()); + + let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)])); + stmt.bind(RecordBatch::new_empty(schema)).unwrap(); + + assert_eq!(stmt.execute_update().unwrap(), Some(0)); + assert_eq!(stmt.last_query_id, None); + } + + #[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, + query_timeout_seconds: None, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, + bound_batches: vec![], + binding_supplied: false, + last_query_id: None, + prepared_parameter_schema: None, + prepared_result_schema: 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, + query_timeout_seconds: None, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, + bound_batches: vec![], + binding_supplied: false, + last_query_id: None, + prepared_parameter_schema: None, + prepared_result_schema: 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 valid_prepare_bind_metadata_uses_stable_names_and_existing_conversions() { + let schema = parameter_schema_from_prepare( + 3, + &[ + bind_metadata("", "FIXED", Some(10), Some(2)), + bind_metadata("named", "TIMESTAMP_NTZ", None, Some(6)), + bind_metadata("unknown", "FUTURE_TYPE", None, None), + ], + true, + TimeUnit::Nanosecond, + ) + .unwrap(); + + assert_eq!(schema.field(0).name(), ""); + assert_eq!(schema.field(0).data_type(), &DataType::Decimal128(10, 2)); + assert_eq!(schema.field(1).name(), "named"); + assert_eq!( + schema.field(1).data_type(), + &DataType::Timestamp(TimeUnit::Microsecond, None) + ); + assert_eq!(schema.field(2).name(), "unknown"); + assert_eq!(schema.field(2).data_type(), &DataType::Null); + } + + #[test] + fn invalid_prepare_bind_metadata_is_not_exposed() { + let bind = bind_metadata("", "FIXED", Some(10), Some(0)); + assert!( + parameter_schema_from_prepare( + 2, + std::slice::from_ref(&bind), + true, + TimeUnit::Nanosecond + ) + .is_none() + ); + let mut missing_type = bind; + missing_type.r#type.clear(); + assert!( + parameter_schema_from_prepare(1, &[missing_type], true, TimeUnit::Nanosecond).is_none() + ); + assert!(parameter_schema_from_prepare(-1, &[], true, TimeUnit::Nanosecond).is_none()); + } + + #[test] + fn parameter_schema_requires_valid_prepared_metadata_and_query_reset_clears_it() { + let mut stmt = make_stmt(); + assert_eq!( + stmt.get_parameter_schema().unwrap_err().status, + Status::NotImplemented + ); + stmt.prepared_parameter_schema = Some(Schema::empty()); + assert!(stmt.get_parameter_schema().unwrap().fields().is_empty()); + stmt.set_sql_query("SELECT 1").unwrap(); + assert!(stmt.prepared_parameter_schema.is_none()); + } + + #[test] + fn parameter_schema_cache_is_invalidated_by_conversion_and_ingest_options() { + let cases = [ + ( + OptionStatement::Other( + "adbc.snowflake.sql.client_option.use_high_precision".into(), + ), + OptionValue::String("disabled".into()), + ), + ( + OptionStatement::Other( + "adbc.snowflake.sql.client_option.max_timestamp_precision".into(), + ), + OptionValue::String("microseconds".into()), + ), + ( + OptionStatement::TargetTable, + OptionValue::String("target".into()), + ), + ( + OptionStatement::IngestMode, + OptionValue::String("append".into()), + ), + ( + OptionStatement::TargetCatalog, + OptionValue::String("catalog".into()), + ), + ( + OptionStatement::TargetDbSchema, + OptionValue::String("schema".into()), + ), + ]; + + for (option, value) in cases { + let mut stmt = make_stmt(); + stmt.prepared_parameter_schema = Some(Schema::empty()); + stmt.set_option(option, value).unwrap(); + assert!(stmt.prepared_parameter_schema.is_none()); + } + } + + #[test] + fn temporary_ingestion_rejects_enabled_values() { + for value in ["true", "1", "enabled"] { + let mut stmt = make_stmt(); + let error = stmt + .set_option( + OptionStatement::Temporary, + OptionValue::String(value.into()), + ) + .unwrap_err(); + assert_eq!(error.status, Status::NotImplemented, "value {value}"); + } + } + + #[test] + fn temporary_ingestion_accepts_disabled_values() { + for value in ["false", "0", "disabled"] { + let mut stmt = make_stmt(); + stmt.set_option( + OptionStatement::Temporary, + OptionValue::String(value.into()), + ) + .unwrap(); + } + } + + #[test] + fn temporary_ingestion_rejects_invalid_values() { + let mut stmt = make_stmt(); + let error = stmt + .set_option( + OptionStatement::Temporary, + OptionValue::String("sometimes".into()), + ) + .unwrap_err(); + assert_eq!(error.status, Status::InvalidArguments); + + let error = stmt + .set_option(OptionStatement::Temporary, OptionValue::Int(0)) + .unwrap_err(); + assert_eq!(error.status, Status::InvalidArguments); + } + + #[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(); + let inner = stmt.inner.clone(); + let conn_handle = stmt.inner.sf.connection_new(); + stmt.conn_handle = conn_handle; + stmt.stmt_handle = stmt.inner.sf.statement_new(conn_handle).unwrap(); + + stmt.set_sql_query("SELECT 1").unwrap(); + stmt.set_option( + OptionStatement::Other("adbc.snowflake.statement.query_tag".into()), + OptionValue::String("my_tag".into()), + ) + .unwrap(); + stmt.set_sql_query("SELECT 2").unwrap(); + assert_eq!(stmt.query.as_deref(), Some("SELECT 2")); + assert_eq!( + stmt.get_option_string(OptionStatement::Other( + "adbc.snowflake.statement.query_tag".into() + )) + .unwrap(), + "my_tag" + ); + + drop(stmt); + // Statement::drop releases the statement before its connection. + inner.sf.connection_release(conn_handle).unwrap(); + } + + #[test] + fn rejected_query_tag_does_not_update_adbc_storage() { + let mut stmt = make_stmt(); + let error = stmt + .set_option( + OptionStatement::Other("adbc.snowflake.statement.query_tag".into()), + OptionValue::String("not_stored".into()), + ) + .unwrap_err(); + assert_eq!(error.status, Status::InvalidArguments); + assert_eq!(stmt.query_tag, None); + } + + #[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_tz_2field_uses_storage_unit_groups_and_ignores_offset() { + use arrow_array::{Int32Array, Int64Array, StructArray}; + + for (scale, epoch, expected_ns) in [ + (1, 1_234i64, 1_234_000_000i64), + (4, 1_234_567i64, 1_234_567_000i64), + (7, 1_234_567_890i64, 1_234_567_890i64), + ] { + let epoch = Int64Array::from(vec![epoch]); + let offset = Int32Array::from(vec![960]); // -08:00 display offset + let struct_arr = StructArray::from(vec![ + ( + Arc::new(Field::new("epoch", DataType::Int64, false)), + Arc::new(epoch.clone()) as ArrayRef, + ), + ( + Arc::new(Field::new("offset", DataType::Int32, false)), + Arc::new(offset.clone()) as ArrayRef, + ), + ]); + let result = build_timestamp_tz_2field( + &epoch, + &offset, + &struct_arr, + scale, + false, + DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("UTC"))), + ) + .unwrap(); + let timestamps = result + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(timestamps.value(0), expected_ns, "scale {scale}"); + } + } + + #[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 = [ + 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 = [ + 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_ignores_display_offset() { + let epoch = arrow_array::Int64Array::from(vec![1i64]); + let fraction = arrow_array::Int32Array::from(vec![500_000_000i32]); + let offset = arrow_array::Int32Array::from(vec![960i32]); // -08:00 + let struct_arr = arrow_array::StructArray::from(vec![ + ( + Arc::new(Field::new("epoch", DataType::Int64, false)), + Arc::new(epoch.clone()) as ArrayRef, + ), + ( + Arc::new(Field::new("fraction", DataType::Int32, false)), + Arc::new(fraction.clone()) as ArrayRef, + ), + ( + Arc::new(Field::new("offset", DataType::Int32, false)), + Arc::new(offset.clone()) as ArrayRef, + ), + ]); + let result = build_timestamp_tz_3field( + &epoch, + &fraction, + &offset, + &struct_arr, + 9, + false, + DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("UTC"))), + ) + .unwrap(); + let timestamps = result + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(timestamps.value(0), 1_500_000_000); + } + + #[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 = [ + 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 = [ + 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_single_row_query_binding() { + let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)])); + let batch = RecordBatch::try_new( + schema, + vec![Arc::new(arrow_array::Int64Array::from(vec![7])) as ArrayRef], + ) + .unwrap(); + let result_schema = Arc::new(Schema::new(vec![Field::new( + "value", + DataType::Int64, + false, + )])); + let mut executions = 0; + + let mut reader = execute_bound_rows(&[batch], Arc::new(Schema::empty()), |row| { + executions += 1; + let value = row + .column(0) + .as_any() + .downcast_ref::() + .unwrap() + .value(0); + let output = RecordBatch::try_new( + result_schema.clone(), + vec![Arc::new(arrow_array::Int64Array::from(vec![value]))], + ) + .unwrap(); + Ok(Box::new(ConcatReader { + batches: vec![output].into_iter(), + schema: result_schema.clone(), + })) + }) + .unwrap(); + + assert_eq!(executions, 1); + let output = reader.next().unwrap().unwrap(); + assert_eq!( + output + .column(0) + .as_any() + .downcast_ref::() + .unwrap() + .value(0), + 7 + ); + assert!(reader.next().is_none()); + } + + #[test] + fn test_multi_row_query_binding() { + let parameter_schema = + Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)])); + let batches = vec![ + RecordBatch::try_new( + parameter_schema.clone(), + vec![Arc::new(arrow_array::Int64Array::from(vec![1, 2]))], + ) + .unwrap(), + RecordBatch::try_new( + parameter_schema, + vec![Arc::new(arrow_array::Int64Array::from(vec![3]))], + ) + .unwrap(), + ]; + let result_schema = Arc::new(Schema::new(vec![Field::new( + "value", + DataType::Int64, + false, + )])); + let mut executed_values = Vec::new(); + + let mut reader = execute_bound_rows(&batches, Arc::new(Schema::empty()), |row| { + let value = row + .column(0) + .as_any() + .downcast_ref::() + .unwrap() + .value(0); + executed_values.push(value); + let output = RecordBatch::try_new( + result_schema.clone(), + vec![Arc::new(arrow_array::Int64Array::from(vec![value * 10]))], + ) + .unwrap(); + Ok(Box::new(ConcatReader { + batches: vec![output].into_iter(), + schema: result_schema.clone(), + })) + }) + .unwrap(); + + assert_eq!(executed_values, vec![1, 2, 3]); + let values = reader + .by_ref() + .map(|batch| { + batch + .unwrap() + .column(0) + .as_any() + .downcast_ref::() + .unwrap() + .value(0) + }) + .collect::>(); + assert_eq!(values, vec![10, 20, 30]); + } + + #[test] + fn test_bound_query_schema_mismatch() { + let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)])); + let batch = RecordBatch::try_new( + schema, + vec![Arc::new(arrow_array::Int64Array::from(vec![1, 2]))], + ) + .unwrap(); + let mut execution = 0; + + let error = match execute_bound_rows(&[batch], Arc::new(Schema::empty()), |_| { + execution += 1; + let output_schema = Arc::new(Schema::new(vec![Field::new( + "value", + if execution == 1 { + DataType::Int64 + } else { + DataType::Utf8 + }, + false, + )])); + let output = RecordBatch::new_empty(output_schema.clone()); + Ok(Box::new(ConcatReader { + batches: vec![output].into_iter(), + schema: output_schema, + })) + }) { + Ok(_) => panic!("incompatible result schemas should fail"), + Err(error) => error, + }; + + assert_eq!(error.status, Status::InvalidData); + assert!(error.message.contains("schema mismatch")); + } + + #[test] + fn test_bound_query_stops_after_execution_failure() { + let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(arrow_array::Int64Array::from(vec![1, 2, 3]))], + ) + .unwrap(); + let mut executions = 0; + + let error = match execute_bound_rows(&[batch], Arc::new(Schema::empty()), |_| { + executions += 1; + if executions == 2 { + return Err(Error::with_message_and_status( + "injected execution failure", + Status::InvalidData, + )); + } + Ok(Box::new(ConcatReader { + batches: Vec::new().into_iter(), + schema: schema.clone(), + })) + }) { + Ok(_) => panic!("execution failure should be returned"), + Err(error) => error, + }; + + assert_eq!(executions, 2); + assert_eq!(error.status, Status::InvalidData); + assert!(error.message.contains("injected execution failure")); + } + + #[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![std::f64::consts::PI])) 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"); + // Zero and negative scales + assert_eq!(decimal128_to_string(-42, 0), "-42"); + assert_eq!(decimal128_to_string(123, -2), "12300"); + assert_eq!(decimal128_to_string(-123, -2), "-12300"); + } +} diff --git a/rust/tests/integration.rs b/rust/tests/integration.rs new file mode 100644 index 0000000..2129ece --- /dev/null +++ b/rust/tests/integration.rs @@ -0,0 +1,761 @@ +// 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(); + let rows = stmt.execute_update().expect("create table"); + assert_eq!(rows, None, "sf_core should classify DDL as no-result"); + } + + { + 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(); + let rows = stmt.execute_update().expect("drop table"); + assert_eq!(rows, None, "sf_core should classify DDL as no-result"); + } +} + +// ── 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 the canonical tls_skip_verify option round-trips. +#[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 the legacy OCSP option remains an opaque round-trippable option. +#[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_prepare_parameter_schema_and_describe_only_execution() { + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + let table_name = format!("ADBC_RUST_PREPARE_TEST_{}", std::process::id()); + let mut ddl = conn.new_statement().unwrap(); + ddl.set_sql_query(format!( + "CREATE OR REPLACE TEMP TABLE {table_name} (VALUE NUMBER(10,2))" + )) + .unwrap(); + ddl.execute_update().expect("create prepare test table"); + + // A typed target column gives Snowflake a fully describable statement. + // Snowflake still truthfully reports JSON bind transport as TEXT rather + // than inventing the target column's logical NUMBER type. + let mut parameterized = conn.new_statement().unwrap(); + parameterized + .set_sql_query(format!("INSERT INTO {table_name} (VALUE) VALUES (?)")) + .unwrap(); + parameterized + .prepare() + .expect("prepare parameterized query"); + let parameter_schema = parameterized + .get_parameter_schema() + .expect("parameter metadata"); + assert_eq!(parameter_schema.fields().len(), 1); + assert_eq!(parameter_schema.field(0).data_type(), &DataType::Utf8); + + let mut insert = conn.new_statement().unwrap(); + insert + .set_sql_query(format!("INSERT INTO {table_name} VALUES (1.25)")) + .unwrap(); + let schema = insert.execute_schema().expect("describe insert"); + assert_eq!(schema.fields().len(), 1); + assert_eq!(schema.field(0).name(), "number of rows inserted"); + assert_eq!(schema.field(0).data_type(), &DataType::Int64); + + let mut count = conn.new_statement().unwrap(); + count + .set_sql_query(format!("SELECT COUNT(*) FROM {table_name}")) + .unwrap(); + let batch = count + .execute() + .expect("count rows") + .next() + .expect("count batch") + .expect("count result"); + assert_eq!( + batch + .column(0) + .as_primitive::() + .value(0), + 0, + "execute_schema must not execute the INSERT" + ); +} + +#[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..85cd319 --- /dev/null +++ b/rust/validation/README_sf_core_limitations.md @@ -0,0 +1,81 @@ + + +# 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), converts `DBL_MAX` to `inf`, and cannot losslessly represent negative nanosecond timestamps. +**Workaround in Driver:** After login, the driver executes `ALTER SESSION SET QUERY_RESULT_FORMAT = 'ARROW_FORCE'`. This requests Arrow transport explicitly while preserving the driver's honest wrapper identity. +**Ideal Fix:** + +- **Snowflake backend:** Provide a result-format session parameter that is independent of connector identity. +- **sf_core:** Expose a native way to request Arrow format reliably without depending on a specific `CLIENT_APP_ID`. + +## 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..975a613 --- /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_LTZ(3)" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamptz" ("idx" INT, "res" TIMESTAMP_LTZ(3), "res2" TIMESTAMP_LTZ(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..c2aa5a7 --- /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_LTZ(9)" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamptz" ("idx" INT, "res" TIMESTAMP_LTZ(9), "res2" TIMESTAMP_LTZ(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..6126677 --- /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_LTZ(0)" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamptz" ("idx" INT, "res" TIMESTAMP_LTZ(0), "res2" TIMESTAMP_LTZ(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..70e0bdf --- /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_LTZ(6)" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamptz" ("idx" INT, "res" TIMESTAMP_LTZ(6), "res2" TIMESTAMP_LTZ(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..4c0be52 --- /dev/null +++ b/rust/validation/tests/snowflake.py @@ -0,0 +1,97 @@ +# 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=True, + 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 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 diff --git a/vendor/adbc-drivers-dev/LICENSE.txt b/vendor/adbc-drivers-dev/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/vendor/adbc-drivers-dev/LICENSE.txt @@ -0,0 +1,202 @@ + + 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. diff --git a/vendor/adbc-drivers-dev/MANIFEST.in b/vendor/adbc-drivers-dev/MANIFEST.in new file mode 100644 index 0000000..1d748ff --- /dev/null +++ b/vendor/adbc-drivers-dev/MANIFEST.in @@ -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. + +include adbc_drivers_dev/.env +include adbc_drivers_dev/compose.yaml +recursive-include adbc_drivers_dev/compose/ Dockerfile *.ld *.sh +recursive-include adbc_drivers_dev/templates/ *.toml *.yaml diff --git a/vendor/adbc-drivers-dev/NOTICE.txt b/vendor/adbc-drivers-dev/NOTICE.txt new file mode 100644 index 0000000..5b7f871 --- /dev/null +++ b/vendor/adbc-drivers-dev/NOTICE.txt @@ -0,0 +1,5 @@ +ADBC Drivers Foundry Development Tools +Copyright 2025 ADBC Drivers Contributors + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/vendor/adbc-drivers-dev/README.md b/vendor/adbc-drivers-dev/README.md new file mode 100644 index 0000000..9588a70 --- /dev/null +++ b/vendor/adbc-drivers-dev/README.md @@ -0,0 +1,32 @@ + + +# Vendored `adbc-drivers-dev` + +Source: + +Exact upstream revision: `eea5591d7d1c7fe0047c7d1a1109afe1be50e4b4` + +License: Apache-2.0; the upstream license and notice are included as `LICENSE.txt` and `NOTICE.txt`. + +Included scope: the complete installable `adbc_drivers_dev` package, `pyproject.toml`, `MANIFEST.in`, `LICENSE.txt`, `NOTICE.txt`, and the complete upstream `tests/test_detect_version.py` module from the revision above. + +Local modifications: + +1. `adbc_drivers_dev/make.py` prefixes the development-version git revision with `g`, producing SemVer-compatible versions such as `v1.2.3-dev.1.g0747374`. +2. Linux Go and Rust builds use writable `/tmp` caches when manylinux containers run with the host UID. +3. The manylinux Rust image uses Rust 1.94.0, matching `sf_core`'s declared minimum supported Rust version, installs the Perl IPC and time modules required by vendored OpenSSL builds, and uses a project-specific image suffix so Compose builds the modified image instead of reusing the upstream image. +4. `tests/test_detect_version.py` includes deterministic regressions for the build-script changes. diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/.env b/vendor/adbc-drivers-dev/adbc_drivers_dev/.env new file mode 100644 index 0000000..9bb9602 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/.env @@ -0,0 +1,20 @@ +# 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. + +GO=1.26.1 +LIBCLANG=18.1.1 +MANYLINUX=manylinux2014 +RUST=1.94.0 +VCPKG_REVISION=e0b785fa42e9b3ee27f62e54f65e41358a1d2671 +VCPKG_SHALLOW_SINCE=2026-05-14 diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/__init__.py b/vendor/adbc-drivers-dev/adbc_drivers_dev/__init__.py new file mode 100644 index 0000000..0b1d246 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/__init__.py @@ -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/vendor/adbc-drivers-dev/adbc_drivers_dev/changelog.py b/vendor/adbc-drivers-dev/adbc_drivers_dev/changelog.py new file mode 100644 index 0000000..8203321 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/changelog.py @@ -0,0 +1,111 @@ +# 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 collections +import datetime +from pathlib import Path, PurePosixPath + +import pygit2 +import tomlkit + +from . import title_check + + +def parse_manifest(repo: pygit2.Repository, path: str, tree: pygit2.Tree) -> str: + name = f"{path}/manifest.toml" + name = str(PurePosixPath(name)) + diff = tree.diff_to_tree() + for patch in diff: + if patch.delta.old_file.path == name: + blob = repo[patch.delta.old_file.id] + manifest = tomlkit.loads(blob.data) + driver_name = manifest.get("name") + return driver_name + + raise FileNotFoundError(f"{name} not found in tree {tree.id}") + + +def generate_changelog( + root: Path, subpath: str, version: str, start_ref: str | None, end_ref: str +) -> (str, str): + repo = pygit2.Repository(root) + + end_commit, _ = repo.resolve_refish(end_ref) + if start_ref is None: + start_commit = None + else: + start_commit, _ = repo.resolve_refish(start_ref) + + driver_name = parse_manifest(repo, subpath, end_commit.tree) + + # Parse conventional commit metadata and build a curated changelog + changelog = collections.defaultdict(list) + # Track all commits affecting the subdirectory + all_commits = [] + + walker = repo.walk(end_commit.id, pygit2.GIT_SORT_TIME) + if start_commit: + walker.hide(start_commit.id) + + pred = title_check.any_components() + for commit in walker: + # If the driver lives in the repo root, include all commits. Else, + # scope down to commits that touch the driver subpath. + include = subpath == "." + + if commit.parents: + prev = commit.parents[0] + diff = prev.tree.diff_to_tree(commit.tree) + for patch in diff: + include = include or patch.delta.new_file.path.startswith(subpath) + include = include or patch.delta.old_file.path.startswith(subpath) + if include: + break + else: + for blob in commit.tree: + include = include or blob.name.startswith(subpath) + if include: + break + + message = commit.message.splitlines()[0] + if include: + all_commits.append((commit.short_id, message)) + + parsed = title_check.matches_commit_format(pred, message) + changelog[parsed.category].append(parsed) + + date = datetime.datetime.utcnow().strftime("%Y-%m-%d") + title = f"{driver_name} {version} ({date})" + + lines = [] + for category, heading in ( + ("feat", "New Features"), + ("fix", "Bug Fixes"), + ("perf", "Performance Improvements"), + ("docs", "Documentation Updates"), + ): + if category in changelog and changelog[category]: + lines.append(f"## {heading}") + lines.append("") + for commit in changelog[category]: + lines.append(f"- {commit.subject}") + lines.append("") + + lines.append("## Detailed Changelog") + lines.append("") + for commit in all_commits: + lines.append(f"- {commit[0]}: {commit[1]}") + + rendered = "\n".join(lines).strip() + return title, rendered diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/compose.yaml b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose.yaml new file mode 100644 index 0000000..f42952d --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose.yaml @@ -0,0 +1,57 @@ +# 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. + +services: + manylinux-cpp: + # Raise manylinux baseline to get newer GCC + image: ghcr.io/adbc-drivers/dev:manylinux_2_28-cpp${GO} + build: + context: ./compose/manylinux-cpp + args: + MANYLINUX: manylinux_2_28 + VCPKG_REVISION: ${VCPKG_REVISION} + VCPKG_SHALLOW_SINCE: ${VCPKG_SHALLOW_SINCE} + platforms: + - linux/amd64 + - linux/arm64 + volumes: + - ${SOURCE_ROOT:-.}:/source:rw + + manylinux: + image: ghcr.io/adbc-drivers/dev:${MANYLINUX}-go${GO} + build: + context: ./compose/manylinux + args: + GO: ${GO} + MANYLINUX: ${MANYLINUX} + platforms: + - linux/amd64 + - linux/arm64 + volumes: + - ${SOURCE_ROOT:-.}:/source:rw + + manylinux-rust: + # Local suffix prevents reuse of the upstream image, which lacks IPC::Cmd. + image: ghcr.io/adbc-drivers/dev:${MANYLINUX}-rust${RUST}-snowflake2 + build: + context: ./compose/manylinux-rust + args: + LIBCLANG: ${LIBCLANG} + RUST: ${RUST} + MANYLINUX: ${MANYLINUX} + platforms: + - linux/amd64 + - linux/arm64 + volumes: + - ${SOURCE_ROOT:-.}:/source:rw diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-cpp/Dockerfile b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-cpp/Dockerfile new file mode 100644 index 0000000..81bf4aa --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-cpp/Dockerfile @@ -0,0 +1,61 @@ +# 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. + +ARG MANYLINUX +FROM quay.io/pypa/${MANYLINUX}:latest + +RUN ulimit -n 1024 && \ + yum install -y \ + autoconf \ + automake \ + binutils \ + cmake \ + curl \ + gcc \ + gcc-c++ \ + kernel-devel \ + make \ + ninja-build \ + patch \ + patchutils \ + perl \ + perl-IPC-Cmd \ + pkgconfig \ + tar \ + unzip \ + wget \ + zip \ + && \ + yum clean all +ENV PATH="/usr/local/go/bin:${PATH}" + +ARG VCPKG_REVISION +ARG VCPKG_SHALLOW_SINCE +ARG TARGETPLATFORM + +ENV VCPKG_ROOT=/opt/vcpkg + +RUN git clone --shallow-since="${VCPKG_SHALLOW_SINCE}" https://github.com/microsoft/vcpkg "${VCPKG_ROOT}" \ + && cd "${VCPKG_ROOT}" \ + && git checkout --detach "${VCPKG_REVISION}" \ + && test "$(git rev-parse HEAD)" = "${VCPKG_REVISION}" \ + && ./bootstrap-vcpkg.sh -disableMetrics \ + && chmod -R a+rwX "${VCPKG_ROOT}" +ENV PATH="${VCPKG_ROOT}:${PATH}" + +RUN git config --global --add safe.directory /source \ + && cp /root/.gitconfig /.gitconfig \ + && chmod 777 /.gitconfig + +COPY only-export-adbc.ld /only-export-adbc.ld diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-cpp/only-export-adbc.ld b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-cpp/only-export-adbc.ld new file mode 100644 index 0000000..5741f70 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-cpp/only-export-adbc.ld @@ -0,0 +1,21 @@ +# 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. + +{ + global: + Adbc*; + + local: + *; +}; diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-rust/Dockerfile b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-rust/Dockerfile new file mode 100644 index 0000000..21d48ad --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-rust/Dockerfile @@ -0,0 +1,38 @@ +# 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. + +ARG MANYLINUX +FROM quay.io/pypa/${MANYLINUX}:latest + +RUN ulimit -n 1024 \ + && yum install -y wget openssl openssl-devel openssl-static perl-IPC-Cmd perl-Time-Piece \ + && yum clean all +ENV PATH="/usr/local/rust/bin:${PATH}" + +ARG LIBCLANG +ARG RUST +ARG TARGETPLATFORM + +RUN mkdir -p /.cargo \ + && chmod 777 /.cargo \ + && git config --global --add safe.directory /source \ + && cp /root/.gitconfig /.gitconfig \ + && chmod 777 /.gitconfig + +ADD install_rust.sh / +RUN /install_rust.sh ${RUST} ${TARGETPLATFORM} && rm /install_rust.sh + +ADD install_libclang.sh / +RUN /install_libclang.sh ${LIBCLANG} ${TARGETPLATFORM} && rm /install_libclang.sh +ENV LIBCLANG_PATH=/opt/libclang/libclang-${LIBCLANG}.data/platlib/clang/native/libclang.so diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-rust/install_libclang.sh b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-rust/install_libclang.sh new file mode 100755 index 0000000..7eab3e1 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-rust/install_libclang.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# 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. + +# This is now owned by the LLVM project, but they appear to be a bit behind on +# releases. However for our purposes 18.1.1 is fine. +# https://github.com/sighingnow/libclang/issues/84 + +set -euo pipefail + +main() { + local -r clang="${1}" + local -r platform="${2}" + + if [[ "$platform" == "linux/amd64" ]]; then + local -r url="https://files.pythonhosted.org/packages/1d/fc/716c1e62e512ef1c160e7984a73a5fc7df45166f2ff3f254e71c58076f7c/libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl" + elif [[ "$platform" == "linux/arm64" ]]; then + local -r url="https://files.pythonhosted.org/packages/3c/3d/f0ac1150280d8d20d059608cf2d5ff61b7c3b7f7bcf9c0f425ab92df769a/libclang-18.1.1-py2.py3-none-manylinux2014_aarch64.whl" + else + echo "Unsupported platform: $arch" + exit 1 + fi + + wget --no-verbose -O libclang.whl "${url}" + unzip -d /opt/libclang libclang.whl + rm libclang.whl +} + +main "$@" diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-rust/install_rust.sh b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-rust/install_rust.sh new file mode 100755 index 0000000..cccae66 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux-rust/install_rust.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# 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. + +set -euo pipefail + +main() { + local -r rust="${1}" + local -r platform="${2}" + + if [[ "$platform" == "linux/amd64" ]]; then + export ARCH="x86_64" + elif [[ "$platform" == "linux/arm64" ]]; then + export ARCH="aarch64" + else + echo "Unsupported platform: $arch" + exit 1 + fi + + wget --no-verbose https://static.rust-lang.org/dist/rust-${RUST}-${ARCH}-unknown-linux-gnu.tar.xz + mkdir -p /tmp/rust + tar --strip-components=1 -C /tmp/rust -xf rust-${RUST}-${ARCH}-unknown-linux-gnu.tar.xz + rm rust-${RUST}-${ARCH}-unknown-linux-gnu.tar.xz + /tmp/rust/install.sh + rm -rf /tmp/rust + rustc -V +} + +main "$@" diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux/Dockerfile b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux/Dockerfile new file mode 100644 index 0000000..654f32b --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux/Dockerfile @@ -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. + +ARG MANYLINUX +FROM quay.io/pypa/${MANYLINUX}:latest + +RUN ulimit -n 1024 && yum install -y wget && yum clean all +ENV PATH="/usr/local/go/bin:${PATH}" + +ARG GO +ARG TARGETPLATFORM + +RUN mkdir -p /.cache/go-build \ + && chmod 777 /.cache/go-build \ + && git config --global --add safe.directory /source \ + && cp /root/.gitconfig /.gitconfig \ + && chmod 777 /.gitconfig + +ADD install_go.sh / +RUN /install_go.sh ${GO} ${TARGETPLATFORM} && rm /install_go.sh + +COPY only-export-adbc.ld /only-export-adbc.ld diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux/install_go.sh b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux/install_go.sh new file mode 100755 index 0000000..fc12989 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux/install_go.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# 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. + +set -euo pipefail + +main() { + local -r go="${1}" + local -r platform="${2}" + + if [[ "$platform" == "linux/amd64" ]]; then + export ARCH="amd64" + elif [[ "$platform" == "linux/arm64" ]]; then + export ARCH="arm64" + else + echo "Unsupported platform: $arch" + exit 1 + fi + + wget --no-verbose https://go.dev/dl/go${go}.linux-${ARCH}.tar.gz + tar -C /usr/local -xzf go${go}.linux-${ARCH}.tar.gz + rm go${go}.linux-${ARCH}.tar.gz +} + +main "$@" diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux/only-export-adbc.ld b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux/only-export-adbc.ld new file mode 100644 index 0000000..46bf475 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/compose/manylinux/only-export-adbc.ld @@ -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. + +{ + global: + Adbc*; + + local: + *; +}; diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/copyright.py b/vendor/adbc-drivers-dev/adbc_drivers_dev/copyright.py new file mode 100755 index 0000000..a8686dd --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/copyright.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# 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 +import fnmatch +import re +import subprocess +import sys +import tarfile +import tempfile +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("root", type=Path, help="Root directory of repository") + + args = parser.parse_args() + root = args.root.resolve() + + pattern = r"Copyright \(c\) ([0-9]{4}-)?[0-9]{4} Columnar Technologies Inc\. +All rights reserved\." + header = re.compile(pattern.encode()) + + # ------------------------------------------------------------ + # Load the exclusion file if present + # ------------------------------------------------------------ + exclusion_file = root / ".rat-excludes" + exclusions = [] + if exclusion_file.is_file(): + with exclusion_file.open("r") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#"): + exclusions.append(line) + + # ------------------------------------------------------------ + # Generate a tarball so we can respect .gitignore + # ------------------------------------------------------------ + # Determine what commit to use; if unstaged changes are present, use + # git stash to create a temporary commit. + commit = subprocess.check_output( + ["git", "stash", "create"], cwd=root, text=True + ).strip() + if not commit: + # No unstaged changes. + commit = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=root, text=True + ).strip() + + # ------------------------------------------------------------ + # Check the archive + # ------------------------------------------------------------ + exitcode = 0 + with tempfile.TemporaryDirectory() as scratch: + scratch = Path(scratch).resolve() + archive = scratch / "rat.tar" + subprocess.check_call( + ["git", "archive", "--format=tgz", f"--output={archive}", commit], cwd=root + ) + + with tarfile.open(archive, "r") as tar: + for member in tar.getmembers(): + if member.size == 0: + continue + if not member.isfile(): + continue + if any(fnmatch.fnmatch(member.name, pattern) for pattern in exclusions): + continue + + found = False + with tar.extractfile(member) as f: + lineno = 0 + try: + for line in f: + if header.search(line): + found = True + break + lineno += 1 + if lineno >= 2: + break + except UnicodeDecodeError: + print(f"Cannot read {member.name} as text, skipping") + exitcode = 1 + + if not found: + print(f"Missing copyright header in {member.name}") + exitcode = 1 + + return exitcode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/generate.py b/vendor/adbc-drivers-dev/adbc_drivers_dev/generate.py new file mode 100644 index 0000000..9e11f0e --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/generate.py @@ -0,0 +1,385 @@ +# 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 typing + +from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator + +# Define workflow contexts in a single location +WorkflowContext = typing.Literal["build:test", "build:release", "test", "validate"] +WORKFLOW_CONTEXTS: tuple[WorkflowContext, ...] = ( + "build:test", + "build:release", + "test", + "validate", +) + + +class SecretConfigDict(BaseModel): + """Secret configuration with explicit secret name and contexts.""" + + model_config = {"extra": "forbid"} + + secret: str = Field( + description="The name of the GitHub secret to use for this secret variable" + ) + contexts: list[WorkflowContext] = Field( + default_factory=lambda: list(WORKFLOW_CONTEXTS), + description="Workflow contexts where this secret should be available", + ) + + +class AwsConfig(BaseModel): + """AWS authentication configuration.""" + + model_config = {"extra": "forbid"} + + region: str = Field( + description="AWS region to use for authentication (e.g., us-west-2, us-east-1)" + ) + + +class LangBuildConfig(BaseModel): + # validate_by_{name,alias} are to let us map "additional-make-args" to "additional_make_args" + model_config = { + "extra": "forbid", + "validate_by_name": True, + "validate_by_alias": True, + } + + additional_make_args: list[str] = Field( + default_factory=list, + alias="additional-make-args", + description="A list of additional arguments to pass to adbc-make.", + ) + + go_mod_path: str | None = Field( + default=None, + alias="go-mod-path", + description="Path containing the go.mod file for Go drivers. Used to cache dependencies in CI. Defaults to the language subdir.", + ) + + lang_tools: list[str] = Field( + default_factory=list, + alias="lang-tools", + description="Install tools for these languages to use in the build.", + ) + + environment_contexts: list[str] = Field( + default_factory=lambda: ["test", "validate"], + alias="environment-contexts", + description="What parts of the build require the environment.", + ) + + +class LangValidateSpec(BaseModel): + """A single config to validate.""" + + model_config = { + "extra": "forbid", + "validate_by_name": True, + "validate_by_alias": True, + } + + service_name: str = Field( + default="test-service", + description="docker-compose service to start", + ) + vendor_version: str = Field( + default="latest", + description="version to pass to the validation suite", + ) + + +class LangValidationConfig(BaseModel): + """Options for validation suite.""" + + model_config = { + "extra": "forbid", + "validate_by_name": True, + "validate_by_alias": True, + } + + skip: bool = Field( + default=False, + description="Whether to skip the validation suite in CI (this should only be used temporarily while setting up a driver)", + ) + configs: list[LangValidateSpec] = Field( + default_factory=lambda: [LangValidateSpec()], + description="A list of configurations to run the validation suite with. Each configuration will be run in a separate job", + ) + + +class LangTestConfig(BaseModel): + """Options for test suite.""" + + model_config = { + "extra": "forbid", + "validate_by_name": True, + "validate_by_alias": True, + } + + skip: bool = Field( + default=False, + description="Whether to skip tests in CI (this should only be used temporarily while setting up a driver or for build-only drivers)", + ) + service_name: str = Field( + alias="service-name", + default="test-service", + description="docker-compose service to start for tests", + ) + + +class LangConfig(BaseModel): + model_config = { + "extra": "forbid", + "validate_by_name": True, + "validate_by_alias": True, + } + + build: LangBuildConfig = Field( + default_factory=LangBuildConfig, + description="Configuration for building the driver.", + ) + subdir: str | None = Field( + default=None, + description="Override the default subdirectory for this language. Use '.' to place files at the repository root.", + ) + test: LangTestConfig = Field( + default_factory=LangTestConfig, + description="Configuration for the driver test suite.", + ) + validation: LangValidationConfig = Field( + default_factory=LangValidationConfig, + description="Configuration for the driver validation suite.", + ) + + @model_validator(mode="before") + @classmethod + def true_is_enabled(cls, data: typing.Any) -> typing.Any: + if isinstance(data, bool): + return {} + return data + + +class ValidationConfig(BaseModel): + """Configuration for validation workflows.""" + + # validate_by_{name,alias} are to let us map "extra_dependencies" to "extra-dependencies" + model_config = { + "extra": "forbid", + "validate_by_name": True, + "validate_by_alias": True, + } + + extra_dependencies: dict[str, typing.Any] = Field( + default_factory=dict, + alias="extra-dependencies", + json_schema_extra={ + "description": """Additional dependencies to install for validation workflows. Specify as key-value pairs. Example: + +[validation.extra-dependencies] +pytest = ">=7,<8" +black = "*\"""" + }, + ) + + extra_pypi_dependencies: dict[str, typing.Any] = Field( + default_factory=dict, + alias="extra-pypi-dependencies", + json_schema_extra={ + "description": """Additional dependencies to install for validation workflows. Specify as key-value pairs. Example: + +[validation.extra-pypi-dependencies] +pytest = ">=7,<8" +black = "*\"""" + }, + ) + + +class GenerateConfig(BaseModel): + """Config for workflow generation.""" + + model_config = { + "extra": "forbid", + "json_schema_extra": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Schema for generate.toml", + "description": "You can validate your generate.toml against this schema with tools like tombi (https://tombi-toml.github.io/tombi/docs/linter). Requires placing a `#:schema` directive at the top of your generate.toml file.", + }, + } + + repository: str | None = Field( + default=None, + description="GitHub repository name (if different from driver name)", + ) + + driver: str = Field( + default="(unknown)", + description="Driver name. Should be lowercase (e.g., postgresql, sqlite)", + ) + environment: str | None = Field( + default=None, + description="Name of a GitHub Actions Environment to use when including secrets in workflows", + ) + private: bool = Field( + default=False, + description="Whether the driver is private. Most drivers will be not be private so you can omit this.", + ) + lang: dict[str, LangConfig | None] = Field( + default_factory=dict, + json_schema_extra={ + "description": """Programming language(s) to enable workflows for. Only go and rust are supported. Keys should be lowercase. Set to true to enable with default config, or false (the default) to disable. Example: + +[lang] +go = true + +[lang.rust.build] +additional-make-args = ["example"]""" + }, + ) + secrets: dict[str, str | SecretConfigDict] = Field( + default_factory=dict, + json_schema_extra={ + "description": """Secrets to enable in workflows. By default, no secrets are available in your generated workflows unless you specify them here. + +To make a secret available in all workflows, use the simple syntax: + +[secrets] +MY_TOKEN = "GITHUB_SECRET_NAME" + +If you want more fine-grained control, you can restrict secrets to specific workflows. To do this, specify contexts contexts ('build:release', 'test', 'validate') like this: + +[secrets.DB_PASSWORD] +secret = "TEST_DB_SECRET" +contexts = ["test", "validate"]""" + }, + ) + aws: AwsConfig | None = Field( + default=None, + json_schema_extra={ + "description": """Enables AWS authentication in workflows. Automatically adds AWS_ROLE and AWS_ROLE_SESSION_NAME secrets, and sets id_token permissions. Example: + +[aws] +region = "us-west-2\"""" + }, + ) + gcloud: bool = Field( + default=False, + json_schema_extra={ + "description": """Enables Google Cloud authentication in workflows. Automatically adds GCLOUD_SERVICE_ACCOUNT and GCLOUD_WORKLOAD_IDENTITY_PROVIDER secrets, and sets id_token permissions. Set to true to enable. Example: + +gcloud = true""" + }, + ) + validation: ValidationConfig = Field( + default_factory=ValidationConfig, + json_schema_extra={ + "description": "Configuration for validation workflows. Currently supports specifying extra dependencies." + }, + ) + + # These fields are computed and not part of the input. PrivateAttr us + # automatically omit them when we serialize + _processed_secrets: dict[str, dict[str, str]] = PrivateAttr(default_factory=dict) + _permissions: dict[str, bool] = PrivateAttr(default_factory=dict) + + @field_validator("environment") + @classmethod + def validate_environment(cls, v: str | None) -> str | None: + if v is not None and not v.strip(): + raise ValueError("environment must be non-empty if provided") + return v + + @field_validator("lang", mode="before") + @classmethod + def lang_boolean(cls, value: typing.Any) -> typing.Any: + value = value or {} + return { + k: LangConfig() if v is True else None if v is False else v + for k, v in value.items() + } + + @model_validator(mode="after") + def default_repository(self) -> "GenerateConfig": + if self.repository is None: + self.repository = self.driver + return self + + @model_validator(mode="after") + def process_secrets_and_permissions(self) -> "GenerateConfig": + """Process secrets configuration and set up permissions.""" + self._processed_secrets = {context: {} for context in WORKFLOW_CONTEXTS} + + # Process each secret configuration + for secret_var, secret_config in self.secrets.items(): + if isinstance(secret_config, str): + # Simple string format: apply to all contexts + for context in self._processed_secrets: + self._processed_secrets[context][secret_var] = secret_config + else: + # Dict format with explicit contexts + for context in secret_config.contexts: + self._processed_secrets[context][secret_var] = secret_config.secret + + # Build the "all" context with all unique secrets + all_secrets = {} + for context_secrets in self._processed_secrets.values(): + all_secrets.update(context_secrets) + + if self.aws: + all_secrets["AWS_ROLE"] = "AWS_ROLE" + all_secrets["AWS_ROLE_SESSION_NAME"] = "AWS_ROLE_SESSION_NAME" + + if self.gcloud: + all_secrets["GCLOUD_SERVICE_ACCOUNT"] = "GCLOUD_SERVICE_ACCOUNT" + all_secrets["GCLOUD_WORKLOAD_IDENTITY_PROVIDER"] = ( + "GCLOUD_WORKLOAD_IDENTITY_PROVIDER" + ) + + if self.private: + all_secrets["COLUMNAR_CLOUD_API_TOKEN"] = "COLUMNAR_CLOUD_API_TOKEN" + + # No need to explicitly pass this (also GitHub will complain if you try to) + all_secrets = {k: v for k, v in all_secrets.items() if v != "GITHUB_TOKEN"} + + self._processed_secrets["all"] = all_secrets + + # Set permissions + if self.aws or self.gcloud: + self._permissions["id_token"] = True + + return self + + def to_dict(self) -> dict[str, typing.Any]: + return { + "driver": self.driver, + "repository": self.repository, + "environment": self.environment, + "private": self.private, + "lang": self.lang, + "secrets": self._processed_secrets, + "permissions": self._permissions, + "aws": self.aws.model_dump() if self.aws else None, + "gcloud": self.gcloud, + "validation": { + "extra_dependencies": self.validation.extra_dependencies, + "extra_pypi_dependencies": self.validation.extra_pypi_dependencies, + }, + } + + def __eq__(self, other: object) -> bool: + if not isinstance(other, GenerateConfig): + return NotImplemented + return self.to_dict() == other.to_dict() diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/make.py b/vendor/adbc-drivers-dev/adbc_drivers_dev/make.py new file mode 100644 index 0000000..7fbbd2d --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/make.py @@ -0,0 +1,706 @@ +#!/usr/bin/env python3 +# 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. + +# LOCAL MODIFICATIONS: Snowflake vendors this file with a SemVer-safe git revision +# suffix and writable Go caches for arbitrary-UID manylinux builds. + +""" +A build script for ADBC drivers using doit. + +See: https://pydoit.org/ +""" + +import os +import platform +import shlex +import subprocess +import sys +from pathlib import Path + +import doit +import packaging.version + +match platform.system(): + case "Darwin": + EXT = "dylib" + PLATFORM = "macos" + case "Linux": + EXT = "so" + PLATFORM = "linux" + case "Windows": + EXT = "dll" + PLATFORM = "windows" + case _: + raise RuntimeError(f"Unsupported platform: {platform.system()}") + + +DOIT_CONFIG = { + "default_tasks": ["build"], +} +SMUGGLE_VARS = {"CGO_CFLAGS", "CGO_LDFLAGS", "PROTOC"} + + +def to_bool(value: str | bool) -> bool: + if value is None: + return False + elif isinstance(value, bool): + return value + value = value.lower() + if value in {"1", "true", "yes"}: + return True + elif value in {"0", "false", "no"}: + return False + raise ValueError(f"Cannot convert {value!r} to bool") + + +def is_verbose() -> bool: + return to_bool(get_var("VERBOSE", "False")) + + +def append_flags(env: dict[str, str], var: str, flags: str) -> None: + if var in env: + env[var] += " " + flags + else: + env[var] = flags + + +def architecture() -> str: + match platform.machine(): + case "AMD64": + return "amd64" + case "aarch64": + return "arm64" + case "arm64": + return "arm64" + case "arm64v8": + return "arm64" + case "x86_64": + return "amd64" + case _: + raise ValueError(f"{platform.machine()} is not a recognized architecture") + + +def _check_call(f, *args, **kwargs) -> str: + extra_env = kwargs.pop("env", {}) + if extra_env: + env = os.environ.copy() + for k, v in extra_env.items(): + if k in {"CGO_CFLAGS", "CGO_LDFLAGS"}: + if k in env: + env[k] += " " + v + else: + env[k] = v + elif k in { + "ADBC_DRIVER_BUILD_VERSION", + "ARCH", + "MACOSX_DEPLOYMENT_TARGET", + "SOURCE_ROOT", + }: + env[k] = v + else: + raise TypeError(f"Unsupported env var override {k}") + env.update(extra_env) + kwargs["env"] = env + + if is_verbose(): + # TODO: use log, color + if kwargs.get("cwd") is not None: + cwd = kwargs["cwd"] + else: + cwd = "." + print( + "*", + f"[{cwd}]", + " ".join(shlex.quote(arg) for arg in args[0]), + file=sys.stderr, + ) + if extra_env: + for k, v in extra_env.items(): + print("*", "[env]", f"{k}={v}", file=sys.stderr) + return f(*args, **kwargs, text=True) + + +def check_call(*args, **kwargs) -> str: + return _check_call(subprocess.check_call, *args, **kwargs) + + +def check_output(*args, **kwargs) -> str: + return _check_call(subprocess.check_output, *args, **kwargs).strip() + + +def info(*args, **kwargs): + print("!", *args, **kwargs, file=sys.stderr) + + +def detect_version( + driver_root: Path, + *, + strict: bool = False, +) -> str: + repo_root = driver_root + while not (repo_root / ".git").is_dir(): + if repo_root.parent == repo_root: + raise ValueError(f"{driver_root} is not in a git repository") + repo_root = repo_root.parent + + prefix = str(driver_root.relative_to(repo_root)) + if prefix == ".": + prefix = "v" + else: + prefix = f"{prefix}/v" + + tags = check_output( + [ + "git", + "tag", + "-l", + "--no-column", + "--no-format", + "--no-color", + "--sort", + "-v:refname", + f"{prefix}*", + ], + cwd=repo_root, + ).splitlines() + + if not tags: + if strict: + raise ValueError(f"No tags found for driver {driver_root}") + # use a version that dbc will still accept, not "unknown" like we used to + version = "v0.0.1-dev" + else: + # sort tags, then find distance from all tags to HEAD + # the assumption is that this is monotonically increasing, else we have a problem + versions = [] + for tag in tags: + version_str = tag[len(prefix) - 1 :] + version = packaging.version.parse(version_str) + distance = int( + check_output( + ["git", "rev-list", f"{tag}..HEAD", "--count"], cwd=repo_root + ) + ) + versions.append((version_str, version, distance, tag)) + + versions.sort(key=lambda v: v[1], reverse=True) + for v, prev in zip(versions, versions[1:]): + if v[2] > prev[2]: + raise ValueError( + f"Tag {v[0]} is further from HEAD than {prev[0]}, but has a newer version" + ) + + version, parsed_version, count, tag = versions[0] + if count > 0: + if strict: + raise ValueError( + f"Driver {driver_root} is not on tag {tag}, but has {count} commits since" + ) + if parsed_version.is_prerelease or parsed_version.is_devrelease: + # This is a weird edge case, but just use the previous version (or dev version) + for v in versions: + if not (v[1].is_prerelease or v[1].is_devrelease): + version, parsed_version, count, tag = v + break + else: + version = "v0.0.1" + count = int( + check_output( + ["git", "rev-list", "HEAD", "--count"], cwd=repo_root + ) + ) + rev = check_output(["git", "rev-parse", "--short", "HEAD"], cwd=repo_root) + version += f"-dev.{count}.g{rev}" + + # Append -dirty if there are uncommitted changes + dirty = check_output(["git", "status", "--porcelain"], cwd=repo_root).splitlines() + # Ignore untracked files + if any(not line.startswith("?? ") for line in dirty): + if strict: + info(repo_root, "has uncommitted changes. `git status --porcelain`:") + for line in dirty: + info("> ", line) + raise ValueError(f"{repo_root} has uncommitted changes") + version += "-dirty" + + return version + + +def get_var(name: str, default: str) -> str: + value = os.environ.get(name) + if value is not None: + return value + value = doit.get_var(name, default) + return value + + +def maybe_build_docker( + *, + repo_root: Path, + driver_root: Path, + env: dict[str, str], + args: list[str], + ci: bool, + container: str, +) -> None: + if not ci or platform.system() != "Linux" or to_bool(get_var("DEBUG", "False")): + check_call(args, cwd=driver_root, env=env) + return + + env = env.copy() + env["SOURCE_ROOT"] = str(repo_root) + env["ARCH"] = architecture() + + volumes = get_var("ADDITIONAL_VOLUMES", "") + if volumes: + volumes = volumes.split(",") + + # Some env vars need to be explicitly propagated into Docker + smuggle_env = "" + for var in SMUGGLE_VARS: + if var in env: + smuggle_env += f'{var}="{shlex.quote(env[var])}" ' + elif var in os.environ: + smuggle_env += f'{var}="{shlex.quote(os.environ[var])}" ' + + command = [ + "docker", + "compose", + "run", + "--rm", + "--user", + str(os.getuid()), + ] + + for volume in volumes: + command.extend(["-v", volume]) + + # The manylinux image runs the host UID, which may not exist in /etc/passwd. + # Go then falls back to the root-owned /go module cache. Keep both caches in + # /tmp, which is writable for arbitrary UIDs on GitHub-hosted runners. + if container == "manylinux": + command.extend( + [ + "--env", + "GOCACHE=/tmp/go-build", + "--env", + "GOMODCACHE=/tmp/go-mod", + "--env", + "GOPATH=/tmp/go", + ] + ) + elif container == "manylinux-rust": + # sf_core downloads protoc through the platform cache directory. + # Arbitrary host UIDs cannot write to the default root home. + command.extend( + [ + "--env", + "HOME=/tmp", + "--env", + "XDG_CACHE_HOME=/tmp/.cache", + "--env", + "CARGO_HOME=/.cargo", + ] + ) + + command.extend( + [ + container, + "--", + "bash", + "-c", + f"cd /source/{driver_root.relative_to(repo_root)} && env {smuggle_env} {' '.join(shlex.quote(arg) for arg in args)}", + ] + ) + check_call(command, cwd=Path(__file__).parent, env=env) + + +def build_go( + repo_root: Path, + driver_root: Path, + driver: str, + target: str, + *, + ci: bool = False, +) -> None: + strict = to_bool(get_var("RELEASE", "false")) + version = detect_version(driver_root, strict=strict) + (repo_root / "build").mkdir(exist_ok=True) + + # Embed the version in the library + prop = "github.com/adbc-drivers/driverbase-go/driverbase.infoDriverVersion" + ldflags = " ".join( + [ + # Don't exclude symbols (-s) so panics will have symbol information + # This will exclude DWARF debug tables (-w). + "-w", + f"-X {prop}={version}", + ] + ) + + tags = ["driverlib"] + if to_bool(get_var("DEBUG", "False")): + tags.append("assert") + + extra_tags = get_var("BUILD_TAGS", "") + if extra_tags: + extra_tags = extra_tags.split(",") + extra_tags = [tag.strip() for tag in extra_tags] + extra_tags = [tag for tag in extra_tags if tag] + tags.extend(extra_tags) + + tags = ",".join(tags) + tags = "-tags=" + tags + + info("Building", target, "version", version) + + env = {} + for var in SMUGGLE_VARS: + if var in os.environ: + env[var] = os.environ[var] + + if platform.system() == "Darwin": + append_flags(env, "CGO_CFLAGS", "-mmacosx-version-min=11.0") + append_flags(env, "CGO_LDFLAGS", "-mmacosx-version-min=11.0") + + if ci and platform.system() == "Linux" and not to_bool(get_var("DEBUG", "False")): + check_call(["go", "mod", "vendor"], cwd=driver_root) + ldflags += ( + " -linkmode external -extldflags=-Wl,--version-script=/only-export-adbc.ld" + ) + + # Command differs under Docker so don't invoke this otherwise + maybe_build_docker( + repo_root=repo_root, + driver_root=driver_root, + env=env, + args=[ + "go", + "build", + "-buildmode=c-shared", + tags, + "-o", + f"/source/build/{target}", + "-ldflags", + ldflags, + "./pkg", + ], + ci=ci, + container="manylinux", + ) + else: + check_call( + [ + "go", + "build", + "-buildmode=c-shared", + tags, + "-o", + f"{repo_root / 'build' / target}", + "-ldflags", + ldflags, + "./pkg", + ], + cwd=driver_root, + env=env, + ) + + output = (repo_root / "build" / target).resolve() + output.chmod(0o755) + header = output.with_suffix(".h") + header.unlink(missing_ok=True) + + +def build_rust( + repo_root: Path, + driver_root: Path, + driver: str, + target: str, + *, + ci: bool = False, +) -> None: + strict = to_bool(get_var("RELEASE", "false")) + version = detect_version(driver_root, strict=strict) + (repo_root / "build").mkdir(exist_ok=True) + + debug = to_bool(get_var("DEBUG", "False")) + + # Note: version embedded in library is determined by Cargo.toml + # TODO: check that it matches git tag? + args = [] + if not debug: + args.append("--release") + + features = [] + extra_features = get_var("FEATURES", "") + if extra_features: + extra_features = extra_features.split(",") + extra_features = [tag.strip() for tag in extra_features] + extra_features = [tag for tag in extra_features if tag] + features.extend(extra_features) + + if features: + args.append("--features") + args.append(",".join(features)) + + info("Building", target, "version", version, "features", features) + + env = {} + if platform.system() == "Darwin": + # https://doc.rust-lang.org/nightly/rustc/platform-support/apple-darwin.html#os-version + env["MACOSX_DEPLOYMENT_TARGET"] = "11.0" + + maybe_build_docker( + repo_root=repo_root, + driver_root=driver_root, + env=env, + args=["cargo", "build", *args], + ci=ci, + container="manylinux-rust", + ) + + lib = driver_root / "target" + if debug: + lib = lib / "debug" + else: + lib = lib / "release" + + source_target = target + # Exclusion basically just for Databricks - their crate name is not + # "adbc_driver_databricks" but rather "databricks_adbc" + if target_name := get_var("TARGET_NAME", ""): + source_target = f"lib{target_name}.{EXT}" + if platform.system() == "Windows": + source_target = source_target.removeprefix("lib") + lib = lib / source_target + info("Copying", lib, "to", repo_root / "build" / target) + + lib.rename(repo_root / "build" / target) + output = (repo_root / "build" / target).resolve() + output.chmod(0o755) + + +def build_script( + repo_root: Path, + driver_root: Path, + driver: str, + target: str, + *, + ci: bool = False, +) -> None: + strict = to_bool(get_var("RELEASE", "false")) + version = detect_version(driver_root, strict=strict) + (repo_root / "build").mkdir(exist_ok=True) + + debug = to_bool(get_var("DEBUG", "False")) + + args = [] + if debug: + args.append("test") + else: + args.append("release") + args.append(PLATFORM) + args.append(architecture()) + + info("Building", target, "version", version) + + env = {} + if platform.system() == "Darwin": + env["MACOSX_DEPLOYMENT_TARGET"] = "11.0" + + args = ["./ci/scripts/build.sh", *args] + if ci and PLATFORM == "windows": + # Force use of Git Bash on GitHub Actions + args = [r"C:\Program Files\Git\bin\bash.EXE", *args] + + toolchain = get_var("TOOLCHAIN", "") + if not toolchain: + raise ValueError("Must specify TOOLCHAIN=toolchain for script-based build") + + container = { + "cpp": "manylinux-cpp", + "go": "manylinux", + "rust": "manylinux-rust", + }.get(toolchain) + if container is None: + raise ValueError(f"Unsupported TOOLCHAIN={toolchain} for script-based build") + + # if we're using a script, don't invoke docker for Go; the script itself + # will invoke docker + + maybe_build_docker( + repo_root=repo_root, + driver_root=driver_root, + env=env, + args=args, + ci=ci and toolchain != "go", + container=container, + ) + + output = (repo_root / "build" / target).resolve() + output.chmod(0o755) + + +def check_linux(binary: Path) -> None: + symbols = check_output( + [ + "nm", + "--demangle", + "--dynamic", + str(binary), + ] + ).splitlines() + + # Make sure only 'Adbc*' symbols are exported + bad_symbols = [] + for symbol in symbols: + if " T " not in symbol: + continue + _, _, name = symbol.partition(" T ") + if not name.startswith("Adbc"): + bad_symbols.append(name) + if bad_symbols: + raise RuntimeError( + f"{', '.join(bad_symbols[:3])}... ({len(bad_symbols)} symbols total) should not be exported from {binary}" + ) + + # Like upstream. Match manylinux2014's versions. + # https://peps.python.org/pep-0599/#the-manylinux2014-policy + manylinux = get_var("MANYLINUX", "manylinux2014").lower() + if manylinux == "manylinux2014": + glibc_max = "2.17" + glibcxx_max = "3.4.19" + elif manylinux == "manylinux_2_28": + glibc_max = "2.28" + glibcxx_max = "3.4.32" + + for symbol in symbols: + if "@GLIBC_" in symbol: + version = packaging.version.Version(symbol.partition("@")[2][6:]) + if version > packaging.version.Version(glibc_max): + raise RuntimeError( + f"{symbol} requires too new a glibc (max {glibc_max})" + ) + elif "@GLIBCXX_" in symbol: + version = packaging.version.Version(symbol.partition("@")[2][8:]) + if version > packaging.version.Version(glibcxx_max): + raise RuntimeError( + f"{symbol} requires too new a glibcxx (max {glibcxx_max})" + ) + + +def check_macos(binary: Path) -> None: + output = check_output(["otool", "-l", str(binary)]).splitlines() + minos = None + for line in output: + line = line.strip() + if not line.startswith("minos"): + continue + _, _, minos = line.partition(" ") + break + + if minos is None: + raise RuntimeError("Could not determine minimum macOS version") + + minos = packaging.version.Version(minos) + maxos = packaging.version.Version("11.0") + + if minos > maxos: + raise RuntimeError( + f"{binary} requires macOS {minos} but {maxos} was expected at most" + ) + + +def check(binary: Path) -> None: + if platform.system() == "Linux": + check_linux(binary) + elif platform.system() == "Darwin": + check_macos(binary) + + +def task_build(): + driver = get_var("DRIVER", "") + if not driver: + raise ValueError("Must specify DRIVER=driver") + + ci = to_bool(get_var("CI", False)) + lang = get_var("IMPL_LANG", "go").strip().lower() + + repo_root = Path(".").resolve().absolute() + driver_root = Path(driver) + if driver_root.is_dir(): + driver_root = driver_root.resolve() + elif ( + Path("./go.mod").is_file() or Path("./Cargo.toml").is_file() or lang == "script" + ): + driver_root = Path(".").resolve() + + # Compute dependencies + file_deps = [] + extensions = [".go", ".c", ".cc", ".cpp", ".h", ".rs"] + for dirname, _, filenames in driver_root.walk(): + for filename in filenames: + if filename in {"go.mod", "go.sum", "Cargo.toml", "Cargo.lock"}: + file_deps.append(Path(dirname) / filename) + elif any(filename.endswith(ext) for ext in extensions): + file_deps.append(Path(dirname) / filename) + + target = f"libadbc_driver_{driver}.{EXT}" + + if lang == "go": + actions = [ + lambda: build_go(repo_root, driver_root, driver, target, ci=ci), + ] + elif lang == "rust": + actions = [ + lambda: build_rust(repo_root, driver_root, driver, target, ci=ci), + ] + elif lang == "script": + actions = [ + lambda: build_script(repo_root, driver_root, driver, target, ci=ci), + ] + else: + raise ValueError(f"Unsupported LANG={lang}") + + return { + "actions": actions, + "file_dep": [str(p) for p in file_deps], + "targets": [repo_root / "build" / target], + } + + +def task_check(): + driver = get_var("DRIVER", "") + if not driver: + raise ValueError("Must specify DRIVER=driver") + + repo_root = Path(".").resolve() + target = repo_root / "build" / f"libadbc_driver_{driver}.{EXT}" + + return { + "actions": [ + lambda: check(target), + ], + "file_dep": [target], + "targets": [], + } + + +def main(): + doit.run(globals()) + + +if __name__ == "__main__": + main() diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/package.py b/vendor/adbc-drivers-dev/adbc_drivers_dev/package.py new file mode 100644 index 0000000..59f8a8c --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/package.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +# 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. +""" +Generate a driver package from a manifest and shared library(s). +""" + +import argparse +import collections +import dataclasses +import enum +import io +import itertools +import os +import subprocess +import sys +import tarfile +import typing +from pathlib import Path + +import tomlkit +from ruamel.yaml import YAML + +from .make import detect_version + + +class Architecture(enum.StrEnum): + AMD64 = "amd64" # Also known as x86_64, x64 + ARM64 = "arm64" # Also known as aarch64, arm64v8 + + +class Platform(enum.StrEnum): + LINUX = "linux" + MACOS = "macos" + WINDOWS = "windows" + + +@dataclasses.dataclass +class Package: + name: str + platform: Platform + architecture: Architecture + version: str + files: dict[str, bytes] + + +def normalize_driver_name(filename: str) -> str: + """Convert 'libadbc_driver_redshift.so' to 'redshift'.""" + name, _, _ = filename.partition(".") + name = name.removeprefix("lib") + parts = name.split("_") + if len(parts) != 3: + raise ValueError(f"Invalid driver name: {name}") + elif parts[0] != "adbc": + raise ValueError(f"Invalid driver name: {name}") + elif parts[1] != "driver": + raise ValueError(f"Invalid driver name: {name}") + return parts[2] + + +def validate_manifest(manifest: dict[str, typing.Any]) -> None: + for field in ("name", "description", "publisher", "license", "version"): + if field not in manifest: + raise ValueError(f"Manifest missing required `{field}`") + elif not isinstance(manifest[field], tomlkit.items.String): + raise ValueError(f"Manifest `{field}` must be a string") + elif manifest[field].unwrap() == "": + raise ValueError(f"Manifest `{field}` must not be empty") + + if "Files" not in manifest: + raise ValueError("Manifest missing required `Files` section") + + files = manifest["Files"] + if "driver" not in files: + raise ValueError("Manifest missing required `Files.driver`") + elif not isinstance(files["driver"], tomlkit.items.String): + raise ValueError("Manifest `Files.driver` must be a string") + elif files["driver"].unwrap() == "": + raise ValueError("Manifest `Files.driver` must not be empty") + + +def generate_go_license(license_template: Path, gomod: Path) -> str: + # Ignore the current package's license (as it's already included) + this_package = None + with gomod.open("r") as source: + for line in source: + if line.startswith("module "): + _, _, this_package = line.strip().partition(" ") + if not this_package: + raise RuntimeError("Could not determine module name to ignore") + + license_proc = subprocess.run( + [ + "go-licenses", + "report", + "./...", + "--ignore", + this_package, + "--template", + str(license_template.absolute()), + ], + cwd=license_template.parent, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if license_proc.returncode != 0: + print("Failed to generate license", file=sys.stderr) + print("Stdout:", file=sys.stderr) + print(license_proc.stdout, file=sys.stderr) + print("Stderr:", file=sys.stderr) + print(license_proc.stderr, file=sys.stderr) + license_proc.check_returncode() + license_data = license_proc.stdout + return license_data + + +def generate_rust_license(license_template: Path) -> str: + license_proc = subprocess.run( + [ + "cargo-about", + "generate", + "--all-features", + "--fail", + "--locked", + "--target", + "aarch64-apple-darwin,aarch64-unknown-linux-gnu,x86_64-pc-windows-msvc,x86_64-unknown-linux-gnu", + str(license_template.absolute()), + ], + cwd=license_template.parent, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if license_proc.returncode != 0: + print("Failed to generate license", file=sys.stderr) + print("Stdout:", file=sys.stderr) + print(license_proc.stdout, file=sys.stderr) + print("Stderr:", file=sys.stderr) + print(license_proc.stderr, file=sys.stderr) + license_proc.check_returncode() + license_data = license_proc.stdout + return license_data + + +def generate_custom_license(license_script: Path) -> str: + import platform + + args = [str(license_script.absolute())] + if ( + platform.system() == "Windows" + and os.environ.get("CI", "").lower().strip() == "true" + ): + # Force use of Git Bash on GitHub Actions + args = [r"C:\Program Files\Git\bin\bash.EXE", *args] + license_proc = subprocess.run( + args, + cwd=license_script.parent.parent.parent, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if license_proc.returncode != 0: + print("Failed to generate license", file=sys.stderr) + print("Stdout:", file=sys.stderr) + print(license_proc.stdout, file=sys.stderr) + print("Stderr:", file=sys.stderr) + print(license_proc.stderr, file=sys.stderr) + license_proc.check_returncode() + license_data = license_proc.stdout + return license_data + + +def generate_packages( + manifest: dict[str, typing.Any], + driver_name: str, + driver_root: Path, + drivers: list[tuple[Platform, Architecture, Path]], + strict: bool = False, +) -> list[Package]: + version = detect_version(driver_root, strict=strict) + manifest = manifest.copy() + manifest["version"] = version + packages: list[Package] = [] + + for platform, architecture, driver_path in drivers: + files: dict[str, bytes] = {} + with driver_path.open("rb") as source: + files[driver_path.name] = source.read() + + manifest["Files"] = { + "driver": driver_path.name, + } + validate_manifest(manifest) + files["MANIFEST"] = tomlkit.dumps(manifest).encode("utf-8") + + packages.append( + Package( + driver_name, + platform, + architecture, + version, + files, + ) + ) + return packages + + +def find_drivers( + driver_name: str, input_dirs: list[Path] +) -> list[tuple[Platform, Architecture, Path]]: + drivers: list[tuple[Platform, Architecture, Path]] = [] + for input_dir in input_dirs: + # Get the architecture and platform from the directory name + parts = input_dir.name.split("-") + if len(parts) != 3 or (parts and parts[0] != "drivers"): + print("Invalid input directory name:", input_dir.name, file=sys.stderr) + print("Expected format: drivers--", file=sys.stderr) + return 1 + + platform = Platform(parts[1].lower()) + architecture = Architecture(parts[2].lower()) + for driver_path in itertools.chain( + *(input_dir.rglob(f"*.{ext}") for ext in ("dll", "dylib", "so")) + ): + # Ignore drivers that don't match the given manifest + if normalize_driver_name(driver_path.name) != driver_name: + continue + drivers.append((platform, architecture, driver_path)) + print(f"Found {platform}_{architecture} driver:", driver_path) + return drivers + + +def main(): + # Assume we're being run from a GitHub action where the directory + # structure is as follows + # drivers-linux-amd64/libadbc_driver_mssql.so + # drivers-macos_11_0-arm64/libadbc_driver_mssql.so + # Use the directory structure to infer the architecture and platform + + parser = argparse.ArgumentParser() + parser.add_argument( + "-o", + "--output", + required=True, + type=Path, + help="Directory to write generated packages to", + ) + parser.add_argument( + "--name", + required=True, + help="The driver name", + ) + parser.add_argument( + "--root", + required=True, + type=Path, + help="Path to the driver in version control (to infer version)", + ) + parser.add_argument( + "--release", + action="store_true", + help="This is a release (be more strict)", + ) + parser.add_argument( + "--manifest-template", + required=True, + type=Path, + help="The template manifest", + ) + parser.add_argument( + "input", + nargs="+", + type=Path, + help="Directories containing input drivers (must follow naming convention of CI-generated artifacts)", + ) + + args = parser.parse_args() + + # Discover drivers in the input directories + drivers = find_drivers(args.name, args.input) + + args.output.mkdir(exist_ok=True, parents=True) + + # ------------------------------------------------------------ + # Generate license + # ------------------------------------------------------------ + license_data = None + notice_file = args.manifest_template.with_name("NOTICE.txt") + if not notice_file.is_file(): + notice_file = args.manifest_template.parent.parent / "NOTICE.txt" + if not notice_file.is_file(): + raise RuntimeError(f"NOTICE.txt ({notice_file}) is missing") + + license_script = args.manifest_template.parent / "ci/scripts/generate_license.sh" + license_template = args.manifest_template.with_name("license.tpl") + if license_script.is_file(): + license_data = generate_custom_license(license_script) + elif license_template.is_file(): + if (gomod := args.manifest_template.with_name("go.mod")).is_file(): + license_data = generate_go_license(license_template, gomod) + elif args.manifest_template.with_name("Cargo.toml").is_file(): + license_data = generate_rust_license(license_template) + else: + raise RuntimeError("Could not determine how to generate license") + else: + raise RuntimeError(f"License template ({license_template}) is missing") + + # ------------------------------------------------------------ + # Generate packages + # ------------------------------------------------------------ + with args.manifest_template.open("rb") as source: + driver_manifest = tomlkit.load(source) + + # Track some data for the bucket manifest + pkginfo_version = collections.defaultdict(lambda: {"packages": []}) + bucket_manifest_driver_entry = { + "name": driver_manifest["name"].unwrap(), + # "description" is non-standard so we're using it just to populate a + # description for the driver index. dbc ignores this field so it won't be + # present when the driver is installed. We fall back to name if not found. + "description": ( + driver_manifest["description"].unwrap() + if "description" in driver_manifest + else driver_manifest["name"].unwrap() + ), + "license": driver_manifest["license"].unwrap(), + "path": args.name, + "urls": ["https://adbc-drivers.org"], + } + + for package in generate_packages( + driver_manifest, + args.name, + args.root, + drivers, + strict=args.release, + ): + print( + "Generating", + package.name, + package.platform, + package.architecture, + package.version, + ) + + if license_data: + package.files["LICENSE"] = license_data + else: + raise RuntimeError("LICENSE is missing") + + if notice_file: + with notice_file.open("rb") as source: + package.files["NOTICE"] = source.read() + + filename = f"{package.name}_{package.platform}_{package.architecture}_{package.version}.tar.gz" + subdir = args.output / args.name / package.version + subdir.mkdir(exist_ok=True, parents=True) + output = subdir / filename + print("Output:", output) + # TODO: use zstd + with tarfile.open(output, "w:gz") as tar: + for name, data in package.files.items(): + tarinfo = tarfile.TarInfo(name) + tarinfo.size = len(data) + tarinfo.mtime = 0 + tarinfo.mode = 0o644 + tar.addfile(tarinfo, io.BytesIO(data)) + + pkginfo_version[package.version]["version"] = package.version + pkginfo_version[package.version]["packages"].append( + { + "platform": f"{package.platform}_{package.architecture}", + "url": f"{args.name}/{package.version}/{filename}", + } + ) + bucket_manifest_driver_entry["pkginfo"] = [v for v in pkginfo_version.values()] + bucket_manifest = { + "drivers": [bucket_manifest_driver_entry], + } + + yaml = YAML() + with (args.output / "manifest.yaml").open("w") as sink: + yaml.dump(bucket_manifest, sink) + print("Generated manifest.yaml") + yaml.dump(bucket_manifest, sys.stdout) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/rat/__init__.py b/vendor/adbc-drivers-dev/adbc_drivers_dev/rat/__init__.py new file mode 100644 index 0000000..0492f18 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/rat/__init__.py @@ -0,0 +1,17 @@ +# 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 .cli import main + +__all__ = ["main"] diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/rat/__main__.py b/vendor/adbc-drivers-dev/adbc_drivers_dev/rat/__main__.py new file mode 100644 index 0000000..485606b --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/rat/__main__.py @@ -0,0 +1,19 @@ +# 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 .cli import main + +sys.exit(main()) diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/rat/cli.py b/vendor/adbc-drivers-dev/adbc_drivers_dev/rat/cli.py new file mode 100644 index 0000000..6b0b562 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/rat/cli.py @@ -0,0 +1,222 @@ +# 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 +import fnmatch +import io +import re +import subprocess +import tarfile +import tempfile +import xml.etree.ElementTree as ET +from pathlib import Path + +import platformdirs +import requests + +RAT_VERSION = "0.16.1" + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("root", type=Path, help="Root directory of repository") + + args = parser.parse_args() + root = args.root.resolve() + print("Checking licenses for", root) + + # ------------------------------------------------------------ + # Download RAT if not present + # ------------------------------------------------------------ + cache_dir = Path( + platformdirs.user_cache_dir("adbc-drivers-dev", "ADBC Driver Foundry") + ) + cache_dir.mkdir(parents=True, exist_ok=True) + + rat_path = cache_dir / f"apache-rat-{RAT_VERSION}.jar" + if not rat_path.is_file(): + url = f"https://repo1.maven.org/maven2/org/apache/rat/apache-rat/{RAT_VERSION}/apache-rat-{RAT_VERSION}.jar" + with requests.get(url, stream=True) as response: + response.raise_for_status() + try: + with rat_path.open("wb") as sink: + for chunk in response.iter_content(chunk_size=8192): + sink.write(chunk) + except Exception: + rat_path.unlink(missing_ok=True) + raise + + print("Using Apache RAT:", rat_path) + + # ------------------------------------------------------------ + # RAT does not respect .gitignore. Create and check a tarball instead. + # ------------------------------------------------------------ + commit = subprocess.check_output( + ["git", "stash", "create"], cwd=root, text=True + ).strip() + if not commit: + # No unstaged changes. + commit = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=root, text=True + ).strip() + + # ------------------------------------------------------------ + # Load the exclusion file if present + # ------------------------------------------------------------ + exclusion_file = root / ".rat-excludes" + exclusions = [] + if exclusion_file.is_file(): + with exclusion_file.open("r") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#"): + exclusions.append(line) + + # ------------------------------------------------------------ + # Load the file listing files imported from Apache repos + # ------------------------------------------------------------ + apache_file = root / ".rat-apache" + needs_apache_header = set() + if apache_file.is_file(): + with apache_file.open("r") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#"): + needs_apache_header.add(line) + + with tempfile.TemporaryDirectory() as scratch: + scratch = Path(scratch).resolve() + archive = scratch / "rat.tar" + subprocess.check_call( + ["git", "archive", "--format=tgz", f"--output={archive}", commit], cwd=root + ) + + # ------------------------------------------------------------ + # Invoke RAT + # ------------------------------------------------------------ + report = io.StringIO( + subprocess.check_output( + [ + "java", + "-jar", + str(rat_path), + str(archive), + "-x", + ], + text=True, + ) + ) + + root = ET.parse(report).getroot() + unapproved = 0 + for resource in root.findall("resource"): + approvals = resource.findall("license-approval") + if not approvals: + continue + if approvals[0].attrib["name"] == "true": + continue + + filename = resource.attrib["name"] + if any(fnmatch.fnmatch(filename, exclusion) for exclusion in exclusions): + continue + + if unapproved == 0: + print("Files without licenses or with unapproved licenses found:") + unapproved += 1 + print("-", filename) + + # ------------------------------------------------------------ + # Check other aspects of the copyright header + # ------------------------------------------------------------ + + missing_copyright = [] + missing_apache_header = [] + # Has "This file has been modified..." header but is not listed in .rat-apache + should_not_have_apache_header = [] + # Has "Licensed to the Apache Software Foundation" but is missing + # "This file has been modified..." + should_not_have_licensed_header = [] + copyright_re = re.compile( + r"Copyright \(c\) [0-9]{4}(\s+[0-9]{4})? ADBC Drivers Contributors" + ) + header_re = re.compile( + r"This file has been modified from its original version, which is under the Apache License: Licensed to the Apache Software Foundation" + ) + licensed_re = re.compile("Licensed to the Apache Software Foundation") + sep_re = re.compile(r"[^a-zA-Z0-9,:()]+") + with tarfile.open(archive, "r") as tar: + for member in tar.getmembers(): + if not member.isfile(): + continue + + with tar.extractfile(member) as f: + lines = [] + for _ in range(20): + lines.append(f.readline()) + try: + content = b" ".join(lines).decode("utf-8") + except UnicodeDecodeError: + # decode will fail on non-text files so just use empty + # content so RAT will fail if the file isn't excluded + content = "" + content = sep_re.sub(" ", content) + + if not copyright_re.search(content): + if ( + not member.name.endswith("LICENSE.txt") + and not member.name.endswith("NOTICE.txt") + and not any( + fnmatch.fnmatch(member.name, exclusion) + for exclusion in exclusions + ) + ): + missing_copyright.append(member.name) + + if member.name in needs_apache_header: + if not header_re.search(content): + missing_apache_header.append(member.name) + elif header_re.search(content): + should_not_have_apache_header.append(member.name) + elif licensed_re.search(content): + should_not_have_licensed_header.append(member.name) + + if missing_copyright: + print("Files missing ADBC Drivers Contributors copyright header:") + for name in missing_copyright: + print("-", name) + + if missing_apache_header: + print("Files missing 'This file has been modified' header:") + for name in missing_apache_header: + print("-", name) + + if should_not_have_apache_header: + print("Files that should not have 'This file has been modified' header:") + for name in should_not_have_apache_header: + print("-", name) + + if should_not_have_licensed_header: + print( + "Files that have 'Licensed to the Apache Software Foundation' header," + ) + print("but are missing 'This file has been modified' header:") + for name in should_not_have_licensed_header: + print("-", name) + + unapproved += len(missing_copyright) + unapproved += len(missing_apache_header) + unapproved += len(should_not_have_apache_header) + unapproved += len(should_not_have_licensed_header) + + return 1 if unapproved else 0 diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/release.py b/vendor/adbc-drivers-dev/adbc_drivers_dev/release.py new file mode 100644 index 0000000..8c73c3f --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/release.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# 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 +import subprocess +import tempfile +from pathlib import Path + +import packaging.version +import pygit2 + +from . import changelog + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("root", type=Path, help="The path to the driver root") + parser.add_argument("tag", type=str, help="The tag to release") + parser.add_argument( + "--dry-run", action="store_true", help="Do not actually release" + ) + args = parser.parse_args() + + root = args.root + if "/" in args.tag: + subdir, _, version = args.tag.partition("/") + version = packaging.version.parse(version) + prefix = f"{subdir}/v" + else: + subdir = "." + version = packaging.version.parse(args.tag) + prefix = "v" + + print("Subdir:", subdir) + print("Version:", version) + + # Figure out the previous tag so we can compute the changelog. + # Note that we don't use the Go semver, and we assume that there are no + # dev/other modifiers (which would be invalid here) + repo = pygit2.Repository(root) + tags = [] + for ref in repo.references: + if ref.startswith(f"refs/tags/{prefix}"): + tag = ref[len("refs/tags/") :] + v = packaging.version.parse(tag[len(prefix) :]) + if v.is_prerelease or v.is_devrelease: + continue + tags.append((tag, v)) + + tags.sort(key=lambda v: v[1]) + tags = list(filter(lambda v: v[1] < version, tags)) + + if not tags: + # No previous version + previous_tag = None + else: + previous_tag = tags[-1][0] + + print("Previous tag:", previous_tag, f"(found: {', '.join(t[0] for t in tags)})") + + title, log = changelog.generate_changelog( + root, subdir, version, previous_tag, args.tag + ) + with tempfile.NamedTemporaryFile(mode="w", suffix=".md") as f: + print("#", title) + print() + print(log) + + f.write(log) + f.flush() + + command = [ + "gh", + "release", + "create", + args.tag, + "--draft", + "--title", + title, + "--verify-tag", + "--notes-file", + f.name, + ] + print("*", " ".join(command)) + if args.dry_run: + print("Dry run, not actually releasing") + else: + subprocess.check_call(command) + + +if __name__ == "__main__": + main() diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/dev.yaml b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/dev.yaml new file mode 100644 index 0000000..2adaab3 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/dev.yaml @@ -0,0 +1,80 @@ +<% if private %> +# Copyright (c) 2025-2026 Columnar Technologies Inc. All rights reserved. +<% else %> +# 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. +<% endif %> + +# This is a common workflow for performing basic PR checks like running +# pre-commit. + +name: Dev + +on: + pull_request: {} + push: + branches: + - main + +concurrency: + group: ${{ github.repository }}-${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +permissions: + contents: read + +jobs: + lint: + name: "lint & pre-commit" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + +<% if lang.get("go") %> + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + cache-dependency-path: <{go_mod_path}>/go.sum + check-latest: true + go-version-file: <{go_mod_path}>/go.mod +<% endif %> + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.x" + + - name: Install + run: pip install pre-commit + + - name: pre-commit (cache) + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + + - name: pre-commit + run: | + pre-commit run --all-files --color=always --show-diff-on-failure --verbose + + - name: Analyze workflows with zizmor + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + with: + advanced-security: false diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/dev_issues.yaml b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/dev_issues.yaml new file mode 100644 index 0000000..6bd21d6 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/dev_issues.yaml @@ -0,0 +1,52 @@ +<% if private %> +# Copyright (c) 2025-2026 Columnar Technologies Inc. All rights reserved. +<% else %> +# Copyright (c) 2025-2026 ADBC Drivers Contributors +<% endif %> +# +# This file has been modified from its original version, which is +# under the Apache License: +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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: Issues Bot + +on: + issue_comment: + types: + - created + - edited + +jobs: + issue_assign: + name: "Assign issue" + permissions: + issues: write + if: github.event.comment.body == 'take' + runs-on: ubuntu-slim + steps: + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + assignees: context.payload.comment.user.login + }); diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/dev_pr.yaml b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/dev_pr.yaml new file mode 100644 index 0000000..d0e0beb --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/dev_pr.yaml @@ -0,0 +1,71 @@ +<% if private %> +# Copyright (c) 2025-2026 Columnar Technologies Inc. All rights reserved. +<% else %> +# 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. +<% endif %> + +# This is a common workflow for performing basic PR checks, like ensuring the +# title format. + +name: Dev PR + +on: + pull_request: + types: + - opened + - edited + - reopened + - synchronize + - ready_for_review + - review_requested + +concurrency: + group: ${{ github.repository }}-${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +permissions: + contents: read + +jobs: + pr_standard: + name: "Check PR" + runs-on: ubuntu-slim + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + +<% if driver == "dev" %> + - name: Check PR title format + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + python adbc_drivers_dev/title_check.py "$(pwd)" "$PR_TITLE" +<% else %> + - name: Check PR title format + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + git clone --depth 1 https://github.com/adbc-drivers/dev + python dev/adbc_drivers_dev/title_check.py "$(pwd)" "$PR_TITLE" +<% endif %> + + # TODO: assign milestone diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/go_test_pr.yaml b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/go_test_pr.yaml new file mode 100644 index 0000000..b8ec1b3 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/go_test_pr.yaml @@ -0,0 +1,134 @@ +<% if private %> +# Copyright (c) 2025-2026 Columnar Technologies Inc. All rights reserved. +<% else %> +# 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. +<% endif %> + +# !!!! AUTO-GENERATED FILE. DO NOT EDIT. !!!! +# USE adbc-gen-workflow (see adbc-drivers/dev) TO UPDATE THIS FILE. + +# This is a common workflow for testing a PR when permissions are needed. + +name: Go Test PR + +on: + workflow_dispatch: + inputs: + pr: + description: "The ID of the PR to test (e.g. 42)" + required: true + type: string + ref: + description: "The commit/ref to checkout" + required: true + type: string + +concurrency: + group: ${{ github.repository }}-${{ github.workflow }}-${{ inputs.pr }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +permissions: + contents: read + packages: read + pull-requests: read + +jobs: + setup: + name: configure job + runs-on: ubuntu-slim + outputs: + repository: ${{ steps.get_repo.outputs.repository }} + steps: + - name: get repository + id: get_repo + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ inputs.pr }} + run: | + echo repository=$(gh pr --repo ${{ github.repository }} view "${PR}" --json headRepository,headRepositoryOwner --jq '"\(.headRepositoryOwner.login)/\(.headRepository.name)"') | tee -a $GITHUB_OUTPUT + + - name: set job summary + env: + PR: ${{ inputs.pr }} + REF: ${{ inputs.ref }} + REPOSITORY: ${{ steps.get_repo.outputs.repository }} + run: | + echo "**PR:** https://github.com/${{ github.repository }}/pull/${PR}" >> $GITHUB_STEP_SUMMARY + echo "**Remote:** ${REPOSITORY}@${REF}" >> $GITHUB_STEP_SUMMARY + + test: + uses: adbc-drivers/<{repository}>/.github/workflows/go_test.yaml@main # zizmor: ignore[unpinned-uses] + needs: + - setup + with: + repository: ${{ needs.setup.outputs.repository }} + ref: ${{ inputs.ref }} +<% if permissions.get("id_token") %> + permissions: + id-token: write +<% endif %> + secrets: + # https://github.com/orgs/community/discussions/25238#discussioncomment-3247035 +<% for name, val in secrets["all"].items() %> + <{val}>: ${{ secrets.<{val}> }} +<% endfor %> + + report: + name: comment on PR + runs-on: ubuntu-slim + if: ${{ always() }} + needs: + - setup + - test + permissions: + actions: read + pull-requests: write + env: + TEST_RESULT: ${{ needs.test.result }} + steps: + - id: get_run + env: + GH_TOKEN: ${{ github.token }} + run: | + echo workflow_run_url=$(gh run --repo ${{ github.repository }} view ${{ github.run_id }} --json url --jq '.url') | tee -a $GITHUB_OUTPUT + if [[ "$TEST_RESULT" == 'success' ]]; then + echo message=":heavy_check_mark: **Test passed:** " | tee -a $GITHUB_OUTPUT + else + echo message=":x: **Test failed:** " | tee -a $GITHUB_OUTPUT + fi + + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUTS: ${{ toJSON(inputs) }} + MESSAGE: ${{ steps.get_run.outputs.message }} + REPOSITORY: ${{ needs.setup.outputs.repository }} + WORKFLOW_RUN_URL: ${{ needs.setup.outputs.workflow_run_url }} + with: + github-token: ${{ github.token }} + script: | + const inputs = JSON.parse(process.env.INPUTS); + const message = process.env.MESSAGE; + const repository = process.env.REPOSITORY; + const workflow_run_url = process.env.WORKFLOW_RUN_URL; + github.rest.issues.createComment({ + issue_number: inputs["pr"], + owner: context.repo.owner, + repo: context.repo.repo, + body: `${message} ${repository}@${inputs["ref"]}\nWorkflow run: ${workflow_run_url}` + }) diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/golangci.toml b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/golangci.toml new file mode 100644 index 0000000..a878677 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/golangci.toml @@ -0,0 +1,41 @@ +<% if private %> +# Copyright (c) 2025-2026 Columnar Technologies Inc. All rights reserved. +<% else %> +# 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. +<% endif %> + +version = "2" + +[formatters] +enable = [ + "gofmt", +] + +[issues] +max-issues-per-linter = 0 +max-same-issues = 0 +uniq-by-line = false + +[linters] +enable = [ + "errcheck", + "govet", + "ineffassign", + "intrange", + "mirror", + "nolintlint", + "staticcheck", + "unused", +] diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/pixi.toml b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/pixi.toml new file mode 100644 index 0000000..8574c59 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/pixi.toml @@ -0,0 +1,56 @@ +<% if private %> +# Copyright (c) 2025-2026 Columnar Technologies Inc. All rights reserved. +<% else %> +# 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. +<% endif %> + +# !!!! AUTO-GENERATED FILE. DO NOT EDIT. !!!! +# USE adbc-gen-workflow (see adbc-drivers/dev) TO UPDATE THIS FILE. + +[workspace] +<% if private %> +authors = ["Columnar Technologies"] +<% else %> +authors = ["ADBC Drivers Contributors"] +<% endif %> +channels = ["conda-forge"] +name = "adbc-driver-<{driver}>" +platforms = ["linux-64", "osx-arm64", "win-64", "linux-aarch64"] +version = "0.1.0" + +[tasks] +<% if lang == "go" %> +make = "adbc-make run build DRIVER=<{driver}> VERBOSE=true" +test = "go test -tags assert -v ./..." +<% elif lang == "rust" %> +make = "adbc-make run build DRIVER=<{driver}> VERBOSE=true IMPL_LANG=rust" +test = "cargo test" +<% endif %> +release = "adbc-release" +validate = "pytest -vvs --junit-xml=validation-report.xml -rfEsxX validation/tests/" +gendocs = "python -m validation.tests.generate_documentation" + +[dependencies] +python = ">=3.14.3,<3.15" +<% for key, value in validation["extra_dependencies"].items() %> +<{key}> = "<{value}>" +<% endfor %> + +[pypi-dependencies] +adbc-drivers-dev = { git = "https://github.com/adbc-drivers/dev" } +adbc-drivers-validation = { git = "https://github.com/adbc-drivers/validation" } +<% for key, value in validation["extra_pypi_dependencies"].items() %> +<{key}> = "<{value}>" +<% endfor %> diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/test.yaml b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/test.yaml new file mode 100644 index 0000000..b4e6110 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/templates/test.yaml @@ -0,0 +1,907 @@ +<% if private %> +# Copyright (c) 2025-2026 Columnar Technologies Inc. All rights reserved. +<% else %> +# 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. +<% endif %> + +# !!!! AUTO-GENERATED FILE. DO NOT EDIT. !!!! +# USE adbc-gen-workflow (see adbc-drivers/dev) TO UPDATE THIS FILE. + +# This is a common workflow for building & testing drivers: +# +# - Build the driver +# - Start dependencies +# - Run tests +# - Build the shared library + +name: <{lang_human}> <{workflow_name}> + +on: +<% if not release %> + pull_request: + branches: + - main + paths: + - "<{lang_path_glob}>" +<% for path in pull_request_trigger_paths %> + - <{path}> +<% endfor %> +<% endif %> + push: +<% if release %> + tags: + - "<{lang_tag_prefix}>v**" +<% else %> + branches: + - main + paths: + - "<{lang_path_glob}>" +<% for path in pull_request_trigger_paths %> + - <{path}> +<% endfor %> +<% endif %> + +<% if not release and secrets["all"] %> + 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: +<% for name, val in secrets["all"].items() %> + <{val}>: + required: true +<% endfor %> +<% endif %> +<% if release %> + workflow_dispatch: {} +<% endif %> + +concurrency: +<% if environment %> + # Must share concurrency group with release workflow since it also builds/tests + group: ${{ github.repository }}-${{ github.ref }}-<{environment.replace(' ', '-')}> +<% else %> + group: ${{ github.repository }}-${{ github.ref }}-${{ github.workflow }} +<% endif %> + cancel-in-progress: true + +defaults: + run: + shell: bash + +permissions: + contents: read + +jobs: +<% if not lang_config.test.skip %> + test: + name: "Test/${{ matrix.platform }}_${{ matrix.arch }}" + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: true + matrix: + include: + - { platform: linux, arch: amd64, runner: ubuntu-latest } + - { platform: macos, arch: arm64, runner: macos-latest } + - { platform: windows, arch: amd64, runner: windows-latest } +<% if environment and "test" in lang_config.build.environment_contexts or "build:test" in lang_config.build.environment_contexts %> + environment: <{environment}> +<% endif %> + permissions: + contents: read +<% if permissions.get("id_token") %> + id-token: write +<% endif %> + 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" + +<% if release or not secrets["all"] %> + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' +<% else %> + - 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' +<% endif %> + +<% if "go" in lang_tools %> + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + cache: <{"false" if release else "true"}> + cache-dependency-path: <{go_mod_path}>/go.sum + check-latest: true + go-version-file: <{go_mod_path}>/go.mod +<% endif %> +<% if "rust" in lang_tools %> + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 + with: + cache: <{"false" if release else "true"}> + components: "clippy" +<% endif %> + + - uses: prefix-dev/setup-pixi@5185adfbffb4bd703da3010310260805d89ebb11 # v0.9.6 + with: + pixi-version: v0.63.2 + run-install: false + +<% if aws %> + - name: AWS Login + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + aws-region: <{aws.region}> + role-to-assume: ${{ secrets.aws_role }} + role-session-name: ${{ secrets.aws_role_session_name }} +<% endif %> +<% if gcloud %> + - name: Google Cloud Login + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 + with: + service_account: ${{ secrets.gcloud_service_account }} + workload_identity_provider: ${{ secrets.gcloud_workload_identity_provider }} +<% endif %> + + - name: Build +<% if secrets["build:test"] %> + env: +<% for name, val in secrets["build:test"].items() %> + <{name}>: ${{ secrets.<{val}> }} # zizmor: ignore[secrets-outside-env] +<% endfor %> +<% endif %> + working-directory: <{ lang_subdir }> + 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 + +<% if lang == "go" %> + go build ./... +<% elif lang == "rust" %> + cargo build --locked +<% elif lang == "script" %> + ./ci/scripts/build.sh test ${{ matrix.platform }} ${{ matrix.arch }} +<% else %> +<{not_implemented(lang)}> +<% endif %> + + - 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: <{ lang_subdir }> + run: | + if [[ -f compose.yaml ]]; then + if ! docker compose up --detach --wait <{lang_config.test.service_name}>; then + echo "Service failed to start" + echo "Logs:" + docker compose logs <{lang_config.test.service_name}> + exit 1 + fi + fi + + - name: Test +<% if secrets["test"] %> + env: +<% for name, val in secrets["test"].items() %> + <{name}>: ${{ secrets.<{val}> }} # zizmor: ignore[secrets-outside-env] +<% endfor %> +<% endif %> + working-directory: <{ lang_subdir }> + 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 + +<% if lang == "go" %> + go test -tags assert -v ./... +<% elif lang == "rust" %> + cargo test +<% elif lang == "script" %> + ./ci/scripts/test.sh ${{ matrix.platform }} ${{ matrix.arch }} +<% else %> +<{not_implemented(lang)}> +<% endif %> + + if [[ -f ci/scripts/post-test.sh ]]; then + ./ci/scripts/post-test.sh + fi + +<% if lang == "go" %> + - name: go mod tidy + if: runner.os == 'Linux' + working-directory: <{ lang_subdir }> + run: | + go mod tidy --diff +<% elif lang == "rust" %> + - name: clippy + working-directory: <{ lang_subdir }> + run: | + cargo clippy --locked --no-deps -- -A unused_variables -D warnings +<% endif %> +<% endif %> + +<% if not lang_config.validation.skip %> + validate: + name: "Validate <{driver}> ${{ matrix.vendor_version }}/${{ matrix.platform }}_${{ matrix.arch }}" + runs-on: ${{ matrix.runner }} +<% if driver == "snowflake" %> + # Go and Rust validation use fixed table names in the same live schema. + concurrency: + group: snowflake-live-validation + cancel-in-progress: false +<% endif %> + strategy: + fail-fast: true + matrix: + include: + # I think we only need to test one platform, but we can change that later +<% for config in lang_config.validation.configs %> + - { platform: linux, arch: amd64, runner: ubuntu-latest, service_name: "<{config.service_name}>", vendor_version: "<{config.vendor_version}>" } +<% endfor %> +<% if environment and "validate" in lang_config.build.environment_contexts or "build:test" in lang_config.build.environment_contexts %> + environment: <{environment}> +<% endif %> + permissions: + contents: read +<% if permissions.get("id_token") %> + id-token: write +<% endif %> + 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" + +<% if release or not secrets["all"] %> + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' +<% else %> + - 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' +<% endif %> + +<% if "go" in lang_tools %> + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + cache: <{"false" if release else "true"}> + cache-dependency-path: <{go_mod_path}>/go.sum + check-latest: true + go-version-file: <{go_mod_path}>/go.mod +<% endif %> +<% if "rust" in lang_tools %> + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 + with: + cache: <{"false" if release else "true"}> + components: "clippy" +<% endif %> + + - uses: prefix-dev/setup-pixi@5185adfbffb4bd703da3010310260805d89ebb11 # v0.9.6 + with: + pixi-version: v0.63.2 + run-install: false + +<% if aws %> + - name: AWS Login + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + aws-region: <{aws.region}> + role-to-assume: ${{ secrets.aws_role }} + role-session-name: ${{ secrets.aws_role_session_name }} +<% endif %> +<% if gcloud %> + - name: Google Cloud Login + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 + with: + service_account: ${{ secrets.gcloud_service_account }} + workload_identity_provider: ${{ secrets.gcloud_workload_identity_provider }} +<% endif %> + + - 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 +<% if secrets["build:test"] %> + env: +<% for name, val in secrets["build:test"].items() %> + <{name}>: ${{ secrets.<{val}> }} # zizmor: ignore[secrets-outside-env] +<% endfor %> +<% endif %> + working-directory: <{ lang_subdir }> + 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=<{driver}> IMPL_LANG=<{lang}> <{' '.join(lang_config.build.additional_make_args) }> + - 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: <{ lang_subdir }> + run: | + if [[ -f compose.yaml ]]; then + if ! docker compose up --detach --wait ${{ matrix.service_name }}; then + echo "Service failed to start" + echo "Logs:" + docker compose logs ${{ matrix.service_name }} + exit 1 + fi + fi + + - name: Validate + if: runner.os == 'Linux' +<% if secrets["validate"] %> + env: +<% for name, val in secrets["validate"].items() %> + <{name}>: ${{ secrets.<{val}> }} # zizmor: ignore[secrets-outside-env] +<% endfor %> +<% endif %> + working-directory: <{ lang_subdir }> + 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 }} ${{ matrix.service_name }} + fi + + docker ps + if [[ -n "${{ matrix.vendor_version }}" ]]; then + pixi run validate --vendor-version "${{ matrix.vendor_version }}" + else + pixi run validate + fi + + if [[ -f ci/scripts/post-test.sh ]]; then + ./ci/scripts/post-test.sh + fi + + cp validation-report.xml validation-report-${{ matrix.vendor_version }}.xml + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: validation-report-${{ matrix.vendor_version }} + path: "<{ lang_subdir }>/validation-report-${{ matrix.vendor_version }}.xml" + retention-days: 7 + + - name: Generate docs + working-directory: <{ lang_subdir }> + run: | + pixi run gendocs --output generated + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: docs-${{ matrix.vendor_version }} + path: "<{ lang_subdir }>/generated/<{driver}>.md" + retention-days: 7 +<% endif %> + + build: + name: "Build <{driver}>/${{ matrix.platform }}_${{ matrix.arch }}" +<% if not lang_config.test.skip %> + needs: test +<% endif %> + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: true + 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 } + - { platform: windows, arch: amd64, runner: windows-latest } +<% if environment and "build:release" in lang_config.build.environment_contexts %> + environment: <{environment}> +<% endif %> + permissions: + contents: read + packages: read + + steps: +<% if release or not secrets["all"] %> + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' +<% else %> + - 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' +<% endif %> + +<% if "go" in lang_tools %> + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + cache: <{"false" if release else "true"}> + cache-dependency-path: <{go_mod_path}>/go.sum + check-latest: true + go-version-file: <{go_mod_path}>/go.mod +<% endif %> +<% if "rust" in lang_tools %> + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 + with: + cache: <{"false" if release else "true"}> + components: "clippy" +<% endif %> + + - uses: prefix-dev/setup-pixi@5185adfbffb4bd703da3010310260805d89ebb11 # v0.9.6 + with: + pixi-version: v0.63.2 + run-install: false + + - name: Install dev tools + working-directory: <{ lang_subdir }> + 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: <{ lang_subdir }> +<% if secrets["build:release"] %> + env: +<% for name, val in secrets["build:release"].items() %> + <{name}>: ${{ secrets.<{val}> }} # zizmor: ignore[secrets-outside-env] +<% endfor %> +<% endif %> + 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=<{driver}> IMPL_LANG=<{lang}> <{' '.join(lang_config.build.additional_make_args) }><% if release %> RELEASE=true<% endif %> + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: drivers-${{ matrix.platform }}-${{ matrix.arch }} + path: "<{ lang_subdir }>/build/libadbc_driver_<{driver}>.*" + retention-days: 2 + + package: + name: "Generate Packages" + runs-on: ubuntu-latest + needs: + - build +<% if not lang_config.validation.skip %> + - validate +<% endif %> + permissions: + contents: read + + steps: +<% if release or not secrets["all"] %> + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' +<% else %> + - 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' +<% endif %> + +<% if "go" in lang_tools %> + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + cache: <{"false" if release else "true"}> + check-latest: true + go-version: "stable" +<% endif %> +<% if "rust" in lang_tools %> + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 + with: + cache: <{"false" if release else "true"}> + components: "clippy" +<% endif %> + + - uses: prefix-dev/setup-pixi@5185adfbffb4bd703da3010310260805d89ebb11 # v0.9.6 + with: + pixi-version: v0.63.2 + run-install: false + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: "drivers-*" + path: "~/drivers" + + - name: Install tools + working-directory: <{ lang_subdir }> + run: | +<% if "go" in lang_tools %> + # XXX: Google has effectively abandoned go-licenses, and it broke + # XXX: can't install go-licenses under go 1.25 + # Use our fork instead + # https://github.com/google/go-licenses/issues/312 + go install github.com/lidavidm/go-licenses@master +<% endif %> +<% if "rust" in lang_tools %> + cargo install cargo-about@0.9.0 --locked -F cli +<% endif %> + + - name: Generate packages + working-directory: <{ lang_subdir }> + run: | + pixi install + + pixi run adbc-gen-package \ + --name <{driver}> \ + --root "$(pwd)" \ + --manifest-template "$(pwd)/manifest.toml" \ + <% if release %>--release<% endif %> \ + -o ~/packages \ + ~/drivers/drivers-*-*/ + + ls ~/packages + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: all-packages + path: ~/packages + retention-days: 7 + +<% if not lang_config.validation.skip %> + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: "validation-report-*" + path: "~/validation-report" + + - name: Generate combined docs + working-directory: <{ lang_subdir }> + run: | + ls -laR ~/validation-report + find ~/validation-report -type f -name '*.xml' + cp $(find ~/validation-report -type f -name '*.xml') . + ls *.xml + pixi run gendocs --output generated + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: docs + path: "<{ lang_subdir }>/generated/<{driver}>.md" + retention-days: 2 +<% endif %> + + 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 } + - { platform: windows, arch: amd64, runner: windows-latest } +<% if environment and "build:test" in lang_config.build.environment_contexts %> + environment: <{environment}> +<% endif %> + steps: + # for now, install dbc from main + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + cache: <{"false" if release else "true"}> + go-version: 'stable' + - uses: prefix-dev/setup-pixi@5185adfbffb4bd703da3010310260805d89ebb11 # v0.9.6 + 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}" +<% if release or not secrets["all"] %> + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + submodules: 'recursive' +<% else %> + - 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' +<% endif %> + - name: load package +<% if private or secrets["build:test"] %> + env: +<% if private %> + COLUMNAR_CLOUD_API_TOKEN: ${{ secrets.COLUMNAR_CLOUD_API_TOKEN }} # zizmor: ignore[secrets-outside-env] +<% endif %> +<% if secrets["build:test"] %> +<% for name, val in secrets["build:test"].items() %> + <{name}>: ${{ secrets.<{val}> }} # zizmor: ignore[secrets-outside-env] +<% endfor %> +<% endif %> +<% endif %> + working-directory: <{ lang_subdir }> + 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 +<% if private %> + export COLUMNAR_DRIVER_LICENSE_KEY=$(curl --silent --fail -XGET -H 'Content-Type: application/json' -H "Authorization: Bearer $COLUMNAR_CLOUD_API_TOKEN" https://heimdall.columnar.tech/license) +<% endif %> + pixi exec -s adbc-driver-manager -s pyarrow -s pytest python -m pytest -vs ci/test_package.py + + release: +<% if release %> + name: "Release" +<% else %> + name: "Release (Dry Run)" +<% endif %> + runs-on: ubuntu-latest + needs: + - package + - test-packages +<% if not lang_config.validation.skip %> + - validate +<% endif %> + permissions: +<% if release %> + contents: write +<% else %> + contents: read +<% endif %> + + steps: +<% if release or not secrets["all"] %> + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' +<% else %> + - 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' +<% endif %> + + - uses: prefix-dev/setup-pixi@5185adfbffb4bd703da3010310260805d89ebb11 # v0.9.6 + with: + pixi-version: v0.63.2 + run-install: false + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: "all-packages" + path: "~/packages" + +<% if not lang_config.validation.skip %> + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: "docs" + path: "~/packages" +<% endif %> + +<% if release %> + - name: Release + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: <{ lang_subdir }> + run: | + tag="${GITHUB_REF_NAME}" +<% if lang == "rust" %> + declared_version=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') + if [[ "${declared_version}" != "${tag#*v}" ]]; then + echo "Declared driver version in Cargo.toml ${declared_version}" + echo "does not match tag ${tag#*v}" + echo "Please update the version before release." + exit 1 + fi +<% endif %> + pixi run release $(pwd) $tag + gh release upload $tag $(find ~/packages -name '*.tar.gz') $(find ~/packages -name 'manifest.yaml') $(find ~/packages -name '*.md') + + - name: Release (dry-run) + if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: <{ lang_subdir }> + run: | + git tag <{lang_tag_prefix}>v1000.0.0 + tag=<{lang_tag_prefix}>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') +<% else %> + - name: Release (dry-run) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: <{ lang_subdir }> + run: | + git tag <{lang_tag_prefix}>v1000.0.0 + tag=<{lang_tag_prefix}>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') +<% endif %> diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/title_check.py b/vendor/adbc-drivers-dev/adbc_drivers_dev/title_check.py new file mode 100644 index 0000000..61c85d6 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/title_check.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# Copyright (c) 2025 ADBC Drivers Contributors +# +# This file has been modified from its original version, which is +# under the Apache License: +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 https://github.com/apache/arrow-adbc/blob/main/dev/adbc_dev/title_check.py +# Commit d9dad703bbaf499406f548276d46fad08b292144 +# Modified to work with pygit2 + +import argparse +import re +import sys +import typing +from pathlib import Path + +COMMIT_TYPES = { + "build", + "chore", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "test", +} + + +class Commit(typing.NamedTuple): + category: str + components: list[str] + breaking_change: bool + subject: str + + failed_validation_reasons: list[str] + + +AllowedComponents = typing.Callable[[str], bool] + + +def any_components() -> AllowedComponents: + return lambda _: True + + +def path_components(root: Path) -> AllowedComponents: + def _pred(component: str) -> bool: + # Check if the component is a valid file or directory in the repo + path = root / component + return path.is_dir() + + return _pred + + +def matches_commit_format( + allowed_component: AllowedComponents, title: str +) -> list[str]: + """Check a title and return a list of reasons why it's invalid.""" + # Relax the initial regex a bit, do more friendly validation below + # We'll allow a deviation from Conventional Commits (feat!(foo) instead of + # feat(foo)!) since that appears to have snuck in already + commit_type = "([a-z]+)" + breaking = "(!?)" + scope = r"(?:\(([^\)]*)\))?" + delimiter = "(!?):" + subject = " (.+)" + commit = re.compile(f"^{commit_type}{breaking}{scope}{delimiter}{subject}$") + valid_component = re.compile(r"^[a-zA-Z0-9_/\-\.]+$") + + m = commit.match(title) + if m is None: + commit_spec = "https://www.conventionalcommits.org/en/v1.0.0/" + return Commit( + category="", + components=[], + breaking_change=False, + subject="", + failed_validation_reasons=[f"Format is incorrect, see {commit_spec}"], + ) + + reasons = [] + commit_type = m.group(1) + if commit_type not in COMMIT_TYPES: + reasons.append(f"Invalid commit type: {commit_type}") + + breaking = m.group(2) + components = m.group(3) + if components is not None: + if not components.strip(): + reasons.append("Invalid components: must not be empty") + else: + components = components.split(",") + for component in components: + if component != component.strip(): + reasons.append( + f"Invalid component: must have no trailing space: {component}" + ) + elif not valid_component.match(component): + reasons.append( + "Invalid component: must be alphanumeric " + f"plus [.-/]: {component}" + ) + elif not allowed_component(component): + reasons.append( + "Invalid component: must reference a file " + f"or directory in the repo: {component}" + ) + + delimiter = m.group(4) + subject = m.group(5) + if subject.strip() != subject: + reasons.append(f"Invalid subject: must have no trailing space: {subject}") + if subject.strip().endswith("."): + reasons.append(f"Invalid subject: must not end in a period: {subject}") + + if bool(breaking) and bool(delimiter): + # feat!(foo)!: subject + reasons.append("Can only provide breaking-change '!' once") + + return Commit( + category=commit_type, + components=components or [], + breaking_change=bool(breaking) or bool(delimiter), + subject=subject, + failed_validation_reasons=reasons, + ) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("root", type=Path, help="The root of the repository") + parser.add_argument("title", help="The PR title to check") + + args = parser.parse_args() + + print(f'PR title: "{args.title}"') + + commit = matches_commit_format(path_components(args.root), args.title) + if not commit.failed_validation_reasons: + print("Title is valid") + return 0 + + print("Title is invalid:") + for reason in commit.failed_validation_reasons: + print("-", reason) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/vendor/adbc-drivers-dev/adbc_drivers_dev/workflow.py b/vendor/adbc-drivers-dev/adbc_drivers_dev/workflow.py new file mode 100644 index 0000000..2bb02f3 --- /dev/null +++ b/vendor/adbc-drivers-dev/adbc_drivers_dev/workflow.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +# 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. + +"""Generate workflows for a particular repository.""" + +import argparse +import functools +import json +import re +import subprocess +import sys +import typing +from pathlib import Path + +import jinja2 +import packaging.version +import tomlkit + +from .generate import GenerateConfig + + +def write_workflow( + root: Path, template, filename: str, params: dict[str, typing.Any] +) -> None: + rendered = template.render(**params) + rendered = rendered.splitlines() + rendered = "\n".join(line.rstrip() for line in rendered) + rendered += "\n" + sink = root / filename + with sink.open("w") as f: + f.write(rendered) + if not rendered.endswith("\n"): + f.write("\n") + print("Wrote", sink) + + +def generate_schema(args) -> int: + """Generate JSON schema from Pydantic models.""" + schema = GenerateConfig.model_json_schema(mode="validation") + output_path = Path(__file__).parent.parent / "schema" / "generate-schema.json" + with output_path.open("w") as f: + json.dump(schema, f, indent=2) + f.write("\n") # trailing newlines are nice + print(f"Wrote schema to {output_path}") + return 0 + + +def template_not_implemented(message: str) -> str: + raise NotImplementedError(message) + + +def generate_workflows(args) -> int: + env = jinja2.Environment( + loader=jinja2.PackageLoader("adbc_drivers_dev"), + autoescape=jinja2.select_autoescape(), + block_start_string="<%", + block_end_string="%>", + variable_start_string="<{", + variable_end_string="}>", + trim_blocks=True, + undefined=jinja2.StrictUndefined, + ) + env.globals["not_implemented"] = template_not_implemented + + config_path = args.repository / ".github/workflows/generate.toml" + try: + with config_path.open("rb") as f: + params = tomlkit.load(f).unwrap() + except FileNotFoundError: + print(f"{config_path} not found.", file=sys.stderr) + + config_path.parent.mkdir(parents=True, exist_ok=True) + with config_path.open("w") as f: + # Add schema directive for tombi validation + f.write( + "#:schema https://raw.githubusercontent.com/adbc-drivers/dev/refs/heads/main/schema/generate-schema.json\n\n" + ) + tomlkit.dump( + GenerateConfig().model_dump( + mode="json", + by_alias=True, + exclude_none=True, + ), + f, + ) + print("Wrote out defaults, please fill it in.", file=sys.stderr) + + return 1 + + params = GenerateConfig.model_validate(params) + print(params) + workflows = args.repository / ".github/workflows" + langs = { + "go": ("Go", "go"), + "rust": ("Rust", "rust"), + "script": ("Custom", "src"), + } + + retcode = 0 + for lang, (lang_human, lang_subdir_default) in langs.items(): + lang_config = params.lang.get(lang) + if not lang_config: + continue + + lang_subdir = ( + lang_config.subdir + if lang_config.subdir is not None + else lang_subdir_default + ) + lang_tag_prefix = "" if lang_subdir == "." else f"{lang_subdir}/" + lang_path_glob = "**" if lang_subdir == "." else f"{lang_subdir}/**" + + (args.repository / lang_subdir).mkdir(parents=True, exist_ok=True) + + lang_tools = {lang} + lang_tools.update(lang_config.build.lang_tools) + + template = env.get_template("test.yaml") + go_mod_path = ( + (lang_config.build.go_mod_path or lang_subdir) + if lang == "go" + else langs["go"][1] + ) + lang_ctx = { + "lang": lang, + "lang_human": lang_human, + "lang_subdir": lang_subdir, + "lang_tag_prefix": lang_tag_prefix, + "lang_path_glob": lang_path_glob, + "go_mod_path": go_mod_path, + "lang_config": lang_config, + "lang_tools": lang_tools, + } + write_workflow( + workflows, + template, + f"{lang}_test.yaml", + { + **params.to_dict(), + **lang_ctx, + "pull_request_trigger_paths": [f".github/workflows/{lang}_test.yaml"], + "release": False, + "workflow_name": "Test", + }, + ) + write_workflow( + workflows, + template, + f"{lang}_release.yaml", + { + **params.to_dict(), + **lang_ctx, + "release": True, + "workflow_name": "Release", + }, + ) + template = env.get_template("go_test_pr.yaml") + if params._processed_secrets["all"]: + write_workflow( + workflows, + template, + f"{lang}_test_pr.yaml", + { + **params.to_dict(), + }, + ) + + template = env.get_template("pixi.toml") + write_workflow( + args.repository / lang_subdir, + template, + "pixi.toml", + { + **params.to_dict(), + **lang_ctx, + }, + ) + + license_template = args.repository / lang_subdir / "license.tpl" + if not license_template.is_file(): + print(f"Missing {license_template}", file=sys.stderr) + retcode = 1 + + if lang == "go": + template = env.get_template("golangci.toml") + write_workflow( + args.repository, + template, + ".golangci.toml", + { + **params.to_dict(), + }, + ) + + for dev in ["dev.yaml", "dev_issues.yaml", "dev_pr.yaml"]: + template = env.get_template(dev) + write_workflow( + workflows, + template, + dev, + { + **params.to_dict(), + "go_mod_path": go_mod_path, + }, + ) + + return retcode + + +@functools.cache +def latest_action_version(action: str) -> (packaging.version.Version, str, str): + # XXX: this won't work with repos that have multiple actions + result = subprocess.check_output( + [ + "git", + "ls-remote", + # don't use --refs which will hide the commit behind annotated tags + "--tags", + "--exit-code", + "--quiet", + f"https://github.com/{action}", + ], + text=True, + ) + # do some remapping: we may have both refs/tags/v9 and refs/tags/v9^{} + # when the remote uses annotated tags. we want to keep only the latter if + # it exists + + # raw ref: sha + refs = {} + for line in result.strip().splitlines(): + sha, ref = line.split() + tag = ref.removeprefix("refs/tags/").removesuffix("^{}") + if tag == "master" or "-node" in tag or tag == "testEnableForGHES": + # aws-actions/configure-aws-credentials, others have weird tags + continue + refs[ref] = (sha, tag) + + for ref in list(refs): + if ref.endswith("^{}") and ref[:-3] in refs: + del refs[ref[:-3]] + + tags = [] + for ref, (sha, tag) in refs.items(): + version = packaging.version.parse(tag.lstrip("v")) + tags.append((version, tag, sha)) + + tags.sort(key=lambda x: x[0]) + latest = tags[-1] + return latest + + +def update_actions() -> None: + root = Path(__file__).parent / "templates" + templates = root.rglob("*.yaml") + + action_re = re.compile(r"uses: ([\w\-/]+)@([\w\-.]+)(\W*#.*)?") + + for template in templates: + print("Updating", template) + + with template.open("r") as f: + content = f.read() + + def replace_action(match: re.Match[str]) -> str: + latest = latest_action_version(match.group(1)) + + if match.group(2) == latest[2]: + print(f" {match.group(1)} already at {latest[2]} ({latest[1]})") + else: + print( + f" {match.group(1)} updated from {match.group(2)} to {latest[2]} ({latest[1]})" + ) + return f"uses: {match.group(1)}@{latest[2]} # {latest[1]}" + + new_content = action_re.sub(replace_action, content) + + with template.open("w") as f: + f.write(new_content) + + +def main(): + parser = argparse.ArgumentParser() + subcommand = parser.add_subparsers(dest="subcommand", required=True) + + generate = subcommand.add_parser("generate") + generate.add_argument("repository", type=Path) + + subcommand.add_parser("generate-schema") + subcommand.add_parser("update-actions") + + args = parser.parse_args() + + if args.subcommand == "generate": + return generate_workflows(args) + elif args.subcommand == "generate-schema": + return generate_schema(args) + elif args.subcommand == "update-actions": + update_actions() + return 0 + else: + parser.print_help() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/vendor/adbc-drivers-dev/pyproject.toml b/vendor/adbc-drivers-dev/pyproject.toml new file mode 100644 index 0000000..6034f6c --- /dev/null +++ b/vendor/adbc-drivers-dev/pyproject.toml @@ -0,0 +1,62 @@ +# 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. + +[project] +name = "adbc-drivers-dev" +version = "0.1" + +dependencies = [ + "doit", + "jinja2", + "packaging", + "platformdirs", + "pydantic>=2.0", + "pygit2", + "requests", + "ruamel.yaml>=0.18.11,<0.19", + "tomlkit >=0.13.2,<0.14", +] + +[project.scripts] +pre-commit-copyright = "adbc_drivers_dev.copyright:main" +pre-commit-rat = "adbc_drivers_dev.rat:main" +adbc-gen-package = "adbc_drivers_dev.package:main" +adbc-gen-workflow = "adbc_drivers_dev.workflow:main" +adbc-make = "adbc_drivers_dev.make:main" +adbc-release = "adbc_drivers_dev.release:main" + +[build-system] +requires = ["setuptools >= 77.0.3"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ["adbc_drivers_dev", "adbc_drivers_dev.rat"] + +[tool.pixi.workspace] +channels = ["conda-forge"] +platforms = ["linux-64", "osx-arm64"] + +[tool.pixi.pypi-dependencies] +adbc-drivers-dev = { path = ".", editable = true } + +[tool.pixi.environments] +test = ["test"] + +[tool.pixi.tasks] + +[tool.pixi.feature.test.dependencies] +pytest = "*" + +[tool.pixi.feature.test.tasks] +test = "pytest -vs" diff --git a/vendor/adbc-drivers-dev/tests/test_detect_version.py b/vendor/adbc-drivers-dev/tests/test_detect_version.py new file mode 100644 index 0000000..0d66546 --- /dev/null +++ b/vendor/adbc-drivers-dev/tests/test_detect_version.py @@ -0,0 +1,248 @@ +# 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. + +# LOCAL MODIFICATIONS: Snowflake adds deterministic regressions for the vendored +# suffix and arbitrary-UID manylinux cache fixes. + +import subprocess + +import pytest +from adbc_drivers_dev.make import detect_version, maybe_build_docker + + +def test_manylinux_build_uses_writable_go_caches(tmp_path, monkeypatch): + from adbc_drivers_dev import make + + calls = [] + monkeypatch.setattr( + make, "check_call", lambda *args, **kwargs: calls.append((args, kwargs)) + ) + monkeypatch.setattr(make, "get_var", lambda name, default: default) + monkeypatch.setattr(make.platform, "system", lambda: "Linux") + + maybe_build_docker( + repo_root=tmp_path, + driver_root=tmp_path, + env={}, + args=["go", "build", "./pkg"], + ci=True, + container="manylinux", + ) + + command = calls[0][0][0] + service_index = command.index("manylinux") + assert command[service_index - 6 : service_index] == [ + "--env", + "GOCACHE=/tmp/go-build", + "--env", + "GOMODCACHE=/tmp/go-mod", + "--env", + "GOPATH=/tmp/go", + ] + + +def test_manylinux_rust_build_uses_writable_platform_cache(tmp_path, monkeypatch): + from adbc_drivers_dev import make + + calls = [] + monkeypatch.setattr( + make, "check_call", lambda *args, **kwargs: calls.append((args, kwargs)) + ) + monkeypatch.setattr(make, "get_var", lambda name, default: default) + monkeypatch.setattr(make.platform, "system", lambda: "Linux") + + maybe_build_docker( + repo_root=tmp_path, + driver_root=tmp_path, + env={}, + args=["cargo", "build", "--release"], + ci=True, + container="manylinux-rust", + ) + + command = calls[0][0][0] + service_index = command.index("manylinux-rust") + assert command[service_index - 6 : service_index] == [ + "--env", + "HOME=/tmp", + "--env", + "XDG_CACHE_HOME=/tmp/.cache", + "--env", + "CARGO_HOME=/.cargo", + ] + + +@pytest.fixture() +def repo(tmp_path): + subprocess.check_call(["git", "init", "--quiet"], cwd=tmp_path) + subprocess.check_call(["git", "config", "user.name", "Test User"], cwd=tmp_path) + subprocess.check_call( + ["git", "config", "user.email", "test@example.com"], cwd=tmp_path + ) + subprocess.check_call( + ["git", "commit", "--quiet", "--allow-empty", "-m", "chore: initial commit"], + cwd=tmp_path, + ) + return tmp_path + + +def test_no_tags(repo): + version = detect_version(repo) + assert version == "v0.0.1-dev" + + +def test_no_tags_strict(repo): + with pytest.raises(ValueError, match="No tags found"): + detect_version(repo, strict=True) + + +def test_on_tag(repo): + subprocess.check_call(["git", "tag", "v1.2.3"], cwd=repo) + version = detect_version(repo) + assert version == "v1.2.3" + + version = detect_version(repo, strict=True) + assert version == "v1.2.3" + + +def test_on_tag_subdir(repo): + subprocess.check_call(["git", "tag", "rust/v1.2.3"], cwd=repo) + version = detect_version(repo / "rust") + assert version == "v1.2.3" + + version = detect_version(repo / "rust", strict=True) + assert version == "v1.2.3" + + +def test_after_tag(repo): + subprocess.check_call(["git", "tag", "v1.2.3"], cwd=repo) + subprocess.check_call( + ["git", "commit", "--quiet", "--allow-empty", "-m", "feat: add new feature"], + cwd=repo, + ) + version = detect_version(repo) + assert version.startswith("v1.2.3-dev.1."), version + + +def test_after_tag_uses_semver_revision_prefix(repo, monkeypatch): + subprocess.check_call(["git", "tag", "v1.2.3"], cwd=repo) + subprocess.check_call( + ["git", "commit", "--quiet", "--allow-empty", "-m", "feat: add new feature"], + cwd=repo, + ) + + from adbc_drivers_dev import make + + original_check_output = make.check_output + + def check_output(args, *posargs, **kwargs): + if args == ["git", "rev-parse", "--short", "HEAD"]: + return "0747374" + return original_check_output(args, *posargs, **kwargs) + + monkeypatch.setattr(make, "check_output", check_output) + assert detect_version(repo) == "v1.2.3-dev.1.g0747374" + + +def test_after_tag_subdir(repo): + subprocess.check_call(["git", "tag", "rust/v1.2.3"], cwd=repo) + subprocess.check_call( + ["git", "commit", "--quiet", "--allow-empty", "-m", "feat: add new feature"], + cwd=repo, + ) + version = detect_version(repo / "rust") + assert version.startswith("v1.2.3-dev.1."), version + + +def test_after_prerelease(repo): + subprocess.check_call(["git", "tag", "v1.2.3-alpha.1"], cwd=repo) + subprocess.check_call( + ["git", "commit", "--quiet", "--allow-empty", "-m", "feat: add new feature"], + cwd=repo, + ) + version = detect_version(repo) + # This is a weird edge case + assert version.startswith("v0.0.1-dev.2."), version + + +def test_after_prerelease2(repo): + subprocess.check_call(["git", "tag", "v1.2.3"], cwd=repo) + subprocess.check_call( + ["git", "commit", "--quiet", "--allow-empty", "-m", "feat: add new feature"], + cwd=repo, + ) + subprocess.check_call(["git", "tag", "v1.3.0-alpha.1"], cwd=repo) + subprocess.check_call( + ["git", "commit", "--quiet", "--allow-empty", "-m", "feat: add new feature"], + cwd=repo, + ) + version = detect_version(repo) + assert version.startswith("v1.2.3-dev.2."), version + + +def test_after_prerelease_on_tag(repo): + subprocess.check_call(["git", "tag", "v1.2.3-alpha.1"], cwd=repo) + subprocess.check_call( + ["git", "commit", "--quiet", "--allow-empty", "-m", "feat: add new feature"], + cwd=repo, + ) + subprocess.check_call(["git", "tag", "v1.2.3"], cwd=repo) + version = detect_version(repo) + assert version == "v1.2.3" + + +def test_versions_are_inverted(repo): + # detect when versions are out of order + subprocess.check_call(["git", "tag", "v1.2.3"], cwd=repo) + subprocess.check_call( + ["git", "commit", "--quiet", "--allow-empty", "-m", "feat: add new feature"], + cwd=repo, + ) + subprocess.check_call(["git", "tag", "v1.2.3-alpha.1"], cwd=repo) + + with pytest.raises( + ValueError, + match="Tag v1.2.3 is further from HEAD than v1.2.3-alpha.1, but has a newer version", + ): + detect_version(repo) + + +def test_strict_not_on_tag(repo): + subprocess.check_call( + ["git", "commit", "--quiet", "--allow-empty", "-m", "feat: add new feature"], + cwd=repo, + ) + subprocess.check_call(["git", "tag", "v1.0.0"], cwd=repo) + subprocess.check_call( + ["git", "commit", "--quiet", "--allow-empty", "-m", "feat: add new feature"], + cwd=repo, + ) + subprocess.check_call(["git", "tag", "v1.2.3"], cwd=repo) + subprocess.check_call( + ["git", "commit", "--quiet", "--allow-empty", "-m", "feat: add new feature"], + cwd=repo, + ) + with pytest.raises( + ValueError, match="is not on tag v1.2.3, but has 1 commits since" + ): + detect_version(repo, strict=True) + + +def test_strict_dirty(repo): + subprocess.check_call(["git", "tag", "v1.2.3"], cwd=repo) + with (repo / "foobar.txt").open("w"): + pass + subprocess.check_call(["git", "add", "foobar.txt"], cwd=repo) + with pytest.raises(ValueError, match="has uncommitted changes"): + detect_version(repo, strict=True)