From 304f9537104fcbdd8d9b88d3a74a274be035b971 Mon Sep 17 00:00:00 2001 From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:43:46 +0200 Subject: [PATCH] Separate duckdb crate to fix c++ build/link issues (#6551) * call ffi * remove duckdb dep * rename windmill_duckdb_ffi_internal * static lib * ci * back to dylib, bug isn't fixed in static * feature flag and copy dynamic lib * fix dynlib in docker * load libwindmill_duckdb_ffi_internal at runtime on usage * lazy static deadlocks * Cache dynamic library handles * update auto s3 path insert from editor bar * Fix duckdb S3 freezing worker because of blocking task in tokio async context * build dll windows GH workflow * try fix windows build * revert build.rs * nit fixes CI * Dockerfile update (not tested yet * build dev sh for duckdb lib * mistake * attach windmill_duckdb_ffi_internal.so artefact * rhel9 * docker fixes * fix dockerfile * better err msg * forgot lib prefix .so * add column_order * fix column_order --- .dockerignore | 1 + .github/workflows/build-publish-rh-image.yml | 14 +- .github/workflows/build_windows_worker_.yml | 16 +- .github/workflows/docker-image.yml | 7 + .github/workflows/publish_windows_worker.yml | 12 + Dockerfile | 13 +- backend/.cargo/config.toml | 2 + backend/Cargo.lock | 45 +- backend/Cargo.toml | 7 +- .../windmill-duckdb-ffi-internal/Cargo.lock | 1532 +++++++++++++++++ .../windmill-duckdb-ffi-internal/Cargo.toml | 14 + .../README_DEV.md | 15 + backend/windmill-duckdb-ffi-internal/build.rs | 5 + .../windmill-duckdb-ffi-internal/build_dev.sh | 2 + .../windmill-duckdb-ffi-internal/src/lib.rs | 450 +++++ backend/windmill-worker/Cargo.toml | 4 +- .../windmill-worker/src/duckdb_executor.rs | 471 ++--- frontend/src/lib/components/EditorBar.svelte | 11 +- 18 files changed, 2240 insertions(+), 381 deletions(-) create mode 100644 backend/windmill-duckdb-ffi-internal/Cargo.lock create mode 100644 backend/windmill-duckdb-ffi-internal/Cargo.toml create mode 100644 backend/windmill-duckdb-ffi-internal/README_DEV.md create mode 100644 backend/windmill-duckdb-ffi-internal/build.rs create mode 100755 backend/windmill-duckdb-ffi-internal/build_dev.sh create mode 100644 backend/windmill-duckdb-ffi-internal/src/lib.rs diff --git a/.dockerignore b/.dockerignore index aa681c16c9..aa56cc82ef 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,3 +3,4 @@ frontend/build/ frontend/.svelte-kit/ backend/target/ +backend/windmill-duckdb-ffi-internal/target/ \ No newline at end of file diff --git a/.github/workflows/build-publish-rh-image.yml b/.github/workflows/build-publish-rh-image.yml index 848c30c7f6..a0f69acae0 100644 --- a/.github/workflows/build-publish-rh-image.yml +++ b/.github/workflows/build-publish-rh-image.yml @@ -97,6 +97,12 @@ jobs: image: ${{ steps.meta-ee-public.outputs.tags}}-amd64 path: "/windmill/target/release/windmill" + - uses: shrink/actions-docker-extract@v3 + id: extract-duckdb-ffi-internal + with: + image: ${{ steps.meta-ee-public.outputs.tags}}-amd64 + path: "/usr/src/app/libwindmill_duckdb_ffi_internal.so" + # - uses: shrink/actions-docker-extract@v3 # id: extract-ee-arm64 # with: @@ -111,8 +117,12 @@ jobs: - uses: actions/upload-artifact@v4 with: name: RHEL9-amd64 build - path: ${{ steps.extract-ee-amd64.outputs.destination - }}/windmill-ee-amd64-rhel9 + path: ${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel9 + + - uses: actions/upload-artifact@v4 + with: + name: RHEL9-amd64 dynamic libraries build + path: ${{ steps.extract-duckdb-ffi-internal.outputs.destination }}/libwindmill_duckdb_ffi_internal.so # - uses: actions/upload-artifact@v4 # with: diff --git a/.github/workflows/build_windows_worker_.yml b/.github/workflows/build_windows_worker_.yml index 446606503d..f46d255320 100644 --- a/.github/workflows/build_windows_worker_.yml +++ b/.github/workflows/build_windows_worker_.yml @@ -41,7 +41,13 @@ jobs: run: | ./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private - - name: Cargo build windows + - name: Cargo build dynamic libraries windows + timeout-minutes: 90 + run: | + cd backend/windmill-duckdb-ffi-internal + cargo build --release -p windmill_duckdb_ffi_internal + + - name: Cargo build binary windows timeout-minutes: 90 run: | vcpkg.exe install openssl-windows:x64-windows @@ -56,8 +62,14 @@ jobs: run: | Rename-Item -Path ".\backend\target\release\windmill.exe" -NewName "windmill-ee.exe" - - name: Upload artifact + - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: windmill-ee-binary path: ./backend/target/release/windmill-ee.exe + + - name: Upload dynamic libraries artifact + uses: actions/upload-artifact@v4 + with: + name: windmill_duckdb_ffi_internal.dll + path: ./backend/windmill-duckdb-ffi-internal/target/release/windmill_duckdb_ffi_internal.dll diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 5425ccb7e1..9c6f255aae 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -220,6 +220,12 @@ jobs: image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }} path: "/usr/src/app/windmill" + - uses: shrink/actions-docker-extract@v3 + id: extract-duckdb-ffi-internal + with: + image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }} + path: "/usr/src/app/libwindmill_duckdb_ffi_internal.so" + - uses: shrink/actions-docker-extract@v3 id: extract-ee with: @@ -237,6 +243,7 @@ jobs: files: | ${{ steps.extract.outputs.destination }}/* ${{ steps.extract-ee.outputs.destination }}/* + ${{ steps.extract-duckdb-ffi-internal.outputs.destination }}/* # attach_arm64_binary_to_release: # needs: [build, build_ee] diff --git a/.github/workflows/publish_windows_worker.yml b/.github/workflows/publish_windows_worker.yml index 809e610d58..a36eca7d85 100644 --- a/.github/workflows/publish_windows_worker.yml +++ b/.github/workflows/publish_windows_worker.yml @@ -43,6 +43,12 @@ jobs: run: | ./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private + - name: Cargo build dynamic libraries windows + timeout-minutes: 90 + run: | + cd backend/windmill-duckdb-ffi-internal + cargo build --release -p windmill_duckdb_ffi_internal + - name: Cargo build windows timeout-minutes: 90 run: | @@ -63,3 +69,9 @@ jobs: with: files: | ./backend/target/release/windmill-ee.exe + + - name: Attach dynamic libraries to release + uses: softprops/action-gh-release@v2 + with: + files: | + ./backend/windmill-duckdb-ffi-internal/target/release/windmill_duckdb_ffi_internal.dll diff --git a/Dockerfile b/Dockerfile index 3d56c251f6..8588af2569 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,16 @@ ARG DEBIAN_IMAGE=debian:bookworm-slim ARG RUST_IMAGE=rust:1.88-slim-bookworm +# Build libwindmill_duckdb_ffi_internal.so separately +FROM ${RUST_IMAGE} AS windmill_duckdb_ffi_internal_builder + +WORKDIR /windmill-duckdb-ffi-internal +RUN apt-get update && apt-get install -y pkg-config clang=1:14.0-55.* libclang-dev=1:14.0-55.* cmake=3.25.* && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* +COPY ./backend/windmill-duckdb-ffi-internal . +RUN cargo build --release -p windmill_duckdb_ffi_internal + FROM ${RUST_IMAGE} AS rust_base RUN apt-get update && apt-get install -y git libssl-dev pkg-config npm @@ -82,7 +92,6 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features" - FROM ${DEBIAN_IMAGE} ARG TARGETPLATFORM @@ -191,6 +200,7 @@ ENV TZ=Etc/UTC COPY --from=builder /frontend/build /static_frontend COPY --from=builder /windmill/target/release/windmill ${APP}/windmill +COPY --from=windmill_duckdb_ffi_internal_builder /windmill-duckdb-ffi-internal/target/release/libwindmill_duckdb_ffi_internal.so ${APP}/libwindmill_duckdb_ffi_internal.so COPY --from=denoland/deno:2.2.1 --chmod=755 /usr/bin/deno /usr/bin/deno @@ -204,6 +214,7 @@ COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/ ENV RUSTUP_HOME="/usr/local/rustup" ENV CARGO_HOME="/usr/local/cargo" +ENV LD_LIBRARY_PATH="." WORKDIR ${APP} diff --git a/backend/.cargo/config.toml b/backend/.cargo/config.toml index 7d27c4a1e1..5babef3f8a 100644 --- a/backend/.cargo/config.toml +++ b/backend/.cargo/config.toml @@ -5,10 +5,12 @@ incremental = true rustflags = [ "-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup", + "-C", "link-args=-Wl,-rpath,$ORIGIN/" ] [target.aarch64-apple-darwin] rustflags = [ "-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup", + "-C", "link-args=-Wl,-rpath,$ORIGIN/" ] \ No newline at end of file diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 5543ac0221..ddea74c612 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -458,9 +458,6 @@ name = "arrow-schema" version = "55.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af7686986a3bf2254c9fb130c623cdcb2f8e1f15763e7c71c310f0834da3d292" -dependencies = [ - "bitflags 2.9.4", -] [[package]] name = "arrow-select" @@ -1915,12 +1912,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "cbc" version = "0.1.2" @@ -4788,24 +4779,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "duckdb" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ab83a22530667ffc8cc0e31c0549bb07bea5dba3b957a8e315effc38923701" -dependencies = [ - "arrow", - "cast", - "fallible-iterator 0.3.0", - "fallible-streaming-iterator", - "hashlink 0.10.0", - "libduckdb-sys", - "num-integer", - "rust_decimal", - "smallvec", - "strum 0.27.2", -] - [[package]] name = "dunce" version = "1.0.5" @@ -7620,21 +7593,6 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" -[[package]] -name = "libduckdb-sys" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e02f6069513efb67a0743aff3b846090de14763802b0e95c352ebc6e1bdc1da" -dependencies = [ - "cc", - "flate2", - "pkg-config", - "serde", - "serde_json", - "tar", - "vcpkg", -] - [[package]] name = "libffi" version = "3.2.0" @@ -15168,6 +15126,7 @@ dependencies = [ "k8s-openapi", "kube", "lazy_static", + "libloading 0.8.8", "memchr", "object_store", "once_cell", @@ -15803,7 +15762,6 @@ dependencies = [ "deno_web", "deno_webidl", "dotenv", - "duckdb", "dyn-iter", "flume", "futures", @@ -15813,6 +15771,7 @@ dependencies = [ "itertools 0.14.0", "jsonwebtoken 8.3.0", "lazy_static", + "libloading 0.8.8", "mappable-rc", "mysql_async", "native-tls", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 38bf7dfd2a..3e981d14d7 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -31,6 +31,7 @@ members = [ "./parsers/windmill-sql-datatype-parser-wasm", "./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu" ] +exclude = ["./windmill-duckdb-ffi-internal"] [workspace.package] version = "1.541.0" @@ -99,8 +100,7 @@ java = ["windmill-worker/java"] ruby = ["windmill-worker/ruby"] all_languages = ["python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql", "bigquery", "csharp", "nu", "php", "java", "ruby"] # For windows we have another set of languages enabled -# NOTE: DuckDB is ignored because of compilation problems -all_languages_windows = ["python", "deno_core", "rust", "mysql", "oracledb", "mssql", "bigquery", "csharp", "nu", "php", "java"] +all_languages_windows = ["python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql", "bigquery", "csharp", "nu", "php", "java"] [patch.crates-io] object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7" } @@ -150,6 +150,7 @@ aws-sigv4.workspace = true aws-sdk-config.workspace = true kube.workspace = true k8s-openapi.workspace = true +libloading.workspace = true [target.'cfg(not(target_env = "msvc"))'.dependencies] tikv-jemallocator = { optional = true, workspace = true } @@ -249,7 +250,6 @@ json-pointer = "^0" itertools = "^0" regex = "^1" semver = "^1" -duckdb = { version = "^1.3.2", features = ["bundled"] } aws-sigv4 = "^1.3.4" aws-sdk-config = "=1.68.0" async-trait = "0.1.88" @@ -404,6 +404,7 @@ size = "0.5.0" flume = { version = "0.11.1", features = ["async"] } kube = { version = "1.1.0", features = ["runtime", "derive"] } k8s-openapi = { version = "0.25.0", features = ["latest"] } +libloading = "0.8.8" # Macro-related proc-macro2 = "1.0" diff --git a/backend/windmill-duckdb-ffi-internal/Cargo.lock b/backend/windmill-duckdb-ffi-internal/Cargo.lock new file mode 100644 index 0000000000..44353d9c7c --- /dev/null +++ b/backend/windmill-duckdb-ffi-internal/Cargo.lock @@ -0,0 +1,1532 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "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.3", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "arrow" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f15b4c6b148206ff3a2b35002e08929c2462467b62b9c02036d9c34f9ef994" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30feb679425110209ae35c3fbf82404a39a4c0436bb3ec36164d8bffed2a4ce4" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num", +] + +[[package]] +name = "arrow-array" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70732f04d285d49054a48b72c54f791bb3424abae92d27aafdf776c98af161c8" +dependencies = [ + "ahash 0.8.12", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown 0.15.5", + "num", +] + +[[package]] +name = "arrow-buffer" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "169b1d5d6cb390dd92ce582b06b23815c7953e9dfaaea75556e89d890d19993d" +dependencies = [ + "bytes", + "half", + "num", +] + +[[package]] +name = "arrow-cast" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f12eccc3e1c05a766cafb31f6a60a46c2f8efec9b74c6e0648766d30686af8" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "atoi", + "base64", + "chrono", + "comfy-table", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-data" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de1ce212d803199684b658fc4ba55fb2d7e87b213de5af415308d2fee3619c2" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num", +] + +[[package]] +name = "arrow-ord" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6506e3a059e3be23023f587f79c82ef0bcf6d293587e3272d20f2d30b969b5a7" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52bf7393166beaf79b4bed9bfdf19e97472af32ce5b6b48169d321518a08cae2" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7686986a3bf2254c9fb130c623cdcb2f8e1f15763e7c71c310f0834da3d292" +dependencies = [ + "bitflags", +] + +[[package]] +name = "arrow-select" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2b45757d6a2373faa3352d02ff5b54b098f5e21dccebc45a21806bc34501e5" +dependencies = [ + "ahash 0.8.12", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num", +] + +[[package]] +name = "arrow-string" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0377d532850babb4d927a06294314b316e23311503ed580ec6ce6a0158f49d40" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num", + "regex", + "regex-syntax", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "comfy-table" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8e18d0dca9578507f13f9803add0df13362b02c501c1c17734f0dbb52eaf0b" +dependencies = [ + "unicode-segmentation", + "unicode-width", +] + +[[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.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "duckdb" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ab83a22530667ffc8cc0e31c0549bb07bea5dba3b957a8e315effc38923701" +dependencies = [ + "arrow", + "cast", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libduckdb-sys", + "num-integer", + "rust_decimal", + "smallvec", + "strum", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.4+wasi-0.2.4", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown 0.15.5", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lexical-core" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b765c31809609075565a70b4b71402281283aeda7ecaf4818ac14a7b2ade8958" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de6f9cb01fb0b08060209a057c048fcbab8717b4c1ecd2eac66ebfe39a65b0f2" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72207aae22fc0a121ba7b6d479e42cbfea549af1479c3f3a4f12c70dd66df12e" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a82e24bf537fd24c177ffbbdc6ebcc8d54732c35b50a3f28cc3f4e4c949a0b3" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5afc668a27f460fb45a81a757b6bf2f43c2d7e30cb5a2dcd3abf294c78d62bd" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629ddff1a914a836fb245616a7888b62903aae58fa771e1d83943035efa0f978" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libduckdb-sys" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e02f6069513efb67a0743aff3b846090de14763802b0e95c352ebc6e1bdc1da" +dependencies = [ + "cc", + "flate2", + "pkg-config", + "serde", + "serde_json", + "tar", + "vcpkg", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[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-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 = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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 = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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 = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.4+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windmill_duckdb_ffi_internal" +version = "0.1.0" +dependencies = [ + "chrono", + "duckdb", + "rust_decimal", + "serde", + "serde_json", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xattr" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] diff --git a/backend/windmill-duckdb-ffi-internal/Cargo.toml b/backend/windmill-duckdb-ffi-internal/Cargo.toml new file mode 100644 index 0000000000..ee3c58c1de --- /dev/null +++ b/backend/windmill-duckdb-ffi-internal/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "windmill_duckdb_ffi_internal" +version = "0.1.0" +edition = "2024" + +[dependencies] +chrono = "0.4.41" +duckdb = { version = "^1.3.2", features = ["bundled"] } +rust_decimal = "1.37.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "^1", features = ["preserve_order", "raw_value"] } + +[lib] +crate-type = ["cdylib"] diff --git a/backend/windmill-duckdb-ffi-internal/README_DEV.md b/backend/windmill-duckdb-ffi-internal/README_DEV.md new file mode 100644 index 0000000000..97dd8a5eb0 --- /dev/null +++ b/backend/windmill-duckdb-ffi-internal/README_DEV.md @@ -0,0 +1,15 @@ +This crate is compiled separately because it causes nasty issues when compiled with the deno_core feature flag enabled (lib c++ interactions). + +The main issue was : +Errors in DuckDB always worked correctly, except when attached to a Ducklake and when the deno_core feature flag was set. +For example: + +```sql +ATTACH 'ducklake' AS dl; USE dl; +CREATE TABLE IF NOT EXISTS t (x string not null); +INSERT INTO t VALUES (NULL); +``` + +causes `Constraint Error: NOT NULL constraint failed: t.x` normally, but here we see `Unknown exception in ExecutorTask::Execute`. This opaque errors comes directly from the C++ DuckDB library : https://github.com/duckdb/duckdb/blob/f99fed1e0b16a842573f9dad529f6c170a004f6e/src/parallel/executor_task.cpp#L58 + +To solve this, we compile duckdb separately from the main backend crate and call it with FFI diff --git a/backend/windmill-duckdb-ffi-internal/build.rs b/backend/windmill-duckdb-ffi-internal/build.rs new file mode 100644 index 0000000000..cf335460b4 --- /dev/null +++ b/backend/windmill-duckdb-ffi-internal/build.rs @@ -0,0 +1,5 @@ +fn main() { + // Duckdb requires Windows Restart Manager library on Windows + #[cfg(target_os = "windows")] + println!("cargo:rustc-link-lib=Rstrtmgr"); +} diff --git a/backend/windmill-duckdb-ffi-internal/build_dev.sh b/backend/windmill-duckdb-ffi-internal/build_dev.sh new file mode 100755 index 0000000000..0b42352732 --- /dev/null +++ b/backend/windmill-duckdb-ffi-internal/build_dev.sh @@ -0,0 +1,2 @@ +cargo build --release -p windmill_duckdb_ffi_internal +cp target/release/libwindmill_duckdb_ffi_internal.* ../target/debug/ \ No newline at end of file diff --git a/backend/windmill-duckdb-ffi-internal/src/lib.rs b/backend/windmill-duckdb-ffi-internal/src/lib.rs new file mode 100644 index 0000000000..e2c918c3fe --- /dev/null +++ b/backend/windmill-duckdb-ffi-internal/src/lib.rs @@ -0,0 +1,450 @@ +use std::{ + collections::HashMap, + ffi::{c_char, CStr, CString}, + ptr::null_mut, +}; + +use duckdb::{params_from_iter, types::TimeUnit, Row}; +use rust_decimal::{prelude::FromPrimitive, Decimal}; +use serde::Deserialize; +use serde_json::value::RawValue; + +#[derive(Deserialize, Clone, Debug, PartialEq, Default)] +pub struct Arg { + pub name: String, + pub arg_type: String, + pub json_value: serde_json::Value, +} + +#[unsafe(no_mangle)] +pub extern "C" fn run_duckdb_ffi( + query_block_list: *const *const c_char, + query_block_list_count: usize, + job_args: *const c_char, + token: *const c_char, + base_internal_url: *const c_char, + w_id: *const c_char, + column_order_ptr: *mut *mut c_char, +) -> *mut c_char { + let (r, column_order) = match convert_args( + query_block_list, + query_block_list_count, + job_args, + token, + base_internal_url, + w_id, + ) + .and_then( + |(query_block_list, job_args, token, base_internal_url, w_id)| { + run_duckdb_internal( + query_block_list, + query_block_list_count, + job_args, + token, + base_internal_url, + w_id, + ) + }, + ) { + Ok(result) => result, + Err(err) => { + let err = serde_json::to_string(&err) + .unwrap_or_else(|_| "Unknown error in duckdb ffi lib".to_string()); + (format!("ERROR {}", err), None) + } + }; + + unsafe { + if let Some(column_order) = column_order { + let column_order = + serde_json::to_string(&column_order).unwrap_or_else(|_| "[]".to_string()); + let c_column_order = + CString::new(column_order).unwrap_or_else(|_| CString::new("[]").unwrap()); + *column_order_ptr = c_column_order.into_raw(); + } else { + *column_order_ptr = null_mut(); + } + } + // CString::into_raw because it needs to outlive this function call. + // The caller is responsible for freeing the memory through CString::from_raw. + CString::new(r).map(|s| s.into_raw()).unwrap_or_else(|e| { + println!("Failed to allocate error string in duckdb ffi lib: {:?}", e); + null_mut() + }) +} + +fn convert_args<'a>( + query_block_list: *const *const c_char, + query_block_list_count: usize, + job_args: *const c_char, + token: *const c_char, + base_internal_url: *const c_char, + w_id: *const c_char, +) -> Result< + ( + impl Iterator, + HashMap, + &'a str, + &'a str, + &'a str, + ), + String, +> { + let query_block_list = unsafe { + std::slice::from_raw_parts(query_block_list, query_block_list_count) + .iter() + .map(|q| { + CStr::from_ptr(*q).to_str().unwrap_or_else(|e| { + println!( + "Invalid query_block string pointer in duckdb ffi: {}", + e.to_string() + ); + "Invalid query_block string pointer in duckdb ffi" + }) + }) + }; + let job_args_str = unsafe { CStr::from_ptr(job_args) } + .to_str() + .map_err(|e| format!("Invalid job_args string: {}", e.to_string()))?; + let job_args: Vec = serde_json::from_str(job_args_str) + .map_err(|e| format!("Invalid job_args JSON: {}", e.to_string()))?; + let job_args: HashMap = job_args + .into_iter() + .map(|arg| { + let duckdb_value = json_value_to_duckdb_value(&arg.json_value, &arg.arg_type) + .unwrap_or_else(|e| { + println!( + "Error converting job_arg {} to duckdb value: {}", + arg.name, e + ); + duckdb::types::Value::Null + }); + (arg.name, duckdb_value) + }) + .collect(); + let token = unsafe { CStr::from_ptr(token) } + .to_str() + .map_err(|e| format!("Invalid token string: {}", e.to_string()))?; + let base_internal_url = unsafe { CStr::from_ptr(base_internal_url) } + .to_str() + .map_err(|e| format!("Invalid base_internal_url string: {}", e.to_string()))?; + let w_id = unsafe { CStr::from_ptr(w_id) } + .to_str() + .map_err(|e| format!("Invalid w_id string: {}", e.to_string()))?; + Ok((query_block_list, job_args, token, base_internal_url, w_id)) +} + +// TODO : Better error return to leverage different error kinds on the worker side +fn run_duckdb_internal<'a>( + query_block_list: impl Iterator, + query_block_list_count: usize, + job_args: HashMap, + token: &str, + base_internal_url: &str, + w_id: &str, +) -> Result<(String, Option>), String> { + let conn = duckdb::Connection::open_in_memory().map_err(|e| e.to_string())?; + + let (s3_access_key, s3_secret_key) = token.split_at(token.rfind('.').unwrap_or(0)); + let s3_secret_key = &s3_secret_key[1..]; + let (s3_endpoint_ssl, s3_endpoint) = base_internal_url + .split_once("://") + .unwrap_or(("http", &base_internal_url)); + let s3_endpoint_ssl = match s3_endpoint_ssl { + "https" => true, + _ => false, + }; + + conn.execute_batch(&format!( + "INSTALL httpfs; LOAD httpfs; + INSTALL azure; LOAD azure; + CREATE OR REPLACE SECRET s3_secret ( + TYPE s3, + PROVIDER config, + KEY_ID '{s3_access_key}', + SECRET '{s3_secret_key}', + ENDPOINT '{s3_endpoint}/api/w/{w_id}/s3_proxy', + URL_STYLE path, + USE_SSL {s3_endpoint_ssl} + ); + CREATE OR REPLACE SECRET gcs_secret ( + TYPE gcs, + KEY_ID '{s3_access_key}', + SECRET '{s3_secret_key}', + ENDPOINT '{s3_endpoint}/api/w/{w_id}/s3_proxy', + USE_SSL {s3_endpoint_ssl} + ); + ", + )) + .map_err(|e| format!("Error setting up S3 secret: {}", e.to_string()))?; + + let mut result: Option> = None; + let mut column_order = None; + + for (query_block_index, query_block) in query_block_list.enumerate() { + result = Some( + do_duckdb_inner( + &conn, + query_block, + &job_args, + query_block_index != query_block_list_count - 1, + &mut column_order, + ) + .map_err(|e| e.to_string())?, + ); + } + let result = result.unwrap_or_else(|| RawValue::from_string("[]".to_string()).unwrap()); + Ok((result.get().to_string(), column_order)) +} + +fn do_duckdb_inner( + conn: &duckdb::Connection, + query: &str, + job_args: &HashMap, + skip_collect: bool, + column_order: &mut Option>, +) -> Result, String> { + let mut rows_vec = vec![]; + + let (query, job_args) = interpolate_named_args(query, &job_args); + + let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?; + + let mut rows = stmt + .query(params_from_iter(job_args)) + .map_err(|e| e.to_string())?; + + if skip_collect { + return Ok(RawValue::from_string("[]".to_string()).unwrap()); + } + + // Statement needs to be stepped at least once or stmt.column_names() will panic + let mut column_names = None; + loop { + let row = rows.next(); + match row { + Ok(Some(row)) => { + // Set column names if not already set + let stmt = row.as_ref(); + let column_names = match column_names.as_ref() { + Some(column_names) => column_names, + None => { + column_names = Some(stmt.column_names()); + column_names.as_ref().unwrap() + } + }; + + let row = row_to_value(row, &column_names.as_slice()).map_err(|e| e.to_string())?; + rows_vec.push(row); + } + Ok(None) => break, + Err(e) => { + return Err(e.to_string()); + } + } + } + + *column_order = column_names; + + serde_json::value::to_raw_value(&rows_vec).map_err(|e| e.to_string()) +} + +// duckdb-rs does not support named parameters, +// and it raises an error when passing unused arguments. We cannot prepare batch statements +// but only single SQL statements so it doesn't work when all arguments are not used by +// every single statement. +fn interpolate_named_args<'a>( + query: &str, + args: &'a HashMap, +) -> (String, Vec<&'a duckdb::types::Value>) { + let mut query = query.to_string(); + + let mut values = vec![]; + for (arg_name, arg_value) in args { + let pat = format!("${}", arg_name); + if !query.contains(&pat) { + continue; + } + values.push(arg_value); + query = query.replace(&pat, &format!("${}", values.len())); + } + (query, values) +} + +fn row_to_value(row: &Row<'_>, column_names: &[String]) -> Result, String> { + let mut obj = serde_json::Map::new(); + for (i, key) in column_names.iter().enumerate() { + let value: duckdb::types::Value = row.get(i).map_err(|e| e.to_string())?; + let json_value = match value { + duckdb::types::Value::Null => serde_json::Value::Null, + duckdb::types::Value::Boolean(b) => serde_json::Value::Bool(b), + duckdb::types::Value::TinyInt(i) => serde_json::Value::Number(i.into()), + duckdb::types::Value::SmallInt(i) => serde_json::Value::Number(i.into()), + duckdb::types::Value::Int(i) => serde_json::Value::Number(i.into()), + duckdb::types::Value::BigInt(i) => serde_json::Value::Number(i.into()), + duckdb::types::Value::HugeInt(i) => serde_json::Value::String(i.to_string()), + duckdb::types::Value::UTinyInt(u) => serde_json::Value::Number(u.into()), + duckdb::types::Value::USmallInt(u) => serde_json::Value::Number(u.into()), + duckdb::types::Value::UInt(u) => serde_json::Value::Number(u.into()), + duckdb::types::Value::UBigInt(u) => serde_json::Value::Number(u.into()), + duckdb::types::Value::Float(f) => serde_json::Value::Number( + serde_json::Number::from_f64(f as f64) + .ok_or_else(|| ("Could not convert to f64".to_string()))?, + ), + duckdb::types::Value::Double(f) => serde_json::Value::Number( + serde_json::Number::from_f64(f) + .ok_or_else(|| ("Could not convert to f64".to_string()))?, + ), + duckdb::types::Value::Decimal(d) => serde_json::Value::String(d.to_string()), + duckdb::types::Value::Timestamp(_, ts) => serde_json::Value::String(ts.to_string()), + duckdb::types::Value::Text(s) => serde_json::Value::String(s), + duckdb::types::Value::Blob(b) => serde_json::Value::Array( + b.into_iter() + .map(|byte| serde_json::Value::Number(byte.into())) + .collect(), + ), + duckdb::types::Value::Date32(d) => serde_json::Value::Number(d.into()), + duckdb::types::Value::Time64(_, t) => serde_json::Value::String(t.to_string()), + duckdb::types::Value::Interval { months, days, nanos } => serde_json::json!({ + "months": months, + "days": days, + "nanos": nanos + }), + duckdb::types::Value::List(values) => serde_json::Value::Array( + values + .into_iter() + .map(|v| serde_json::Value::String(format!("{:?}", v))) + .collect(), + ), + duckdb::types::Value::Enum(e) => serde_json::Value::String(e), + duckdb::types::Value::Struct(fields) => serde_json::Value::Object( + fields + .iter() + .map(|(k, v)| (k.clone(), serde_json::Value::String(format!("{:?}", v)))) + .collect(), + ), + duckdb::types::Value::Array(values) => serde_json::Value::Array( + values + .into_iter() + .map(|v| serde_json::Value::String(format!("{:?}", v))) + .collect(), + ), + duckdb::types::Value::Map(map) => serde_json::Value::Object( + map.iter() + .map(|(k, v)| { + ( + format!("{:?}", k), + serde_json::Value::String(format!("{:?}", v)), + ) + }) + .collect(), + ), + duckdb::types::Value::Union(value) => { + serde_json::Value::String(format!("{:?}", *value)) + } + }; + obj.insert(key.clone(), json_value); + } + serde_json::value::to_raw_value(&obj).map_err(|e| e.to_string()) +} + +fn json_value_to_duckdb_value( + json_value: &serde_json::Value, + arg_type: &str, +) -> Result { + let arg_type = arg_type.to_lowercase(); + let duckdb_value = match json_value { + serde_json::Value::Null => duckdb::types::Value::Null, + serde_json::Value::Bool(b) => duckdb::types::Value::Boolean(*b), + + serde_json::Value::String(s) + if matches!( + arg_type.as_str(), + "timestamp" | "timestamptz" | "timestamp with time zone" | "datetime" + ) => + { + string_to_duckdb_timestamp(&s)? + } + serde_json::Value::String(s) if arg_type.as_str() == "date" => string_to_duckdb_date(&s)?, + serde_json::Value::String(s) if arg_type.as_str() == "time" => string_to_duckdb_time(&s)?, + serde_json::Value::String(s) => duckdb::types::Value::Text(s.clone()), + + serde_json::Value::Number(n) if n.is_i64() => { + let v = n.as_i64().unwrap(); + match arg_type.as_str() { + "tinyint" | "int1" => duckdb::types::Value::TinyInt(v as i8), + "smallint" | "int2" | "short" => duckdb::types::Value::SmallInt(v as i16), + "integer" | "int4" | "int" | "signed" => duckdb::types::Value::Int(v as i32), + "bigint" | "int8" | "long" => duckdb::types::Value::BigInt(v), + "hugeint" => duckdb::types::Value::HugeInt(v as i128), + "float" | "float4" | "real" => duckdb::types::Value::Float(v as f32), + "double" | "float8" => duckdb::types::Value::Double(v as f64), + _ => duckdb::types::Value::BigInt(v), // default fallback + } + } + + serde_json::Value::Number(n) if n.is_u64() => { + let v = n.as_u64().unwrap(); + match arg_type.as_str() { + "utinyint" => duckdb::types::Value::UTinyInt(v as u8), + "usmallint" => duckdb::types::Value::USmallInt(v as u16), + "uinteger" => duckdb::types::Value::UInt(v as u32), + "ubigint" | "uhugeint" => duckdb::types::Value::UBigInt(v), + _ => duckdb::types::Value::UBigInt(v), // default fallback + } + } + + serde_json::Value::Number(n) if n.is_f64() => { + let v = n.as_f64().unwrap(); + match arg_type.as_str() { + "float" | "float4" | "real" => duckdb::types::Value::Float(v as f32), + "double" | "float8" => duckdb::types::Value::Double(v), + "decimal" | "numeric" => duckdb::types::Value::Decimal( + Decimal::from_f64(v) + .ok_or_else(|| ("Could not convert f64 to Decimal".to_string()))?, + ), + _ => duckdb::types::Value::Double(v), // default fallback + } + } + + serde_json::Value::Array(arr) => { + duckdb::types::Value::Text(serde_json::to_string(arr).map_err(|e| e.to_string())?) + } + serde_json::Value::Object(map) => { + duckdb::types::Value::Text(serde_json::to_string(map).map_err(|e| e.to_string())?) + } + + value @ _ => { + return Err(format!( + "Unsupported type in query: {:?} and signature {arg_type:?}", + value + )); + } + }; + Ok(duckdb_value) +} + +fn string_to_duckdb_timestamp(s: &str) -> Result { + let ts = + chrono::DateTime::parse_from_rfc3339(s).map_err(|e: chrono::ParseError| e.to_string())?; + Ok(duckdb::types::Value::Timestamp( + TimeUnit::Millisecond, + ts.timestamp_millis(), + )) +} + +fn string_to_duckdb_date(s: &str) -> Result { + use chrono::Datelike; + let date = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") + .map_err(|e| (format!("Invalid date format: {}", e)))?; + Ok(duckdb::types::Value::Date32(date.num_days_from_ce())) +} + +fn string_to_duckdb_time(s: &str) -> Result { + use chrono::Timelike; + let time = chrono::NaiveTime::parse_from_str(s, "%H:%M:%S").unwrap(); + Ok(duckdb::types::Value::Time64( + TimeUnit::Microsecond, + time.num_seconds_from_midnight() as i64, + )) +} diff --git a/backend/windmill-worker/Cargo.toml b/backend/windmill-worker/Cargo.toml index b285224ed4..772a6195f9 100644 --- a/backend/windmill-worker/Cargo.toml +++ b/backend/windmill-worker/Cargo.toml @@ -33,7 +33,7 @@ rust = ["dep:windmill-parser-rust"] nu = ["dep:windmill-parser-nu"] java = ["dep:windmill-parser-java"] ruby = ["dep:windmill-parser-ruby"] -duckdb = ["dep:duckdb"] +duckdb = ["dep:libloading"] [dependencies] windmill-queue.workspace = true @@ -97,7 +97,6 @@ deno_permissions = { workspace = true, optional = true } deno_io = { workspace = true, optional = true } deno_error = { workspace = true, optional = true } async-stream.workspace = true -duckdb = { workspace = true, optional = true } postgres-native-tls.workspace = true native-tls.workspace = true @@ -125,6 +124,7 @@ winapi = { workspace = true, optional = true } pep440_rs.workspace = true process-wrap.workspace = true async-once-cell.workspace = true +libloading = { workspace = true, optional = true } opentelemetry = { workspace = true, optional = true } bollard = { workspace = true, optional = true } diff --git a/backend/windmill-worker/src/duckdb_executor.rs b/backend/windmill-worker/src/duckdb_executor.rs index 062f4433e8..aea5f7be15 100644 --- a/backend/windmill-worker/src/duckdb_executor.rs +++ b/backend/windmill-worker/src/duckdb_executor.rs @@ -1,19 +1,18 @@ -use std::collections::HashMap; +use std::cell::RefCell; use std::env; +use std::ffi::{c_char, CString}; +use std::ptr::NonNull; use std::sync::{Arc, Mutex}; -use duckdb::types::TimeUnit; -use duckdb::{params_from_iter, Row}; -use rust_decimal::prelude::FromPrimitive; -use rust_decimal::Decimal; +use libloading::{Library, Symbol}; +use serde::Serialize; use serde_json::value::RawValue; use serde_json::{json, Value}; -use tokio::task; use uuid::Uuid; use windmill_common::error::{to_anyhow, Error, Result}; use windmill_common::s3_helpers::S3Object; use windmill_common::utils::sanitize_string_from_password; -use windmill_common::worker::{to_raw_value, Connection}; +use windmill_common::worker::Connection; use windmill_common::workspaces::{get_ducklake_from_db_unchecked, DucklakeCatalogResourceType}; use windmill_parser_sql::{parse_duckdb_sig, parse_sql_blocks}; use windmill_queue::{CanceledBy, MiniPulledJob}; @@ -27,63 +26,6 @@ use crate::pg_executor::PgDatabase; use crate::sanitized_sql_params::sanitize_and_interpolate_unsafe_sql_args; use windmill_common::client::AuthedClient; -fn do_duckdb_inner( - conn: &duckdb::Connection, - query: &str, - job_args: &HashMap, - skip_collect: bool, - column_order: &mut Option>, -) -> Result> { - let mut rows_vec = vec![]; - - let (query, job_args) = interpolate_named_args(query, &job_args); - - let mut stmt = conn - .prepare(&query) - .map_err(|e| Error::ExecutionErr(e.to_string()))?; - - let mut rows = stmt - .query(params_from_iter(job_args)) - .map_err(|e| Error::ExecutionErr(e.to_string()))?; - - if skip_collect { - return Ok(to_raw_value(&json!([]))); - } - - // Statement needs to be stepped at least once or stmt.column_names() will panic - let mut column_names = None; - loop { - let row = rows.next(); - match row { - Ok(Some(row)) => { - // Set column names if not already set - let stmt = row.as_ref(); - let column_names = match column_names.as_ref() { - Some(column_names) => column_names, - None => { - column_names = Some(stmt.column_names()); - column_names.as_ref().unwrap() - } - }; - - let row = row_to_value(row, &column_names.as_slice()) - .map_err(|e| Error::ExecutionErr(e.to_string()))?; - rows_vec.push(row); - } - Ok(None) => break, - Err(e) => { - return Err(Error::ExecutionErr(e.to_string())); - } - } - } - - if let (Some(column_order), Some(column_names)) = (column_order.as_mut(), column_names) { - *column_order = column_names.clone(); - } - - return Ok(to_raw_value(&rows_vec)); -} - pub async fn do_duckdb( job: &MiniPulledJob, client: &AuthedClient, @@ -92,7 +34,8 @@ pub async fn do_duckdb( mem_peak: &mut i32, canceled_by: &mut Option, worker_name: &str, - column_order_ref: &mut Option>, + // TODO + #[allow(unused_variables)] column_order_ref: &mut Option>, occupancy_metrics: &mut OccupancyMetrics, ) -> Result> { let token = client.token.clone(); @@ -109,7 +52,7 @@ pub async fn do_duckdb( let query = transform_s3_uris(query).await?; let job_args = { - let mut m: HashMap = HashMap::new(); + let mut m = Vec::new(); for sig_arg in sig.into_iter() { let json_value = job_args .remove(&sig_arg.name) @@ -125,17 +68,17 @@ pub async fn do_duckdb( s3_obj.storage.as_deref().unwrap_or("_default_"), s3_obj.s3 ); - m.insert(sig_arg.name, duckdb::types::Value::Text(uri)); + m.push(Arg { + json_value: serde_json::Value::String(uri), + name: sig_arg.name, + arg_type: "string".to_string(), + }); } else { - let duckdb_value = json_value_to_duckdb_value( - &json_value, - sig_arg - .otyp - .clone() - .unwrap_or_else(|| "text".to_string()) - .as_str(), - )?; - m.insert(sig_arg.name, duckdb_value); + m.push(Arg { + json_value, + name: sig_arg.name, + arg_type: sig_arg.otyp.unwrap_or_else(|| "text".to_string()), + }); } } m @@ -185,63 +128,15 @@ pub async fn do_duckdb( let base_internal_url = client.base_internal_url.clone(); let w_id = job.workspace_id.clone(); - // duckdb::Connection is not Send so we run the queries in a single blocking task - let (result, column_order) = task::spawn_blocking(move || { - let conn = duckdb::Connection::open_in_memory() - .map_err(|e| Error::ConnectingToDatabase(e.to_string()))?; - - let (s3_access_key, s3_secret_key) = token.split_at(token.rfind('.').unwrap_or(0)); - let s3_secret_key = &s3_secret_key[1..]; - let (s3_endpoint_ssl, s3_endpoint) = base_internal_url - .split_once("://") - .unwrap_or(("http", &base_internal_url)); - let s3_endpoint_ssl = match s3_endpoint_ssl { - "https" => true, - _ => false, - }; - - conn.execute_batch(&format!( - "INSTALL httpfs; LOAD httpfs; - INSTALL azure; LOAD azure; - CREATE OR REPLACE SECRET s3_secret ( - TYPE s3, - PROVIDER config, - KEY_ID '{s3_access_key}', - SECRET '{s3_secret_key}', - ENDPOINT '{s3_endpoint}/api/w/{w_id}/s3_proxy', - URL_STYLE path, - USE_SSL {s3_endpoint_ssl} - ); - CREATE OR REPLACE SECRET gcs_secret ( - TYPE gcs, - KEY_ID '{s3_access_key}', - SECRET '{s3_secret_key}', - ENDPOINT '{s3_endpoint}/api/w/{w_id}/s3_proxy', - USE_SSL {s3_endpoint_ssl} - ); - ", - )) - .map_err(|e| { - Error::ExecutionErr(format!("Error setting up S3 secret: {}", e.to_string())) - })?; - - let mut result: Option> = None; - let mut column_order = None; - - for (query_block_index, query_block) in query_block_list.iter().enumerate() { - result = Some( - do_duckdb_inner( - &conn, - query_block.as_str(), - &job_args, - query_block_index != query_block_list.len() - 1, - &mut column_order, - ) - .map_err(|e| Error::ExecutionErr(e.to_string()))?, - ); - } - let result = result.unwrap_or_else(|| to_raw_value(&json!([]))); - Ok::<_, Error>((result, column_order)) + let (result, column_order) = tokio::task::spawn_blocking(move || { + run_duckdb_ffi_safe( + query_block_list.iter().map(String::as_str), + query_block_list.len(), + job_args, + &token, + &base_internal_url, + &w_id, + ) }) .await .map_err(to_anyhow)??; @@ -281,184 +176,132 @@ pub async fn do_duckdb( } } -fn row_to_value(row: &Row<'_>, column_names: &[String]) -> Result> { - let mut obj = serde_json::Map::new(); - for (i, key) in column_names.iter().enumerate() { - let value: duckdb::types::Value = - row.get(i).map_err(|e| Error::ExecutionErr(e.to_string()))?; - let json_value = match value { - duckdb::types::Value::Null => serde_json::Value::Null, - duckdb::types::Value::Boolean(b) => serde_json::Value::Bool(b), - duckdb::types::Value::TinyInt(i) => serde_json::Value::Number(i.into()), - duckdb::types::Value::SmallInt(i) => serde_json::Value::Number(i.into()), - duckdb::types::Value::Int(i) => serde_json::Value::Number(i.into()), - duckdb::types::Value::BigInt(i) => serde_json::Value::Number(i.into()), - duckdb::types::Value::HugeInt(i) => serde_json::Value::String(i.to_string()), - duckdb::types::Value::UTinyInt(u) => serde_json::Value::Number(u.into()), - duckdb::types::Value::USmallInt(u) => serde_json::Value::Number(u.into()), - duckdb::types::Value::UInt(u) => serde_json::Value::Number(u.into()), - duckdb::types::Value::UBigInt(u) => serde_json::Value::Number(u.into()), - duckdb::types::Value::Float(f) => serde_json::Value::Number( - serde_json::Number::from_f64(f as f64) - .ok_or_else(|| Error::ExecutionErr("Could not convert to f64".to_string()))?, - ), - duckdb::types::Value::Double(f) => serde_json::Value::Number( - serde_json::Number::from_f64(f) - .ok_or_else(|| Error::ExecutionErr("Could not convert to f64".to_string()))?, - ), - duckdb::types::Value::Decimal(d) => serde_json::Value::String(d.to_string()), - duckdb::types::Value::Timestamp(_, ts) => serde_json::Value::String(ts.to_string()), - duckdb::types::Value::Text(s) => serde_json::Value::String(s), - duckdb::types::Value::Blob(b) => serde_json::Value::Array( - b.into_iter() - .map(|byte| serde_json::Value::Number(byte.into())) - .collect(), - ), - duckdb::types::Value::Date32(d) => serde_json::Value::Number(d.into()), - duckdb::types::Value::Time64(_, t) => serde_json::Value::String(t.to_string()), - duckdb::types::Value::Interval { months, days, nanos } => serde_json::json!({ - "months": months, - "days": days, - "nanos": nanos - }), - duckdb::types::Value::List(values) => serde_json::Value::Array( - values - .into_iter() - .map(|v| serde_json::Value::String(format!("{:?}", v))) - .collect(), - ), - duckdb::types::Value::Enum(e) => serde_json::Value::String(e), - duckdb::types::Value::Struct(fields) => serde_json::Value::Object( - fields - .iter() - .map(|(k, v)| (k.clone(), serde_json::Value::String(format!("{:?}", v)))) - .collect(), - ), - duckdb::types::Value::Array(values) => serde_json::Value::Array( - values - .into_iter() - .map(|v| serde_json::Value::String(format!("{:?}", v))) - .collect(), - ), - duckdb::types::Value::Map(map) => serde_json::Value::Object( - map.iter() - .map(|(k, v)| { - ( - format!("{:?}", k), - serde_json::Value::String(format!("{:?}", v)), - ) - }) - .collect(), - ), - duckdb::types::Value::Union(value) => { - serde_json::Value::String(format!("{:?}", *value)) +thread_local! { + static DUCKDB_FFI_LIB_SINGLETON: RefCell<*const DuckDbFfiLib> = RefCell::new(std::ptr::null()); +} + +struct DuckDbFfiLib { + run_duckdb_ffi: Symbol< + 'static, + unsafe extern "C" fn( + query_block_list: *const *const c_char, + query_block_list_count: usize, + job_args: *const c_char, + token: *const c_char, + base_internal_url: *const c_char, + w_id: *const c_char, + column_order_ptr: *mut *mut c_char, + ) -> *mut c_char, + >, +} + +impl DuckDbFfiLib { + fn get_singleton() -> Result<&'static DuckDbFfiLib> { + DUCKDB_FFI_LIB_SINGLETON.with(|cell| unsafe { + let mut singleton = cell.borrow_mut(); + if singleton.is_null() { + let lib = DuckDbFfiLib::init()?; + let boxed_lib = Box::new(lib); + let lib_ptr = Box::leak(boxed_lib); + *singleton = lib_ptr as *const _; + Ok(NonNull::new_unchecked(*singleton as *mut DuckDbFfiLib).as_ref()) + } else { + Ok(&**singleton) } - }; - obj.insert(key.clone(), json_value); + }) + } + fn init() -> Result { + let lib = unsafe { + Library::new(if cfg!(target_os = "macos") { + "libwindmill_duckdb_ffi_internal.dylib" + } else if cfg!(target_os = "windows") { + "windmill_duckdb_ffi_internal.dll" + } else { + "libwindmill_duckdb_ffi_internal.so" + }) + .map_err(|e| { + Error::InternalErr(format!( + "Could not init duckdb. Make sure you have the latest windmill_duckdb_ffi_lib.{} alongside your binary : https://github.com/windmill-labs/windmill/releases \n{}", + if cfg!(target_os = "macos") { "dylib" } + else if cfg!(target_os = "windows") { "dll" } + else { "so" }, + e.to_string() + )) + })? + }; + let lib = Box::leak(Box::new(lib)); + Ok(DuckDbFfiLib { + run_duckdb_ffi: unsafe { lib.get(b"run_duckdb_ffi").map_err(to_anyhow)? }, + }) } - serde_json::value::to_raw_value(&obj).map_err(|e| e.into()) } -fn json_value_to_duckdb_value( - json_value: &serde_json::Value, - arg_type: &str, -) -> Result { - let arg_type = arg_type.to_lowercase(); - let duckdb_value = match json_value { - serde_json::Value::Null => duckdb::types::Value::Null, - serde_json::Value::Bool(b) => duckdb::types::Value::Boolean(*b), +// Read backend/windmill-duckdb-ffi-internal/README_DEV.md for details about why we use FFI +fn run_duckdb_ffi_safe<'a>( + query_block_list: impl Iterator, + query_block_list_count: usize, + job_args: Vec, + token: &str, + base_internal_url: &str, + w_id: &str, +) -> Result<(Box, Option>)> { + let query_block_list = query_block_list + .map(|s| { + CString::new(s).map_err(|e| { + Error::ExecutionErr(format!("Failed CString conversion: {}", e.to_string())) + }) + }) + .collect::>>()?; + let query_block_list = query_block_list + .iter() + .map(|s| s.as_ptr()) + .collect::>(); + let job_args = serde_json::to_string(&job_args).map_err(to_anyhow)?; - serde_json::Value::String(s) - if matches!( - arg_type.as_str(), - "timestamp" | "timestamptz" | "timestamp with time zone" | "datetime" - ) => - { - string_to_duckdb_timestamp(&s)? - } - serde_json::Value::String(s) if arg_type.as_str() == "date" => string_to_duckdb_date(&s)?, - serde_json::Value::String(s) if arg_type.as_str() == "time" => string_to_duckdb_time(&s)?, - serde_json::Value::String(s) => duckdb::types::Value::Text(s.clone()), + let job_args = CString::new(job_args).map_err(to_anyhow)?; + let token = CString::new(token).map_err(to_anyhow)?; + let base_internal_url = CString::new(base_internal_url).map_err(to_anyhow)?; + let w_id = CString::new(w_id).map_err(to_anyhow)?; - serde_json::Value::Number(n) if n.is_i64() => { - let v = n.as_i64().unwrap(); - match arg_type.as_str() { - "tinyint" | "int1" => duckdb::types::Value::TinyInt(v as i8), - "smallint" | "int2" | "short" => duckdb::types::Value::SmallInt(v as i16), - "integer" | "int4" | "int" | "signed" => duckdb::types::Value::Int(v as i32), - "bigint" | "int8" | "long" => duckdb::types::Value::BigInt(v), - "hugeint" => duckdb::types::Value::HugeInt(v as i128), - "float" | "float4" | "real" => duckdb::types::Value::Float(v as f32), - "double" | "float8" => duckdb::types::Value::Double(v as f64), - _ => duckdb::types::Value::BigInt(v), // default fallback - } - } - - serde_json::Value::Number(n) if n.is_u64() => { - let v = n.as_u64().unwrap(); - match arg_type.as_str() { - "utinyint" => duckdb::types::Value::UTinyInt(v as u8), - "usmallint" => duckdb::types::Value::USmallInt(v as u16), - "uinteger" => duckdb::types::Value::UInt(v as u32), - "ubigint" | "uhugeint" => duckdb::types::Value::UBigInt(v), - _ => duckdb::types::Value::UBigInt(v), // default fallback - } - } - - serde_json::Value::Number(n) if n.is_f64() => { - let v = n.as_f64().unwrap(); - match arg_type.as_str() { - "float" | "float4" | "real" => duckdb::types::Value::Float(v as f32), - "double" | "float8" => duckdb::types::Value::Double(v), - "decimal" | "numeric" => { - duckdb::types::Value::Decimal(Decimal::from_f64(v).ok_or_else(|| { - Error::ExecutionErr("Could not convert f64 to Decimal".to_string()) - })?) - } - _ => duckdb::types::Value::Double(v), // default fallback - } - } - - serde_json::Value::Array(arr) => { - duckdb::types::Value::Text(serde_json::to_string(arr).map_err(to_anyhow)?) - } - serde_json::Value::Object(map) => { - duckdb::types::Value::Text(serde_json::to_string(map).map_err(to_anyhow)?) - } - - value @ _ => { - return Err(Error::ExecutionErr(format!( - "Unsupported type in query: {:?} and signature {arg_type:?}", - value - ))) - } + let run_duckdb_ffi = &DuckDbFfiLib::get_singleton()?.run_duckdb_ffi; + let mut column_order: *mut c_char = std::ptr::null_mut(); + let result_cstr = unsafe { + let ptr = run_duckdb_ffi( + query_block_list.as_ptr(), + query_block_list_count, + job_args.as_ptr(), + token.as_ptr(), + base_internal_url.as_ptr(), + w_id.as_ptr(), + &mut column_order, + ); + CString::from_raw(ptr) // Using from_raw to take ownership and ensure it gets freed }; - Ok(duckdb_value) -} -fn string_to_duckdb_timestamp(s: &str) -> Result { - let ts = chrono::DateTime::parse_from_rfc3339(s) - .map_err(|e: chrono::ParseError| Error::ExecutionErr(e.to_string()))?; - Ok(duckdb::types::Value::Timestamp( - TimeUnit::Millisecond, - ts.timestamp_millis(), - )) -} + let column_order = if column_order.is_null() { + None + } else { + Some(unsafe { + serde_json::from_str::>(&CString::from_raw(column_order).to_string_lossy())? + }) + }; -fn string_to_duckdb_date(s: &str) -> Result { - use chrono::Datelike; - let date = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") - .map_err(|e| Error::ExecutionErr(format!("Invalid date format: {}", e)))?; - Ok(duckdb::types::Value::Date32(date.num_days_from_ce())) -} + let result_str = result_cstr + .to_str() + .map_err(|e| { + Error::ExecutionErr(format!( + "Failed to convert result C string to Rust string: {}", + e.to_string() + )) + })? + .to_string(); -fn string_to_duckdb_time(s: &str) -> Result { - use chrono::Timelike; - let time = chrono::NaiveTime::parse_from_str(s, "%H:%M:%S").unwrap(); - Ok(duckdb::types::Value::Time64( - TimeUnit::Microsecond, - time.num_seconds_from_midnight() as i64, - )) + if result_str.starts_with("ERROR") { + Err(Error::ExecutionErr(result_str[6..].to_string())) + } else { + let result = serde_json::value::RawValue::from_string(result_str).map_err(to_anyhow)?; + Ok((result, column_order)) + } } struct ParsedAttachDbResource<'a> { @@ -699,26 +542,12 @@ impl Drop for UseBigQueryCredentialsFile { } } -// duckdb-rs does not support named parameters, -// and it raises an error when passing unused arguments. We cannot prepare batch statements -// but only single SQL statements so it doesn't work when all arguments are not used by -// every single statement. -fn interpolate_named_args<'a>( - query: &str, - args: &'a HashMap, -) -> (String, Vec<&'a duckdb::types::Value>) { - let mut query = query.to_string(); - - let mut values = vec![]; - for (arg_name, arg_value) in args { - let pat = format!("${}", arg_name); - if !query.contains(&pat) { - continue; - } - values.push(arg_value); - query = query.replace(&pat, &format!("${}", values.len())); - } - (query, values) +// Shared with ffi module +#[derive(Serialize, Clone, Debug, PartialEq, Default)] +pub struct Arg { + pub name: String, + pub arg_type: String, + pub json_value: serde_json::Value, } // input should contain a single statement. remove all comments before and after it diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index 3dfe898301..bfda38b927 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -135,7 +135,7 @@ 'csharp', 'nu', 'java', - 'ruby' + 'ruby' // for related places search: ADD_NEW_LANG ].includes(lang ?? '') ) @@ -155,7 +155,7 @@ 'csharp', 'nu', 'java', - 'ruby' + 'ruby' // for related places search: ADD_NEW_LANG ].includes(lang ?? '') ) @@ -175,7 +175,7 @@ 'csharp', 'nu', 'java', - 'ruby', + 'ruby' // for related places search: ADD_NEW_LANG ].includes(lang ?? '') ) @@ -715,10 +715,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS on:selectAndClose={(s3obj) => { let s = `'${formatS3Object(s3obj.detail)}'` if (lang === 'duckdb') { - if (s3obj.detail?.s3.endsWith('.json')) s = `read_json(${s})` - if (s3obj.detail?.s3.endsWith('.csv')) s = `read_csv(${s})` - if (s3obj.detail?.s3.endsWith('.parquet')) s = `read_parquet(${s})` - editor?.insertAtCursor(s) + editor?.insertAtCursor(`SELECT * FROM ${s}`) } else if (lang === 'python3') { if (!editor?.getCode().includes('import wmill')) { editor?.insertAtBeginning('import wmill\n')