Merge branch 'main' into glm/resource-type-edition

This commit is contained in:
Guilhem
2026-05-04 11:48:33 +02:00
committed by GitHub
28 changed files with 418 additions and 133 deletions
+1 -3
View File
@@ -52,8 +52,6 @@ chore: upgrade sqlx to 0.7
4. Stage ONLY the modified/relevant files: `git add <file1> <file2> ...`
5. Create the commit with conventional format:
```bash
git commit -m "<type>: <description>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
git commit -m "<type>: <description>"
```
6. Run `git status` to verify the commit succeeded
+82
View File
@@ -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/
```
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
true
null
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
+198
View File
@@ -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<Postgres>) -> 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<ScriptRunnableSettingsHandle> 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::<i32>)
.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(())
}
+2 -2
View File
@@ -64,7 +64,7 @@ async fn list_worker_groups(
Extension(db): Extension<DB>,
) -> error::JsonResult<Vec<Config>> {
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<Option<serde_json::Value>> {
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);
+1 -1
View File
@@ -365,7 +365,7 @@ impl EmbeddingsDb {
let hub_resource_types = response.json::<Vec<HubResourceType>>().await?;
let resource_types: Vec<ResourceType> =
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?;
+2 -2
View File
@@ -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<Option<Group>> {
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
)
+12 -3
View File
@@ -979,7 +979,10 @@ async fn create_script_internal<'c>(
.await?;
}
let clashing_script = sqlx::query_as::<_, Script<ScriptRunnableSettingsHandle>>(
"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<ScriptRunnableSettingsHandle>>(
"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<ScriptRunnableSettingsHandle>>(
"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)
+2 -2
View File
@@ -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
)
+14 -11
View File
@@ -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<ScriptRunnableSettingsHandle>>(
"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
)
+36 -6
View File
@@ -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"));
}
+4 -46
View File
@@ -312,52 +312,10 @@ pub async fn fetch_script_for_update<'a>(
e: impl sqlx::Executor<'a, Database = sqlx::Postgres>,
) -> crate::error::Result<Option<Script<ScriptRunnableSettingsHandle>>> {
sqlx::query_as::<_, Script<ScriptRunnableSettingsHandle>>(
"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)
@@ -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<McpOAuthClient> =
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
+1 -1
View File
@@ -566,7 +566,7 @@ pub async fn get_schedule_opt<'c>(
path: &str,
) -> Result<Option<Schedule>> {
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)
+2 -2
View File
@@ -1510,7 +1510,7 @@ async fn list_resource_types(
) -> JsonResult<Vec<ResourceType>> {
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
)
+13
View File
@@ -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<ScriptRunnableSettingsHandle>`.
/// 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<SR> {
pub workspace_id: String,