diff --git a/backend/windmill-common/src/query_builders.rs b/backend/windmill-common/src/query_builders.rs index f077687d6f..8548be3fe1 100644 --- a/backend/windmill-common/src/query_builders.rs +++ b/backend/windmill-common/src/query_builders.rs @@ -2395,8 +2395,12 @@ fn expand_load_table_metadata(json_str: &str, db_type: DbType) -> Result, database_name: Option<&str>, + is_ducklake: bool, ) -> Result { match db_type { DbType::Duckdb => { // For ducklake, the ducklake ATTACH is handled by the ducklake wrapper. - let mut q = String::from( - "SELECT + let select_cols = "SELECT COLUMN_NAME as field, DATA_TYPE as DataType, COLUMN_DEFAULT as DefaultValue, @@ -2420,14 +2424,34 @@ fn make_load_table_metadata_query( false as IsIdentity, CASE WHEN IS_NULLABLE = true THEN 'YES' ELSE 'NO' END as IsNullable, false as IsEnum, - TABLE_NAME as table_name -FROM information_schema.columns c -WHERE table_schema = current_schema()", - ); + TABLE_NAME as table_name"; if let Some(t) = table { - q.push_str(&format!(" AND TABLE_NAME = '{}'", escape_sql_literal(t))); + let parts: Vec<&str> = t.split('.').collect(); + let tname = parts[parts.len() - 1]; + let schema_filter = if parts.len() > 1 { + format!("table_schema = '{}'", escape_sql_literal(parts[0])) + } else { + "table_schema = current_schema()".to_string() + }; + Ok(format!( + "{}\nFROM information_schema.columns c\nWHERE {} AND TABLE_NAME = '{}'", + select_cols, + schema_filter, + escape_sql_literal(tname) + )) + } else if is_ducklake { + // Ducklake schema browsing is not supported in the frontend: keep the + // single-schema behavior so table keys stay unqualified. + Ok(format!( + "{}\nFROM information_schema.columns c\nWHERE table_schema = current_schema()", + select_cols + )) + } else { + Ok(format!( + "{},\n TABLE_SCHEMA as schema_name\nFROM information_schema.columns c\nWHERE table_catalog = current_database() AND table_schema NOT IN ('information_schema', 'pg_catalog')", + select_cols + )) } - Ok(q) } DbType::Mysql => { let explicit_db = database_name.filter(|s| !s.is_empty()); @@ -4348,6 +4372,25 @@ mod tests { let sql = expand_code(marker, &ScriptLang::DuckDb); assert!(sql.contains("COLUMN_NAME as field")); assert!(sql.contains("TABLE_NAME = 'users'")); + assert!(sql.contains("table_schema = current_schema()")); + } + + #[test] + fn test_expand_load_table_metadata_duckdb_schema_table() { + let marker = r#"-- WM_INTERNAL_DB_LOAD_TABLE_METADATA {"table":"myschema.users"}"#; + let sql = expand_code(marker, &ScriptLang::DuckDb); + assert!(sql.contains("TABLE_NAME = 'users'")); + assert!(sql.contains("table_schema = 'myschema'")); + } + + #[test] + fn test_expand_load_table_metadata_duckdb_all_tables() { + let marker = r#"-- WM_INTERNAL_DB_LOAD_TABLE_METADATA {}"#; + let sql = expand_code(marker, &ScriptLang::DuckDb); + assert!(sql.contains("TABLE_SCHEMA as schema_name")); + assert!(sql.contains("TABLE_NAME as table_name")); + assert!(sql.contains("table_catalog = current_database()")); + assert!(sql.contains("table_schema NOT IN ('information_schema', 'pg_catalog')")); } #[test] @@ -4358,6 +4401,15 @@ mod tests { assert!(sql.contains("TABLE_NAME = 'users'")); } + #[test] + fn test_expand_load_table_metadata_ducklake_all_tables_stays_single_schema() { + let marker = r#"-- WM_INTERNAL_DB_LOAD_TABLE_METADATA {"ducklake":"lake"}"#; + let sql = expand_code(marker, &ScriptLang::DuckDb); + assert!(sql.starts_with("ATTACH 'ducklake://lake' AS dl;USE dl;\n")); + assert!(sql.contains("table_schema = current_schema()")); + assert!(!sql.contains("schema_name")); + } + // ----------------------------------------------------------------------- // FOREIGN_KEYS // ----------------------------------------------------------------------- diff --git a/frontend/src/lib/components/DBManager.svelte b/frontend/src/lib/components/DBManager.svelte index 46177bc494..c1fdf475de 100644 --- a/frontend/src/lib/components/DBManager.svelte +++ b/frontend/src/lib/components/DBManager.svelte @@ -162,7 +162,13 @@ if (!selected.schemaKey && schemaKeys.length) { let schemaKey = initialSchemaKey ?? - ('public' in dbSchema.schema ? 'public' : 'dbo' in dbSchema.schema ? 'dbo' : schemaKeys[0]) + ('public' in dbSchema.schema + ? 'public' + : 'dbo' in dbSchema.schema + ? 'dbo' + : 'main' in dbSchema.schema + ? 'main' + : schemaKeys[0]) let tableKey = initialTableKey && dbSchema.schema?.[schemaKey]?.[initialTableKey] ? initialTableKey diff --git a/frontend/src/lib/components/apps/components/display/dbtable/metadata.ts b/frontend/src/lib/components/apps/components/display/dbtable/metadata.ts index 2fdb61522d..45efb00592 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/metadata.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/metadata.ts @@ -356,7 +356,11 @@ export async function getTablesByResource( const paths: string[] = [] for (const key in s?.schema) { for (const subKey in s.schema[key]) { - paths.push(`${subKey}`) + if (key === 'main') { + paths.push(`${subKey}`) + } else { + paths.push(`${key}.${subKey}`) + } } } diff --git a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts index ee30619366..b215b4f683 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts @@ -235,6 +235,44 @@ return schema }, argName: 'database' }, + duckdb: { + code: `SELECT table_schema, table_name, column_name, data_type as udt_name, column_default, is_nullable FROM information_schema.columns WHERE table_catalog = current_database() AND table_schema NOT IN ('information_schema', 'pg_catalog')`, + processingFn: (rows) => { + const schemas = rows.reduce((acc, a) => { + const table_schema = a.table_schema + delete a.table_schema + acc[table_schema] = acc[table_schema] || [] + if (a.table_name || a.column_name) acc[table_schema].push(a) + return acc + }, {}) + + const data = {} + for (const key in schemas) { + data[key] = schemas[key].reduce((acc, a) => { + const table_name = a.table_name + delete a.table_name + acc[table_name] = acc[table_name] || {} + const p: { + type: string + required: boolean + default?: string + } = { + type: a.udt_name, + required: a.is_nullable === 'NO' + } + if (a.column_default) { + p.default = a.column_default + } + acc[table_name][a.column_name] = p + return acc + }, {}) + } + + return data + }, + lang: 'duckdb', + argName: 'database' + }, mssql: { argName: 'database', code: `select TABLE_SCHEMA, TABLE_NAME, DATA_TYPE, COLUMN_NAME, COLUMN_DEFAULT from information_schema.columns where table_schema != 'sys'`, @@ -381,7 +419,12 @@ export function getPrimaryKeys(tableMetadata?: TableMetadata): string[] { } export function dbSupportsSchemas(dbType: DbType): boolean { - return dbType === 'postgresql' || dbType === 'snowflake' || dbType === 'bigquery' + return ( + dbType === 'postgresql' || + dbType === 'snowflake' || + dbType === 'bigquery' || + dbType === 'duckdb' + ) } export function datatypeHasLength(datatype: string): boolean {