diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md index d610fa3f65..3f97552466 100644 --- a/.agents/skills/commit/SKILL.md +++ b/.agents/skills/commit/SKILL.md @@ -52,8 +52,6 @@ chore: upgrade sqlx to 0.7 4. Stage ONLY the modified/relevant files: `git add ...` 5. Create the commit with conventional format: ```bash - git commit -m ": - - Co-Authored-By: Claude Opus 4.5 " + git commit -m ": " ``` 6. Run `git status` to verify the commit succeeded diff --git a/.agents/skills/update-sqlx/SKILL.md b/.agents/skills/update-sqlx/SKILL.md new file mode 100644 index 0000000000..ce4eaa5ff7 --- /dev/null +++ b/.agents/skills/update-sqlx/SKILL.md @@ -0,0 +1,82 @@ +--- +name: update-sqlx +description: How to safely update SQLx offline query cache. MUST use when SQL queries change. +--- + +# SQLx Offline Query Cache + +Windmill uses `SQLX_OFFLINE=true` in CI, which requires all `sqlx::query!` / `sqlx::query_as!` macros to have matching cached query data in `backend/.sqlx/`. + +## When to Run + +Run after any change to SQL queries in Rust source files. Without it, CI will fail with: +``` +error: `SQLX_OFFLINE=true` but there is no cached data for this query +``` + +## The Problem + +`cargo sqlx prepare --workspace` **deletes all existing cache files** and regenerates only the ones found in the current compilation. If you don't compile with every feature flag (especially `private` for EE files), you will **silently delete EE query caches**, breaking CI for enterprise tests. + +The standard `./update_sqlx.sh` script tries to compile with all features, but it often fails locally because the EE symlinks can be out of sync with `main`. + +## Safe Procedure + +Always preserve the existing EE caches from `origin/main`. Use this workflow: + +```bash +cd backend + +# 1. Restore the full cache from main (includes EE caches) +git checkout origin/main -- .sqlx/ + +# 2. Run prepare with OSS features (what compiles locally) +# This regenerates OSS caches to match your code changes. +cargo sqlx prepare --workspace -- --workspace --features all_sqlx_features + +# 3. Restore any EE caches that were deleted in step 2. +# These are files present in origin/main but missing after prepare. +git ls-tree origin/main backend/.sqlx/ \ + | awk '{print $4}' | sed 's|backend/\.sqlx/||' | sort > /tmp/main_files.txt + +find backend/.sqlx -name "*.json" -printf '%P\n' | sort > /tmp/current_files.txt + +comm -23 /tmp/main_files.txt /tmp/current_files.txt > /tmp/missing_files.txt + +while read f; do + git show "origin/main:backend/.sqlx/$f" > "backend/.sqlx/$f" +done < /tmp/missing_files.txt + +# 4. Verify nothing was lost from main +find backend/.sqlx -name "*.json" -printf '%P\n' | sort > /tmp/current_files.txt +comm -23 /tmp/main_files.txt /tmp/current_files.txt | wc -l +# Should output: 0 +``` + +## If EE Compiles Locally + +If your EE repo happens to be in sync, you can use the full script (faster): + +```bash +cd backend +./update_sqlx.sh +``` + +But if it fails with EE compilation errors, use the safe procedure above. + +## What NOT to Do + +- **Never** run `cargo sqlx prepare --workspace` with only OSS features and commit the result — it will delete EE caches. +- **Never** set `SQLX_OFFLINE=true` for local `cargo sqlx prepare` — use a live database per CLAUDE.md. (CI runs with `SQLX_OFFLINE=true`, which is why the cache must be complete.) +- **Never** skip the verification step (step 4 above). + +## Verification + +After committing, the diff against `origin/main` should show: +- A few **new** cache files (for your changed queries) +- A few **deleted** cache files (for old queries that no longer exist) +- **Zero** net deletions from the EE cache set + +```bash +git diff origin/main --stat backend/.sqlx/ +``` diff --git a/backend/.sqlx/query-ba9ab074f466bc2c581f018e2592f5a453e8a766c35dbf919d29c96966d63c75.json b/backend/.sqlx/query-25784f87ccf0bc13b93739d34d416908b75d0d17392c2565e70b4738039b56a1.json similarity index 75% rename from backend/.sqlx/query-ba9ab074f466bc2c581f018e2592f5a453e8a766c35dbf919d29c96966d63c75.json rename to backend/.sqlx/query-25784f87ccf0bc13b93739d34d416908b75d0d17392c2565e70b4738039b56a1.json index c7114f6591..8cae07c761 100644 --- a/backend/.sqlx/query-ba9ab074f466bc2c581f018e2592f5a453e8a766c35dbf919d29c96966d63c75.json +++ b/backend/.sqlx/query-25784f87ccf0bc13b93739d34d416908b75d0d17392c2565e70b4738039b56a1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM group_ WHERE workspace_id = $1 ORDER BY name asc LIMIT $2 OFFSET $3", + "query": "SELECT workspace_id, name, summary, extra_perms FROM group_ WHERE workspace_id = $1 ORDER BY name asc LIMIT $2 OFFSET $3", "describe": { "columns": [ { @@ -38,5 +38,5 @@ false ] }, - "hash": "ba9ab074f466bc2c581f018e2592f5a453e8a766c35dbf919d29c96966d63c75" + "hash": "25784f87ccf0bc13b93739d34d416908b75d0d17392c2565e70b4738039b56a1" } diff --git a/backend/.sqlx/query-45e4d13f5806122faecdb1d9ab18159555b652869a036b006f4a151e999b17b7.json b/backend/.sqlx/query-27b243e7ff9838a8fc0a4a8e51ac5d33d8617fc75c0d9d08e428db488e0be409.json similarity index 79% rename from backend/.sqlx/query-45e4d13f5806122faecdb1d9ab18159555b652869a036b006f4a151e999b17b7.json rename to backend/.sqlx/query-27b243e7ff9838a8fc0a4a8e51ac5d33d8617fc75c0d9d08e428db488e0be409.json index 12b8201ebb..ba1ffe0ac6 100644 --- a/backend/.sqlx/query-45e4d13f5806122faecdb1d9ab18159555b652869a036b006f4a151e999b17b7.json +++ b/backend/.sqlx/query-27b243e7ff9838a8fc0a4a8e51ac5d33d8617fc75c0d9d08e428db488e0be409.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM resource WHERE workspace_id = $1 AND resource_type != 'state' AND resource_type != 'cache'", + "query": "SELECT workspace_id, path, value, description, resource_type, extra_perms, created_by, edited_at, labels FROM resource WHERE workspace_id = $1 AND resource_type != 'state' AND resource_type != 'cache'", "describe": { "columns": [ { @@ -35,13 +35,13 @@ }, { "ordinal": 6, - "name": "edited_at", - "type_info": "Timestamptz" + "name": "created_by", + "type_info": "Varchar" }, { "ordinal": 7, - "name": "created_by", - "type_info": "Varchar" + "name": "edited_at", + "type_info": "Timestamptz" }, { "ordinal": 8, @@ -66,5 +66,5 @@ true ] }, - "hash": "45e4d13f5806122faecdb1d9ab18159555b652869a036b006f4a151e999b17b7" + "hash": "27b243e7ff9838a8fc0a4a8e51ac5d33d8617fc75c0d9d08e428db488e0be409" } diff --git a/backend/.sqlx/query-d9c8f6ec7bd10e533876526255c15e376ccb4f898b9c0ab8840b2930bda24fdc.json b/backend/.sqlx/query-2c9a56fd46767c15dabc7813cd2b167a2f73a06d661d31c375697a68f7066aa9.json similarity index 76% rename from backend/.sqlx/query-d9c8f6ec7bd10e533876526255c15e376ccb4f898b9c0ab8840b2930bda24fdc.json rename to backend/.sqlx/query-2c9a56fd46767c15dabc7813cd2b167a2f73a06d661d31c375697a68f7066aa9.json index 463a3f32f0..7ca04a8311 100644 --- a/backend/.sqlx/query-d9c8f6ec7bd10e533876526255c15e376ccb4f898b9c0ab8840b2930bda24fdc.json +++ b/backend/.sqlx/query-2c9a56fd46767c15dabc7813cd2b167a2f73a06d661d31c375697a68f7066aa9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM group_ WHERE name = $1 AND workspace_id = $2", + "query": "SELECT workspace_id, name, summary, extra_perms FROM group_ WHERE name = $1 AND workspace_id = $2", "describe": { "columns": [ { @@ -37,5 +37,5 @@ false ] }, - "hash": "d9c8f6ec7bd10e533876526255c15e376ccb4f898b9c0ab8840b2930bda24fdc" + "hash": "2c9a56fd46767c15dabc7813cd2b167a2f73a06d661d31c375697a68f7066aa9" } diff --git a/backend/.sqlx/query-03d63d2e64b012f624d2731b5bcb8849c74a9474777be61edf0ed43ddda07ef3.json b/backend/.sqlx/query-45d5e9ead8193a04fd00c44a488590fdd2f7c4de45117a18360651655d153545.json similarity index 81% rename from backend/.sqlx/query-03d63d2e64b012f624d2731b5bcb8849c74a9474777be61edf0ed43ddda07ef3.json rename to backend/.sqlx/query-45d5e9ead8193a04fd00c44a488590fdd2f7c4de45117a18360651655d153545.json index 31ec682852..e4db87ec7d 100644 --- a/backend/.sqlx/query-03d63d2e64b012f624d2731b5bcb8849c74a9474777be61edf0ed43ddda07ef3.json +++ b/backend/.sqlx/query-45d5e9ead8193a04fd00c44a488590fdd2f7c4de45117a18360651655d153545.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins')", + "query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset FROM resource_type WHERE workspace_id = $1", "describe": { "columns": [ { @@ -25,13 +25,13 @@ }, { "ordinal": 4, - "name": "edited_at", - "type_info": "Timestamptz" + "name": "created_by", + "type_info": "Varchar" }, { "ordinal": 5, - "name": "created_by", - "type_info": "Varchar" + "name": "edited_at", + "type_info": "Timestamptz" }, { "ordinal": 6, @@ -46,7 +46,6 @@ ], "parameters": { "Left": [ - "Text", "Text" ] }, @@ -61,5 +60,5 @@ false ] }, - "hash": "03d63d2e64b012f624d2731b5bcb8849c74a9474777be61edf0ed43ddda07ef3" + "hash": "45d5e9ead8193a04fd00c44a488590fdd2f7c4de45117a18360651655d153545" } diff --git a/backend/.sqlx/query-5d6adbe21b9f8dd984d1bfc750fb81763d8650c1316bb0b20816f1a5d61a678c.json b/backend/.sqlx/query-500b68d23314cb0f9edd570e576d5b9822bebe88f90ae007d943f2c761858190.json similarity index 78% rename from backend/.sqlx/query-5d6adbe21b9f8dd984d1bfc750fb81763d8650c1316bb0b20816f1a5d61a678c.json rename to backend/.sqlx/query-500b68d23314cb0f9edd570e576d5b9822bebe88f90ae007d943f2c761858190.json index 79625b6baf..bb8731d445 100644 --- a/backend/.sqlx/query-5d6adbe21b9f8dd984d1bfc750fb81763d8650c1316bb0b20816f1a5d61a678c.json +++ b/backend/.sqlx/query-500b68d23314cb0f9edd570e576d5b9822bebe88f90ae007d943f2c761858190.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT *\n FROM usr\n WHERE workspace_id = $1\n ", + "query": "SELECT workspace_id, username, email, is_admin, created_at, operator, disabled, role, added_via FROM usr\n WHERE workspace_id = $1", "describe": { "columns": [ { @@ -47,11 +47,6 @@ "ordinal": 8, "name": "added_via", "type_info": "Jsonb" - }, - { - "ordinal": 9, - "name": "is_service_account", - "type_info": "Bool" } ], "parameters": { @@ -68,9 +63,8 @@ false, false, true, - true, - false + true ] }, - "hash": "5d6adbe21b9f8dd984d1bfc750fb81763d8650c1316bb0b20816f1a5d61a678c" + "hash": "500b68d23314cb0f9edd570e576d5b9822bebe88f90ae007d943f2c761858190" } diff --git a/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json b/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json index 36ddb8ab9f..713ccb9dd3 100644 --- a/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json +++ b/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json @@ -15,7 +15,7 @@ ] }, "nullable": [ - true + null ] }, "hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55" diff --git a/backend/.sqlx/query-b8d392ccfcccafe0c19511b3567bc11779b1052b0948c410468a8aeba1d26d33.json b/backend/.sqlx/query-623b061ccaa6bb883e95771fde8c911a165c9c430b7db389370361ca74d737f4.json similarity index 78% rename from backend/.sqlx/query-b8d392ccfcccafe0c19511b3567bc11779b1052b0948c410468a8aeba1d26d33.json rename to backend/.sqlx/query-623b061ccaa6bb883e95771fde8c911a165c9c430b7db389370361ca74d737f4.json index 4871744e03..d2084d76d8 100644 --- a/backend/.sqlx/query-b8d392ccfcccafe0c19511b3567bc11779b1052b0948c410468a8aeba1d26d33.json +++ b/backend/.sqlx/query-623b061ccaa6bb883e95771fde8c911a165c9c430b7db389370361ca74d737f4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'admins') ORDER BY name", + "query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins')", "describe": { "columns": [ { @@ -25,13 +25,13 @@ }, { "ordinal": 4, - "name": "edited_at", - "type_info": "Timestamptz" + "name": "created_by", + "type_info": "Varchar" }, { "ordinal": 5, - "name": "created_by", - "type_info": "Varchar" + "name": "edited_at", + "type_info": "Timestamptz" }, { "ordinal": 6, @@ -46,6 +46,7 @@ ], "parameters": { "Left": [ + "Text", "Text" ] }, @@ -60,5 +61,5 @@ false ] }, - "hash": "b8d392ccfcccafe0c19511b3567bc11779b1052b0948c410468a8aeba1d26d33" + "hash": "623b061ccaa6bb883e95771fde8c911a165c9c430b7db389370361ca74d737f4" } diff --git a/backend/.sqlx/query-ce9e56ff451bae10af2c396352f5f93f78658e57b79dc5295553cacc328eb2b7.json b/backend/.sqlx/query-6f95d6927a9be75f6c371c799ebe3755142647d0257b76e9befd0c4f31c332b1.json similarity index 70% rename from backend/.sqlx/query-ce9e56ff451bae10af2c396352f5f93f78658e57b79dc5295553cacc328eb2b7.json rename to backend/.sqlx/query-6f95d6927a9be75f6c371c799ebe3755142647d0257b76e9befd0c4f31c332b1.json index b64a1eb468..c97cc9ff8d 100644 --- a/backend/.sqlx/query-ce9e56ff451bae10af2c396352f5f93f78658e57b79dc5295553cacc328eb2b7.json +++ b/backend/.sqlx/query-6f95d6927a9be75f6c371c799ebe3755142647d0257b76e9befd0c4f31c332b1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM config WHERE name LIKE 'worker__%'", + "query": "SELECT name, config FROM config WHERE name LIKE 'worker__%'", "describe": { "columns": [ { @@ -22,5 +22,5 @@ true ] }, - "hash": "ce9e56ff451bae10af2c396352f5f93f78658e57b79dc5295553cacc328eb2b7" + "hash": "6f95d6927a9be75f6c371c799ebe3755142647d0257b76e9befd0c4f31c332b1" } diff --git a/backend/.sqlx/query-e5fb3531f8bc7ef1f7484524f8c3bc9c48f71a44827ba0d01ac5588dc31082a2.json b/backend/.sqlx/query-704d873d4585e00aa0a88cc9949d3a6cc806b33a844b8e0e600cb8bfae428e84.json similarity index 81% rename from backend/.sqlx/query-e5fb3531f8bc7ef1f7484524f8c3bc9c48f71a44827ba0d01ac5588dc31082a2.json rename to backend/.sqlx/query-704d873d4585e00aa0a88cc9949d3a6cc806b33a844b8e0e600cb8bfae428e84.json index 7be961c050..9f80167752 100644 --- a/backend/.sqlx/query-e5fb3531f8bc7ef1f7484524f8c3bc9c48f71a44827ba0d01ac5588dc31082a2.json +++ b/backend/.sqlx/query-704d873d4585e00aa0a88cc9949d3a6cc806b33a844b8e0e600cb8bfae428e84.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM usr\n WHERE workspace_id = $1", + "query": "\n SELECT workspace_id, username, email, is_admin, created_at, operator, disabled, role, added_via, is_service_account\n FROM usr\n WHERE workspace_id = $1\n ", "describe": { "columns": [ { @@ -72,5 +72,5 @@ false ] }, - "hash": "e5fb3531f8bc7ef1f7484524f8c3bc9c48f71a44827ba0d01ac5588dc31082a2" + "hash": "704d873d4585e00aa0a88cc9949d3a6cc806b33a844b8e0e600cb8bfae428e84" } diff --git a/backend/.sqlx/query-60b3a59805d463a61eed68072d1ea032b00fc9bd7a6db22f530f67eb9730fa3b.json b/backend/.sqlx/query-8f308f841b2d60a6be2bc4ae6aeae506781295a3bcf036dbbc8600f229e61408.json similarity index 83% rename from backend/.sqlx/query-60b3a59805d463a61eed68072d1ea032b00fc9bd7a6db22f530f67eb9730fa3b.json rename to backend/.sqlx/query-8f308f841b2d60a6be2bc4ae6aeae506781295a3bcf036dbbc8600f229e61408.json index ed09f2833f..920ce385f3 100644 --- a/backend/.sqlx/query-60b3a59805d463a61eed68072d1ea032b00fc9bd7a6db22f530f67eb9730fa3b.json +++ b/backend/.sqlx/query-8f308f841b2d60a6be2bc4ae6aeae506781295a3bcf036dbbc8600f229e61408.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM usr WHERE username = $1 AND workspace_id = $2", + "query": "SELECT workspace_id, username, email, is_admin, created_at, operator, disabled, role, added_via, is_service_account FROM usr WHERE username = $1 AND workspace_id = $2", "describe": { "columns": [ { @@ -73,5 +73,5 @@ false ] }, - "hash": "60b3a59805d463a61eed68072d1ea032b00fc9bd7a6db22f530f67eb9730fa3b" + "hash": "8f308f841b2d60a6be2bc4ae6aeae506781295a3bcf036dbbc8600f229e61408" } diff --git a/backend/.sqlx/query-d233e07d19e8e339e1378c1bfc5d78d592c00ffb6f42c3d072f56305b40e50f9.json b/backend/.sqlx/query-c9064664829d304013920e3f710c4dfab99b263e5271045c542c34216e2b47cb.json similarity index 73% rename from backend/.sqlx/query-d233e07d19e8e339e1378c1bfc5d78d592c00ffb6f42c3d072f56305b40e50f9.json rename to backend/.sqlx/query-c9064664829d304013920e3f710c4dfab99b263e5271045c542c34216e2b47cb.json index b235a60c1a..1eb8cf3595 100644 --- a/backend/.sqlx/query-d233e07d19e8e339e1378c1bfc5d78d592c00ffb6f42c3d072f56305b40e50f9.json +++ b/backend/.sqlx/query-c9064664829d304013920e3f710c4dfab99b263e5271045c542c34216e2b47cb.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM config WHERE name = $1", + "query": "SELECT name, config FROM config WHERE name = $1", "describe": { "columns": [ { @@ -24,5 +24,5 @@ true ] }, - "hash": "d233e07d19e8e339e1378c1bfc5d78d592c00ffb6f42c3d072f56305b40e50f9" + "hash": "c9064664829d304013920e3f710c4dfab99b263e5271045c542c34216e2b47cb" } diff --git a/backend/.sqlx/query-7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234.json b/backend/.sqlx/query-d0a95698b9a2c5e2543e94276d854d7e509c7db2c2ac7d395b7b53ad5dbc25e6.json similarity index 79% rename from backend/.sqlx/query-7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234.json rename to backend/.sqlx/query-d0a95698b9a2c5e2543e94276d854d7e509c7db2c2ac7d395b7b53ad5dbc25e6.json index 29f4baacc2..ffd1670c04 100644 --- a/backend/.sqlx/query-7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234.json +++ b/backend/.sqlx/query-d0a95698b9a2c5e2543e94276d854d7e509c7db2c2ac7d395b7b53ad5dbc25e6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM resource_type WHERE workspace_id = $1", + "query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset from resource_type WHERE (workspace_id = $1 OR workspace_id = 'admins') ORDER BY name", "describe": { "columns": [ { @@ -25,13 +25,13 @@ }, { "ordinal": 4, - "name": "edited_at", - "type_info": "Timestamptz" + "name": "created_by", + "type_info": "Varchar" }, { "ordinal": 5, - "name": "created_by", - "type_info": "Varchar" + "name": "edited_at", + "type_info": "Timestamptz" }, { "ordinal": 6, @@ -60,5 +60,5 @@ false ] }, - "hash": "7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234" + "hash": "d0a95698b9a2c5e2543e94276d854d7e509c7db2c2ac7d395b7b53ad5dbc25e6" } diff --git a/backend/.sqlx/query-eb1f7f01461f5a7540c273b37e5d578c31cf151ab3ef813f7aada76533761e12.json b/backend/.sqlx/query-e253b9e7e6450652589d6ee7ffa86d600e449cd399ac781af8b40c1c444972c3.json similarity index 82% rename from backend/.sqlx/query-eb1f7f01461f5a7540c273b37e5d578c31cf151ab3ef813f7aada76533761e12.json rename to backend/.sqlx/query-e253b9e7e6450652589d6ee7ffa86d600e449cd399ac781af8b40c1c444972c3.json index ce532af385..c44d3d711d 100644 --- a/backend/.sqlx/query-eb1f7f01461f5a7540c273b37e5d578c31cf151ab3ef813f7aada76533761e12.json +++ b/backend/.sqlx/query-e253b9e7e6450652589d6ee7ffa86d600e449cd399ac781af8b40c1c444972c3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * from resource_type ORDER BY name", + "query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset from resource_type ORDER BY name", "describe": { "columns": [ { @@ -25,13 +25,13 @@ }, { "ordinal": 4, - "name": "edited_at", - "type_info": "Timestamptz" + "name": "created_by", + "type_info": "Varchar" }, { "ordinal": 5, - "name": "created_by", - "type_info": "Varchar" + "name": "edited_at", + "type_info": "Timestamptz" }, { "ordinal": 6, @@ -58,5 +58,5 @@ false ] }, - "hash": "eb1f7f01461f5a7540c273b37e5d578c31cf151ab3ef813f7aada76533761e12" + "hash": "e253b9e7e6450652589d6ee7ffa86d600e449cd399ac781af8b40c1c444972c3" } diff --git a/backend/tests/workspace_export.rs b/backend/tests/workspace_export.rs new file mode 100644 index 0000000000..823ff22a43 --- /dev/null +++ b/backend/tests/workspace_export.rs @@ -0,0 +1,198 @@ +use sqlx::postgres::Postgres; +use sqlx::Pool; +use windmill_test_utils::{initialize_tracing, ApiServer}; + +/// Integration test: exercises every explicit-column query in `tarball_workspace`. +/// +/// Creates one entity of each type (folder, script, resource, resource_type, +/// variable, schedule, group) in the test workspace, then calls the tarball +/// export endpoint with all include_* flags enabled. Success means every +/// `SELECT col1, col2, ...` list matches the database schema. +/// +/// Tables exercised (one explicit-column query each): +/// folder, script, resource, resource_type, variable, schedule, usr, group_ +#[sqlx::test(fixtures("base"))] +async fn test_tarball_export_all_tables(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let base_url = format!("http://localhost:{port}"); + + let client = windmill_api_client::create_client(&base_url, "SECRET_TOKEN".to_string()); + let http = client.client(); + + // ---- folder ---- + sqlx::query( + r#"INSERT INTO folder + (workspace_id, name, display_name, owners, extra_perms, summary) + VALUES ($1, $2, $3, $4, '{}'::jsonb, $5)"#, + ) + .bind("test-workspace") + .bind("test_folder") + .bind("Test Folder") + .bind(vec!["u/test-user"]) + .bind("a test folder") + .execute(&db) + .await?; + + // ---- script (exercises the 30-column Script query) ---- + client + .create_script( + "test-workspace", + &windmill_api_client::types::NewScript { + content: "export function main() { return 42; }".to_string(), + language: windmill_api_client::types::ScriptLang::Bun, + path: "f/test_folder/test_script".to_string(), + summary: "test script".to_string(), + description: "script for export test".to_string(), + kind: Some("script".to_string()), + tag: Some("test".to_string()), + lock: None, + parent_hash: None, + schema: Default::default(), + is_template: None, + draft_only: None, + dedicated_worker: None, + ws_error_handler_muted: None, + priority: None, + cache_ttl: None, + concurrent_limit: None, + concurrency_time_window_s: None, + timeout: None, + delete_after_secs: None, + restart_unless_cancelled: None, + visible_to_runner_only: None, + auto_kind: None, + on_behalf_of_email: None, + has_preprocessor: None, + codebase: None, + envs: vec![], + deployment_message: None, + assets: vec![], + modules: None, + concurrency_key: None, + }, + ) + .await?; + + // ---- resource ---- + sqlx::query( + r#"INSERT INTO resource + (workspace_id, path, value, description, resource_type, created_by) + VALUES ($1, $2, $3, $4, $5, $6)"#, + ) + .bind("test-workspace") + .bind("f/test_folder/test_res") + .bind(serde_json::json!({"url": "http://example.com"})) + .bind("test resource") + .bind("http") + .bind("test-user") + .execute(&db) + .await?; + + // ---- resource_type ---- + sqlx::query( + r#"INSERT INTO resource_type + (workspace_id, name, schema, description, created_by) + VALUES ($1, $2, $3, $4, $5)"#, + ) + .bind("test-workspace") + .bind("http") + .bind(serde_json::json!({"type": "object"})) + .bind("HTTP resource type") + .bind("system") + .execute(&db) + .await?; + + // ---- variable ---- + sqlx::query( + r#"INSERT INTO variable + (workspace_id, path, value, is_secret, description, account) + VALUES ($1, $2, $3, $4, $5, $6)"#, + ) + .bind("test-workspace") + .bind("f/test_folder/test_var") + .bind("test_value") + .bind(false) + .bind("test variable") + .bind(None::) + .execute(&db) + .await?; + + // ---- schedule ---- + client + .create_schedule( + "test-workspace", + &windmill_api_client::types::NewSchedule { + schedule: "0 0 0 * * *".to_string(), + script_path: "f/test_folder/test_script".to_string(), + path: "f/test_folder/test_schedule".to_string(), + is_flow: false, + timezone: "UTC".to_string(), + args: Default::default(), + enabled: Some(false), + description: Some("test schedule".to_string()), + summary: Some("test schedule".to_string()), + tag: None, + cron_version: Some("v2".to_string()), + on_failure: None, + on_failure_times: None, + on_failure_exact: None, + on_failure_extra_args: None, + on_recovery: None, + on_recovery_times: None, + on_recovery_extra_args: None, + on_success: None, + on_success_extra_args: None, + ws_error_handler_muted: None, + retry: None, + no_flow_overlap: None, + }, + ) + .await?; + + // ---- group_ (base fixture already has "all", create one more for include_groups) ---- + sqlx::query( + "INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, '{}'::jsonb)", + ) + .bind("test-workspace") + .bind("testgroup") + .bind("test group") + .execute(&db) + .await?; + + // ---- tarball export: hits ALL explicit-column queries at once ---- + let params = [ + "archive_type=tar", + "include_schedules=true", + "include_users=true", + "include_groups=true", + "include_settings=true", + "include_workspace_dependencies=true", + "settings_version=v1", + ]; + + let resp = http + .get(format!( + "{}/api/w/test-workspace/workspaces/tarball?{}", + base_url, + params.join("&") + )) + .bearer_auth("SECRET_TOKEN") + .send() + .await?; + + assert_eq!( + resp.status(), + 200, + "tarball export failed: {}", + resp.text().await.unwrap_or_default() + ); + + // Verify we got actual bytes back + let body = resp.bytes().await?; + assert!(!body.is_empty(), "tarball export returned empty body"); + + Ok(()) +} + diff --git a/backend/windmill-api-configs/src/lib.rs b/backend/windmill-api-configs/src/lib.rs index 0901e73e67..e18afc35c8 100644 --- a/backend/windmill-api-configs/src/lib.rs +++ b/backend/windmill-api-configs/src/lib.rs @@ -64,7 +64,7 @@ async fn list_worker_groups( Extension(db): Extension, ) -> error::JsonResult> { let mut configs_raw = - sqlx::query_as!(Config, "SELECT * FROM config WHERE name LIKE 'worker__%'") + sqlx::query_as!(Config, "SELECT name, config FROM config WHERE name LIKE 'worker__%'") .fetch_all(&db) .await?; // Remove the 'worker__' prefix from all config names @@ -119,7 +119,7 @@ async fn get_config( ) -> error::JsonResult> { require_devops_role(&db, &authed.email).await?; - let config = sqlx::query_as!(Config, "SELECT * FROM config WHERE name = $1", name) + let config = sqlx::query_as!(Config, "SELECT name, config FROM config WHERE name = $1", name) .fetch_optional(&db) .await? .map(|c| c.config); diff --git a/backend/windmill-api-embeddings/src/lib.rs b/backend/windmill-api-embeddings/src/lib.rs index ae8eca67cf..f0c87633d2 100644 --- a/backend/windmill-api-embeddings/src/lib.rs +++ b/backend/windmill-api-embeddings/src/lib.rs @@ -365,7 +365,7 @@ impl EmbeddingsDb { let hub_resource_types = response.json::>().await?; let resource_types: Vec = - sqlx::query_as!(ResourceType, "SELECT * from resource_type ORDER BY name",) + sqlx::query_as!(ResourceType, "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset from resource_type ORDER BY name",) .fetch_all(pg_db) .await?; diff --git a/backend/windmill-api-groups/src/groups.rs b/backend/windmill-api-groups/src/groups.rs index af67f884c9..263daad624 100644 --- a/backend/windmill-api-groups/src/groups.rs +++ b/backend/windmill-api-groups/src/groups.rs @@ -109,7 +109,7 @@ async fn list_groups( let rows = sqlx::query_as!( Group, - "SELECT * FROM group_ WHERE workspace_id = $1 ORDER BY name asc LIMIT $2 OFFSET $3", + "SELECT workspace_id, name, summary, extra_perms FROM group_ WHERE workspace_id = $1 ORDER BY name asc LIMIT $2 OFFSET $3", w_id, per_page as i64, offset as i64 @@ -574,7 +574,7 @@ pub async fn get_group_opt<'c>( ) -> Result> { let group_opt = sqlx::query_as!( Group, - "SELECT * FROM group_ WHERE name = $1 AND workspace_id = $2", + "SELECT workspace_id, name, summary, extra_perms FROM group_ WHERE name = $1 AND workspace_id = $2", name, w_id ) diff --git a/backend/windmill-api-scripts/src/scripts.rs b/backend/windmill-api-scripts/src/scripts.rs index 426b92ac17..de0f38cd34 100644 --- a/backend/windmill-api-scripts/src/scripts.rs +++ b/backend/windmill-api-scripts/src/scripts.rs @@ -979,7 +979,10 @@ async fn create_script_internal<'c>( .await?; } let clashing_script = sqlx::query_as::<_, Script>( - "SELECT * FROM script WHERE path = $1 AND archived = false AND workspace_id = $2", + &format!( + "SELECT {} FROM script WHERE path = $1 AND archived = false AND workspace_id = $2", + windmill_common::scripts::SCRIPT_COLUMNS, + ), ) .bind(&ns.path) .bind(&w_id) @@ -1762,7 +1765,10 @@ async fn get_script_by_path( .await? } else { sqlx::query_as::<_, ScriptWithStarred>( - "SELECT *, NULL as starred FROM script WHERE path = $1 AND workspace_id = $2 ORDER BY created_at DESC LIMIT 1", + &format!( + "SELECT {}, NULL as starred FROM script WHERE path = $1 AND workspace_id = $2 ORDER BY created_at DESC LIMIT 1", + windmill_common::scripts::SCRIPT_COLUMNS, + ), ) .bind(path) .bind(w_id) @@ -2277,7 +2283,10 @@ async fn get_script_by_hash_internal<'c>( .await? } else { sqlx::query_as::<_, ScriptWithStarred>( - "SELECT *, NULL as starred FROM script WHERE hash = $1 AND workspace_id = $2", + &format!( + "SELECT {}, NULL as starred FROM script WHERE hash = $1 AND workspace_id = $2", + windmill_common::scripts::SCRIPT_COLUMNS, + ), ) .bind(hash) .bind(workspace_id) diff --git a/backend/windmill-api-users/src/users.rs b/backend/windmill-api-users/src/users.rs index f664ab09b8..382a69201b 100644 --- a/backend/windmill-api-users/src/users.rs +++ b/backend/windmill-api-users/src/users.rs @@ -385,7 +385,7 @@ async fn list_users( let rows = sqlx::query_as!( User, " - SELECT * + SELECT workspace_id, username, email, is_admin, created_at, operator, disabled, role, added_via, is_service_account FROM usr WHERE workspace_id = $1 ", @@ -1174,7 +1174,7 @@ async fn get_workspace_user( let user = sqlx::query_as!( User, - "SELECT * FROM usr WHERE username = $1 AND workspace_id = $2", + "SELECT workspace_id, username, email, is_admin, created_at, operator, disabled, role, added_via, is_service_account FROM usr WHERE username = $1 AND workspace_id = $2", &username_to_update, &w_id ) diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index 91b9ab972e..e6b3b9ee0f 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -490,7 +490,7 @@ pub(crate) async fn tarball_workspace( Some(t) => Err(Error::BadRequest(format!("Invalid Archive Type {t}"))), }?; { - let folders = sqlx::query_as::<_, Folder>("SELECT * FROM folder WHERE workspace_id = $1") + let folders = sqlx::query_as::<_, Folder>("SELECT name, workspace_id, display_name, owners, extra_perms, summary, edited_at, created_by, default_permissioned_as FROM folder WHERE workspace_id = $1") .bind(&w_id) .fetch_all(&mut *tx) .await?; @@ -507,10 +507,13 @@ pub(crate) async fn tarball_workspace( { let scripts = sqlx::query_as::<_, Script>( - "SELECT * FROM script as o WHERE workspace_id = $1 AND archived = false - AND (draft_only IS NULL OR draft_only = false) - AND created_at = (select max(created_at) from script where path = o.path AND \ - workspace_id = $1)", + &format!( + "SELECT {} FROM script as o WHERE workspace_id = $1 AND archived = false + AND (draft_only IS NULL OR draft_only = false) + AND created_at = (select max(created_at) from script where path = o.path AND \ + workspace_id = $1)", + windmill_common::scripts::SCRIPT_COLUMNS, + ), ) .bind(&w_id) .fetch_all(&mut *tx) @@ -595,7 +598,7 @@ pub(crate) async fn tarball_workspace( if !skip_resources.unwrap_or(false) { let resources = sqlx::query_as!( Resource, - "SELECT * FROM resource WHERE workspace_id = $1 AND resource_type != 'state' AND resource_type != 'cache'", + "SELECT workspace_id, path, value, description, resource_type, extra_perms, created_by, edited_at, labels FROM resource WHERE workspace_id = $1 AND resource_type != 'state' AND resource_type != 'cache'", &w_id ) .fetch_all(&mut *tx) @@ -612,7 +615,7 @@ pub(crate) async fn tarball_workspace( if !skip_resource_types.unwrap_or(false) { let resource_types = sqlx::query_as!( ResourceType, - "SELECT * FROM resource_type WHERE workspace_id = $1", + "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset FROM resource_type WHERE workspace_id = $1", &w_id ) .fetch_all(&mut *tx) @@ -651,9 +654,9 @@ pub(crate) async fn tarball_workspace( if !skip_variables.unwrap_or(false) { let variables = sqlx::query_as::<_, ExportableListableVariable>(if !skip_secrets.unwrap_or(false) { - "SELECT * FROM variable WHERE workspace_id = $1 AND expires_at IS NULL" + "SELECT workspace_id, path, value, is_secret, description, extra_perms, account, is_oauth, expires_at, labels FROM variable WHERE workspace_id = $1 AND expires_at IS NULL" } else { - "SELECT * FROM variable WHERE workspace_id = $1 AND is_secret = false AND expires_at IS NULL" + "SELECT workspace_id, path, value, is_secret, description, extra_perms, account, is_oauth, expires_at, labels FROM variable WHERE workspace_id = $1 AND is_secret = false AND expires_at IS NULL" }) .bind(&w_id) .fetch_all(&mut *tx) @@ -727,7 +730,7 @@ pub(crate) async fn tarball_workspace( if include_schedules.unwrap_or(false) { let schedules = sqlx::query_as::<_, Schedule>( - "SELECT * FROM schedule + "SELECT workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, args, extra_perms, email, permissioned_as, error, on_failure, on_failure_times, on_failure_exact, on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, on_success, on_success_extra_args, ws_error_handler_muted, retry, no_flow_overlap, summary, description, tag, paused_until, cron_version, dynamic_skip, labels FROM schedule WHERE workspace_id = $1", ) .bind(&w_id) @@ -998,7 +1001,7 @@ pub(crate) async fn tarball_workspace( if include_users.unwrap_or(false) { let users = sqlx::query!( - "SELECT * FROM usr + "SELECT workspace_id, username, email, is_admin, created_at, operator, disabled, role, added_via FROM usr WHERE workspace_id = $1", &w_id ) diff --git a/backend/windmill-common/src/query_builders.rs b/backend/windmill-common/src/query_builders.rs index e51d24fa44..f077687d6f 100644 --- a/backend/windmill-common/src/query_builders.rs +++ b/backend/windmill-common/src/query_builders.rs @@ -2430,18 +2430,30 @@ WHERE table_schema = current_schema()", Ok(q) } DbType::Mysql => { - let db_name = database_name.unwrap_or(""); + let explicit_db = database_name.filter(|s| !s.is_empty()); let table_filter = if let Some(t) = table { let parts: Vec<&str> = t.split('.').collect(); let tname = parts[parts.len() - 1]; - let schema = if parts.len() > 1 { parts[0] } else { db_name }; + let schema_sql = if parts.len() > 1 { + format!("'{}'", escape_sql_literal(parts[0])) + } else { + explicit_db + .map(|dn| format!("'{}'", escape_sql_literal(dn))) + .unwrap_or_else(|| "DATABASE()".to_string()) + }; format!( - "\nWHERE\n TABLE_NAME = '{}' AND TABLE_SCHEMA = '{}'", + "\nWHERE\n TABLE_NAME = '{}' AND TABLE_SCHEMA = {}", escape_sql_literal(tname), - escape_sql_literal(schema) + schema_sql ) } else { - "\nWHERE\n TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys', '_vt')".to_string() + let schema_predicate = explicit_db + .map(|dn| format!("TABLE_SCHEMA = '{}'", escape_sql_literal(dn))) + .unwrap_or_else(|| "TABLE_SCHEMA = DATABASE()".to_string()); + format!( + "\nWHERE\n {}\n AND TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys', '_vt')", + schema_predicate + ) }; let extra_col = if table.is_none() { ",\n TABLE_NAME as table_name" @@ -4250,10 +4262,28 @@ mod tests { } #[test] - fn test_expand_load_table_metadata_mysql_all_tables() { + fn test_expand_load_table_metadata_mysql_single_table_uses_session_db() { + let marker = r#"-- WM_INTERNAL_DB_LOAD_TABLE_METADATA {"table":"users"}"#; + let sql = expand_code(marker, &ScriptLang::Mysql); + assert!(sql.contains("TABLE_NAME = 'users'")); + assert!(sql.contains("TABLE_SCHEMA = DATABASE()")); + } + + #[test] + fn test_expand_load_table_metadata_mysql_all_tables_uses_session_db() { let marker = r#"-- WM_INTERNAL_DB_LOAD_TABLE_METADATA {}"#; let sql = expand_code(marker, &ScriptLang::Mysql); assert!(sql.contains("TABLE_NAME as table_name")); + assert!(sql.contains("TABLE_SCHEMA = DATABASE()")); + assert!(sql.contains("TABLE_SCHEMA NOT IN")); + } + + #[test] + fn test_expand_load_table_metadata_mysql_all_tables_with_database_name() { + let marker = r#"-- WM_INTERNAL_DB_LOAD_TABLE_METADATA {"databaseName":"mydb"}"#; + let sql = expand_code(marker, &ScriptLang::Mysql); + assert!(sql.contains("TABLE_NAME as table_name")); + assert!(sql.contains("TABLE_SCHEMA = 'mydb'")); assert!(sql.contains("TABLE_SCHEMA NOT IN")); } diff --git a/backend/windmill-common/src/scripts.rs b/backend/windmill-common/src/scripts.rs index 192085d902..dd4c0bb42e 100644 --- a/backend/windmill-common/src/scripts.rs +++ b/backend/windmill-common/src/scripts.rs @@ -312,52 +312,10 @@ pub async fn fetch_script_for_update<'a>( e: impl sqlx::Executor<'a, Database = sqlx::Postgres>, ) -> crate::error::Result>> { sqlx::query_as::<_, Script>( - "SELECT - workspace_id, - hash, - path, - parent_hashes, - summary, - description, - content, - created_by, - created_at, - archived, - schema, - deleted, - is_template, - extra_perms, - lock, - lock_error_logs, - language, - kind, - tag, - draft_only, - envs, - concurrency_key, - concurrent_limit, - concurrency_time_window_s, - debounce_key, - debounce_delay_s, - dedicated_worker, - runnable_settings_handle, - ws_error_handler_muted, - priority, - cache_ttl, - cache_ignore_s3_path, - timeout, - delete_after_use, - delete_after_secs, - restart_unless_cancelled, - visible_to_runner_only, - auto_kind, - codebase, - has_preprocessor, - on_behalf_of_email, - assets, - modules, - labels - FROM script WHERE path = $1 AND workspace_id = $2 AND archived = false ORDER BY created_at DESC LIMIT 1 FOR UPDATE", + &format!( + "SELECT {} FROM script WHERE path = $1 AND workspace_id = $2 AND archived = false ORDER BY created_at DESC LIMIT 1 FOR UPDATE", + SCRIPT_COLUMNS, + ), ) .bind(path) .bind(w_id) diff --git a/backend/windmill-mcp/src/client_registration.rs b/backend/windmill-mcp/src/client_registration.rs index 56f992540a..eca4ed650f 100644 --- a/backend/windmill-mcp/src/client_registration.rs +++ b/backend/windmill-mcp/src/client_registration.rs @@ -122,7 +122,7 @@ pub async fn get_or_refresh_mcp_client( let redirect_uri = format!("{}/api/mcp/oauth/callback", base_url); let cached_client: Option = - sqlx::query_as("SELECT * FROM mcp_oauth_client WHERE mcp_server_url = $1") + sqlx::query_as("SELECT mcp_server_url, client_id, client_secret, client_secret_expires_at, token_endpoint FROM mcp_oauth_client WHERE mcp_server_url = $1") .bind(mcp_server_url) .fetch_optional(db) .await diff --git a/backend/windmill-queue/src/schedule.rs b/backend/windmill-queue/src/schedule.rs index 6b0a9904b5..3f6c587be2 100644 --- a/backend/windmill-queue/src/schedule.rs +++ b/backend/windmill-queue/src/schedule.rs @@ -566,7 +566,7 @@ pub async fn get_schedule_opt<'c>( path: &str, ) -> Result> { let schedule_opt = sqlx::query_as::<_, Schedule>( - "SELECT * FROM schedule WHERE path = $1 AND workspace_id = $2", + "SELECT workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, args, extra_perms, email, permissioned_as, error, on_failure, on_failure_times, on_failure_exact, on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, on_success, on_success_extra_args, ws_error_handler_muted, retry, no_flow_overlap, summary, description, tag, paused_until, cron_version, dynamic_skip, labels FROM schedule WHERE path = $1 AND workspace_id = $2", ) .bind(path) .bind(w_id) diff --git a/backend/windmill-store/src/resources.rs b/backend/windmill-store/src/resources.rs index a331542acc..ed471bfaa7 100644 --- a/backend/windmill-store/src/resources.rs +++ b/backend/windmill-store/src/resources.rs @@ -1510,7 +1510,7 @@ async fn list_resource_types( ) -> JsonResult> { let rows = sqlx::query_as!( ResourceType, - "SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'admins') ORDER \ + "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset from resource_type WHERE (workspace_id = $1 OR workspace_id = 'admins') ORDER \ BY name", &w_id ) @@ -1544,7 +1544,7 @@ async fn get_resource_type( let resource_type_o = sqlx::query_as!( ResourceType, - "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins')", + "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins')", &name, &w_id ) diff --git a/backend/windmill-types/src/scripts.rs b/backend/windmill-types/src/scripts.rs index 7bf19e6147..e9e9337e6d 100644 --- a/backend/windmill-types/src/scripts.rs +++ b/backend/windmill-types/src/scripts.rs @@ -318,6 +318,19 @@ pub fn id_to_codebase_info(id: &str) -> CodebaseInfo { CodebaseInfo { is_tar, is_esm } } +/// Column list for `SELECT ... FROM script` when targeting `Script`. +/// Shared across all query sites so a schema change only needs one edit. +pub const SCRIPT_COLUMNS: &str = concat!( + "workspace_id, hash, path, parent_hashes, summary, description, content, ", + "created_by, created_at, archived, schema, deleted, is_template, extra_perms, ", + "lock, lock_error_logs, language, kind, tag, draft_only, envs, ", + "dedicated_worker, ws_error_handler_muted, priority, cache_ttl, cache_ignore_s3_path, ", + "timeout, delete_after_use, delete_after_secs, restart_unless_cancelled, ", + "visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, ", + "assets, modules, labels, concurrency_key, concurrent_limit, ", + "concurrency_time_window_s, debounce_key, debounce_delay_s, runnable_settings_handle", +); + #[derive(Serialize, sqlx::FromRow, Debug)] pub struct Script { pub workspace_id: String,