feat: add schema browsing support for duckdb database manager

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-06-11 21:02:09 +02:00
parent 66c0334e70
commit ee43f538ef
4 changed files with 118 additions and 13 deletions
+62 -10
View File
@@ -2395,8 +2395,12 @@ fn expand_load_table_metadata(json_str: &str, db_type: DbType) -> Result<Expande
return Ok(ExpandedQuery::with_language(code, ScriptLang::Bun));
}
let query =
make_load_table_metadata_query(db_type, p.table.as_deref(), p.database_name.as_deref())?;
let query = make_load_table_metadata_query(
db_type,
p.table.as_deref(),
p.database_name.as_deref(),
p.ducklake.is_some(),
)?;
Ok(ExpandedQuery::sql(maybe_wrap_ducklake(
query,
p.ducklake.as_deref(),
@@ -2407,12 +2411,12 @@ fn make_load_table_metadata_query(
db_type: DbType,
table: Option<&str>,
database_name: Option<&str>,
is_ducklake: bool,
) -> Result<String, String> {
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
// -----------------------------------------------------------------------
+7 -1
View File
@@ -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
@@ -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}`)
}
}
}
@@ -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 {