From fafa8096709ee4d2909374ff858181b3484dbd1d Mon Sep 17 00:00:00 2001
From: Ruben Fiszel
Date: Wed, 4 Mar 2026 07:14:00 +0000
Subject: [PATCH 1/7] chore(main): release 1.649.0 (#8198)
* chore(main): release 1.649.0
* Apply automatic changes
---------
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
---
CHANGELOG.md | 22 +++
backend/Cargo.lock | 172 +++++++++---------
backend/Cargo.toml | 4 +-
backend/windmill-api/openapi.yaml | 2 +-
benchmarks/lib.ts | 2 +-
cli/src/main.ts | 2 +-
frontend/package-lock.json | 4 +-
frontend/package.json | 2 +-
lsp/Pipfile | 2 +-
openflow.openapi.yaml | 2 +-
.../WindmillClient/WindmillClient.psd1 | 2 +-
python-client/wmill/pyproject.toml | 2 +-
typescript-client/jsr.json | 2 +-
typescript-client/package.json | 2 +-
version.txt | 2 +-
15 files changed, 126 insertions(+), 98 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 15fca3e649..ae23b08efa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,27 @@
# Changelog
+## [1.649.0](https://github.com/windmill-labs/windmill/compare/v1.648.0...v1.649.0) (2026-03-03)
+
+
+### Features
+
+* **frontend:** add script recorder for offline replay ([#8200](https://github.com/windmill-labs/windmill/issues/8200)) ([c97d8b4](https://github.com/windmill-labs/windmill/commit/c97d8b4715f86ea83ab2c0223ba859ced690829a))
+* move index management out of /srch/, add storage size reporting ([#8169](https://github.com/windmill-labs/windmill/issues/8169)) ([ee01acd](https://github.com/windmill-labs/windmill/commit/ee01acd9a6a2cd68a3f226988bfb46f6a6e64c08))
+
+
+### Bug Fixes
+
+* clean up slow-load toast interval on component destroy ([#8207](https://github.com/windmill-labs/windmill/issues/8207)) ([26f4f2b](https://github.com/windmill-labs/windmill/commit/26f4f2b399b828185b553289d6560e12261030a3))
+* **frontend:** prevent subflow expansion from hiding all insertion points ([#8203](https://github.com/windmill-labs/windmill/issues/8203)) ([e97da86](https://github.com/windmill-labs/windmill/commit/e97da860672171e33054a77d71f4824bb09e540d))
+* gracefully handle malformed OAuth entries in instance config ([#8205](https://github.com/windmill-labs/windmill/issues/8205)) ([cac4bdd](https://github.com/windmill-labs/windmill/commit/cac4bdd54f0c3ea80844ac31f7597f418ff7d8ae))
+* skip stop_after_if evaluation for skipped (identity) flow steps ([#8201](https://github.com/windmill-labs/windmill/issues/8201)) ([e6f7775](https://github.com/windmill-labs/windmill/commit/e6f7775d4d9a052aefc37260c6ed161146841cd7))
+* use exact matching for python requirements directive parsing ([#8199](https://github.com/windmill-labs/windmill/issues/8199)) ([2b2be38](https://github.com/windmill-labs/windmill/commit/2b2be38f129bbe58b6bb3815c4bd94aa03a3da90))
+
+
+### Performance Improvements
+
+* use two-step query in input history to leverage v2_job index ([#8197](https://github.com/windmill-labs/windmill/issues/8197)) ([50defdd](https://github.com/windmill-labs/windmill/commit/50defdded113b4d2cf0991b3fb642d1cd9a462b7))
+
## [1.648.0](https://github.com/windmill-labs/windmill/compare/v1.647.2...v1.648.0) (2026-03-02)
diff --git a/backend/Cargo.lock b/backend/Cargo.lock
index a2a27a9413..d26d5b76d7 100644
--- a/backend/Cargo.lock
+++ b/backend/Cargo.lock
@@ -860,9 +860,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
-version = "1.16.0"
+version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9"
+checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -870,9 +870,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
-version = "0.37.1"
+version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549"
+checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
dependencies = [
"cc",
"cmake",
@@ -1334,9 +1334,9 @@ dependencies = [
[[package]]
name = "aws-smithy-xml"
-version = "0.60.14"
+version = "0.60.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa"
+checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3"
dependencies = [
"xmlparser",
]
@@ -6173,20 +6173,20 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
- "r-efi",
+ "r-efi 5.3.0",
"wasip2",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
-version = "0.4.1"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
- "r-efi",
+ "r-efi 6.0.0",
"wasip2",
"wasip3",
]
@@ -7421,9 +7421,9 @@ dependencies = [
[[package]]
name = "ipnet"
-version = "2.11.0"
+version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "ipnetwork"
@@ -10723,6 +10723,12 @@ 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 = "radium"
version = "0.7.0"
@@ -13850,7 +13856,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
dependencies = [
"fastrand",
- "getrandom 0.4.1",
+ "getrandom 0.4.2",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
@@ -15732,7 +15738,7 @@ dependencies = [
[[package]]
name = "windmill"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15796,7 +15802,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15809,7 +15815,7 @@ dependencies = [
[[package]]
name = "windmill-api"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"argon2",
@@ -15947,7 +15953,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15970,7 +15976,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15983,7 +15989,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16009,7 +16015,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16019,7 +16025,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16036,7 +16042,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -16059,7 +16065,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16082,7 +16088,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16098,7 +16104,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16118,7 +16124,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16138,7 +16144,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16152,7 +16158,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16179,7 +16185,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16204,7 +16210,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"flate2",
@@ -16222,7 +16228,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16243,7 +16249,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16263,7 +16269,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16293,7 +16299,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16320,7 +16326,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"lazy_static",
"serde",
@@ -16332,7 +16338,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"argon2",
"axum 0.7.9",
@@ -16355,7 +16361,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16369,7 +16375,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16399,7 +16405,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16413,7 +16419,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16432,7 +16438,7 @@ dependencies = [
[[package]]
name = "windmill-common"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"aes-gcm",
"anyhow",
@@ -16531,7 +16537,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16550,7 +16556,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"regex",
"serde",
@@ -16565,7 +16571,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16589,7 +16595,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"futures",
@@ -16606,7 +16612,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16622,7 +16628,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16643,7 +16649,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16674,7 +16680,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16698,7 +16704,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-stream",
@@ -16732,7 +16738,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"futures",
@@ -16750,7 +16756,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16759,7 +16765,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16771,7 +16777,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16783,7 +16789,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"gosyn",
@@ -16795,7 +16801,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16807,7 +16813,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16819,7 +16825,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -16830,7 +16836,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16841,7 +16847,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16854,7 +16860,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16878,7 +16884,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16892,7 +16898,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16909,7 +16915,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16924,7 +16930,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16943,7 +16949,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"serde",
@@ -16954,7 +16960,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16991,7 +16997,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"const_format",
@@ -17029,7 +17035,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17040,7 +17046,7 @@ dependencies = [
[[package]]
name = "windmill-store"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17069,7 +17075,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -17092,7 +17098,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17125,7 +17131,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17145,7 +17151,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17179,7 +17185,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17214,7 +17220,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17237,7 +17243,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17261,7 +17267,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17285,7 +17291,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17320,7 +17326,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17348,7 +17354,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17371,7 +17377,7 @@ dependencies = [
[[package]]
name = "windmill-types"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17389,7 +17395,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
-version = "1.648.0"
+version = "1.649.0"
dependencies = [
"anyhow",
"async-once-cell",
diff --git a/backend/Cargo.toml b/backend/Cargo.toml
index 2d964c45b4..195d8bd30d 100644
--- a/backend/Cargo.toml
+++ b/backend/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "windmill"
-version = "1.648.0"
+version = "1.649.0"
authors.workspace = true
edition.workspace = true
@@ -76,7 +76,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
-version = "1.648.0"
+version = "1.649.0"
authors = ["Ruben Fiszel "]
edition = "2021"
diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml
index 4dc1d252c4..163217862e 100644
--- a/backend/windmill-api/openapi.yaml
+++ b/backend/windmill-api/openapi.yaml
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
- version: 1.648.0
+ version: 1.649.0
title: Windmill API
contact:
diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts
index 41204c956a..cc1417c4b3 100644
--- a/benchmarks/lib.ts
+++ b/benchmarks/lib.ts
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
-export const VERSION = "v1.648.0";
+export const VERSION = "v1.649.0";
export async function login(email: string, password: string): Promise {
return await windmill.UserService.login({
diff --git a/cli/src/main.ts b/cli/src/main.ts
index 38b68bb0f4..b584c540d8 100755
--- a/cli/src/main.ts
+++ b/cli/src/main.ts
@@ -67,7 +67,7 @@ export {
workspaceAdd,
};
-export const VERSION = "1.648.0";
+export const VERSION = "1.649.0";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 520148b1d6..c24e48e02d 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
- "version": "1.648.0",
+ "version": "1.649.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
- "version": "1.648.0",
+ "version": "1.649.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
diff --git a/frontend/package.json b/frontend/package.json
index 9232f07db8..d6c6554459 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
- "version": "1.648.0",
+ "version": "1.649.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
diff --git a/lsp/Pipfile b/lsp/Pipfile
index c01fab216e..37aecedea0 100644
--- a/lsp/Pipfile
+++ b/lsp/Pipfile
@@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
-wmill = ">=1.648.0"
+wmill = ">=1.649.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml
index 9f9ee14ffb..2b181a6c7b 100644
--- a/openflow.openapi.yaml
+++ b/openflow.openapi.yaml
@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
- version: 1.648.0
+ version: 1.649.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel
diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1
index 53f6b21227..ec7cea297d 100644
--- a/powershell-client/WindmillClient/WindmillClient.psd1
+++ b/powershell-client/WindmillClient/WindmillClient.psd1
@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
- ModuleVersion = '1.648.0'
+ ModuleVersion = '1.649.0'
# Supported PSEditions
# CompatiblePSEditions = @()
diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml
index 0d6daad51f..368b0de11b 100644
--- a/python-client/wmill/pyproject.toml
+++ b/python-client/wmill/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
-version = "1.648.0"
+version = "1.649.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"
diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json
index e4842b836b..8b07a94942 100644
--- a/typescript-client/jsr.json
+++ b/typescript-client/jsr.json
@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
- "version": "1.648.0",
+ "version": "1.649.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]
diff --git a/typescript-client/package.json b/typescript-client/package.json
index 71689ffd27..a5cd2f4395 100644
--- a/typescript-client/package.json
+++ b/typescript-client/package.json
@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
- "version": "1.648.0",
+ "version": "1.649.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"sideEffects": false,
diff --git a/version.txt b/version.txt
index 53ea075c24..c34844244a 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-1.648.0
+1.649.0
From 424ca59dfe3e730f5388d9cac4ea7e69773614d3 Mon Sep 17 00:00:00 2001
From: Ruben Fiszel
Date: Wed, 4 Mar 2026 08:53:25 +0000
Subject: [PATCH 2/7] feat: make WINDMILL_DIR configurable via environment
variable (#8215)
* fix: auto-heal corrupted python runtime cache on remote workers
Co-Authored-By: Claude Opus 4.6
* Revert "fix: auto-heal corrupted python runtime cache on remote workers"
This reverts commit 0ea013a5545ffb14e21a8d280d01b184ba50b032.
* feat: make WINDMILL_DIR configurable via environment variable
Allow users to configure the base directory for Windmill's tmp/cache files
via the WINDMILL_DIR env var (default: /tmp/windmill). This fixes Python
runtime cache corruption on RHEL systems where systemd-tmpfiles-clean
removes files from /tmp.
Converts TMP_DIR (renamed to WINDMILL_DIR) and all derived cache directory
constants from compile-time const &str (concatcp!) to runtime lazy_static
String values.
Co-Authored-By: Claude Opus 4.6
* chore: update ee ref
Co-Authored-By: Claude Opus 4.6
* chore: update ee ref
Co-Authored-By: Claude Opus 4.6
* fix: deref ERROR_DIR lazy_static for AsRef and Display traits
Co-Authored-By: Claude Opus 4.6
* chore: update ee ref to branch name for CI compatibility
Co-Authored-By: Claude Opus 4.6
* fix: deref lazy_static constants in all executor files
Co-Authored-By: Claude Opus 4.6
* chore: update ee ref
Co-Authored-By: Claude Opus 4.6
* chore: update ee ref
Co-Authored-By: Claude Opus 4.6
* chore: update ee ref
Co-Authored-By: Claude Opus 4.6
* fix: panic if WINDMILL_DIR has trailing slash
Co-Authored-By: Claude Opus 4.6
* fix: also reject trailing backslash in WINDMILL_DIR for Windows
Co-Authored-By: Claude Opus 4.6
* fix: deref GO_BIN_CACHE_DIR in test utils
Co-Authored-By: Claude Opus 4.6
* fix: replace remaining hardcoded /tmp/windmill paths and validate empty WINDMILL_DIR
Co-Authored-By: Claude Opus 4.6
* fix: nsjail powershell mount dst, Windows path assumptions, pwsh deref consistency
Co-Authored-By: Claude Opus 4.6
* fix: restore Windows /tmp path translation in go and bun executors
The Windows path translation replaces /tmp with the Windows temp dir
(e.g. C:\tmp) before normalizing slashes. Without this, the default
WINDMILL_DIR=/tmp/windmill produces paths without a drive letter on
Windows.
Co-Authored-By: Claude Opus 4.6
* chore: update ee-repo-ref to 6fd5a2ce908235a17975ad4dbdf0051cd89334f3
This commit updates the EE repository reference after PR #436 was merged in windmill-ee-private.
Previous ee-repo-ref: e8c03e16720833230ebd1878b4c63642ecc6c80f
New ee-repo-ref: 6fd5a2ce908235a17975ad4dbdf0051cd89334f3
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.6
Co-authored-by: windmill-internal-app[bot]
---
backend/ee-repo-ref.txt | 2 +-
backend/src/main.rs | 60 ++++++++--------
backend/src/monitor.rs | 17 ++---
backend/tests/nativets_stress.rs | 2 +-
backend/tests/python_jobs.rs | 23 ++++---
backend/windmill-api-settings/src/lib.rs | 2 +-
backend/windmill-api/src/jobs.rs | 16 ++---
backend/windmill-api/src/lib.rs | 2 +-
backend/windmill-api/src/service_logs.rs | 8 ++-
backend/windmill-api/src/workspaces_export.rs | 3 +-
backend/windmill-common/src/bench.rs | 5 +-
backend/windmill-common/src/jobs.rs | 6 +-
backend/windmill-common/src/scripts.rs | 4 +-
backend/windmill-common/src/tracing_init.rs | 8 +--
backend/windmill-common/src/worker.rs | 29 +++++---
backend/windmill-runtime-nativets/src/lib.rs | 21 +++---
backend/windmill-store/src/resources.rs | 4 +-
backend/windmill-test-utils/src/lib.rs | 2 +-
.../nsjail/run.powershell.config.proto | 2 +-
.../windmill-worker/src/ansible_executor.rs | 6 +-
backend/windmill-worker/src/bash_executor.rs | 2 +-
backend/windmill-worker/src/bun_executor.rs | 6 +-
backend/windmill-worker/src/common.rs | 11 ++-
.../windmill-worker/src/csharp_executor.rs | 34 ++++-----
backend/windmill-worker/src/deno_executor.rs | 6 +-
backend/windmill-worker/src/global_cache.rs | 10 +--
backend/windmill-worker/src/go_executor.rs | 25 ++++---
backend/windmill-worker/src/java_executor.rs | 33 ++++-----
backend/windmill-worker/src/nu_executor.rs | 6 +-
backend/windmill-worker/src/pwsh_executor.rs | 10 +--
.../windmill-worker/src/python_executor.rs | 10 +--
.../windmill-worker/src/python_versions.rs | 21 +++---
backend/windmill-worker/src/ruby_executor.rs | 17 +++--
backend/windmill-worker/src/rust_executor.rs | 27 ++++----
backend/windmill-worker/src/worker.rs | 69 ++++++++++---------
35 files changed, 269 insertions(+), 240 deletions(-)
diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt
index e0d990384a..a71cab586d 100644
--- a/backend/ee-repo-ref.txt
+++ b/backend/ee-repo-ref.txt
@@ -1 +1 @@
-9b3339730eb4bb0b564c7c56ac546f33fb3d8905
\ No newline at end of file
+6fd5a2ce908235a17975ad4dbdf0051cd89334f3
diff --git a/backend/src/main.rs b/backend/src/main.rs
index c4440b7f34..97980484a5 100644
--- a/backend/src/main.rs
+++ b/backend/src/main.rs
@@ -62,7 +62,7 @@ use windmill_common::{
},
worker::{
is_native_mode_from_env, reload_custom_tags_setting, Connection, HUB_CACHE_DIR,
- HUB_RT_CACHE_DIR, NATIVE_MODE_RESOLVED, TMP_DIR, TMP_LOGS_DIR, WORKER_GROUP,
+ HUB_RT_CACHE_DIR, NATIVE_MODE_RESOLVED, TMP_LOGS_DIR, WINDMILL_DIR, WORKER_GROUP,
},
KillpillSender, DEFAULT_HUB_BASE_URL, METRICS_ENABLED,
};
@@ -238,8 +238,8 @@ async fn cache_hub_scripts(file_path: Option) -> anyhow::Result<()> {
)
})?;
- create_dir_all(HUB_CACHE_DIR)?;
- create_dir_all(BUN_BUNDLE_CACHE_DIR)?;
+ create_dir_all(&*HUB_CACHE_DIR)?;
+ create_dir_all(&*BUN_BUNDLE_CACHE_DIR)?;
for path in paths.values() {
tracing::info!("Caching hub script at {path}");
@@ -249,7 +249,7 @@ async fn cache_hub_scripts(file_path: Option) -> anyhow::Result<()> {
.as_ref()
.is_some_and(|x| x == &ScriptLang::Deno)
{
- let job_dir = format!("{}/cache_init/{}", TMP_DIR, Uuid::new_v4());
+ let job_dir = format!("{}/cache_init/{}", *WINDMILL_DIR, Uuid::new_v4());
create_dir_all(&job_dir)?;
let _ = windmill_worker::generate_deno_lock(
&Uuid::nil(),
@@ -267,7 +267,7 @@ async fn cache_hub_scripts(file_path: Option) -> anyhow::Result<()> {
tokio::fs::remove_dir_all(job_dir).await?;
} else if res.language.as_ref().is_some_and(|x| x == &ScriptLang::Bun) {
let job_id = Uuid::new_v4();
- let job_dir = format!("{}/cache_init/{}", TMP_DIR, job_id);
+ let job_dir = format!("{}/cache_init/{}", *WINDMILL_DIR, job_id);
create_dir_all(&job_dir)?;
if let Some(lock) = res.lockfile {
let _ = windmill_worker::prepare_job_dir(&lock, &job_dir).await?;
@@ -384,9 +384,9 @@ async fn cache_hub_resource_types() -> anyhow::Result<()> {
println!("Fetched {} resource types from hub", resource_types.len());
- create_dir_all(HUB_RT_CACHE_DIR)?;
+ create_dir_all(&*HUB_RT_CACHE_DIR)?;
- let cache_path = format!("{}/{}", HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
+ let cache_path = format!("{}/{}", *HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
let content = serde_json::to_string_pretty(&resource_types)
.with_context(|| "Failed to serialize resource types")?;
@@ -398,7 +398,7 @@ async fn cache_hub_resource_types() -> anyhow::Result<()> {
}
pub async fn sync_cached_resource_types(db: &sqlx::Pool) -> anyhow::Result<()> {
- let cache_path = format!("{}/{}", HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
+ let cache_path = format!("{}/{}", *HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
if tokio::fs::metadata(&cache_path).await.is_err() {
tracing::info!(
@@ -969,7 +969,7 @@ Windmill Community Edition {GIT_VERSION}
DirBuilder::new()
.recursive(true)
- .create("/tmp/windmill")
+ .create(&*WINDMILL_DIR)
.expect("could not create initial server dir");
#[cfg(feature = "tantivy")]
@@ -1794,27 +1794,27 @@ pub async fn run_workers(
let mut handles = Vec::with_capacity(num_workers as usize);
for x in [
- TMP_LOGS_DIR,
- UV_CACHE_DIR,
- DENO_CACHE_DIR,
- DENO_CACHE_DIR_DEPS,
- DENO_CACHE_DIR_NPM,
- BUN_CACHE_DIR,
- PY310_CACHE_DIR,
- PY311_CACHE_DIR,
- PY312_CACHE_DIR,
- PY313_CACHE_DIR,
- BUN_BUNDLE_CACHE_DIR,
- GO_CACHE_DIR,
- GO_BIN_CACHE_DIR,
- RUST_CACHE_DIR,
- CSHARP_CACHE_DIR,
- NU_CACHE_DIR,
- HUB_CACHE_DIR,
- POWERSHELL_CACHE_DIR,
- JAVA_CACHE_DIR,
- RUBY_CACHE_DIR,
- TAR_JAVA_CACHE_DIR, // for related places search: ADD_NEW_LANG
+ &*TMP_LOGS_DIR,
+ &*UV_CACHE_DIR,
+ &*DENO_CACHE_DIR,
+ &*DENO_CACHE_DIR_DEPS,
+ &*DENO_CACHE_DIR_NPM,
+ &*BUN_CACHE_DIR,
+ &*PY310_CACHE_DIR,
+ &*PY311_CACHE_DIR,
+ &*PY312_CACHE_DIR,
+ &*PY313_CACHE_DIR,
+ &*BUN_BUNDLE_CACHE_DIR,
+ &*GO_CACHE_DIR,
+ &*GO_BIN_CACHE_DIR,
+ &*RUST_CACHE_DIR,
+ &*CSHARP_CACHE_DIR,
+ &*NU_CACHE_DIR,
+ &*HUB_CACHE_DIR,
+ &*POWERSHELL_CACHE_DIR,
+ &*JAVA_CACHE_DIR,
+ &*RUBY_CACHE_DIR,
+ &*TAR_JAVA_CACHE_DIR, // for related places search: ADD_NEW_LANG
] {
DirBuilder::new()
.recursive(true)
diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs
index 17e79c3393..6baba34a02 100644
--- a/backend/src/monitor.rs
+++ b/backend/src/monitor.rs
@@ -73,7 +73,7 @@ use windmill_common::{
load_periodic_bash_script_interval_from_env, load_whitelist_env_vars_from_env,
load_worker_config, reload_custom_tags_setting, store_pull_query,
store_suspended_pull_query, Connection, WorkerConfig, DEFAULT_TAGS_PER_WORKSPACE,
- DEFAULT_TAGS_WORKSPACES, INDEXER_CONFIG, SCRIPT_TOKEN_EXPIRY, SMTP_CONFIG, TMP_DIR,
+ DEFAULT_TAGS_WORKSPACES, INDEXER_CONFIG, SCRIPT_TOKEN_EXPIRY, SMTP_CONFIG, WINDMILL_DIR,
WORKER_CONFIG, WORKER_GROUP,
},
KillpillSender, BASE_URL, CRITICAL_ALERTS_ON_DB_OVERSIZE, CRITICAL_ALERT_MUTE_UI_ENABLED,
@@ -595,7 +595,7 @@ async fn sleep_until_next_minute_start_plus_one_s() {
use windmill_common::tracing_init::TMP_WINDMILL_LOGS_SERVICE;
async fn find_two_highest_files(hostname: &str) -> (Option, Option) {
- let log_dir = format!("{}/{}/", TMP_WINDMILL_LOGS_SERVICE, hostname);
+ let log_dir = format!("{}/{}/", *TMP_WINDMILL_LOGS_SERVICE, hostname);
let rd_dir = tokio::fs::read_dir(log_dir).await;
if let Ok(mut log_files) = rd_dir {
let mut highest_file: Option = None;
@@ -614,7 +614,8 @@ async fn find_two_highest_files(hostname: &str) -> (Option, Option () {
.iter()
.map(|f| format!("{}/{}", f.hostname, f.file_path))
.collect();
- delete_log_files_from_disk_and_store(paths, TMP_WINDMILL_LOGS_SERVICE, windmill_common::tracing_init::LOGS_SERVICE).await;
+ delete_log_files_from_disk_and_store(paths, &*TMP_WINDMILL_LOGS_SERVICE, windmill_common::tracing_init::LOGS_SERVICE).await;
}
Err(e) => tracing::error!("Error deleting log file: {:?}", e),
@@ -1140,7 +1141,7 @@ async fn delete_expired_jobs_batch(
.filter_map(|opt| opt)
.flat_map(|inner_vec| inner_vec.into_iter())
.collect();
- delete_log_files_from_disk_and_store(paths, TMP_DIR, "").await;
+ delete_log_files_from_disk_and_store(paths, &*WINDMILL_DIR, "").await;
}
Err(e) => tracing::error!("Error deleting job logs: {:?}", e),
}
@@ -1367,7 +1368,7 @@ pub async fn reload_maven_settings_xml_setting(conn: &Connection) {
let settings_xml = MAVEN_SETTINGS_XML.read().await.clone();
match settings_xml {
Some(ref content) if !content.trim().is_empty() => {
- let m2_dir = format!("{JAVA_HOME_DIR}/.m2");
+ let m2_dir = format!("{}/.m2", *JAVA_HOME_DIR);
if let Err(e) = tokio::fs::create_dir_all(&m2_dir).await {
tracing::error!("Failed to create .m2 directory: {e:#}");
return;
@@ -1378,7 +1379,7 @@ pub async fn reload_maven_settings_xml_setting(conn: &Connection) {
}
}
_ => {
- let settings_path = format!("{JAVA_HOME_DIR}/.m2/settings.xml");
+ let settings_path = format!("{}/.m2/settings.xml", *JAVA_HOME_DIR);
let _ = tokio::fs::remove_file(&settings_path).await;
}
}
diff --git a/backend/tests/nativets_stress.rs b/backend/tests/nativets_stress.rs
index 9717403830..082a43be2c 100644
--- a/backend/tests/nativets_stress.rs
+++ b/backend/tests/nativets_stress.rs
@@ -206,7 +206,7 @@ fn spawn_workers(
std::fs::DirBuilder::new()
.recursive(true)
- .create(windmill_worker::GO_BIN_CACHE_DIR)
+ .create(&*windmill_worker::GO_BIN_CACHE_DIR)
.expect("could not create initial worker dir");
let (tx, _) = KillpillSender::new(n + 1);
diff --git a/backend/tests/python_jobs.rs b/backend/tests/python_jobs.rs
index dd494de007..22c2313b91 100644
--- a/backend/tests/python_jobs.rs
+++ b/backend/tests/python_jobs.rs
@@ -1,7 +1,7 @@
-use windmill_test_utils::*;
use sqlx::postgres::Postgres;
use sqlx::Pool;
use windmill_common::scripts::ScriptLang;
+use windmill_test_utils::*;
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "lockfile_python"))]
@@ -188,7 +188,8 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
- concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
+ concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
+ .into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -207,14 +208,14 @@ def main():
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base"))]
async fn test_python_global_site_packages(db: Pool) -> anyhow::Result<()> {
- use windmill_common::{cache::concatcp, worker::ROOT_CACHE_DIR};
+ use windmill_common::worker::ROOT_CACHE_DIR;
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Shared for all 3.12.*
- let path = concatcp!(ROOT_CACHE_DIR, "python_3_12/global-site-packages").to_owned();
+ let path = format!("{}python_3_12/global-site-packages", *ROOT_CACHE_DIR);
std::fs::create_dir_all(&path).unwrap();
std::fs::write(path + "/my_global_site_package_3_12_any.py", "").unwrap();
@@ -237,7 +238,9 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
- concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
+ concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
+ )
+ .into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -271,7 +274,9 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
- concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
+ concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
+ )
+ .into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -310,7 +315,8 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
- concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
+ concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
+ .into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -347,7 +353,8 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
- concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
+ concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
+ .into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
diff --git a/backend/windmill-api-settings/src/lib.rs b/backend/windmill-api-settings/src/lib.rs
index 04a1eb617b..bf493f9420 100644
--- a/backend/windmill-api-settings/src/lib.rs
+++ b/backend/windmill-api-settings/src/lib.rs
@@ -1085,7 +1085,7 @@ async fn sync_cached_resource_types(
require_super_admin(&db, &authed.email).await?;
use windmill_common::worker::HUB_RT_CACHE_DIR;
- let cache_path = format!("{}/resource_types.json", HUB_RT_CACHE_DIR);
+ let cache_path = format!("{}/resource_types.json", *HUB_RT_CACHE_DIR);
let content = tokio::fs::read_to_string(&cache_path).await.map_err(|e| {
error::Error::NotFound(format!(
diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs
index 420bbf24b9..d58dcd3c69 100644
--- a/backend/windmill-api/src/jobs.rs
+++ b/backend/windmill-api/src/jobs.rs
@@ -47,7 +47,7 @@ use windmill_common::runtime_assets::{register_runtime_asset, InsertRuntimeAsset
use windmill_common::scripts::ScriptRunnableSettingsInline;
use windmill_common::triggers::TriggerMetadata;
use windmill_common::utils::{RunnableKind, WarnAfterExt};
-use windmill_common::worker::{Connection, CLOUD_HOSTED, TMP_DIR};
+use windmill_common::worker::{Connection, CLOUD_HOSTED, WINDMILL_DIR};
use windmill_common::workspace_dependencies::{
RawWorkspaceDependencies, MIN_VERSION_WORKSPACE_DEPENDENCIES,
};
@@ -1412,7 +1412,7 @@ async fn get_logs_from_disk(
if log_offset > 0 {
if let Some(file_index) = log_file_index.clone() {
for file_p in &file_index {
- if !tokio::fs::metadata(format!("{TMP_DIR}/{file_p}"))
+ if !tokio::fs::metadata(format!("{}/{file_p}", *WINDMILL_DIR))
.await
.is_ok()
{
@@ -1427,7 +1427,7 @@ async fn get_logs_from_disk(
"#.to_string(),
));
for file_p in file_index.clone() {
- let mut file = tokio::fs::File::open(format!("{TMP_DIR}/{file_p}")).await.map_err(to_anyhow)?;
+ let mut file = tokio::fs::File::open(format!("{}/{file_p}", *WINDMILL_DIR)).await.map_err(to_anyhow)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await.map_err(to_anyhow)?;
yield Ok(bytes::Bytes::from(buffer)) as anyhow::Result;
@@ -5888,7 +5888,7 @@ async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::R
));
}
- let local_file = format!("{TMP_DIR}/logs/{file_p}");
+ let local_file = format!("{}/logs/{file_p}", *WINDMILL_DIR);
if tokio::fs::metadata(&local_file).await.is_ok() {
let mut file = tokio::fs::File::open(local_file).await.map_err(to_anyhow)?;
let mut buffer = Vec::new();
@@ -5934,10 +5934,10 @@ async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::R
}
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
- return Err(error::Error::NotFound(format!(
- "File not found on server logs volume /tmp/windmill/logs and no distributed logs s3 storage for {}",
- file_p
- )));
+ return Err(error::Error::NotFound(format!(
+ "File not found on server logs volume {}/logs and no distributed logs s3 storage for {}",
+ *WINDMILL_DIR, file_p
+ )));
}
async fn get_job_update(
diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs
index 9b713cad0a..528e60b02f 100644
--- a/backend/windmill-api/src/lib.rs
+++ b/backend/windmill-api/src/lib.rs
@@ -262,7 +262,7 @@ pub async fn run_server(
) -> anyhow::Result<()> {
let user_db = UserDB::new(db.clone());
- for x in [HUB_CACHE_DIR] {
+ for x in [&*HUB_CACHE_DIR] {
DirBuilder::new()
.recursive(true)
.create(x)
diff --git a/backend/windmill-api/src/service_logs.rs b/backend/windmill-api/src/service_logs.rs
index 20d2b58d0e..c83bb21f2c 100644
--- a/backend/windmill-api/src/service_logs.rs
+++ b/backend/windmill-api/src/service_logs.rs
@@ -102,7 +102,11 @@ async fn get_log_file(
#[cfg(feature = "parquet")]
if let Some(s3_client) = s3_client {
let path = format!("{}{}", windmill_common::tracing_init::LOGS_SERVICE, path);
- let file = s3_client.get(&windmill_object_store::object_store_reexports::Path::from(path)).await;
+ let file = s3_client
+ .get(&windmill_object_store::object_store_reexports::Path::from(
+ path,
+ ))
+ .await;
match file {
Ok(file) => {
let bytes = file.bytes().await;
@@ -126,7 +130,7 @@ async fn get_log_file(
}
}
}
- let file = tokio::fs::read(format!("{}{}", TMP_WINDMILL_LOGS_SERVICE, path)).await;
+ let file = tokio::fs::read(format!("{}{}", *TMP_WINDMILL_LOGS_SERVICE, path)).await;
if let Ok(bytes) = file {
Ok(content_plain(Body::from(bytes::Bytes::from(bytes))))
} else {
diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs
index c881e9ae9b..091c465a61 100644
--- a/backend/windmill-api/src/workspaces_export.rs
+++ b/backend/windmill-api/src/workspaces_export.rs
@@ -43,6 +43,7 @@ use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings
use windmill_common::scripts::ScriptRunnableSettingsHandle;
use windmill_common::utils::require_admin;
use windmill_common::variables::decrypt;
+use windmill_common::worker::WINDMILL_DIR;
use windmill_common::{
db::UserDB,
error::{to_anyhow, Error, Result},
@@ -372,7 +373,7 @@ pub(crate) async fn tarball_workspace(
let mut tx = user_db.begin(&authed).await?;
- let tmp_dir = TempDir::new_in("/tmp/windmill/")?;
+ let tmp_dir = TempDir::new_in(&*WINDMILL_DIR)?;
let name = match archive_type.as_deref() {
Some("tar") | None => Ok(format!("windmill-{w_id}.tar")),
diff --git a/backend/windmill-common/src/bench.rs b/backend/windmill-common/src/bench.rs
index b87872119b..1ed2b9aa56 100644
--- a/backend/windmill-common/src/bench.rs
+++ b/backend/windmill-common/src/bench.rs
@@ -1,5 +1,5 @@
use crate::{
- worker::{write_file, TMP_DIR},
+ worker::{write_file, WINDMILL_DIR},
DB,
};
use serde::Serialize;
@@ -113,7 +113,8 @@ impl BenchmarkInfo {
"Writing benchmark {path}, duration of benchmark: {total_duration}ms and RPS: {}{pool_info}",
self.iters as f64 / total_duration as f64 * 1000.0
);
- write_file(TMP_DIR, path, &serde_json::to_string(&self).unwrap()).expect("write profiling");
+ write_file(&WINDMILL_DIR, path, &serde_json::to_string(&self).unwrap())
+ .expect("write profiling");
Ok(())
}
}
diff --git a/backend/windmill-common/src/jobs.rs b/backend/windmill-common/src/jobs.rs
index bc40e4cd75..583914c573 100644
--- a/backend/windmill-common/src/jobs.rs
+++ b/backend/windmill-common/src/jobs.rs
@@ -18,7 +18,7 @@ use crate::{
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
users::username_to_permissioned_as,
utils::{StripPath, HTTP_CLIENT},
- worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE, TMP_DIR},
+ worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE, WINDMILL_DIR},
FlowVersionInfo, ScriptHashInfo, Tag,
};
@@ -225,7 +225,7 @@ pub async fn get_logs_from_disk(
if log_offset > 0 {
if let Some(file_index) = log_file_index.clone() {
for file_p in &file_index {
- if !tokio::fs::metadata(format!("{TMP_DIR}/{file_p}"))
+ if !tokio::fs::metadata(format!("{}/{file_p}", *WINDMILL_DIR))
.await
.is_ok()
{
@@ -236,7 +236,7 @@ pub async fn get_logs_from_disk(
let logs = logs.to_string();
let stream = async_stream::stream! {
for file_p in file_index.clone() {
- let mut file = tokio::fs::File::open(format!("{TMP_DIR}/{file_p}")).await.map_err(to_anyhow)?;
+ let mut file = tokio::fs::File::open(format!("{}/{file_p}", *WINDMILL_DIR)).await.map_err(to_anyhow)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await.map_err(to_anyhow)?;
yield Ok(bytes::Bytes::from(buffer)) as anyhow::Result;
diff --git a/backend/windmill-common/src/scripts.rs b/backend/windmill-common/src/scripts.rs
index 68f77a2303..cfff28dbcb 100644
--- a/backend/windmill-common/src/scripts.rs
+++ b/backend/windmill-common/src/scripts.rs
@@ -206,12 +206,12 @@ pub async fn get_full_hub_script_by_path(
let version = path_iterator
.next()
.ok_or_else(|| Error::internal_err(format!("expected hub path to have version number")))?;
- let cache_path = format!("{HUB_CACHE_DIR}/{version}");
+ let cache_path = format!("{}/{version}", *HUB_CACHE_DIR);
let script;
if tokio::fs::metadata(&cache_path).await.is_err() {
script = get_full_hub_script_by_path_inner(path, http_client, db).await?;
if let Err(e) = crate::worker::write_file(
- HUB_CACHE_DIR,
+ &HUB_CACHE_DIR,
&version,
&serde_json::to_string(&script).map_err(to_anyhow)?,
) {
diff --git a/backend/windmill-common/src/tracing_init.rs b/backend/windmill-common/src/tracing_init.rs
index 701a0887d0..f4892b67c9 100644
--- a/backend/windmill-common/src/tracing_init.rs
+++ b/backend/windmill-common/src/tracing_init.rs
@@ -6,8 +6,6 @@
* LICENSE-AGPL for a copy of the license.
*/
-use const_format::concatcp;
-
use std::{
collections::HashMap,
sync::{Arc, RwLock},
@@ -61,7 +59,9 @@ fn create_targets_filter(default_env_filter: LevelFilter) -> Targets {
pub const LOGS_SERVICE: &str = "logs/services/";
-pub const TMP_WINDMILL_LOGS_SERVICE: &str = concatcp!("/tmp/windmill/", LOGS_SERVICE);
+lazy_static::lazy_static! {
+ pub static ref TMP_WINDMILL_LOGS_SERVICE: String = format!("{}/{}", *crate::worker::WINDMILL_DIR, LOGS_SERVICE);
+}
pub fn initialize_tracing(
hostname: &str,
@@ -108,7 +108,7 @@ pub fn initialize_tracing(
use tracing_appender::rolling::{RollingFileAppender, Rotation};
- let log_dir = format!("{}/{}/", TMP_WINDMILL_LOGS_SERVICE, hostname);
+ let log_dir = format!("{}/{}/", *TMP_WINDMILL_LOGS_SERVICE, hostname);
std::fs::create_dir_all(&log_dir).unwrap();
let file_appender = RollingFileAppender::builder()
.rotation(Rotation::MINUTELY)
diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs
index 1a2d55b896..8d95541f61 100644
--- a/backend/windmill-common/src/worker.rs
+++ b/backend/windmill-common/src/worker.rs
@@ -2,7 +2,6 @@
use anyhow::anyhow;
use axum::http::HeaderMap;
use bytes::Bytes;
-use const_format::concatcp;
use itertools::Itertools;
use regex::Regex;
use reqwest_middleware::ClientWithMiddleware;
@@ -274,7 +273,9 @@ lazy_static::lazy_static! {
pub static ref ROOT_STANDALONE_BUNDLE_DIR: String = format!("{}/.windmill/standalone_bundle", std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()));
}
-pub const ROOT_CACHE_NOMOUNT_DIR: &str = concatcp!(TMP_DIR, "/cache_nomount/");
+lazy_static::lazy_static! {
+ pub static ref ROOT_CACHE_NOMOUNT_DIR: String = format!("{}/cache_nomount/", *WINDMILL_DIR);
+}
/// Whether native mode is forced by the environment (NATIVE_MODE=true env var or WORKER_GROUP=native).
/// This does NOT account for native_mode set in the DB worker group config — for that, read
@@ -490,13 +491,23 @@ pub async fn store_pull_query(wc: &WorkerConfig) {
*l = queries;
}
-pub const TMP_DIR: &str = "/tmp/windmill";
-pub const TMP_LOGS_DIR: &str = concatcp!(TMP_DIR, "/logs");
-
-pub const HUB_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "hub");
-pub const HUB_RT_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "hub_rt");
-
-pub const ROOT_CACHE_DIR: &str = concatcp!(TMP_DIR, "/cache/");
+lazy_static::lazy_static! {
+ pub static ref WINDMILL_DIR: String = {
+ let dir = std::env::var("WINDMILL_DIR")
+ .unwrap_or_else(|_| "/tmp/windmill".to_string());
+ if dir.is_empty() {
+ panic!("WINDMILL_DIR must not be empty");
+ }
+ if dir.ends_with('/') || dir.ends_with('\\') {
+ panic!("WINDMILL_DIR must not end with a trailing slash, got: {dir}");
+ }
+ dir
+ };
+ pub static ref TMP_LOGS_DIR: String = format!("{}/logs", *WINDMILL_DIR);
+ pub static ref ROOT_CACHE_DIR: String = format!("{}/cache/", *WINDMILL_DIR);
+ pub static ref HUB_CACHE_DIR: String = format!("{}hub", *ROOT_CACHE_DIR);
+ pub static ref HUB_RT_CACHE_DIR: String = format!("{}hub_rt", *ROOT_CACHE_DIR);
+}
pub fn write_file(dir: &str, path: &str, content: &str) -> error::Result {
let path = format!("{}/{}", dir, path);
diff --git a/backend/windmill-runtime-nativets/src/lib.rs b/backend/windmill-runtime-nativets/src/lib.rs
index a523106cbc..e81925e7bb 100644
--- a/backend/windmill-runtime-nativets/src/lib.rs
+++ b/backend/windmill-runtime-nativets/src/lib.rs
@@ -45,7 +45,7 @@ use uuid::Uuid;
use windmill_common::error::Error;
use windmill_common::result_stream::append_result_stream_db;
-use windmill_common::worker::{write_file, Connection, TMP_DIR};
+use windmill_common::worker::{write_file, Connection, WINDMILL_DIR};
// ── Permission container ─────────────────────────────────────────────
@@ -151,7 +151,9 @@ static RUNTIME_SNAPSHOT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/FETCH
pub(crate) const WINDMILL_CLIENT: &str = include_str!("./windmill-client.js");
-const ERROR_DIR: &str = const_format::concatcp!(TMP_DIR, "/native_errors");
+lazy_static::lazy_static! {
+ static ref ERROR_DIR: String = format!("{}/native_errors", *WINDMILL_DIR);
+}
lazy_static! {
static ref RE_PROXY: Regex =
@@ -263,14 +265,14 @@ fn capture_proxy(s: &str) -> Option<(String, Option<(String, String)>)> {
}
fn write_error_expr(expr: &str, uuid: &Uuid) {
- if let Err(e) = std::fs::create_dir_all(ERROR_DIR) {
- tracing::error!("failed to create error dir {ERROR_DIR}: {e}");
+ if let Err(e) = std::fs::create_dir_all(&*ERROR_DIR) {
+ tracing::error!("failed to create error dir {}: {e}", *ERROR_DIR);
return;
}
- let dir_entries = match std::fs::read_dir(ERROR_DIR) {
+ let dir_entries = match std::fs::read_dir(&*ERROR_DIR) {
Ok(entries) => entries.count(),
Err(_) => {
- tracing::error!("failed to read error dir {ERROR_DIR}");
+ tracing::error!("failed to read error dir {}", *ERROR_DIR);
return;
}
};
@@ -279,15 +281,16 @@ fn write_error_expr(expr: &str, uuid: &Uuid) {
tracing::info!("native error for job {uuid}: {expr}");
}
if dir_entries >= 100 {
- tracing::info!("Too many error files in {ERROR_DIR}, skipping write");
+ tracing::info!("Too many error files in {}, skipping write", *ERROR_DIR);
return;
}
let path = format!("/{uuid}.js");
tracing::info!(
- "nativets job {uuid} failed, writing error expr to {ERROR_DIR}/{path} for debugging: {path}"
+ "nativets job {uuid} failed, writing error expr to {}/{path} for debugging: {path}",
+ *ERROR_DIR
);
- if let Err(e) = write_file(ERROR_DIR, &path, expr) {
+ if let Err(e) = write_file(&ERROR_DIR, &path, expr) {
tracing::error!("failed to write error expr to file {path}: {e}");
}
}
diff --git a/backend/windmill-store/src/resources.rs b/backend/windmill-store/src/resources.rs
index bcac53e7d2..6b414ca10f 100644
--- a/backend/windmill-store/src/resources.rs
+++ b/backend/windmill-store/src/resources.rs
@@ -47,7 +47,7 @@ use windmill_common::{
StripPath,
},
variables,
- worker::{CLOUD_HOSTED, TMP_DIR},
+ worker::{CLOUD_HOSTED, WINDMILL_DIR},
PgDatabase,
};
@@ -1752,7 +1752,7 @@ async fn write_ssh_file(
var_path: &str,
) -> std::result::Result {
let id_file_name = format!(".ssh_id_priv_{}", Uuid::new_v4());
- let loc = std::path::Path::new(TMP_DIR)
+ let loc = std::path::Path::new(&*WINDMILL_DIR)
.join("ssh_ids")
.join(id_file_name);
diff --git a/backend/windmill-test-utils/src/lib.rs b/backend/windmill-test-utils/src/lib.rs
index adbeb83ee7..69e7fc0558 100644
--- a/backend/windmill-test-utils/src/lib.rs
+++ b/backend/windmill-test-utils/src/lib.rs
@@ -380,7 +380,7 @@ pub fn spawn_test_worker(
std::fs::DirBuilder::new()
.recursive(true)
- .create(windmill_worker::GO_BIN_CACHE_DIR)
+ .create(&*windmill_worker::GO_BIN_CACHE_DIR)
.expect("could not create initial worker dir");
let (tx, rx) = KillpillSender::new(1);
diff --git a/backend/windmill-worker/nsjail/run.powershell.config.proto b/backend/windmill-worker/nsjail/run.powershell.config.proto
index 87b6abda21..afe9d5df9f 100644
--- a/backend/windmill-worker/nsjail/run.powershell.config.proto
+++ b/backend/windmill-worker/nsjail/run.powershell.config.proto
@@ -127,7 +127,7 @@ iface_no_lo: true
mount {
src: "{CACHE_DIR}"
- dst: "/tmp/windmill/cache/powershell"
+ dst: "{CACHE_DIR}"
is_bind: true
rw: false
mandatory: false
diff --git a/backend/windmill-worker/src/ansible_executor.rs b/backend/windmill-worker/src/ansible_executor.rs
index aa250de4ab..253384f6f2 100644
--- a/backend/windmill-worker/src/ansible_executor.rs
+++ b/backend/windmill-worker/src/ansible_executor.rs
@@ -33,9 +33,9 @@ use crate::{
read_and_check_result, start_child_process, transform_json, OccupancyMetrics,
},
handle_child::handle_child,
+ is_sandboxing_enabled,
python_executor::{create_dependencies_dir, handle_python_reqs, uv_pip_compile},
- is_sandboxing_enabled, DISABLE_NUSER, GIT_PATH, HOME_ENV, NSJAIL_PATH, PATH_ENV, PROXY_ENVS,
- PY_INSTALL_DIR, TZ_ENV,
+ DISABLE_NUSER, GIT_PATH, HOME_ENV, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, PY_INSTALL_DIR, TZ_ENV,
};
use windmill_common::client::AuthedClient;
@@ -1184,7 +1184,7 @@ mount {{
job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_ANSIBLE_CONTENT
- .replace("{PY_INSTALL_DIR}", PY_INSTALL_DIR)
+ .replace("{PY_INSTALL_DIR}", &*PY_INSTALL_DIR)
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
diff --git a/backend/windmill-worker/src/bash_executor.rs b/backend/windmill-worker/src/bash_executor.rs
index 64270b4044..30348895cf 100644
--- a/backend/windmill-worker/src/bash_executor.rs
+++ b/backend/windmill-worker/src/bash_executor.rs
@@ -194,7 +194,7 @@ exit $exit_status
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
- .replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
+ .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut cmd_args = vec![
diff --git a/backend/windmill-worker/src/bun_executor.rs b/backend/windmill-worker/src/bun_executor.rs
index 9c46db572d..08a61645ce 100644
--- a/backend/windmill-worker/src/bun_executor.rs
+++ b/backend/windmill-worker/src/bun_executor.rs
@@ -737,7 +737,7 @@ async fn pull_codebase(w_id: &str, id: &str, job_dir: &str) -> Result Error {
pub async fn clean_cache() -> error::Result<()> {
tracing::info!("Started cleaning cache");
- tokio::fs::remove_dir_all(ROOT_CACHE_DIR).await?;
+ tokio::fs::remove_dir_all(&*ROOT_CACHE_DIR).await?;
tracing::info!("Finished cleaning cache");
Ok(())
}
@@ -1557,4 +1557,3 @@ mod tests {
assert!(result.is_err());
}
}
-
diff --git a/backend/windmill-worker/src/csharp_executor.rs b/backend/windmill-worker/src/csharp_executor.rs
index e4c32648d0..4c3e63f4c9 100644
--- a/backend/windmill-worker/src/csharp_executor.rs
+++ b/backend/windmill-worker/src/csharp_executor.rs
@@ -13,14 +13,11 @@ use itertools::Itertools;
#[cfg(feature = "csharp")]
use tokio::{fs::File, io::AsyncReadExt, process::Command};
#[cfg(feature = "csharp")]
-use windmill_common::{
- utils::calculate_hash,
- worker::write_file,
-};
+use windmill_common::{utils::calculate_hash, worker::write_file};
-use windmill_common::error::{self, Error};
#[cfg(feature = "csharp")]
use crate::global_cache::save_cache;
+use windmill_common::error::{self, Error};
#[cfg(feature = "csharp")]
use windmill_queue::append_logs;
@@ -105,8 +102,8 @@ pub async fn generate_nuget_lockfile(
let mut gen_lockfile_cmd = Command::new(DOTNET_PATH.as_str());
gen_lockfile_cmd
.current_dir(job_dir)
- .env("DOTNET_CLI_HOME", CSHARP_CACHE_DIR)
- .env("NUGET_PACKAGES", format!("{CSHARP_CACHE_DIR}/nuget"))
+ .env("DOTNET_CLI_HOME", &*CSHARP_CACHE_DIR)
+ .env("NUGET_PACKAGES", format!("{}/nuget", *CSHARP_CACHE_DIR))
.env("DOTNET_CLI_TELEMETRY_OPTOUT", "true")
.env("DOTNET_NOLOGO", "true")
.env("MSBUILDDISABLENODEREUSE", "1")
@@ -367,8 +364,8 @@ async fn build_cs_proj(
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.env("HOME", HOME_ENV.as_str())
- .env("DOTNET_CLI_HOME", CSHARP_CACHE_DIR)
- .env("NUGET_PACKAGES", format!("{CSHARP_CACHE_DIR}/nuget"))
+ .env("DOTNET_CLI_HOME", &*CSHARP_CACHE_DIR)
+ .env("NUGET_PACKAGES", format!("{}/nuget", *CSHARP_CACHE_DIR))
.env("DOTNET_CLI_TELEMETRY_OPTOUT", "true")
.env("DOTNET_NOLOGO", "true")
.env("MSBUILDDISABLENODEREUSE", "1")
@@ -434,7 +431,7 @@ async fn build_cs_proj(
}
}
- let bin_path = format!("{}/{hash}", CSHARP_CACHE_DIR);
+ let bin_path = format!("{}/{hash}", *CSHARP_CACHE_DIR);
#[cfg(unix)]
let target = format!("{job_dir}/Main");
#[cfg(windows)]
@@ -516,11 +513,10 @@ pub async fn handle_csharp_job(
inner_content,
requirements_o.unwrap_or(&String::new())
));
- let bin_path = format!("{}/{hash}", CSHARP_CACHE_DIR);
+ let bin_path = format!("{}/{hash}", *CSHARP_CACHE_DIR);
let remote_path = format!("{CSHARP_OBJECT_STORE_PREFIX}{hash}");
- let (cache, cache_logs) =
- crate::global_cache::load_cache(&bin_path, &remote_path, false).await;
+ let (cache, cache_logs) = crate::global_cache::load_cache(&bin_path, &remote_path, false).await;
let cache_logs = if cache {
#[cfg(unix)]
@@ -591,11 +587,11 @@ pub async fn handle_csharp_job(
"run.config.proto",
&NSJAIL_CONFIG_RUN_CSHARP_CONTENT
.replace("{JOB_DIR}", job_dir)
- .replace("{CACHE_DIR}", CSHARP_CACHE_DIR)
+ .replace("{CACHE_DIR}", &*CSHARP_CACHE_DIR)
.replace("{CACHE_HASH}", &hash)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
- .replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
+ .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
@@ -608,8 +604,8 @@ pub async fn handle_csharp_job(
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
- .env("DOTNET_CLI_HOME", CSHARP_CACHE_DIR)
- .env("NUGET_PACKAGES", format!("{CSHARP_CACHE_DIR}/nuget"))
+ .env("DOTNET_CLI_HOME", &*CSHARP_CACHE_DIR)
+ .env("NUGET_PACKAGES", format!("{}/nuget", *CSHARP_CACHE_DIR))
.env("DOTNET_CLI_TELEMETRY_OPTOUT", "true")
.env("DOTNET_NOLOGO", "true")
.env("DOTNET_ROOT", DOTNET_ROOT.as_str())
@@ -640,8 +636,8 @@ pub async fn handle_csharp_job(
.envs(get_proxy_envs_for_lang(&ScriptLang::CSharp).await?)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
- .env("DOTNET_CLI_HOME", CSHARP_CACHE_DIR)
- .env("NUGET_PACKAGES", format!("{CSHARP_CACHE_DIR}/nuget"))
+ .env("DOTNET_CLI_HOME", &*CSHARP_CACHE_DIR)
+ .env("NUGET_PACKAGES", format!("{}/nuget", *CSHARP_CACHE_DIR))
.env("DOTNET_CLI_TELEMETRY_OPTOUT", "true")
.env("DOTNET_NOLOGO", "true")
.env("DOTNET_ROOT", DOTNET_ROOT.as_str())
diff --git a/backend/windmill-worker/src/deno_executor.rs b/backend/windmill-worker/src/deno_executor.rs
index 66d8ae8673..19831eadd0 100644
--- a/backend/windmill-worker/src/deno_executor.rs
+++ b/backend/windmill-worker/src/deno_executor.rs
@@ -443,7 +443,8 @@ try {{
}
let allow_read = format!(
- "--allow-read=./,/tmp/windmill/cache/deno/,{}",
+ "--allow-read=./,{}/,{}",
+ *DENO_CACHE_DIR,
DENO_PATH.as_str()
);
if let Some(deno_flags) = DENO_FLAGS.as_ref() {
@@ -504,7 +505,8 @@ try {{
*has_stream = handle_result.result_stream.is_some();
// logs.push_str(format!("execute: {:?}\n", start.elapsed().as_millis()).as_str());
- if let Err(e) = tokio::fs::remove_dir_all(format!("{DENO_CACHE_DIR}/gen/file/{job_dir}")).await
+ if let Err(e) =
+ tokio::fs::remove_dir_all(format!("{}/gen/file/{job_dir}", *DENO_CACHE_DIR)).await
{
tracing::error!("failed to remove deno gen tmp cache dir: {}", e);
}
diff --git a/backend/windmill-worker/src/global_cache.rs b/backend/windmill-worker/src/global_cache.rs
index 812be2fc7a..ea5a9ded46 100644
--- a/backend/windmill-worker/src/global_cache.rs
+++ b/backend/windmill-worker/src/global_cache.rs
@@ -18,8 +18,8 @@ pub async fn build_tar_and_push(
custom_folder_name: Option,
platform_agnostic: bool,
) -> error::Result<()> {
- use windmill_object_store::object_store_reexports::Path;
use tokio::fs::create_dir_all;
+ use windmill_object_store::object_store_reexports::Path;
use crate::TAR_PYBASE_CACHE_DIR;
@@ -33,7 +33,7 @@ pub async fn build_tar_and_push(
folder.split("/").last().unwrap().to_owned()
};
- let prefix = &format!("{TAR_PYBASE_CACHE_DIR}/{}", lang);
+ let prefix = &format!("{}/{}", *TAR_PYBASE_CACHE_DIR, lang);
let tar_path = format!("{prefix}/{folder_name}_tar.tar");
create_dir_all(prefix).await?;
@@ -197,7 +197,9 @@ pub async fn exists_in_cache(bin_path: &str, _remote_path: &str) -> bool {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = windmill_object_store::get_object_store().await {
return os
- .get(&windmill_object_store::object_store_reexports::Path::from(_remote_path))
+ .get(&windmill_object_store::object_store_reexports::Path::from(
+ _remote_path,
+ ))
.await
.is_ok();
}
@@ -221,7 +223,7 @@ pub async fn save_cache(
let file_to_cache = if is_dir {
let tar_path = format!(
"{}/tar/{}_tar.tar",
- windmill_common::worker::ROOT_CACHE_DIR,
+ *windmill_common::worker::ROOT_CACHE_DIR,
local_cache_path
.split("/")
.last()
diff --git a/backend/windmill-worker/src/go_executor.rs b/backend/windmill-worker/src/go_executor.rs
index 35307baa72..29fff1a2ba 100644
--- a/backend/windmill-worker/src/go_executor.rs
+++ b/backend/windmill-worker/src/go_executor.rs
@@ -2,6 +2,7 @@ use crate::{common::MaybeLock, get_proxy_envs_for_lang};
use std::{collections::HashMap, fs::DirBuilder, process::Stdio};
use windmill_common::scripts::ScriptLang;
+use crate::global_cache::save_cache;
use itertools::Itertools;
use serde_json::value::RawValue;
use tokio::{
@@ -15,7 +16,6 @@ use windmill_common::{
utils::calculate_hash,
worker::{write_file, Connection, GoAnnotations},
};
-use crate::global_cache::save_cache;
use windmill_parser_go::{parse_go_imports, REQUIRE_PARSE};
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
@@ -108,10 +108,9 @@ pub async fn handle_go_job(
.expect("could not create go job dir");
let hash = calculate_hash(&format!("{}{:?}v2", inner_content, &maybe_lock));
- let bin_path = format!("{}/{hash}", GO_BIN_CACHE_DIR);
+ let bin_path = format!("{}/{hash}", *GO_BIN_CACHE_DIR);
let remote_path = format!("{GO_OBJECT_STORE_PREFIX}{hash}");
- let (cache, cache_logs) =
- crate::global_cache::load_cache(&bin_path, &remote_path, false).await;
+ let (cache, cache_logs) = crate::global_cache::load_cache(&bin_path, &remote_path, false).await;
let (skip_go_mod, skip_tidy) = if cache {
(true, true)
@@ -238,15 +237,15 @@ func Run(req Req) (interface{{}}, error){{
.env("GOPATH", {
#[cfg(unix)]
{
- GO_CACHE_DIR
+ GO_CACHE_DIR.as_str()
}
#[cfg(windows)]
{
- windows_gopath()
+ &windows_gopath()
}
})
.env("HOME", HOME_ENV.as_str())
- .env("GOCACHE", GO_CACHE_DIR)
+ .env("GOCACHE", GO_CACHE_DIR.as_str())
.envs(PROXY_ENVS.clone())
.args(vec!["build", "main.go"])
.stdout(Stdio::piped())
@@ -347,7 +346,7 @@ func Run(req Req) (interface{{}}, error){{
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
- .replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
+ .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
@@ -384,11 +383,11 @@ func Run(req Req) (interface{{}}, error){{
.env("GOPATH", {
#[cfg(unix)]
{
- GO_CACHE_DIR
+ GO_CACHE_DIR.as_str()
}
#[cfg(windows)]
{
- windows_gopath()
+ &windows_gopath()
}
})
.env("HOME", HOME_ENV.as_str());
@@ -508,7 +507,7 @@ pub async fn install_go_dependencies(
#[cfg(windows)]
child_cmd.env("GOPATH", windows_gopath());
#[cfg(unix)]
- child_cmd.env("GOPATH", GO_CACHE_DIR);
+ child_cmd.env("GOPATH", GO_CACHE_DIR.as_str());
#[cfg(windows)]
set_windows_env_vars(&mut child_cmd);
@@ -591,11 +590,11 @@ pub async fn install_go_dependencies(
.env("GOPATH", {
#[cfg(unix)]
{
- GO_CACHE_DIR
+ GO_CACHE_DIR.as_str()
}
#[cfg(windows)]
{
- windows_gopath()
+ &windows_gopath()
}
})
.args(vec!["mod", mod_command])
diff --git a/backend/windmill-worker/src/java_executor.rs b/backend/windmill-worker/src/java_executor.rs
index 49129848ce..efbe403145 100644
--- a/backend/windmill-worker/src/java_executor.rs
+++ b/backend/windmill-worker/src/java_executor.rs
@@ -1,5 +1,6 @@
use std::{collections::HashMap, path::PathBuf, process::Stdio};
+use crate::global_cache::save_cache;
use anyhow::{anyhow, bail};
use async_recursion::async_recursion;
use itertools::Itertools;
@@ -15,7 +16,6 @@ use windmill_common::{
utils::calculate_hash,
worker::{copy_dir_recursively, write_file, Connection},
};
-use crate::global_cache::save_cache;
use windmill_parser::Arg;
use windmill_parser_java::parse_java_sig_meta;
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
@@ -185,8 +185,8 @@ pub async fn resolve<'a>(
cmd.env_clear()
.current_dir(job_dir.to_owned())
.env("PATH", PATH_ENV.as_str())
- .env("HOME", JAVA_HOME_DIR)
- .env("COURSIER_CACHE", COURSIER_CACHE_DIR)
+ .env("HOME", &*JAVA_HOME_DIR)
+ .env("COURSIER_CACHE", &*COURSIER_CACHE_DIR)
.envs(PROXY_ENVS.clone());
// Configure proxies
@@ -208,7 +208,7 @@ pub async fn resolve<'a>(
cmd.arg(&format!("-Dhttp.nonProxyHosts=\"{}\"", val));
}
}
- cmd.arg(&format!("-Duser.home={}", JAVA_HOME_DIR));
+ cmd.arg(&format!("-Duser.home={}", *JAVA_HOME_DIR));
if metadata(TRUST_STORE_PATH.clone()).await.is_ok() {
cmd.args(&[
&format!("-Djavax.net.ssl.trustStore={}", *TRUST_STORE_PATH),
@@ -223,7 +223,7 @@ pub async fn resolve<'a>(
"--parallel",
&format!("{}", *JAVA_CONCURRENT_DOWNLOADS),
"--cache",
- COURSIER_CACHE_DIR,
+ &*COURSIER_CACHE_DIR,
])
.args(&get_repos(job_id, w_id, conn).await)
.args(&deps.split("\n").collect_vec())
@@ -276,7 +276,8 @@ async fn install<'a>(
match (it.next(), it.next(), it.next()) {
(Some(group_id), Some(artifact_id), Some(version)) => {
let path = format!(
- "{JAVA_REPOSITORY_DIR}/{}/{artifact_id}/{version}",
+ "{}/{}/{artifact_id}/{version}",
+ *JAVA_REPOSITORY_DIR,
group_id.replace(".", "/")
);
Ok(RequiredDependency {
@@ -312,7 +313,7 @@ async fn install<'a>(
metadata(TRUST_STORE_PATH.clone()).await,
);
let job_dir = job_dir.to_owned();
- let fetch_dir = format!("{JAVA_CACHE_DIR}/tmp-fetch-{}", Uuid::new_v4());
+ let fetch_dir = format!("{}/tmp-fetch-{}", *JAVA_CACHE_DIR, Uuid::new_v4());
let fetch_dir2 = fetch_dir.clone();
par_install_language_dependencies_all_at_once(
deps,
@@ -334,8 +335,8 @@ async fn install<'a>(
cmd.env_clear()
.current_dir(&job_dir)
.env("PATH", PATH_ENV.as_str())
- .env("HOME", JAVA_HOME_DIR)
- .env("COURSIER_CACHE", COURSIER_CACHE_DIR)
+ .env("HOME", &*JAVA_HOME_DIR)
+ .env("COURSIER_CACHE", &*COURSIER_CACHE_DIR)
.envs(PROXY_ENVS.clone());
// Configure proxies
{
@@ -357,7 +358,7 @@ async fn install<'a>(
}
}
- cmd.arg(&format!("-Duser.home={}", JAVA_HOME_DIR));
+ cmd.arg(&format!("-Duser.home={}", *JAVA_HOME_DIR));
if trust_store_metadata.is_ok() {
cmd.args(&[
&format!("-Djavax.net.ssl.trustStore={}", *TRUST_STORE_PATH),
@@ -400,7 +401,7 @@ async fn install<'a>(
if depth == 3 {
copy_dir_recursively(
&PathBuf::from(path),
- &PathBuf::from(JAVA_REPOSITORY_DIR),
+ &PathBuf::from(&*JAVA_REPOSITORY_DIR),
)?;
return Ok(());
@@ -465,7 +466,7 @@ async fn compile<'a>(
let reserved_variables =
get_reserved_variables(job, &client.token, conn, parent_runnable_path.clone()).await?;
let hash = compute_hash(inner_content, *requirements_o);
- let bin_path = format!("{}/{hash}", JAVA_CACHE_DIR);
+ let bin_path = format!("{}/{hash}", *JAVA_CACHE_DIR);
let remote_path = format!("java_jar/{hash}");
let (cache, ..) = crate::global_cache::load_cache(&bin_path, &remote_path, true).await;
@@ -501,7 +502,7 @@ async fn compile<'a>(
cmd.env_clear()
.current_dir(job_dir.to_owned())
.env("PATH", PATH_ENV.as_str())
- .env("HOME", JAVA_HOME_DIR)
+ .env("HOME", &*JAVA_HOME_DIR)
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(envs)
.envs(reserved_variables)
@@ -604,7 +605,7 @@ async fn run<'a>(
"run.config.proto",
&NSJAIL_CONFIG_RUN_JAVA_CONTENT
.replace("{JOB_DIR}", job_dir)
- .replace("{CACHE_DIR}", JAVA_CACHE_DIR)
+ .replace("{CACHE_DIR}", &*JAVA_CACHE_DIR)
.replace("{SHARED_MOUNT}", &shared_mount)
// .replace("{CACHED_TARGET}", &shared_mount)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
@@ -613,7 +614,7 @@ async fn run<'a>(
cmd.env_clear()
.current_dir(job_dir)
.env("PATH", PATH_ENV.as_str())
- .env("HOME", JAVA_HOME_DIR)
+ .env("HOME", &*JAVA_HOME_DIR)
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(envs)
.envs(reserved_variables)
@@ -671,7 +672,7 @@ async fn run<'a>(
cmd.env_clear()
.current_dir(job_dir.to_owned())
.env("PATH", PATH_ENV.as_str())
- .env("HOME", JAVA_HOME_DIR)
+ .env("HOME", &*JAVA_HOME_DIR)
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(envs)
.envs(reserved_variables);
diff --git a/backend/windmill-worker/src/nu_executor.rs b/backend/windmill-worker/src/nu_executor.rs
index a4320a3612..ca309105b5 100644
--- a/backend/windmill-worker/src/nu_executor.rs
+++ b/backend/windmill-worker/src/nu_executor.rs
@@ -16,8 +16,8 @@ use crate::{
build_command_with_isolation, create_args_and_out_file, get_reserved_variables,
read_result, start_child_process, OccupancyMetrics, DEV_CONF_NSJAIL,
},
- get_proxy_envs_for_lang, handle_child, is_sandboxing_enabled, DISABLE_NUSER, NSJAIL_PATH, PATH_ENV,
- TRACING_PROXY_CA_CERT_PATH,
+ get_proxy_envs_for_lang, handle_child, is_sandboxing_enabled, DISABLE_NUSER, NSJAIL_PATH,
+ PATH_ENV, TRACING_PROXY_CA_CERT_PATH,
};
use windmill_common::client::AuthedClient;
use windmill_common::scripts::ScriptLang;
@@ -253,7 +253,7 @@ async fn run<'a>(
.replace("{NU_PATH}", &NU_PATH)
.replace("{SHARED_MOUNT}", &shared_mount)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
- .replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
+ .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
diff --git a/backend/windmill-worker/src/pwsh_executor.rs b/backend/windmill-worker/src/pwsh_executor.rs
index acae81a588..961a49a58a 100644
--- a/backend/windmill-worker/src/pwsh_executor.rs
+++ b/backend/windmill-worker/src/pwsh_executor.rs
@@ -159,7 +159,7 @@ try {
async fn scan_module_directories() -> Result, Error> {
let mut module_dirs = HashMap::new();
- let cache_dir = std::path::Path::new(POWERSHELL_CACHE_DIR);
+ let cache_dir = std::path::Path::new(&*POWERSHELL_CACHE_DIR);
if let Ok(entries) = fs::read_dir(cache_dir) {
for entry in entries {
@@ -391,7 +391,7 @@ pub async fn handle_powershell_job(
.join(", ");
let install_string = generate_powershell_install_code()
- .replace("{path}", POWERSHELL_CACHE_DIR)
+ .replace("{path}", &*POWERSHELL_CACHE_DIR)
.replace("{job_id}", &job.id.to_string())
.replace("{has_private_repo}", &format!("${has_private_repo}"))
.replace("{has_credentials}", &format!("${has_credentials}"))
@@ -442,7 +442,7 @@ $PSModulePathBackup = $env:PSModulePath
$env:PSModulePath = \"$PSHome/Modules\"
Get-Module -ListAvailable | Import-Module
$env:PSModulePath = \"{}:$PSModulePathBackup\"",
- POWERSHELL_CACHE_DIR
+ *POWERSHELL_CACHE_DIR
);
#[cfg(windows)]
@@ -452,7 +452,7 @@ $PSModulePathBackup = $env:PSModulePath
$env:PSModulePath = \"C:\\Program Files\\PowerShell\\7\\Modules\"
Get-Module -ListAvailable | Import-Module
$env:PSModulePath = \"{};$PSModulePathBackup\"",
- POWERSHELL_CACHE_DIR
+ *POWERSHELL_CACHE_DIR
);
// NOTE: powershell error handling / termination is quite tricky compared to bash
@@ -525,7 +525,7 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
- .replace("{CACHE_DIR}", POWERSHELL_CACHE_DIR),
+ .replace("{CACHE_DIR}", &*POWERSHELL_CACHE_DIR),
)?;
let cmd_args = vec![
"--config",
diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs
index 047521a0b4..b289f05380 100644
--- a/backend/windmill-worker/src/python_executor.rs
+++ b/backend/windmill-worker/src/python_executor.rs
@@ -278,7 +278,7 @@ pub async fn uv_pip_compile(
"requirements.txt",
// Target to /tmp/windmill/cache/uv
"--cache-dir",
- UV_CACHE_DIR,
+ &*UV_CACHE_DIR,
];
args.extend(["-p", &py_version_str, "--python-preference", "only-managed"]);
@@ -805,7 +805,7 @@ mount {{
"run.config.proto",
&NSJAIL_CONFIG_RUN_PYTHON3_CONTENT
.replace("{JOB_DIR}", job_dir)
- .replace("{PY_INSTALL_DIR}", PY_INSTALL_DIR)
+ .replace("{PY_INSTALL_DIR}", &*PY_INSTALL_DIR)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{SHARED_DEPENDENCIES}", shared_deps.as_str())
@@ -815,7 +815,7 @@ mount {{
"{ADDITIONAL_PYTHON_PATHS}",
additional_python_paths_folders.as_str(),
)
- .replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
+ .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
} else {
@@ -1410,10 +1410,10 @@ async fn spawn_uv_install(
&nsjail_proto,
NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT
.replace("{WORKER_DIR}", worker_dir)
- .replace("{PY_INSTALL_DIR}", &PY_INSTALL_DIR)
+ .replace("{PY_INSTALL_DIR}", &*PY_INSTALL_DIR)
.replace("{TARGET_DIR}", &venv_p)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
- .replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
+ .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.as_str(),
)?;
diff --git a/backend/windmill-worker/src/python_versions.rs b/backend/windmill-worker/src/python_versions.rs
index 52636808a6..78b03deb07 100644
--- a/backend/windmill-worker/src/python_versions.rs
+++ b/backend/windmill-worker/src/python_versions.rs
@@ -18,6 +18,8 @@ use windmill_common::{
use anyhow::{anyhow, bail};
use windmill_queue::append_logs;
+#[cfg(unix)]
+use crate::python_executor::UV_PATH;
use crate::{
common::{start_child_process, OccupancyMetrics},
handle_child::handle_child,
@@ -25,8 +27,6 @@ use crate::{
HOME_ENV, INSTANCE_PYTHON_VERSION, PATH_ENV, PROXY_ENVS, PY_INSTALL_DIR, UV_CACHE_DIR,
WIN_ENVS,
};
-#[cfg(unix)]
-use crate::python_executor::UV_PATH;
impl From for PyVAlias {
fn from(value: PyV) -> Self {
@@ -234,7 +234,8 @@ impl PyV {
pub(crate) fn to_cache_dir(&self, ignore_patch: bool) -> String {
use windmill_common::worker::ROOT_CACHE_DIR;
format!(
- "{ROOT_CACHE_DIR}{}",
+ "{}{}",
+ *ROOT_CACHE_DIR,
self.to_cache_dir_top_level(ignore_patch)
)
}
@@ -311,7 +312,7 @@ impl PyV {
Command::new(uv_cmd)
.env_clear()
.envs(WIN_ENVS.to_vec())
- .env("UV_CACHE_DIR", UV_CACHE_DIR)
+ .env("UV_CACHE_DIR", &*UV_CACHE_DIR)
.args([
"python",
"list",
@@ -539,8 +540,8 @@ impl PyV {
])
// TODO: Do we need these?
.envs([
- ("UV_PYTHON_INSTALL_DIR", PY_INSTALL_DIR),
- ("UV_CACHE_DIR", UV_CACHE_DIR),
+ ("UV_PYTHON_INSTALL_DIR", &*PY_INSTALL_DIR),
+ ("UV_CACHE_DIR", &*UV_CACHE_DIR),
])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
@@ -630,11 +631,9 @@ impl PyV {
"--system",
"--python-preference=only-managed",
])
- .envs([
- ("UV_PYTHON_INSTALL_DIR", PY_INSTALL_DIR),
- ("UV_PYTHON_PREFERENCE", "only-managed"),
- ("UV_CACHE_DIR", UV_CACHE_DIR),
- ])
+ .env("UV_PYTHON_INSTALL_DIR", &*PY_INSTALL_DIR)
+ .env("UV_PYTHON_PREFERENCE", "only-managed")
+ .env("UV_CACHE_DIR", &*UV_CACHE_DIR)
// .stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
diff --git a/backend/windmill-worker/src/ruby_executor.rs b/backend/windmill-worker/src/ruby_executor.rs
index 869b58d7de..11031fa481 100644
--- a/backend/windmill-worker/src/ruby_executor.rs
+++ b/backend/windmill-worker/src/ruby_executor.rs
@@ -1,7 +1,6 @@
use std::{collections::HashMap, process::Stdio};
use anyhow::anyhow;
-use const_format::concatcp;
use itertools::Itertools;
use regex::Regex;
use tokio::{
@@ -122,7 +121,7 @@ pub async fn prepare<'a>(
.write_all(&wrap(inner_content)?.into_bytes())
.await?;
- let mini_wm_path = format!("{RUBY_CACHE_DIR}/gems/windmill-internal/windmill");
+ let mini_wm_path = format!("{}/gems/windmill-internal/windmill", *RUBY_CACHE_DIR);
if !std::fs::metadata(&mini_wm_path).is_ok() {
fs::create_dir_all(&mini_wm_path).await?;
@@ -339,7 +338,7 @@ Your Gemfile syntax will continue to work as-is."
&NSJAIL_CONFIG_LOCK_RUBY_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
- .replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
+ .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL), // .replace("{BUILD}", &build_dir),
)?;
let mut cmd = Command::new(NSJAIL_PATH.as_str());
@@ -588,7 +587,7 @@ async fn install<'a>(
// 123...zx-activesupport-8.0.2
// ^^^^^^^^ hash based on source and type (GEM or GIT)
let handle = format!("{}-{}-{}", hash, pkg, version);
- let path = format!("{RUBY_CACHE_DIR}/gems/{}", &handle);
+ let path = format!("{}/gems/{}", *RUBY_CACHE_DIR, &handle);
deps.push(RequiredDependency {
path,
@@ -632,7 +631,7 @@ async fn install<'a>(
&NSJAIL_CONFIG_DOWNLOAD_RUBY_CONTENT
.replace("{TARGET}", &dependency.path)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
- .replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
+ .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL), // .replace("{BUILD}", &build_dir),
)?;
let mut cmd = Command::new(NSJAIL_PATH.as_str());
@@ -741,9 +740,9 @@ async fn install<'a>(
};
// Include builtin windmill client
{
- const WM_INTERNAL: &str = concatcp!(RUBY_CACHE_DIR, "/gems/windmill-internal");
- res.top_level_paths.push(WM_INTERNAL.to_owned());
- res.rubylib += format!(":{WM_INTERNAL}").as_str();
+ let wm_internal = format!("{}/gems/windmill-internal", *RUBY_CACHE_DIR);
+ res.top_level_paths.push(wm_internal.clone());
+ res.rubylib += format!(":{wm_internal}").as_str();
}
Ok(res)
}
@@ -800,7 +799,7 @@ mount {{
.replace("{JOB_DIR}", job_dir)
.replace("{SHARED_MOUNT}", &shared_mount)
.replace("{SHARED_DEPENDENCIES}", &shared_deps)
- .replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
+ .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
)?;
diff --git a/backend/windmill-worker/src/rust_executor.rs b/backend/windmill-worker/src/rust_executor.rs
index 54d90550b0..216c59f2cd 100644
--- a/backend/windmill-worker/src/rust_executor.rs
+++ b/backend/windmill-worker/src/rust_executor.rs
@@ -5,6 +5,7 @@ use std::{collections::HashMap, process::Stdio};
use uuid::Uuid;
use windmill_parser_rust::parse_rust_deps_into_manifest;
+use crate::global_cache::save_cache;
use itertools::Itertools;
use tokio::{
fs::{create_dir_all, File},
@@ -16,7 +17,6 @@ use windmill_common::{
utils::calculate_hash,
worker::{write_file, Connection},
};
-use crate::global_cache::save_cache;
use windmill_queue::MiniPulledJob;
use windmill_queue::{append_logs, CanceledBy};
@@ -337,7 +337,7 @@ async fn get_build_dir(
if !is_sandboxing_enabled() {
// If nsjail is disabled then entire worker has shared build directory
// It drastically improves cache hit-rate.
- Some((format!("{RUST_CACHE_DIR}/build/{worker_name}"), true))
+ Some((format!("{}/build/{worker_name}", *RUST_CACHE_DIR), true))
} else {
// If nsjail is enabled, having global shared directory is vulnerability and target for an attack
// Instead we either:
@@ -345,7 +345,8 @@ async fn get_build_dir(
// 2. If user is not known or something else goes wrong - use random build dir. This is equivalent to no cache at all.
Some((
format!(
- "{RUST_CACHE_DIR}/build/{}@{}@{}",
+ "{}/build/{}@{}@{}",
+ *RUST_CACHE_DIR,
&job.workspace_id,
p.replace('/', "."),
&job.created_by
@@ -355,7 +356,10 @@ async fn get_build_dir(
}
}
})
- .unwrap_or((format!("{RUST_CACHE_DIR}/build/{}", Uuid::new_v4()), false));
+ .unwrap_or((
+ format!("{}/build/{}", *RUST_CACHE_DIR, Uuid::new_v4()),
+ false,
+ ));
{
let (t, r, g) = (
@@ -449,7 +453,7 @@ pub async fn build_rust_crate(
is_preview: bool,
) -> error::Result {
ensure_rust_runtime_dirs();
- let bin_path = format!("{}/{hash}", RUST_CACHE_DIR);
+ let bin_path = format!("{}/{hash}", *RUST_CACHE_DIR);
let build_dir = get_build_dir(job, job_dir, conn, worker_name, is_preview).await?;
@@ -459,9 +463,9 @@ pub async fn build_rust_crate(
"download.config.proto",
&NSJAIL_CONFIG_COMPILE_RUST_CONTENT
.replace("{JOB_DIR}", job_dir)
- .replace("{CACHE_DIR}", RUST_CACHE_DIR)
+ .replace("{CACHE_DIR}", &*RUST_CACHE_DIR)
.replace("{CARGO_HOME}", CARGO_HOME.as_str())
- .replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
+ .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{BUILD}", &build_dir),
)?;
@@ -605,14 +609,13 @@ pub async fn handle_rust_job(
check_executor_binary_exists("cargo", CARGO_PATH.as_str(), "rust")?;
let hash = compute_rust_hash(inner_content, requirements_o);
- let bin_path = format!("{}/{hash}", RUST_CACHE_DIR);
+ let bin_path = format!("{}/{hash}", *RUST_CACHE_DIR);
let remote_path = format!("{RUST_OBJECT_STORE_PREFIX}{hash}");
let reserved_variables =
get_reserved_variables(job, &client.token, conn, parent_runnable_path).await?;
- let (cache, cache_logs) =
- crate::global_cache::load_cache(&bin_path, &remote_path, false).await;
+ let (cache, cache_logs) = crate::global_cache::load_cache(&bin_path, &remote_path, false).await;
let cache_logs = if cache {
let target = format!("{job_dir}/main");
@@ -669,10 +672,10 @@ pub async fn handle_rust_job(
"run.config.proto",
&NSJAIL_CONFIG_RUN_RUST_CONTENT
.replace("{JOB_DIR}", job_dir)
- .replace("{CACHE_DIR}", RUST_CACHE_DIR)
+ .replace("{CACHE_DIR}", &*RUST_CACHE_DIR)
.replace("{CACHE_HASH}", &hash)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
- .replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
+ .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{SHARED_MOUNT}", shared_mount),
)?;
diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs
index c354d34ee2..49a1048afd 100644
--- a/backend/windmill-worker/src/worker.rs
+++ b/backend/windmill-worker/src/worker.rs
@@ -37,7 +37,7 @@ use windmill_common::{
utils::{create_directory_async, WarnAfterExt},
worker::{
make_pull_query, write_file, Connection, HttpClient, MAX_TIMEOUT,
- MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS, ROOT_CACHE_DIR, ROOT_CACHE_NOMOUNT_DIR, TMP_DIR,
+ MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS, ROOT_CACHE_DIR, ROOT_CACHE_NOMOUNT_DIR, WINDMILL_DIR,
},
worker_group_job_stats::JobStatsMap,
KillpillSender,
@@ -47,7 +47,6 @@ use windmill_common::{
use windmill_common::ee_oss::LICENSE_KEY_VALID;
use anyhow::Result;
-use const_format::concatcp;
#[cfg(feature = "prometheus")]
use prometheus::IntCounter;
@@ -196,45 +195,47 @@ use windmill_common::bench::{benchmark_init, benchmark_verify, BenchmarkInfo, Be
use windmill_common::add_time;
-pub const PY310_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_3_10");
-pub const PY311_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_3_11");
-pub const PY312_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_3_12");
-pub const PY313_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_3_13");
+lazy_static::lazy_static! {
+ pub static ref PY310_CACHE_DIR: String = format!("{}python_3_10", *ROOT_CACHE_DIR);
+ pub static ref PY311_CACHE_DIR: String = format!("{}python_3_11", *ROOT_CACHE_DIR);
+ pub static ref PY312_CACHE_DIR: String = format!("{}python_3_12", *ROOT_CACHE_DIR);
+ pub static ref PY313_CACHE_DIR: String = format!("{}python_3_13", *ROOT_CACHE_DIR);
-pub const TAR_JAVA_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "tar/java");
+ pub static ref TAR_JAVA_CACHE_DIR: String = format!("{}tar/java", *ROOT_CACHE_DIR);
-pub const UV_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "uv");
-pub const PY_INSTALL_DIR: &str = concatcp!(ROOT_CACHE_DIR, "py_runtime");
-pub const TAR_PYBASE_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "tar");
-pub const DENO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "deno");
-pub const DENO_CACHE_DIR_DEPS: &str = concatcp!(ROOT_CACHE_DIR, "deno/deps");
-pub const DENO_CACHE_DIR_NPM: &str = concatcp!(ROOT_CACHE_DIR, "deno/npm");
+ pub static ref UV_CACHE_DIR: String = format!("{}uv", *ROOT_CACHE_DIR);
+ pub static ref PY_INSTALL_DIR: String = format!("{}py_runtime", *ROOT_CACHE_DIR);
+ pub static ref TAR_PYBASE_CACHE_DIR: String = format!("{}tar", *ROOT_CACHE_DIR);
+ pub static ref DENO_CACHE_DIR: String = format!("{}deno", *ROOT_CACHE_DIR);
+ pub static ref DENO_CACHE_DIR_DEPS: String = format!("{}deno/deps", *ROOT_CACHE_DIR);
+ pub static ref DENO_CACHE_DIR_NPM: String = format!("{}deno/npm", *ROOT_CACHE_DIR);
-pub const GO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "go");
-pub const RUST_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "rust");
-pub const NU_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "nu");
-pub const CSHARP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "csharp");
+ pub static ref GO_CACHE_DIR: String = format!("{}go", *ROOT_CACHE_DIR);
+ pub static ref RUST_CACHE_DIR: String = format!("{}rust", *ROOT_CACHE_DIR);
+ pub static ref NU_CACHE_DIR: String = format!("{}nu", *ROOT_CACHE_DIR);
+ pub static ref CSHARP_CACHE_DIR: String = format!("{}csharp", *ROOT_CACHE_DIR);
-// Java
-pub const JAVA_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "java");
-pub const COURSIER_CACHE_DIR: &str = concatcp!(JAVA_CACHE_DIR, "/coursier-cache");
-pub const JAVA_REPOSITORY_DIR: &str = concatcp!(JAVA_CACHE_DIR, "/repository");
-pub const JAVA_HOME_DIR: &str = concatcp!(JAVA_CACHE_DIR, "/home");
+ // Java
+ pub static ref JAVA_CACHE_DIR: String = format!("{}java", *ROOT_CACHE_DIR);
+ pub static ref COURSIER_CACHE_DIR: String = format!("{}/coursier-cache", *JAVA_CACHE_DIR);
+ pub static ref JAVA_REPOSITORY_DIR: String = format!("{}/repository", *JAVA_CACHE_DIR);
+ pub static ref JAVA_HOME_DIR: String = format!("{}/home", *JAVA_CACHE_DIR);
-// Ruby
-pub const RUBY_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "ruby");
+ // Ruby
+ pub static ref RUBY_CACHE_DIR: String = format!("{}ruby", *ROOT_CACHE_DIR);
-// for related places search: ADD_NEW_LANG
-pub const BUN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_NOMOUNT_DIR, "bun");
-pub const BUN_BUNDLE_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "bun");
-pub const BUN_CODEBASE_BUNDLE_CACHE_DIR: &str = concatcp!(ROOT_CACHE_NOMOUNT_DIR, "script_bundle");
+ // for related places search: ADD_NEW_LANG
+ pub static ref BUN_CACHE_DIR: String = format!("{}bun", *ROOT_CACHE_NOMOUNT_DIR);
+ pub static ref BUN_BUNDLE_CACHE_DIR: String = format!("{}bun", *ROOT_CACHE_DIR);
+ pub static ref BUN_CODEBASE_BUNDLE_CACHE_DIR: String = format!("{}script_bundle", *ROOT_CACHE_NOMOUNT_DIR);
-pub const GO_BIN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "gobin");
-pub const POWERSHELL_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "powershell");
-pub const COMPOSER_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "composer");
+ pub static ref GO_BIN_CACHE_DIR: String = format!("{}gobin", *ROOT_CACHE_DIR);
+ pub static ref POWERSHELL_CACHE_DIR: String = format!("{}powershell", *ROOT_CACHE_DIR);
+ pub static ref COMPOSER_CACHE_DIR: String = format!("{}composer", *ROOT_CACHE_DIR);
-pub const TRACING_PROXY_CA_CERT_PATH: &str =
- concatcp!(ROOT_CACHE_NOMOUNT_DIR, "tracing_proxy_ca.pem");
+ pub static ref TRACING_PROXY_CA_CERT_PATH: String =
+ format!("{}tracing_proxy_ca.pem", *ROOT_CACHE_NOMOUNT_DIR);
+}
const NUM_SECS_PING: u64 = 5;
const NUM_SECS_READINGS: u64 = 60;
@@ -1375,7 +1376,7 @@ pub async fn run_worker(
let start_time = Instant::now();
- let worker_dir = format!("{TMP_DIR}/{worker_name}");
+ let worker_dir = format!("{}/{worker_name}", *WINDMILL_DIR);
tracing::debug!(worker = %worker_name, hostname = %hostname, worker_dir = %worker_dir, "Creating worker dir");
#[cfg(feature = "python")]
From 53caecf1da8d76e246178dfb9b86d330f0ec52fd Mon Sep 17 00:00:00 2001
From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com>
Date: Wed, 4 Mar 2026 11:46:08 +0100
Subject: [PATCH 3/7] feat: Ducklake typechecker (#8118)
* Typedchecked ducklake queries
* Display script preview error as SQL error
* Fix duplication
* fix replacer
* Revert "fix replacer"
This reverts commit c5492033c850cabc8bf18a50151c089b83cd6826.
* Don't recompile regex every call
* nit OOB
* avoid potential panic
* Apply suggestions from code review
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* safety throw
* Update backend/windmill-worker/src/duckdb_executor.rs
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* Try catch individual chunks in prepareDatatableQueries
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* format
* nit comment
* Revert "Try catch individual chunks in prepareDatatableQueries"
This reverts commit ae64a8ad27deb7e5ddda10163c6db04a428827f2.
* Correct try catch
* better error messages
* nit unused variable
* comment
* handle non describable queries
* npm i
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
---
.../windmill-duckdb-ffi-internal/Cargo.lock | 1 +
.../windmill-duckdb-ffi-internal/Cargo.toml | 1 +
.../windmill-duckdb-ffi-internal/src/lib.rs | 255 +++++++++++++++---
.../windmill-worker/src/duckdb_executor.rs | 81 ++++++
frontend/src/lib/infer.svelte.ts | 174 ++++++++----
5 files changed, 423 insertions(+), 89 deletions(-)
diff --git a/backend/windmill-duckdb-ffi-internal/Cargo.lock b/backend/windmill-duckdb-ffi-internal/Cargo.lock
index 07e428c633..559196a3c2 100644
--- a/backend/windmill-duckdb-ffi-internal/Cargo.lock
+++ b/backend/windmill-duckdb-ffi-internal/Cargo.lock
@@ -2164,6 +2164,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"duckdb",
+ "regex",
"rust_decimal",
"serde",
"serde_json",
diff --git a/backend/windmill-duckdb-ffi-internal/Cargo.toml b/backend/windmill-duckdb-ffi-internal/Cargo.toml
index 7043b33ee5..7eb6869ab9 100644
--- a/backend/windmill-duckdb-ffi-internal/Cargo.toml
+++ b/backend/windmill-duckdb-ffi-internal/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2024"
[dependencies]
chrono = "0.4.41"
duckdb = { version = "1.4.4", features = ["bundled"] }
+regex = "1"
rust_decimal = "1.37.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
diff --git a/backend/windmill-duckdb-ffi-internal/src/lib.rs b/backend/windmill-duckdb-ffi-internal/src/lib.rs
index c5c819e60b..2701319d6e 100644
--- a/backend/windmill-duckdb-ffi-internal/src/lib.rs
+++ b/backend/windmill-duckdb-ffi-internal/src/lib.rs
@@ -1,12 +1,14 @@
use std::{
collections::HashMap,
- ffi::{CStr, CString, c_char, c_uint},
+ ffi::{c_char, c_uint, CStr, CString},
ptr::null_mut,
+ sync::LazyLock,
};
-use duckdb::{Row, core::LogicalTypeId, params_from_iter, types::TimeUnit};
-use rust_decimal::{Decimal, prelude::FromPrimitive};
-use serde::Deserialize;
+use duckdb::{core::LogicalTypeId, params_from_iter, types::TimeUnit, Row};
+use regex::Regex;
+use rust_decimal::{prelude::FromPrimitive, Decimal};
+use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
#[derive(Deserialize, Clone, Debug, PartialEq, Default)]
@@ -96,6 +98,218 @@ pub extern "C" fn run_duckdb_ffi(
})
}
+#[derive(Serialize, Debug)]
+struct PrepareQueryColumnInfo {
+ name: String,
+ #[serde(rename = "type")]
+ type_name: String,
+}
+
+#[derive(Serialize, Debug)]
+struct PrepareQueryResult {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ columns: Option>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ error: Option,
+}
+
+fn is_setup_statement(query: &str) -> bool {
+ let trimmed = query.trim_start();
+ let upper = trimmed.to_uppercase();
+ upper.starts_with("ATTACH")
+ || upper.starts_with("USE")
+ || upper.starts_with("INSTALL")
+ || upper.starts_with("LOAD")
+ || upper.starts_with("SET")
+ || upper.starts_with("RESET")
+ || upper.starts_with("CREATE OR REPLACE SECRET")
+ || upper.starts_with("CREATE SECRET")
+}
+
+/// Returns true if the query is expected to return a result set and can be wrapped with DESCRIBE.
+fn is_describable_query(query: &str) -> bool {
+ let trimmed = query.trim_start();
+ let upper = trimmed.to_uppercase();
+ upper.starts_with("SELECT")
+ || upper.starts_with("WITH")
+ || upper.starts_with("VALUES")
+ || upper.starts_with("TABLE")
+ || upper.starts_with("FROM")
+}
+
+static PARAM_RE: LazyLock = LazyLock::new(|| Regex::new(r"\$\d+").expect("invalid regex"));
+
+fn replace_params_with_null(query: &str) -> String {
+ PARAM_RE.replace_all(query, "NULL").to_string()
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn prepare_duckdb_ffi(
+ query_block_list: *const *const c_char,
+ query_block_list_count: usize,
+ token: *const c_char,
+ base_internal_url: *const c_char,
+ w_id: *const c_char,
+) -> *mut c_char {
+ let r = match convert_prepare_args(
+ query_block_list,
+ query_block_list_count,
+ token,
+ base_internal_url,
+ w_id,
+ )
+ .and_then(|(query_block_list, token, base_internal_url, w_id)| {
+ prepare_duckdb_internal(query_block_list, 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)
+ }
+ };
+
+ 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 setup_duckdb_connection(
+ conn: &duckdb::Connection,
+ token: &str,
+ base_internal_url: &str,
+ w_id: &str,
+) -> Result<(), String> {
+ let (s3_access_key, s3_secret_key) = token.rsplit_once('.').unwrap_or(("", token));
+ let (s3_endpoint_ssl, s3_endpoint) = base_internal_url
+ .split_once("://")
+ .unwrap_or(("http", &base_internal_url));
+ let s3_endpoint_ssl = s3_endpoint_ssl == "https";
+
+ 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()))
+}
+
+fn convert_prepare_args<'a>(
+ query_block_list: *const *const c_char,
+ query_block_list_count: usize,
+ token: *const c_char,
+ base_internal_url: *const c_char,
+ w_id: *const c_char,
+) -> Result<(Vec<&'a str>, &'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"
+ })
+ })
+ .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, token, base_internal_url, w_id))
+}
+
+fn prepare_duckdb_internal(
+ query_block_list: Vec<&str>,
+ token: &str,
+ base_internal_url: &str,
+ w_id: &str,
+) -> Result {
+ let conn = duckdb::Connection::open_in_memory().map_err(|e| e.to_string())?;
+
+ setup_duckdb_connection(&conn, token, base_internal_url, w_id)?;
+
+ let mut results: Vec = vec![];
+
+ // IMPORTANT: Setup statements (ATTACH, USE, INSTALL, etc.) are executed but intentionally
+ // do not produce a PrepareQueryResult entry. The frontend prepends these as connection setup
+ // before the actual user queries, and mapPrepareResults expects results.length to equal the
+ // number of user queries (not setup statements). If a new setup-like statement is added to
+ // the connection flow (e.g. in setup_duckdb_connection or transform_attach_ducklake) without
+ // also being caught by is_setup_statement, the result count will mismatch and the frontend
+ // will throw.
+ for query_block in &query_block_list {
+ if is_setup_statement(query_block) {
+ conn.execute_batch(query_block)
+ .map_err(|e| format!("Error executing setup statement: {}", e.to_string()))?;
+ continue;
+ }
+
+ let modified_query = replace_params_with_null(query_block);
+ // Validate the query parses correctly by preparing it
+ if let Err(e) = conn.prepare(&modified_query) {
+ results.push(PrepareQueryResult { columns: None, error: Some(e.to_string()) });
+ continue;
+ }
+
+ // DESCRIBE only works on queries that return result sets (SELECT, WITH, VALUES, TABLE,
+ // FROM). For non-returning statements (INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, etc.)
+ // we skip DESCRIBE and assume no columns.
+ if !is_describable_query(&modified_query) {
+ results.push(PrepareQueryResult { columns: Some(vec![]), error: None });
+ continue;
+ }
+
+ // Note: We have to use a DESCRIBE statement and cannot simply use the
+ // methods returned by .prepare() because they panic if the statement was
+ // not executed at least once (which we specifically do not want to do).
+ let describe_query = format!("DESCRIBE {}", modified_query);
+ match conn.prepare(&describe_query).and_then(|mut stmt| {
+ let rows = stmt.query_map([], |row| {
+ Ok(PrepareQueryColumnInfo {
+ name: row.get::<_, String>(0)?,
+ type_name: row.get::<_, String>(1)?,
+ })
+ })?;
+ rows.collect::, _>>()
+ }) {
+ Ok(columns) => {
+ results.push(PrepareQueryResult { columns: Some(columns), error: None });
+ }
+ Err(e) => {
+ results.push(PrepareQueryResult { columns: None, error: Some(e.to_string()) });
+ }
+ }
+ }
+
+ serde_json::to_string(&results).map_err(|e| e.to_string())
+}
+
fn convert_args<'a>(
query_block_list: *const *const c_char,
query_block_list_count: usize,
@@ -170,38 +384,7 @@ fn run_duckdb_internal<'a>(
) -> 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()))?;
+ setup_duckdb_connection(&conn, token, base_internal_url, w_id)?;
let mut results: Vec>> = vec![];
let mut column_order = None;
diff --git a/backend/windmill-worker/src/duckdb_executor.rs b/backend/windmill-worker/src/duckdb_executor.rs
index 73136e1cc0..45e2f647a5 100644
--- a/backend/windmill-worker/src/duckdb_executor.rs
+++ b/backend/windmill-worker/src/duckdb_executor.rs
@@ -161,6 +161,22 @@ pub async fn do_duckdb(
let base_internal_url = client.base_internal_url.clone();
let w_id = job.workspace_id.clone();
+ if annotations.prepare {
+ let result = tokio::task::spawn_blocking(move || {
+ prepare_duckdb_ffi_safe(
+ query_block_list.iter().map(String::as_str),
+ &token,
+ &base_internal_url,
+ &w_id,
+ )
+ })
+ .await
+ .map_err(|e| Error::from(to_anyhow(e)))
+ .and_then(|r| r)?;
+
+ return Ok(result);
+ }
+
let result = tokio::task::spawn_blocking(move || {
run_duckdb_ffi_safe(
query_block_list.iter().map(String::as_str),
@@ -248,6 +264,18 @@ struct DuckDbFfiLib {
collect_first_row_only: bool,
) -> *mut c_char,
>,
+ prepare_duckdb_ffi: Option<
+ Symbol<
+ 'static,
+ unsafe extern "C" fn(
+ query_block_list: *const *const c_char,
+ query_block_list_count: usize,
+ token: *const c_char,
+ base_internal_url: *const c_char,
+ w_id: *const c_char,
+ ) -> *mut c_char,
+ >,
+ >,
free_cstr: Symbol<'static, unsafe extern "C" fn(string: *mut c_char) -> ()>,
}
@@ -307,8 +335,11 @@ impl DuckDbFfiLib {
}
}
+ let prepare_duckdb_ffi = unsafe { lib.get(b"prepare_duckdb_ffi").ok() };
+
Ok(DuckDbFfiLib {
run_duckdb_ffi: unsafe { lib.get(b"run_duckdb_ffi").map_err(to_anyhow)? },
+ prepare_duckdb_ffi,
free_cstr: unsafe { lib.get(b"free_cstr").map_err(to_anyhow)? },
})
}
@@ -388,6 +419,56 @@ fn run_duckdb_ffi_safe<'a>(
}
}
+fn prepare_duckdb_ffi_safe<'a>(
+ query_block_list: impl Iterator- ,
+ token: &str,
+ base_internal_url: &str,
+ w_id: &str,
+) -> Result> {
+ 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 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)?;
+
+ let lib = DuckDbFfiLib::get_singleton()?;
+ let prepare_fn = lib.prepare_duckdb_ffi.as_ref().ok_or_else(|| {
+ Error::InternalErr(
+ "prepare_duckdb_ffi not available in duckdb ffi library. Please update to the latest windmill_duckdb_ffi_lib.".to_string(),
+ )
+ })?;
+ let free_cstr = &lib.free_cstr;
+
+ let result_str = unsafe {
+ let ptr = prepare_fn(
+ query_block_list.as_ptr(),
+ query_block_list.len(),
+ token.as_ptr(),
+ base_internal_url.as_ptr(),
+ w_id.as_ptr(),
+ );
+ let str = CStr::from_ptr(ptr).to_string_lossy().to_string();
+ free_cstr(ptr);
+ str
+ };
+
+ if result_str.starts_with("ERROR") {
+ Err(Error::ExecutionErr(result_str[6..].to_string()))
+ } else {
+ Ok(serde_json::value::RawValue::from_string(result_str).map_err(to_anyhow)?)
+ }
+}
+
struct ParsedAttachDbResource<'a> {
resource_path: &'a str,
name: &'a str,
diff --git a/frontend/src/lib/infer.svelte.ts b/frontend/src/lib/infer.svelte.ts
index 419020631c..cbdfa95505 100644
--- a/frontend/src/lib/infer.svelte.ts
+++ b/frontend/src/lib/infer.svelte.ts
@@ -4,6 +4,13 @@ import { ChangeOnDeepInequality, MapResource } from './svelte5Utils.svelte'
import { sqlDataTypeToJsTypeHeuristic } from './components/apps/components/display/dbtable/utils'
import { chunkBy, clone, getQueryStmtCountHeuristic } from './utils'
+function extractErrorMessage(e: unknown): string {
+ if (e != null && typeof e === 'object' && 'body' in e) {
+ return (e as any).body?.error?.message ?? JSON.stringify(e)
+ }
+ return e instanceof Error ? e.message : JSON.stringify(e)
+}
+
function computeQueryKey(query: InferAssetsSqlQueryDetails, workspace?: string) {
return `${query.source_kind}::${query.source_name}::${query.source_schema}::${workspace}::${query.query_string}`
}
@@ -21,66 +28,26 @@ export function usePreparedAssetSqlQueries(
),
async (toFetch) => {
let queries = Object.entries(clone(toFetch))
- // We only support preparing datatable source kinds for now.
- queries = queries.filter(([_, q]) => q.source_kind === 'datatable')
+ queries = queries.filter(
+ ([_, q]) => q.source_kind === 'datatable' || q.source_kind === 'ducklake'
+ )
// We only support preparing single-statement queries for now.
queries = queries.filter(([_, q]) => getQueryStmtCountHeuristic(q.query_string) === 1)
if (!queries?.length) return {}
- try {
- // We chunk by source_name to minimize the number of requests.
- // For example if we have 10 queries on the same data table,
- // we can prepare them all with a single script.
- queries.sort((a, b) => a[1].source_name.localeCompare(b[1].source_name))
- let results = (
- await Promise.all(
- chunkBy(queries, ([key, q]) => q.source_name).map(async (chunk) => {
- console.log(
- 'Preparing chunk of queries:',
- chunk.map(([_, q]) => q)
- )
- let queryContent = chunk
- .flatMap(([key, q]) => [
- q.source_schema ? `SET search_path TO ${q.source_schema};` : 'RESET search_path;',
- q.query_string + (q.query_string.trim().endsWith(';') ? '' : ';')
- ])
- .join('\n')
- queryContent =
- '-- prepare\n--result_collection=all_statements_first_row\n' + queryContent
+ let datatableQueries = queries.filter(([_, q]) => q.source_kind === 'datatable')
+ let ducklakeQueries = queries.filter(([_, q]) => q.source_kind === 'ducklake')
- let res = (await JobService.runScriptPreviewAndWaitResult({
- workspace: getWorkspace()!,
- requestBody: {
- language: 'postgresql',
- content: queryContent,
- args: { database: `datatable://${chunk[0][1]?.source_name}` }
- }
- })) as { error?: string; columns?: { name: string; type: string }[] }[]
+ let allResults: [string, PreparedAssetsSqlQuery][] = []
- console.log('Prepared query content:', res)
-
- let res2: [string, PreparedAssetsSqlQuery][] = res.map((r, i) => [
- chunk[i][0],
- r.columns
- ? {
- columns: Object.fromEntries(
- r.columns.map(({ name, type }) => [
- name,
- sqlDataTypeToJsTypeHeuristic(type)
- ])
- )
- }
- : { error: r.error ?? "Couldn't prepare query " }
- ])
- return res2
- })
- )
- ).flat()
-
- return Object.fromEntries(results)
- } catch (e) {
- throw e
+ if (datatableQueries.length) {
+ allResults.push(...(await prepareDatatableQueries(datatableQueries, getWorkspace)))
}
+ if (ducklakeQueries.length) {
+ allResults.push(...(await prepareDucklakeQueries(ducklakeQueries, getWorkspace)))
+ }
+
+ return Object.fromEntries(allResults)
}
)
@@ -96,3 +63,104 @@ export function usePreparedAssetSqlQueries(
}
}
}
+
+type QueryEntry = [string, InferAssetsSqlQueryDetails]
+
+function mapPrepareResults(
+ res: { error?: string; columns?: { name: string; type: string }[] }[],
+ chunk: QueryEntry[]
+): [string, PreparedAssetsSqlQuery][] {
+ if (res.length !== chunk.length) {
+ throw new Error(`Prepare results count mismatch: got ${res.length}, expected ${chunk.length}`)
+ }
+ return res.map((r, i) => [
+ chunk[i]?.[0],
+ r.columns
+ ? {
+ columns: Object.fromEntries(
+ r.columns.map(({ name, type: t }) => [name, sqlDataTypeToJsTypeHeuristic(t)])
+ )
+ }
+ : { error: r.error ?? "Couldn't prepare query " }
+ ])
+}
+
+async function prepareDatatableQueries(
+ queries: QueryEntry[],
+ getWorkspace: () => string | undefined
+): Promise<[string, PreparedAssetsSqlQuery][]> {
+ queries.sort((a, b) => a[1].source_name.localeCompare(b[1].source_name))
+ let results = (
+ await Promise.all(
+ chunkBy(queries, ([_, q]) => q.source_name).map(async (chunk) => {
+ let queryContent = chunk
+ .flatMap(([_, q]) => [
+ q.source_schema ? `SET search_path TO ${q.source_schema};` : 'RESET search_path;',
+ q.query_string + (q.query_string.trim().endsWith(';') ? '' : ';')
+ ])
+ .join('\n')
+ queryContent = '-- prepare\n--result_collection=all_statements_first_row\n' + queryContent
+
+ try {
+ let res = (await JobService.runScriptPreviewAndWaitResult({
+ workspace: getWorkspace()!,
+ requestBody: {
+ language: 'postgresql',
+ content: queryContent,
+ args: { database: `datatable://${chunk[0][1]?.source_name}` }
+ }
+ })) as { error?: string; columns?: { name: string; type: string }[] }[]
+
+ return mapPrepareResults(res, chunk)
+ } catch (e) {
+ const error = extractErrorMessage(e)
+ return chunk.map(([key]) => [key, { error }] as [string, PreparedAssetsSqlQuery])
+ }
+ })
+ )
+ ).flat()
+ return results
+}
+
+async function prepareDucklakeQueries(
+ queries: QueryEntry[],
+ getWorkspace: () => string | undefined
+): Promise<[string, PreparedAssetsSqlQuery][]> {
+ queries.sort((a, b) => a[1].source_name.localeCompare(b[1].source_name))
+ let results = (
+ await Promise.all(
+ chunkBy(queries, ([_, q]) => `${q.source_name}::${q.source_schema ?? ''}`).map(
+ async (chunk) => {
+ let sourceName = chunk[0][1].source_name
+ let sourceSchema = chunk[0][1].source_schema
+ let attachSetup = `ATTACH 'ducklake://${sourceName}' AS dl;\n`
+ attachSetup += sourceSchema ? `USE dl.${sourceSchema};\n` : `USE dl;\n`
+
+ let queryContent = chunk
+ .map(([_, q]) => q.query_string + (q.query_string.trim().endsWith(';') ? '' : ';'))
+ .join('\n')
+ queryContent =
+ '-- prepare\n--result_collection=all_statements_first_row\n' +
+ attachSetup +
+ queryContent
+
+ try {
+ let res = (await JobService.runScriptPreviewAndWaitResult({
+ workspace: getWorkspace()!,
+ requestBody: {
+ language: 'duckdb',
+ content: queryContent,
+ args: {}
+ }
+ })) as { error?: string; columns?: { name: string; type: string }[] }[]
+ return mapPrepareResults(res, chunk)
+ } catch (e) {
+ const error = extractErrorMessage(e)
+ return chunk.map(([key]) => [key, { error }] as [string, PreparedAssetsSqlQuery])
+ }
+ }
+ )
+ )
+ ).flat()
+ return results
+}
From 4bf827bea4d44aca8c5ff7aa67ad449dbcf00673 Mon Sep 17 00:00:00 2001
From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com>
Date: Wed, 4 Mar 2026 11:46:34 +0100
Subject: [PATCH 4/7] feat: persistent Db manager state in URI (#8134)
* DB Manager state in URL
* Fix state not saving
* shorted uri params
* infer db_type from prefix
* Revert "infer db_type from prefix"
This reverts commit 7415fbed3db0d570f321a0b86e0c5db6e876b430.
* dbm syntax
* infer database type
* Omit main and public
* remove legacy #dbmanager:
* Preserve hash
* nit
* Fix remaining dbManagerDrawer objects
---
.../src/lib/components/DBManagerDrawer.svelte | 77 +++----
.../src/lib/components/DatatablePicker.svelte | 5 +-
.../src/lib/components/DucklakePicker.svelte | 5 +-
.../lib/components/ExploreAssetButton.svelte | 7 +-
.../src/lib/components/ResourcePicker.svelte | 4 +-
frontend/src/lib/components/RunsPage.svelte | 4 +-
.../lib/components/assets/AssetButtons.svelte | 3 -
.../assets/AssetsDropdownButton.svelte | 4 +-
.../components/assets/JobAssetsViewer.svelte | 4 +-
.../components/dbManagerDrawerModel.svelte.ts | 207 ++++++++++++++++++
.../graph/renderers/nodes/AssetNode.svelte | 3 +-
.../components/sidebar/FavoriteMenu.svelte | 2 +-
.../CustomInstanceDbSelect.svelte | 4 -
.../CustomInstanceDbWizardModal.svelte | 3 -
.../DataTableSettings.svelte | 6 +-
.../workspaceSettings/DucklakeSettings.svelte | 5 +-
frontend/src/lib/stores.ts | 5 +-
frontend/src/lib/svelte5UtilsKit.svelte.ts | 5 +-
.../src/routes/(root)/(logged)/+layout.svelte | 28 +--
.../(root)/(logged)/assets/+page.svelte | 3 -
.../(root)/(logged)/resources/+page.svelte | 6 +-
21 files changed, 266 insertions(+), 124 deletions(-)
create mode 100644 frontend/src/lib/components/dbManagerDrawerModel.svelte.ts
diff --git a/frontend/src/lib/components/DBManagerDrawer.svelte b/frontend/src/lib/components/DBManagerDrawer.svelte
index c5986e18a4..5996d98075 100644
--- a/frontend/src/lib/components/DBManagerDrawer.svelte
+++ b/frontend/src/lib/components/DBManagerDrawer.svelte
@@ -6,27 +6,20 @@
import DrawerContent from './common/drawer/DrawerContent.svelte'
import Select from './select/Select.svelte'
import { ArrowLeft, Expand, LoaderCircle, Minimize, RefreshCcw } from 'lucide-svelte'
- import type { DbInput } from './dbTypes'
import DBManagerContent from './DBManagerContent.svelte'
import { resource } from 'runed'
+ import { untrack } from 'svelte'
+ import type { DbManagerUriState } from './dbManagerDrawerModel.svelte'
interface Props {
+ uriState: DbManagerUriState
/** Z-index offset for the drawer, useful when opening from within modals */
offset?: number
}
- let { offset = 0 }: Props = $props()
+ let { uriState, offset = 0 }: Props = $props()
- let input: DbInput | undefined = $state()
- let open = $derived(!!input)
-
- // For datatable inputs, track the selected datatable separately
- let selectedDatatable = $state(undefined)
-
- // Check if input is a datatable type
- const isDatatableInput = $derived(
- input?.type === 'database' && input.resourcePath.startsWith('datatable://')
- )
+ let open = $derived(uriState.open)
// Load available datatables when drawer opens with datatable input
const datatables = resource([], async () => {
@@ -39,16 +32,6 @@
}
})
- // Computed input that updates when selectedDatatable changes
- const effectiveInput: DbInput | undefined = $derived.by(() => {
- if (!input) return undefined
- if (!isDatatableInput || !selectedDatatable) return input
- return {
- ...input,
- resourcePath: `datatable://${selectedDatatable}`
- }
- })
-
const datatableItems = $derived(
datatables.current.map((dt) => ({
value: dt,
@@ -56,32 +39,26 @@
}))
)
- export function openDrawer(nInput: DbInput) {
- input = nInput
- if (isDatatableInput) {
- datatables.refetch()
+ // Refetch datatables when switching to a datatable input
+ $effect(() => {
+ if (uriState.isDatatableInput) {
+ untrack(() => datatables.refetch())
}
- // If it's a datatable input, extract the datatable name for the selector
- if (nInput.type === 'database' && nInput.resourcePath.startsWith('datatable://')) {
- selectedDatatable = nInput.resourcePath.replace('datatable://', '')
- datatables.refetch()
- } else {
- selectedDatatable = undefined
- }
- }
- export function closeDrawer() {
- input = undefined
- selectedDatatable = undefined
+ })
+
+ function handleClose() {
+ uriState.closeDrawer()
dbManagerContent?.clearReplResult()
- if (window.location.hash.startsWith('#dbmanager:'))
- history.replaceState('', document.title, window.location.href.replace(/#dbmanager:.*$/, ''))
}
let windowWidth = $state(window.innerWidth)
let expand = $state(false)
$effect(() => {
- if (!open) expand = false
+ if (!open) {
+ expand = false
+ uriState.closeDrawer()
+ }
})
let dbManagerContent: DBManagerContent | undefined = $state()
@@ -96,7 +73,7 @@
size={expand ? `${windowWidth}px` : '1200px'}
preventEscape
{offset}
- on:close={closeDrawer}
+ on:close={handleClose}
>
- {#if effectiveInput && $workspaceStore}
- {#key selectedDatatable}
-
+ {#if uriState.effectiveInput && $workspaceStore}
+ {#key uriState.selectedDatatable}
+
{#snippet dbSelector()}
- {#if isDatatableInput}
+ {#if uriState.isDatatableInput}
{#if datatables.loading}
@@ -125,7 +108,7 @@
diff --git a/frontend/src/lib/components/RunsPage.svelte b/frontend/src/lib/components/RunsPage.svelte
index 59b63ab313..d03495c052 100644
--- a/frontend/src/lib/components/RunsPage.svelte
+++ b/frontend/src/lib/components/RunsPage.svelte
@@ -148,7 +148,9 @@
(v) => {
v.maxTs ? (filters.val.max_ts = new Date(v.maxTs)) : delete filters.val.max_ts
v.minTs ? (filters.val.min_ts = new Date(v.minTs)) : delete filters.val.min_ts
- v.timeframe ? (filters.val.timeframe = v.timeframe) : delete filters.val.timeframe
+ v.timeframe && v.timeframe !== 'Latest runs'
+ ? (filters.val.timeframe = v.timeframe)
+ : delete filters.val.timeframe
}
)
let timeframe = $derived(_timeframe.val)
diff --git a/frontend/src/lib/components/assets/AssetButtons.svelte b/frontend/src/lib/components/assets/AssetButtons.svelte
index 241a0f050e..9364bf710b 100644
--- a/frontend/src/lib/components/assets/AssetButtons.svelte
+++ b/frontend/src/lib/components/assets/AssetButtons.svelte
@@ -8,7 +8,6 @@
type Props = {
s3FilePicker?: any | undefined
- dbManagerDrawer?: any | undefined
resourceEditorDrawer?: ResourceEditorDrawer | undefined
resourceDataCache: Record
asset: Asset
@@ -18,7 +17,6 @@
}
let {
s3FilePicker,
- dbManagerDrawer,
resourceEditorDrawer,
resourceDataCache,
asset,
@@ -71,7 +69,6 @@
onClick?.()}
noText
_resourceMetadata={{ resource_type: resourceDataCacheValue }}
diff --git a/frontend/src/lib/components/assets/AssetsDropdownButton.svelte b/frontend/src/lib/components/assets/AssetsDropdownButton.svelte
index 1e1e1871ae..5c530cf606 100644
--- a/frontend/src/lib/components/assets/AssetsDropdownButton.svelte
+++ b/frontend/src/lib/components/assets/AssetsDropdownButton.svelte
@@ -14,7 +14,7 @@
} from './lib'
import { untrack } from 'svelte'
import { ResourceService, WorkspaceService } from '$lib/gen'
- import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
+ import { workspaceStore } from '$lib/stores'
import Tooltip from '../meltComponents/Tooltip.svelte'
import Tooltip2 from '../Tooltip.svelte'
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
@@ -48,7 +48,6 @@
let blueBgDiv: HTMLDivElement | undefined = $state()
let s3FilePicker: S3FilePicker | undefined = $state()
- let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let resourceEditorDrawer: ResourceEditorDrawer | undefined = $state()
let isOpen = $state(false)
let resourceDataCache: Record = $state({})
@@ -209,7 +208,6 @@
onClick={() => (isOpen = false)}
{asset}
{resourceDataCache}
- {dbManagerDrawer}
{resourceEditorDrawer}
{s3FilePicker}
{ducklakeNotFound}
diff --git a/frontend/src/lib/components/assets/JobAssetsViewer.svelte b/frontend/src/lib/components/assets/JobAssetsViewer.svelte
index 2eccf66d51..86b9c7eca7 100644
--- a/frontend/src/lib/components/assets/JobAssetsViewer.svelte
+++ b/frontend/src/lib/components/assets/JobAssetsViewer.svelte
@@ -1,7 +1,7 @@
@@ -86,7 +85,6 @@
diff --git a/frontend/src/lib/components/dbManagerDrawerModel.svelte.ts b/frontend/src/lib/components/dbManagerDrawerModel.svelte.ts
new file mode 100644
index 0000000000..c03695e0a3
--- /dev/null
+++ b/frontend/src/lib/components/dbManagerDrawerModel.svelte.ts
@@ -0,0 +1,207 @@
+import { z } from 'zod'
+import { useSearchParams } from '$lib/svelte5UtilsKit.svelte'
+import type { DbInput, DbType } from './dbTypes'
+import { isDbType } from './dbTypes'
+
+/**
+ * Single URL param `dbm` encodes the full DB manager state:
+ * firstSegment~path~schema.table
+ *
+ * firstSegment:
+ * datatable – database with datatable:// resource (resourceType always postgresql)
+ * ducklake – ducklake connection
+ * postgresql, mysql, … – regular database (the segment IS the resource type)
+ *
+ * schema.table (third segment, optional):
+ * schema.table – both
+ * .table – table only
+ * schema. – schema only
+ * (omitted) – neither
+ *
+ * Default schemas (omitted from URL, restored on parse):
+ * datatable → public
+ * ducklake → main
+ *
+ * Examples:
+ * datatable~main~.customers (schema "public" implied)
+ * ducklake~main~.orders (schema "main" implied)
+ * postgresql~$res:u/user/my_pg~public.customers
+ */
+
+const dbManagerSchema = z.object({
+ dbm: z.string().nullable()
+})
+
+interface ParsedDbm {
+ type: 'database' | 'datatable' | 'ducklake'
+ path: string
+ resType?: string
+ schema?: string
+ table?: string
+}
+
+function parseDbm(raw: unknown): ParsedDbm | null {
+ if (!raw || typeof raw !== 'string') return null
+ const parts = raw.split('~')
+ if (parts.length < 2 || !parts[1]) return null
+
+ const firstSeg = parts[0]
+ const path = parts[1]
+ const schemaTable = parts[2] ?? ''
+
+ let type: ParsedDbm['type']
+ let resType: string | undefined
+ if (firstSeg === 'datatable') {
+ type = 'datatable'
+ } else if (firstSeg === 'ducklake') {
+ type = 'ducklake'
+ } else if (isDbType(firstSeg)) {
+ type = 'database'
+ resType = firstSeg
+ } else {
+ return null
+ }
+
+ let schema: string | undefined
+ let table: string | undefined
+ if (schemaTable) {
+ const dotIdx = schemaTable.indexOf('.')
+ if (dotIdx === 0) {
+ table = schemaTable.slice(1) || undefined
+ } else if (dotIdx === schemaTable.length - 1) {
+ schema = schemaTable.slice(0, -1) || undefined
+ } else if (dotIdx > 0) {
+ schema = schemaTable.slice(0, dotIdx)
+ table = schemaTable.slice(dotIdx + 1)
+ }
+ }
+
+ // Restore default schema when omitted for datatable/ducklake
+ if (!schema && table && type in defaultSchemas) {
+ schema = defaultSchemas[type]
+ }
+
+ return { type, path, resType, schema, table }
+}
+
+const defaultSchemas: Record = { datatable: 'public', ducklake: 'main' }
+
+function buildDbm(p: ParsedDbm): string {
+ const firstSeg = p.type === 'database' ? p.resType! : p.type
+ const schema = p.schema === defaultSchemas[p.type] ? undefined : p.schema
+ let schemaTable = ''
+ if (schema && p.table) {
+ schemaTable = `${schema}.${p.table}`
+ } else if (p.table) {
+ schemaTable = `.${p.table}`
+ } else if (schema) {
+ schemaTable = `${schema}.`
+ }
+ return schemaTable ? `${firstSeg}~${p.path}~${schemaTable}` : `${firstSeg}~${p.path}`
+}
+
+export interface DbManagerUriState {
+ readonly input: DbInput | undefined
+ readonly effectiveInput: DbInput | undefined
+ readonly isDatatableInput: boolean
+ selectedDatatable: string | undefined
+ selectedSchema: string | undefined
+ selectedTable: string | undefined
+ readonly open: boolean
+ openDrawer: (nInput: DbInput) => void
+ closeDrawer: () => void
+}
+
+export function useDbManagerUriState(): DbManagerUriState {
+ const params = useSearchParams(dbManagerSchema)
+
+ const parsed = $derived(parseDbm(params.dbm))
+
+ let input: DbInput | undefined = $derived.by(() => {
+ if (!parsed) return undefined
+ if (parsed.type === 'ducklake') {
+ return {
+ type: 'ducklake' as const,
+ ducklake: parsed.path,
+ specificTable: parsed.table
+ }
+ }
+ // datatable or database
+ const resType = parsed.type === 'datatable' ? 'postgresql' : parsed.resType
+ if (!isDbType(resType ?? undefined)) return undefined
+ return {
+ type: 'database' as const,
+ resourceType: resType as DbType,
+ resourcePath: parsed.type === 'datatable' ? `datatable://${parsed.path}` : parsed.path,
+ specificSchema: parsed.schema,
+ specificTable: parsed.table
+ }
+ })
+
+ const isDatatableInput = $derived(parsed?.type === 'datatable')
+
+ function updateField(updates: Partial) {
+ const p = parseDbm(params.dbm)
+ if (!p) return
+ Object.assign(p, updates)
+ params.dbm = buildDbm(p)
+ }
+
+ function openDrawer(nInput: DbInput) {
+ if (nInput.type === 'database') {
+ const isDatatable = nInput.resourcePath.startsWith('datatable://')
+ params.dbm = buildDbm({
+ type: isDatatable ? 'datatable' : 'database',
+ path: isDatatable ? nInput.resourcePath.slice('datatable://'.length) : nInput.resourcePath,
+ resType: isDatatable ? undefined : nInput.resourceType,
+ schema: nInput.specificSchema,
+ table: nInput.specificTable
+ })
+ } else {
+ params.dbm = buildDbm({
+ type: 'ducklake',
+ path: nInput.ducklake,
+ table: nInput.specificTable
+ })
+ }
+ }
+
+ function closeDrawer() {
+ params.dbm = null
+ }
+
+ return {
+ get input() {
+ return input
+ },
+ get effectiveInput() {
+ return input
+ },
+ get isDatatableInput() {
+ return isDatatableInput
+ },
+ get selectedDatatable() {
+ return parsed?.type === 'datatable' ? parsed.path : undefined
+ },
+ set selectedDatatable(v: string | undefined) {
+ if (v) updateField({ path: v })
+ },
+ get selectedSchema() {
+ return parsed?.schema
+ },
+ set selectedSchema(v: string | undefined) {
+ updateField({ schema: v })
+ },
+ get selectedTable() {
+ return parsed?.table
+ },
+ set selectedTable(v: string | undefined) {
+ updateField({ table: v })
+ },
+ get open() {
+ return !!input
+ },
+ openDrawer,
+ closeDrawer
+ }
+}
diff --git a/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte
index ad4a11bd2f..68c2bad5df 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte
@@ -235,7 +235,7 @@
import type { Edge, Node } from '@xyflow/svelte'
import { getNodeColorClasses, NODE } from '../../util'
- import { globalDbManagerDrawer, userStore } from '$lib/stores'
+ import { userStore } from '$lib/stores'
import { deepEqual } from 'fast-equals'
import { slide } from 'svelte/transition'
import AssetColumnBadges from '$lib/components/assets/AssetColumnBadges.svelte'
@@ -313,7 +313,6 @@
noText
buttonVariant="accent"
s3FilePicker={flowGraphAssetsCtx?.val.s3FilePicker}
- dbManagerDrawer={globalDbManagerDrawer.val}
_resourceMetadata={cachedResourceMetadata}
/>
diff --git a/frontend/src/lib/components/sidebar/FavoriteMenu.svelte b/frontend/src/lib/components/sidebar/FavoriteMenu.svelte
index d0a14143ca..056bca1489 100644
--- a/frontend/src/lib/components/sidebar/FavoriteMenu.svelte
+++ b/frontend/src/lib/components/sidebar/FavoriteMenu.svelte
@@ -10,7 +10,7 @@
flow: `/flows/get/${path}`,
app: `/apps/get/${path}`,
raw_app: `/apps_raw/get/${path}`,
- asset: `#dbmanager:${path}`
+ asset: '#'
}[kind]
}
export function getFavoriteLabel(path: string, kind: FavoriteKind): string {
diff --git a/frontend/src/lib/components/workspaceSettings/CustomInstanceDbSelect.svelte b/frontend/src/lib/components/workspaceSettings/CustomInstanceDbSelect.svelte
index b74d7c9b92..44a4dcc6bf 100644
--- a/frontend/src/lib/components/workspaceSettings/CustomInstanceDbSelect.svelte
+++ b/frontend/src/lib/components/workspaceSettings/CustomInstanceDbSelect.svelte
@@ -8,14 +8,12 @@
import CustomInstanceDbWizardModal from './CustomInstanceDbWizardModal.svelte'
import { ArrowRight, TriangleAlert } from 'lucide-svelte'
import type { ConfirmationModalHandle } from '../common/confirmationModal/asyncConfirmationModal.svelte'
- import DBManagerDrawer from '../DBManagerDrawer.svelte'
import type { Snippet } from 'svelte'
type Props = {
value: string | undefined
customInstanceDbs: ResourceReturn
confirmationModal: ConfirmationModalHandle
- dbManagerDrawer: DBManagerDrawer | undefined
wizardBottomHint?: Snippet | undefined
class?: string
tag?: CustomInstanceDbTag
@@ -24,7 +22,6 @@
value = $bindable(),
customInstanceDbs,
confirmationModal,
- dbManagerDrawer,
wizardBottomHint,
class: className,
tag
@@ -90,7 +87,6 @@
confirmationModal: ConfirmationModalHandle
- dbManagerDrawer: any | undefined
bottomHint?: Snippet | undefined
opened: { status: CustomInstanceDb | undefined; dbname: string } | undefined
tag?: CustomInstanceDbTag
@@ -33,7 +32,6 @@
let {
customInstanceDbs,
confirmationModal,
- dbManagerDrawer,
bottomHint,
opened = $bindable(),
tag
@@ -76,7 +74,6 @@
class="flex-1"
asset={{ kind: 'resource', path: 'CUSTOM_INSTANCE_DB/' + dbname }}
_resourceMetadata={{ resource_type: 'postgresql' }}
- {dbManagerDrawer}
disabled={!$isCustomInstanceDbEnabled || !enableManageButton}
onClick={() => (opened = undefined)}
/>
diff --git a/frontend/src/lib/components/workspaceSettings/DataTableSettings.svelte b/frontend/src/lib/components/workspaceSettings/DataTableSettings.svelte
index 188d736e9f..6410e6a1af 100644
--- a/frontend/src/lib/components/workspaceSettings/DataTableSettings.svelte
+++ b/frontend/src/lib/components/workspaceSettings/DataTableSettings.svelte
@@ -62,7 +62,7 @@
import { random_adj } from '../random_positive_adjetive'
import { sendUserToast } from '$lib/toast'
import { SettingService, WorkspaceService, type GetSettingsResponse } from '$lib/gen'
- import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
+ import { workspaceStore } from '$lib/stores'
import { createAsyncConfirmationModal } from '../common/confirmationModal/asyncConfirmationModal.svelte'
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
import { resource } from 'runed'
@@ -141,7 +141,6 @@
}
let confirmationModal = createAsyncConfirmationModal()
- let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let dirtyMap = $derived.by(() => {
const map: Record = {}
for (let i = 0; i < tempSettings.dataTables.length; i++) {
@@ -245,7 +244,6 @@
@@ -275,7 +272,6 @@
{/if}
diff --git a/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte b/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte
index ac8c20377d..c264219922 100644
--- a/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte
+++ b/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte
@@ -68,7 +68,7 @@
import { SettingService, WorkspaceService } from '$lib/gen'
import type { GetSettingsResponse } from '$lib/gen'
- import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
+ import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import ExploreAssetButton from '../ExploreAssetButton.svelte'
import Tooltip from '../Tooltip.svelte'
@@ -187,7 +187,6 @@
'Where the data is actually stored, in parquet format. You need to configure a workspace storage first'
}
- let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let confirmationModal = createAsyncConfirmationModal()
@@ -293,7 +292,6 @@
bind:value={ducklake.catalog.resource_path}
{customInstanceDbs}
{confirmationModal}
- {dbManagerDrawer}
tag="ducklake"
>
{#snippet wizardBottomHint()}
@@ -372,7 +370,6 @@
{:else}
{/if}
diff --git a/frontend/src/lib/stores.ts b/frontend/src/lib/stores.ts
index a397cb4b7f..6a9a8e7805 100644
--- a/frontend/src/lib/stores.ts
+++ b/frontend/src/lib/stores.ts
@@ -14,6 +14,7 @@ import {
import { getLocalSetting, type StateStore } from './utils'
import { createState } from './svelte5Utils.svelte'
import { DEFAULT_HUB_BASE_URL } from './hub'
+import type { DbManagerUriState } from './components/dbManagerDrawerModel.svelte'
export interface UserExt {
email: string
@@ -126,9 +127,7 @@ export const codeCompletionSessionEnabled = writable(
export const usedTriggerKinds = writable([])
-export let globalDbManagerDrawer: StateStore = createState({
- val: undefined
-})
+export let globalDbManagerDrawer: StateStore = { val: undefined }
type SQLBaseSchema = {
[schemaKey: string]: {
diff --git a/frontend/src/lib/svelte5UtilsKit.svelte.ts b/frontend/src/lib/svelte5UtilsKit.svelte.ts
index cadd2dee1e..921d41614e 100644
--- a/frontend/src/lib/svelte5UtilsKit.svelte.ts
+++ b/frontend/src/lib/svelte5UtilsKit.svelte.ts
@@ -107,9 +107,10 @@ export function useSearchParams(schema: S): SearchParamsRes
} else {
sp.set(key, serializeParam(v))
}
+ const hash = window.location.hash
const newUrl = sp.toString()
- ? `${window.location.pathname}?${sp}`
- : window.location.pathname
+ ? `${window.location.pathname}?${sp}${hash}`
+ : `${window.location.pathname}${hash}`
history.replaceState(history.state, '', newUrl)
},
enumerable: true,
diff --git a/frontend/src/routes/(root)/(logged)/+layout.svelte b/frontend/src/routes/(root)/(logged)/+layout.svelte
index 4260bbeede..e9b1884df2 100644
--- a/frontend/src/routes/(root)/(logged)/+layout.svelte
+++ b/frontend/src/routes/(root)/(logged)/+layout.svelte
@@ -11,13 +11,7 @@
UserService,
WorkspaceService
} from '$lib/gen'
- import {
- capitalize,
- classNames,
- getModifierKey,
- parseDbInputFromAssetSyntax,
- sendUserToast
- } from '$lib/utils'
+ import { capitalize, classNames, getModifierKey, sendUserToast } from '$lib/utils'
import WorkspaceMenu from '$lib/components/sidebar/WorkspaceMenu.svelte'
import SidebarContent from '$lib/components/sidebar/SidebarContent.svelte'
import CriticalAlertModal from '$lib/components/sidebar/CriticalAlertModal.svelte'
@@ -66,8 +60,8 @@
import AiChatLayout from '$lib/components/copilot/chat/AiChatLayout.svelte'
import { DEFAULT_HUB_BASE_URL } from '$lib/hub'
import DBManagerDrawer from '$lib/components/DBManagerDrawer.svelte'
- import { watchOnce } from 'runed'
import { useIsDarkMode } from '$lib/components/DarkModeObserver.svelte'
+ import { useDbManagerUriState } from '$lib/components/dbManagerDrawerModel.svelte'
interface Props {
children?: import('svelte').Snippet
}
@@ -439,18 +433,8 @@
untrack(() => loadProtectionRules(workspace))
}
})
- watchOnce(
- () => globalDbManagerDrawer.val,
- () => {
- if (!globalDbManagerDrawer.val) return
- const hash = window.location.hash
- if (hash.startsWith('#dbmanager:')) {
- const [_, path] = hash.split('#dbmanager:')
- const dbInput = parseDbInputFromAssetSyntax(path)
- if (dbInput) globalDbManagerDrawer.val?.openDrawer(dbInput)
- }
- }
- )
+
+ globalDbManagerDrawer.val = useDbManagerUriState()
@@ -786,6 +770,6 @@
{/if}
-{#if $workspaceStore}
-
+{#if $workspaceStore && globalDbManagerDrawer.val}
+
{/if}
diff --git a/frontend/src/routes/(root)/(logged)/assets/+page.svelte b/frontend/src/routes/(root)/(logged)/assets/+page.svelte
index 7eac2cc535..032a60af66 100644
--- a/frontend/src/routes/(root)/(logged)/assets/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/assets/+page.svelte
@@ -99,7 +99,6 @@
let assets = $derived(_assets.current?.flatMap((page) => page.assets))
let s3FilePicker: S3FilePicker | undefined = $state()
- let dbManagerDrawer = $derived(globalDbManagerDrawer.val) as any
let assetsUsageDropdown: AssetsUsageDrawer | undefined = $state()
let allS3Storages = resource(
@@ -192,7 +191,6 @@
@@ -337,7 +335,6 @@
{/if}
diff --git a/frontend/src/routes/(root)/(logged)/resources/+page.svelte b/frontend/src/routes/(root)/(logged)/resources/+page.svelte
index f0ac8ae9d6..abf17b88bb 100644
--- a/frontend/src/routes/(root)/(logged)/resources/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/resources/+page.svelte
@@ -35,8 +35,7 @@
enterpriseLicense,
userStore,
workspaceStore,
- userWorkspaces,
- globalDbManagerDrawer
+ userWorkspaces
} from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import {
@@ -557,8 +556,6 @@
}
})
- let dbManagerDrawer = $derived(globalDbManagerDrawer.val) as any
-
let showTable = $derived(
tab == 'workspace' || tab == 'states' || tab == 'cache' || tab == 'theme'
)
@@ -1064,7 +1061,6 @@
{#if path && assetCanBeExplored({ kind: 'resource', path }, { resource_type }) && !$userStore?.operator}
From c0c9388415716ce77d841bd08a46f94e0a529685 Mon Sep 17 00:00:00 2001
From: Guilhem
Date: Wed, 4 Mar 2026 10:53:01 +0000
Subject: [PATCH 5/7] feat: add move, delete, and duplicate to flow node
context menu (#8050)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: add context menu, multi-select actions, and keyboard shortcuts to flow editor
Co-Authored-By: Claude Opus 4.6
* fix: address review feedback on context menu PR
- Revert accidental static import of @scalar/openapi-parser (keep lazy-loaded)
- Restore [data-context-menu] in portalDivs for clickOutside compatibility
- Make noteDisabled reactive ($derived) in ModuleNode
- Use platform-aware shortcut hint (⌫ on Mac, Del on Windows/Linux)
- Optimize resolveSelectedModuleIds with single-pass ancestor map
Co-Authored-By: Claude Opus 4.6
* fix: address additional review feedback on flow context menu PR
- Use $derived.by instead of $derived for computed bounds in SelectionBoundingBox
- Remove redundant structuredClone wrappers around $state.snapshot
- Add null guard for originalModules/targetModules in move handler
- Add upper-bound guard (n < 10000) to copyId loop
- Fix fragile toggle comparison in moveManager with full array equality
Co-Authored-By: Claude Opus 4.6
---------
Co-authored-by: Claude Opus 4.6
---
.../src/lib/components/DropdownV2Inner.svelte | 3 +
.../src/lib/components/FlowBuilder.svelte | 12 +-
.../flows/content/FlowEditorPanel.svelte | 26 ++-
.../components/flows/content/FlowLoop.svelte | 1 -
.../flows/content/FlowSelectionPanel.svelte | 59 +++++--
.../lib/components/flows/flowModuleNextId.ts | 12 ++
.../flows/map/FlowModuleSchemaItem.svelte | 153 +++++++----------
.../map/FlowModuleSchemaItemViewer.svelte | 20 ++-
.../flows/map/FlowModuleSchemaMap.svelte | 154 ++++++++++++++++--
.../lib/components/flows/map/MapItem.svelte | 14 +-
.../components/flows/map/VirtualItem.svelte | 11 +-
.../lib/components/flows/multiSelectUtils.ts | 136 ++++++++++++++++
.../components/graph/DragCoordinator.svelte | 22 ++-
.../src/lib/components/graph/DragGhost.svelte | 18 +-
.../lib/components/graph/FlowGraphV2.svelte | 70 +++++++-
.../components/graph/MoveHandleButton.svelte | 74 +++++++++
.../graph/SelectionBoundingBox.svelte | 111 ++++++++++---
.../components/graph/graphBuilder.svelte.ts | 1 +
.../components/graph/moveManager.svelte.ts | 69 +++++---
.../components/graph/noteManager.svelte.ts | 7 +
.../graph/renderers/edges/BaseEdge.svelte | 2 +-
.../graph/renderers/nodes/ModuleNode.svelte | 48 +++++-
.../graph/renderers/nodes/NodeWrapper.svelte | 18 +-
.../components/graph/selectionUtils.svelte.ts | 13 +-
frontend/src/lib/utils.ts | 2 +
25 files changed, 854 insertions(+), 202 deletions(-)
create mode 100644 frontend/src/lib/components/flows/multiSelectUtils.ts
create mode 100644 frontend/src/lib/components/graph/MoveHandleButton.svelte
diff --git a/frontend/src/lib/components/DropdownV2Inner.svelte b/frontend/src/lib/components/DropdownV2Inner.svelte
index 21157bd6b2..b56a5cce70 100644
--- a/frontend/src/lib/components/DropdownV2Inner.svelte
+++ b/frontend/src/lib/components/DropdownV2Inner.svelte
@@ -62,6 +62,9 @@
{item.displayName}
{@render item.extra?.()}
+ {#if item.shortcut}
+ {item.shortcut}
+ {/if}
{#if item.tooltip}
{#snippet text()}
diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte
index 8ec0acbdf1..d3187c43aa 100644
--- a/frontend/src/lib/components/FlowBuilder.svelte
+++ b/frontend/src/lib/components/FlowBuilder.svelte
@@ -862,14 +862,6 @@
const mod = isMac() ? '⌘' : 'Ctrl+'
- const undoShortcutSnippet = createRawSnippet(() => ({
- render: () => `${mod}Z`
- }))
-
- const redoShortcutSnippet = createRawSnippet(() => ({
- render: () => `${mod}⇧Z`
- }))
-
function getMoreItems(): Item[] {
return [
...baseMenuItems,
@@ -878,7 +870,7 @@
icon: Undo,
action: () => handleUndo(),
disabled: $history.index === 0,
- extra: undoShortcutSnippet,
+ shortcut: `${mod}Z`,
separatorTop: baseMenuItems.length > 0
},
{
@@ -886,7 +878,7 @@
icon: Redo,
action: () => handleRedo(),
disabled: $history.index === $history.history.length - 1,
- extra: redoShortcutSnippet
+ shortcut: `${mod}⇧Z`
},
{
displayName: 'Tutorials',
diff --git a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte
index 8ebf167d51..c8418e5183 100644
--- a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte
+++ b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte
@@ -16,6 +16,11 @@
import FlowResult from './FlowResult.svelte'
import type { StateStore } from '$lib/utils'
import FlowSelectionPanel from './FlowSelectionPanel.svelte'
+ import {
+ resolveSelectedModuleIds,
+ locateModules,
+ areContiguousSiblings
+ } from '../multiSelectUtils'
interface Props {
noEditor?: boolean
@@ -89,10 +94,29 @@
$effect(() => {
computeMissingInputWarnings(flowStore, flowStateStore.val, flowInputsStore)
})
+
+ // Derived state for multi-select operations in the side panel
+ let resolvedModuleIds = $derived(
+ resolveSelectedModuleIds(selectionManager.selectedIds, flowStore.val.value.modules ?? [])
+ )
+ let canMoveSelected = $derived(
+ resolvedModuleIds.length > 0 &&
+ areContiguousSiblings(
+ locateModules(resolvedModuleIds, flowStore.val.value.modules ?? [])
+ )
+ )
{#if selectionManager && selectionManager.selectedIds.length > 1}
-
+ flowModuleSchemaMap?.deleteMultiple(resolvedModuleIds)}
+ onDuplicateSelected={() => flowModuleSchemaMap?.duplicateMultiple(resolvedModuleIds)}
+ onMoveSelected={() => flowModuleSchemaMap?.moveMultiple(resolvedModuleIds)}
+ {canMoveSelected}
+ resolvedCount={resolvedModuleIds.length}
+ />
{:else if selectedId?.startsWith('settings')}
{:else if selectedId === 'Input'}
diff --git a/frontend/src/lib/components/flows/content/FlowLoop.svelte b/frontend/src/lib/components/flows/content/FlowLoop.svelte
index f087079356..886f57386f 100644
--- a/frontend/src/lib/components/flows/content/FlowLoop.svelte
+++ b/frontend/src/lib/components/flows/content/FlowLoop.svelte
@@ -425,7 +425,6 @@
on:blur={() => {
iteratorFieldFocused = false
}}
- autofocus
lang="javascript"
bind:code={mod.value.iterator.expr}
class="h-full"
diff --git a/frontend/src/lib/components/flows/content/FlowSelectionPanel.svelte b/frontend/src/lib/components/flows/content/FlowSelectionPanel.svelte
index b14f090afc..7efb44b82d 100644
--- a/frontend/src/lib/components/flows/content/FlowSelectionPanel.svelte
+++ b/frontend/src/lib/components/flows/content/FlowSelectionPanel.svelte
@@ -2,14 +2,29 @@
import FlowCard from '../common/FlowCard.svelte'
import type { SelectionManager } from '$lib/components/graph/selectionUtils.svelte'
import { Button } from '$lib/components/common'
+ import DropdownV2 from '$lib/components/DropdownV2.svelte'
import { getNoteEditorContext } from '$lib/components/graph/noteEditor.svelte'
- import { StickyNote } from 'lucide-svelte'
+ import { StickyNote, Move, Copy, Trash2 } from 'lucide-svelte'
+ import type { Item } from '$lib/utils'
interface Props {
selectionManager: SelectionManager
noEditor: boolean
+ onDeleteSelected?: () => void
+ onDuplicateSelected?: () => void
+ onMoveSelected?: () => void
+ canMoveSelected?: boolean
+ resolvedCount?: number
}
- let { selectionManager, noEditor }: Props = $props()
+ let {
+ selectionManager,
+ noEditor,
+ onDeleteSelected,
+ onDuplicateSelected,
+ onMoveSelected,
+ canMoveSelected = false,
+ resolvedCount = 0
+ }: Props = $props()
const noteEditorContext = getNoteEditorContext()
@@ -19,20 +34,44 @@
noteEditorContext.noteEditor.createGroupNote(selectionManager.selectedIds)
}
}
+
+ let menuItems: Item[] = $derived([
+ {
+ displayName: 'Move',
+ icon: Move,
+ action: () => onMoveSelected?.(),
+ disabled: !canMoveSelected
+ },
+ {
+ displayName: 'Duplicate',
+ icon: Copy,
+ action: () => onDuplicateSelected?.()
+ },
+ {
+ displayName: `Delete (${resolvedCount})`,
+ icon: Trash2,
+ type: 'delete',
+ action: () => onDeleteSelected?.()
+ }
+ ])
{#snippet action()}
-
+
+
+ {#if resolvedCount > 0}
+
+ {/if}
+
{/snippet}
-
{selectionManager.selectedIds.length} nodes selected
{#each selectionManager.selectedIds as nodeId}
diff --git a/frontend/src/lib/components/flows/flowModuleNextId.ts b/frontend/src/lib/components/flows/flowModuleNextId.ts
index 34bed860f8..e10c900982 100644
--- a/frontend/src/lib/components/flows/flowModuleNextId.ts
+++ b/frontend/src/lib/components/flows/flowModuleNextId.ts
@@ -17,3 +17,15 @@ export function nextId(flowState: FlowState, fullFlow: OpenFlow): string {
}, 0)
return numberToChars(max)
}
+
+// Computes a copy id like "a2", "a3", etc. based on the original id
+export function copyId(originalId: string, flowState: FlowState, fullFlow: OpenFlow): string {
+ const allIds = new Set(dfs(fullFlow.value.modules, (fm) => fm.id).concat(Object.keys(flowState)))
+ for (let n = 2; n < 10000; n++) {
+ const candidate = `${originalId}${n}`
+ if (!allIds.has(candidate)) {
+ return candidate
+ }
+ }
+ return `${originalId}10000`
+}
diff --git a/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte b/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte
index 8836edb708..0a0fe310f0 100644
--- a/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte
+++ b/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte
@@ -2,12 +2,13 @@
import { preventDefault, stopPropagation } from 'svelte/legacy'
import Popover from '$lib/components/Popover.svelte'
- import { classNames, type StateStore } from '$lib/utils'
+ import DropdownV2 from '$lib/components/DropdownV2.svelte'
+ import { classNames, type Item, type StateStore } from '$lib/utils'
import {
Bed,
Database,
Gauge,
- Move,
+ EllipsisVertical,
PhoneIncoming,
Repeat,
Square,
@@ -20,7 +21,7 @@
Timer,
Maximize2
} from 'lucide-svelte'
- import { createEventDispatcher, getContext, onDestroy } from 'svelte'
+ import { createEventDispatcher, getContext } from 'svelte'
import { fade } from 'svelte/transition'
import type { FlowEditorContext } from '../types'
import { twMerge } from 'tailwind-merge'
@@ -48,6 +49,7 @@
import type { ModuleActionInfo } from '$lib/components/flows/flowDiff'
import DiffActionBar from './DiffActionBar.svelte'
import { getGraphContext } from '$lib/components/graph/graphContext'
+ import MoveHandleButton from '$lib/components/graph/MoveHandleButton.svelte'
interface Props {
selected?: boolean
@@ -69,7 +71,6 @@
id?: string | undefined
label: string
path?: string
- modType?: string | undefined
nodeState?: FlowNodeState
concurrency?: boolean
// TODO: Implement for this one. See how concurrency is implemented.
@@ -89,6 +90,7 @@
isOwner?: boolean
enableTestRun?: boolean
maximizeSubflow?: () => void
+ menuItems?: Item[]
}
let {
@@ -106,7 +108,6 @@
id = undefined,
label,
path = '',
- modType = undefined,
nodeState,
concurrency = false,
debouncing = false,
@@ -123,7 +124,8 @@
onEditInput,
flowJob,
enableTestRun = false,
- maximizeSubflow = undefined
+ maximizeSubflow = undefined,
+ menuItems = undefined
}: Props = $props()
// AI action colors take priority over execution state
@@ -138,6 +140,9 @@
const diffManager = flowGraphContext?.diffManager
const moveManager = flowGraphContext?.moveManager
+ // Hide per-node action buttons when multiple nodes are selected (multi-select mode)
+ let isMultiSelected = $derived((flowGraphContext?.selectionManager?.selectedIds?.length ?? 0) > 1)
+
let pickableIds: Record
| undefined = $state(undefined)
const dispatch = createEventDispatcher()
@@ -161,6 +166,7 @@
let outputPicker: OutputPicker | undefined = $state(undefined)
let testJob: any | undefined = $state(undefined)
let outputPickerBarOpen = $state(false)
+ let dropdownOpen = $state(false)
let flowStateStore = $derived(flowEditorContext?.flowStateStore)
@@ -188,42 +194,6 @@
let isDragging = $derived(!!moveManager?.dragging)
- // --- Drag handle logic ---
- let dragCleanup: (() => void) | undefined
-
- function onMovePointerDown(e: Event) {
- const pe = e as PointerEvent
- const startX = pe.clientX
- const startY = pe.clientY
- let didDrag = false
-
- function onMovePointer(me: PointerEvent) {
- const dx = me.clientX - startX
- const dy = me.clientY - startY
- if (!didDrag && Math.sqrt(dx * dx + dy * dy) > 5) {
- didDrag = true
- if (moveManager && id) {
- moveManager.startDrag(id, startX, startY)
- }
- }
- }
-
- function onUp() {
- document.removeEventListener('pointermove', onMovePointer)
- document.removeEventListener('pointerup', onUp)
- dragCleanup = undefined
- if (!didDrag) {
- dispatch('move')
- }
- }
-
- document.addEventListener('pointermove', onMovePointer)
- document.addEventListener('pointerup', onUp)
- dragCleanup = onUp
- }
-
- onDestroy(() => dragCleanup?.())
-
const outputPickerVisible = $derived(
editMode && (isConnectingCandidate || alwaysShowOutputPicker) && !!id && !isDragging
)
@@ -467,6 +437,7 @@
{deletable}
{bold}
bind:editId
+ disableEditId={isMultiSelected}
{hover}
{colorClasses}
>
@@ -475,7 +446,7 @@
{/snippet}
- {#if outputPickerVisible}
+ {#if outputPickerVisible && !isMultiSelected}
-
-
-
- {/if}
-
-
-
-
-
{#if (id && Object.values($flowInputsStore?.[id]?.flowStepWarnings || {}).length > 0) || Boolean(warningMessage)}
{/if}
+
+ {#if !isMultiSelected && id !== 'preprocessor' && moveManager && id}
+
+ dispatch('move')}
+ class="trash group-hover:block"
+ />
+
+ {/if}
+
+ {#if !isMultiSelected && id !== 'preprocessor' && menuItems && menuItems.length > 0}
+
+
+ {#snippet buttonReplacement()}
+
+ {/snippet}
+
+
+ {/if}
{:else if maximizeSubflow !== undefined}
{@render buttonMaximizeSubflow?.()}
{/if}
@@ -603,7 +576,7 @@
onmouseenter={() => (hover = true)}
onmouseleave={() => (hover = false)}
>
- {#if (hover || selected || testRunDropdownOpen) && outputPickerVisible}
+ {#if !isMultiSelected && (hover || selected || testRunDropdownOpen) && outputPickerVisible}
{#if !testIsLoading}