mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 16:00:38 +00:00
feat: add schema browsing support for duckdb database manager
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user