mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 08:02:47 +00:00
fix(datatables): keep fork copies from other workspaces and from cleanup while in use
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
649c43e7c1
commit
ce35cf1217
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT format('data table ''%s'' of ''%s''', d.key, ws.workspace_id) AS \"use!\"\n FROM workspace_settings ws,\n jsonb_each(COALESCE(ws.datatable->'datatables', '{}'::jsonb)) d\n WHERE jsonb_typeof(d.value) = 'object'\n AND NOT (ws.workspace_id = $1 AND d.key = $2)\n AND ((d.value->'reference'->>'workspace_id' = $1\n AND d.value->'reference'->>'datatable' = $2)\n OR (d.value->'database'->>'resource_type' = 'instance'\n AND d.value->'database'->>'resource_path' = $3))\n UNION ALL\n SELECT format('Ducklake ''%s'' of ''%s''', l.key, ws.workspace_id)\n FROM workspace_settings ws,\n jsonb_each(COALESCE(ws.ducklake->'ducklakes', '{}'::jsonb)) l\n WHERE jsonb_typeof(l.value) = 'object'\n AND l.value->'catalog'->>'resource_type' = 'instance'\n AND l.value->'catalog'->>'resource_path' = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "use!",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "3c1be20dfcfe86949a8073382efb5ae764046b218dfde6b4e7bbd3b9f42b7d4b"
|
||||
}
|
||||
@@ -3647,13 +3647,15 @@ async fn import_pg_database(
|
||||
resolve_pg_source_checked(&db, &user_db, &authed, &w_id, &req.target).await?;
|
||||
|
||||
if let Some(ref override_dbname) = req.target_dbname_override {
|
||||
// A `wm_fork_` name does not say whose copy it is: every instance database answers to the
|
||||
// same connection user, so an override would reach another workspace's fork copy. Forks
|
||||
// make their own copies, so nothing but a superadmin needs one.
|
||||
if !windmill_api_auth::is_super_admin_authed(&db, &authed).await? {
|
||||
if !override_dbname.starts_with("wm_fork_") {
|
||||
return Err(Error::BadRequest(
|
||||
"Non-superadmin users can only override target dbname with names starting with 'wm_fork_'"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
return Err(Error::BadRequest(
|
||||
"Only superadmins can override the target database: create forks through the \
|
||||
fork wizard or `wmill workspace fork`, which copy each data table themselves"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
target_pg.dbname = override_dbname.clone();
|
||||
}
|
||||
|
||||
@@ -1430,6 +1430,23 @@ pub async fn drop_forked_datatable_databases(
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
match forked_database_uses(&db, &w_id, dt_name, database).await {
|
||||
Ok(uses) if uses.is_empty() => {}
|
||||
Ok(uses) => {
|
||||
errors.push(format!(
|
||||
"Refusing to drop the database of datatable://{dt_name}: still used by {}",
|
||||
uses.join(", ")
|
||||
));
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(format!(
|
||||
"Could not check what still uses the database of datatable://{dt_name}: {e}"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if database.resource_type
|
||||
== windmill_common::workspaces::DataTableCatalogResourceType::Instance
|
||||
{
|
||||
@@ -1531,6 +1548,45 @@ pub async fn drop_forked_datatable_databases(
|
||||
Ok(Json(errors))
|
||||
}
|
||||
|
||||
/// What besides data table `dt_name` of `w_id` still uses its database: a data table anywhere
|
||||
/// pointing at that entry (a child fork keeping it), and for an instance database, any other data
|
||||
/// table entry or Ducklake catalog naming it. Instance databases share one connection user, so
|
||||
/// nothing but these settings tells a copy still in use from one that is not.
|
||||
async fn forked_database_uses(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
dt_name: &str,
|
||||
database: &windmill_common::workspaces::DataTableDatabase,
|
||||
) -> Result<Vec<String>> {
|
||||
let instance_db = (database.resource_type
|
||||
== windmill_common::workspaces::DataTableCatalogResourceType::Instance)
|
||||
.then_some(database.resource_path.as_str());
|
||||
let uses = sqlx::query_scalar!(
|
||||
r#"SELECT format('data table ''%s'' of ''%s''', d.key, ws.workspace_id) AS "use!"
|
||||
FROM workspace_settings ws,
|
||||
jsonb_each(COALESCE(ws.datatable->'datatables', '{}'::jsonb)) d
|
||||
WHERE jsonb_typeof(d.value) = 'object'
|
||||
AND NOT (ws.workspace_id = $1 AND d.key = $2)
|
||||
AND ((d.value->'reference'->>'workspace_id' = $1
|
||||
AND d.value->'reference'->>'datatable' = $2)
|
||||
OR (d.value->'database'->>'resource_type' = 'instance'
|
||||
AND d.value->'database'->>'resource_path' = $3))
|
||||
UNION ALL
|
||||
SELECT format('Ducklake ''%s'' of ''%s''', l.key, ws.workspace_id)
|
||||
FROM workspace_settings ws,
|
||||
jsonb_each(COALESCE(ws.ducklake->'ducklakes', '{}'::jsonb)) l
|
||||
WHERE jsonb_typeof(l.value) = 'object'
|
||||
AND l.value->'catalog'->>'resource_type' = 'instance'
|
||||
AND l.value->'catalog'->>'resource_path' = $3"#,
|
||||
w_id,
|
||||
dt_name,
|
||||
instance_db,
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
Ok(uses)
|
||||
}
|
||||
|
||||
/// Drop this fork workspace's ducklake namespaces: the `wm_fork_*` metadata schema in each
|
||||
/// lake's catalog database, plus (best effort) the fork's `__wm_forks/<wid>/…` data files in
|
||||
/// the workspace storage. Driven by the `fork_ducklake_namespace` registry written at first
|
||||
|
||||
@@ -6159,7 +6159,7 @@ paths:
|
||||
description: "Target database: 'datatable://name' or '$res:path'"
|
||||
target_dbname_override:
|
||||
type: string
|
||||
description: "Override the target database name"
|
||||
description: "Override the target database name (superadmins only)"
|
||||
fork_behavior:
|
||||
type: string
|
||||
enum:
|
||||
|
||||
Reference in New Issue
Block a user