feat: add ducklake schema support to the database manager (#9633)

* feat: add ducklake schema support to the database manager

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: support schema in wmill.ducklake("name:schema") template helper

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: preserve schema when parsing ducklake asset/favorite paths

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: regenerate system prompts for ducklake schema syntax doc

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-06-17 17:23:14 +00:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 25a891041d
commit 3eeccaf968
20 changed files with 188 additions and 52 deletions
+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
@@ -150,7 +150,7 @@
{/if}
</div>
<DbManager
dbSupportsSchemas={input?.type == 'database' && dbSupportsSchemas(input.resourceType)}
dbSupportsSchemas={dbSupportsSchemas(dbType)}
databaseIsEmpty={!Object.values(dbSchema.schema).flatMap((s) => Object.values(s)).length}
{dbSchema}
colDefs={colDefs.current}
@@ -166,7 +166,7 @@
workspace: $workspaceStore
})}
initialTableKey={input.specificTable}
initialSchemaKey={input.type == 'database' ? input.specificSchema : undefined}
initialSchemaKey={input.specificSchema}
asset={_input.type == 'ducklake'
? { kind: 'ducklake', path: _input.ducklake }
: _input.resourcePath.startsWith('datatable://')
@@ -69,12 +69,17 @@
} else if (asset.kind === 's3object' && isS3Uri(assetUri)) {
s3FilePicker?.open(assetUri)
} else if (asset.kind === 'volume') {
const storage = (await VolumeService.getVolumeStorage({ workspace: $workspaceStore! })) ?? undefined
const storage =
(await VolumeService.getVolumeStorage({ workspace: $workspaceStore! })) ?? undefined
s3FilePicker?.open({ s3: `volumes/${$workspaceStore}/${asset.path}/`, storage })
} else if (asset.kind === 'ducklake') {
let ducklake = asset.path.split('/')[0]
let specificTable = asset.path.split('/')[1] as string | undefined
dbManagerDrawer?.openDrawer({ type: 'ducklake', ducklake, specificTable })
let specificTableSplit = asset.path.split('/')[1]?.split('.') as string[] | undefined
let [specificSchema, specificTable] =
specificTableSplit?.length === 2
? [specificTableSplit[0], specificTableSplit[1]]
: [undefined, specificTableSplit?.[0]]
dbManagerDrawer?.openDrawer({ type: 'ducklake', ducklake, specificSchema, specificTable })
} else if (asset.kind === 'datatable') {
let datatable = asset.path.split('/')[0]
let specificTableSplit = asset.path.split('/')[1]?.split('.') as string[] | undefined
@@ -17,7 +17,7 @@ export function getDbFeatures(dbInput: DbInput): Required<DbFeatures> {
primaryKeys: true,
defaultValues: true,
enforcedForeignKeys: true,
schemas: dbInput.type !== 'ducklake' && dbSupportsSchemas(dbInput.resourceType)
schemas: dbInput.type === 'ducklake' ? true : dbSupportsSchemas(dbInput.resourceType)
}
if (dbInput.type == 'ducklake')
@@ -356,7 +356,7 @@ export async function getTablesByResource(
const paths: string[] = []
for (const key in s?.schema) {
for (const subKey in s.schema[key]) {
paths.push(`${subKey}`)
paths.push(key === 'main' ? `${subKey}` : `${key}.${subKey}`)
}
}
@@ -381,7 +381,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 {
@@ -123,6 +123,7 @@ export function useDbManagerUriState(): DbManagerUriState {
return {
type: 'ducklake' as const,
ducklake: parsed.path,
specificSchema: parsed.schema,
specificTable: parsed.table
}
}
@@ -161,6 +162,7 @@ export function useDbManagerUriState(): DbManagerUriState {
params.dbm = buildDbm({
type: 'ducklake',
path: nInput.ducklake,
schema: nInput.specificSchema,
table: nInput.specificTable
})
}
+28 -18
View File
@@ -297,35 +297,45 @@ export async function getDucklakeSchema({
args: {}
}
})
let mainSchema = Array.isArray(result) && result.length && (result?.[0]?.['result'] ?? [])
let schemas = Array.isArray(result) && result.length && (result?.[0]?.['result'] ?? {})
// Safety for agent workers (duckdb ffi lib used to return JSON as stringified json)
if (typeof mainSchema === 'string') mainSchema = JSON.parse(mainSchema)
if (typeof schemas === 'string') schemas = JSON.parse(schemas)
if (!mainSchema) throw new Error('Failed to get Ducklake schema: ' + JSON.stringify(result))
assert('mainSchema is an object', typeof mainSchema === 'object')
if (!schemas) throw new Error('Failed to get Ducklake schema: ' + JSON.stringify(result))
assert('schemas is an object', typeof schemas === 'object')
let schema: Omit<SQLSchema, 'stringified'> = {
schema: { main: mainSchema },
publicOnly: true,
schema: schemas,
publicOnly: false,
lang: 'ducklake'
}
return { ...schema, stringified: stringifySchema(schema) }
}
// Returns every schema in the ducklake (including empty ones, e.g. freshly created)
// as a nested map { schema: { table: { column: {...} } } }.
const DUCKLAKE_GET_SCHEMA_QUERY = `
SELECT json_group_object(table_name, table_data) AS result FROM (
SELECT json_group_object(schema_name, COALESCE(schema_data, json_object())) AS result FROM (
SELECT
table_name,
json_group_object(
c.column_name,
json_object(
'type', c.data_type,
'default', c.column_default,
'required', c.is_nullable == 'NO'
s.schema_name,
(
SELECT json_group_object(table_name, table_data) FROM (
SELECT
c.table_name,
json_group_object(
c.column_name,
json_object(
'type', c.data_type,
'default', c.column_default,
'required', c.is_nullable == 'NO'
)
) AS table_data
FROM information_schema.columns c
WHERE c.table_catalog = '__ducklake__' AND c.table_schema = s.schema_name
GROUP BY c.table_name
)
) AS table_data
FROM information_schema.columns c
WHERE table_catalog = '__ducklake__' AND table_schema = current_schema()
GROUP BY c.table_name
) AS schema_data
FROM information_schema.schemata s
WHERE s.catalog_name = '__ducklake__'
)`
export function getDbType(input: DbInput): DbType {
+1
View File
@@ -9,6 +9,7 @@ export type DbInput =
| {
type: 'ducklake'
ducklake: string
specificSchema?: string
specificTable?: string
}
+6 -1
View File
@@ -2113,7 +2113,12 @@ export function parseDbInputFromAssetSyntax(path: string): DbInput | null {
const [p2, _p3] = _p2.split('/')
const [p3, p4] = _p3.split('.')
return p1 === 'ducklake'
? { type: 'ducklake', ducklake: p2 || 'main', specificTable: p4 ?? p3 }
? {
type: 'ducklake',
ducklake: p2 || 'main',
specificTable: p4 ?? p3,
specificSchema: p4 ? p3 : undefined
}
: p1 === 'datatable'
? {
type: 'database',