Files
windmill/frontend/src/lib/components/DBManagerContent.svelte
T
Diego ImbertandClaude Opus 4.8 3eeccaf968 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>
2026-06-17 17:23:14 +00:00

217 lines
5.9 KiB
Svelte

<script lang="ts">
import { dbSchemas, workspaceStore, type DBSchema } from '$lib/stores'
import { sendUserToast, sortArray } from '$lib/utils'
import { Loader2 } from 'lucide-svelte'
import { dbSupportsSchemas } from './apps/components/display/dbtable/utils'
import DbManager from './DBManager.svelte'
import {
dbSchemaOpsWithPreviewScripts,
dbTableOpsWithPreviewScripts,
getDbType,
getDucklakeSchema
} from './dbOps'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import SqlRepl from './SqlRepl.svelte'
import SimpleAgTable from './SimpleAgTable.svelte'
import { type Snippet } from 'svelte'
import type { DbInput } from './dbTypes'
import { getDbSchemas, loadAllTablesMetaData } from './apps/components/display/dbtable/metadata'
import type { SelectedTable } from './DBManager.svelte'
import { getDbFeatures } from './apps/components/display/dbtable/dbFeatures'
import { resource } from 'runed'
interface Props {
input?: DbInput
showRepl?: boolean
hasReplResult?: boolean
selectedSchemaKey?: string | undefined
selectedTableKey?: string | undefined
dbSelector?: Snippet<[]>
/** Enable multi-select mode with checkboxes in sidebar */
multiSelectMode?: boolean
/** Selected tables in multi-select mode */
selectedTables?: SelectedTable[]
/** Tables that are already added and should show as disabled */
disabledTables?: SelectedTable[]
onImport?: (mode: 'schema_and_data' | 'schema_only') => void
}
let {
input,
showRepl = true,
hasReplResult = $bindable(false),
selectedSchemaKey = $bindable(undefined),
selectedTableKey = $bindable(undefined),
dbSelector,
multiSelectMode = false,
selectedTables = $bindable([]),
disabledTables = [],
onImport
}: Props = $props()
let dbSchema: DBSchema | undefined = $derived(input && $dbSchemas[getDbSchemasPath(input)])
function getDbSchemasPath(input: DbInput): string {
switch (input.type) {
case 'database':
return input.resourcePath
case 'ducklake':
return 'ducklake://' + input.ducklake
}
}
let colDefs = resource(
() => [input],
async () => {
if (!input) return
return await loadAllTablesMetaData($workspaceStore, input)
}
)
let dbSchemasPromise = resource(
() => [input],
async () => {
if (!input) return
const dbSchemasPath = getDbSchemasPath(input)
if (input.type == 'database') {
$dbSchemas[dbSchemasPath] = await getDbSchemas(
input.resourceType,
input.resourcePath,
$workspaceStore,
(message: string) => sendUserToast(message, true)
)
} else if (input.type == 'ducklake') {
$dbSchemas[dbSchemasPath] = await getDucklakeSchema({
workspace: $workspaceStore!,
ducklake: input.ducklake
})
}
}
)
export const refresh = () => {
colDefs.refetch()
dbSchemasPromise.refetch()
}
export function isLoading() {
return colDefs.loading || dbSchemasPromise.loading
}
let replPanelSize = $state(36)
const REPL_MIN_SIZE = 1.5
let replResultData: undefined | Record<string, any>[] = $state(undefined)
// Sync replResultData state with bindable prop
$effect(() => {
hasReplResult = !!replResultData
})
// Export for parent components
export function clearReplResult() {
replResultData = undefined
}
let _dbManager: DbManager | undefined = $state()
export const dbManager = () => _dbManager
</script>
<svelte:window
onkeydown={(e) => {
if (e.key === 'Escape') {
if (replResultData) {
replResultData = undefined
}
}
}}
/>
{#if dbSchema && $workspaceStore && input}
{@const _input = input}
{@const dbType = getDbType(_input)}
<Splitpanes horizontal>
<Pane class="relative">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class={'absolute inset-0 z-10 p-8 ' +
(replResultData
? 'bg-surface/90'
: 'transition-colors bg-transparent pointer-events-none select-none')}
onclick={(e) => {
// Only proceed if the click is directly on this div and not on the child elements
if (e.target === e.currentTarget) {
replResultData = undefined
}
}}
>
{#if replResultData}
{#key replResultData}
<SimpleAgTable data={replResultData} class="animate-zoom-in" />
{/key}
{/if}
</div>
<DbManager
dbSupportsSchemas={dbSupportsSchemas(dbType)}
databaseIsEmpty={!Object.values(dbSchema.schema).flatMap((s) => Object.values(s)).length}
{dbSchema}
colDefs={colDefs.current}
dbTableOpsFactory={({ colDefs, tableKey }) =>
dbTableOpsWithPreviewScripts({
colDefs,
tableKey,
input: _input,
workspace: $workspaceStore
})}
dbSchemaOps={dbSchemaOpsWithPreviewScripts({
input: _input,
workspace: $workspaceStore
})}
initialTableKey={input.specificTable}
initialSchemaKey={input.specificSchema}
asset={_input.type == 'ducklake'
? { kind: 'ducklake', path: _input.ducklake }
: _input.resourcePath.startsWith('datatable://')
? { kind: 'datatable', path: _input.resourcePath.substring('datatable://'.length) }
: undefined}
{dbType}
refresh={() => refresh()}
{dbSelector}
{onImport}
bind:selectedSchemaKey
bind:selectedTableKey
{multiSelectMode}
bind:selectedTables
bind:this={_dbManager}
{disabledTables}
features={getDbFeatures(input)}
/>
</Pane>
{#if showRepl}
<Pane bind:size={replPanelSize} minSize={REPL_MIN_SIZE} class="relative">
<SqlRepl
{input}
onData={(data) => {
replResultData = data
}}
placeholderTableName={sortArray(
Object.keys(
dbSchema?.schema[
'public' in dbSchema?.schema
? 'public'
: 'dbo' in dbSchema?.schema
? 'dbo'
: Object.keys(dbSchema?.schema ?? {})?.[0]
] ?? {}
)
)?.[0]}
/>
</Pane>
{/if}
</Splitpanes>
{:else}
<Splitpanes>
<Pane class="relative flex justify-center items-center">
<Loader2 class="animate-spin" size={32} />
</Pane>
</Splitpanes>
{/if}