From f798ff4535f44ce1c6258ce716277258d2d176ab Mon Sep 17 00:00:00 2001 From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Date: Thu, 9 Oct 2025 18:28:11 +0200 Subject: [PATCH] feat: Database manager for Ducklake instance catalogs (#6785) * OVERRIDE_DATA_PATH * DB Manager for Ducklake instance catalog debugging --- backend/windmill-api/src/resources.rs | 18 ++- backend/windmill-worker/src/common.rs | 3 +- .../windmill-worker/src/duckdb_executor.rs | 9 ++ .../workspaceSettings/DucklakeSettings.svelte | 113 +++++++++++------- 4 files changed, 96 insertions(+), 47 deletions(-) diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index fc762ec451..c0162ae022 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use crate::{ db::{ApiAuthed, DB}, users::{maybe_refresh_folders, require_owner_of_path, Tokened}, - utils::{check_scopes, BulkDeleteRequest}, + utils::{check_scopes, require_super_admin, BulkDeleteRequest}, var_resource_cache::{cache_resource, get_cached_resource}, webhook_util::{WebhookMessage, WebhookShared}, }; @@ -34,9 +34,11 @@ use windmill_audit::ActionKind; use windmill_common::{ db::{UserDB, UserDbWithOptAuthed}, error::{Error, JsonResult, Result}, + get_database_url, parse_postgres_url, utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath}, variables, worker::CLOUD_HOSTED, + workspaces::get_ducklake_instance_pg_catalog_password, }; pub fn workspaced_service() -> Router { @@ -462,6 +464,20 @@ pub async fn get_resource_value_interpolated_internal( token: &str, allow_cache: bool, ) -> Result> { + // This is a special syntax to help debugging ducklake catalogs stored in the instance + if let Some(dbname) = path.strip_prefix("INSTANCE_DUCKLAKE_CATALOG/") { + require_super_admin(db, &authed.email).await?; + let pg_creds = parse_postgres_url(&get_database_url().await?)?; + return Ok(Some(serde_json::json!({ + "dbname": dbname, + "host": pg_creds.host, + "port": pg_creds.port, + "user": "ducklake_user", + "sslmode": pg_creds.ssl_mode, + "password": get_ducklake_instance_pg_catalog_password(&db).await?, + }))); + } + if allow_cache { if let Some(cached_value) = get_cached_resource(&workspace, &path) { return Ok(Some(cached_value)); diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index ff58d3a41e..38e7b366f6 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -236,7 +236,8 @@ pub async fn transform_json_value( } Value::String(y) if y.starts_with("$res:") => { let path = y.strip_prefix("$res:").unwrap(); - if path.split("/").count() < 2 { + + if path.split("/").count() < 2 && !path.starts_with("INSTANCE_DUCKLAKE_CATALOG/") { return Err(Error::internal_err(format!( "Argument `{name}` is an invalid resource path: {path}", ))); diff --git a/backend/windmill-worker/src/duckdb_executor.rs b/backend/windmill-worker/src/duckdb_executor.rs index 697fe7ff4d..ebc4469028 100644 --- a/backend/windmill-worker/src/duckdb_executor.rs +++ b/backend/windmill-worker/src/duckdb_executor.rs @@ -488,6 +488,15 @@ async fn transform_attach_ducklake( let storage = ducklake.storage.storage.as_deref().unwrap_or("_default_"); let data_path = ducklake.storage.path; + // Ducklake 0.3 only requires DATA_PATH at creation and then stores it internally in the catalog + // But it will fail if DATA_PATH changes afterwards which is annoying for us + // So we always enable override + let extra_args = if extra_args.contains("OVERRIDE_DATA_PATH") { + extra_args + } else { + format!(", OVERRIDE_DATA_PATH TRUE{extra_args}") + }; + let attach_str = format!( "ATTACH 'ducklake:{db_type}:{db_conn_str}' AS {alias_name} (DATA_PATH 's3://{storage}/{data_path}'{extra_args});", ); diff --git a/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte b/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte index b0029c90dc..5886b3e4db 100644 --- a/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte +++ b/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte @@ -176,6 +176,7 @@ } let dbManagerDrawer: DbManagerDrawer | undefined = $state() + let instanceCatalogPopover: Popover | undefined = $state() let confirmationModal = createAsyncConfirmationModal() @@ -291,7 +292,7 @@ contentClasses="py-5 px-6 w-[34rem] bg-surface-secondary -translate-y-2" closeOnOtherPopoverOpen closeOnOutsideClick - {...confirmationModal.props.open ? { isOpen: false } : {}} + bind:this={instanceCatalogPopover} > + {#if showManageCatalogButton} + instanceCatalogPopover?.close()} + /> {/if} - + {/snippet}