Ducklake support in Db Studio (#6784)

* stash

* Ducklake works in app editor

* fix

* fix missing $res:
This commit is contained in:
Diego Imbert
2025-10-09 18:26:42 +02:00
committed by GitHub
parent 8050729e6f
commit 4e7dfd7a90
17 changed files with 338 additions and 196 deletions

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import { WorkspaceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import Select from './select/Select.svelte'
import DbManagerDrawer from './DBManagerDrawer.svelte'
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
import { usePromise } from '$lib/svelte5Utils.svelte'
interface Props {
value?: string | undefined
disabled?: boolean
disablePortal?: boolean
showSchemaExplorer?: boolean
placeholder?: string | undefined
selectInputClass?: string
class?: string
onClear?: () => void
}
let {
value = $bindable(undefined),
disabled = false,
disablePortal = false,
showSchemaExplorer = false,
placeholder = undefined,
selectInputClass = '',
class: className = '',
onClear = undefined
}: Props = $props()
let ducklakes = usePromise(() =>
WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? '' })
)
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
</script>
<div class={className}>
<Select
items={ducklakes.value?.map((d) => ({ value: d })) ?? []}
bind:value
loading={ducklakes.status === 'loading'}
{disabled}
{disablePortal}
{placeholder}
inputClass={selectInputClass}
{onClear}
/>
{#if showSchemaExplorer && value && assetCanBeExplored({ kind: 'ducklake', path: value })}
<ExploreAssetButton
class="mt-1 w-fit"
asset={{ kind: 'ducklake', path: value }}
{dbManagerDrawer}
/>
{/if}
<DbManagerDrawer bind:this={dbManagerDrawer} />
</div>

View File

@@ -38,7 +38,7 @@
import RefreshButton from '$lib/components/apps/components/helpers/RefreshButton.svelte'
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
import InsertRowDrawerButton from '../InsertRowDrawerButton.svelte'
import { assert } from '$lib/utils'
import { getDucklakeSchema, type DbInput } from '$lib/components/dbOps'
interface Props {
id: string
@@ -105,11 +105,10 @@
console.log('compute input')
input = getSelectInput(
resource,
dbInput,
resolvedConfig.type.configuration[resolvedConfig.type.selected].table,
columnDefs,
whereClause,
resolvedConfig.type.selected as DbType
whereClause
)
}, 1000)
}
@@ -133,6 +132,27 @@
let componentContainerHeight: number | undefined = $state(undefined)
let buttonContainerHeight: number | undefined = $state(undefined)
let dbPath = $derived(
resolvedConfig.type.selected !== 'ducklake'
? resolvedConfig.type.configuration?.[resolvedConfig.type.selected]?.resource
: resolvedConfig.type.configuration?.[resolvedConfig.type.selected]?.ducklake
)
let dbInput: DbInput = $derived(
resolvedConfig.type.selected === 'ducklake'
? {
type: 'ducklake',
ducklake: dbPath.split('ducklake://')[1]
}
: {
type: 'database',
resourcePath: dbPath.split('$res:')[1],
resourceType: resolvedConfig.type.selected as DbType
}
)
let dbtype = $derived(
resolvedConfig.type.selected === 'ducklake' ? ('duckdb' as const) : resolvedConfig.type.selected
)
function onUpdate(
e: CustomEvent<{
row: number
@@ -146,14 +166,13 @@
const { columnDef, value, data, oldValue } = e.detail
updateCell?.triggerUpdate(
resolvedConfig.type.configuration[resolvedConfig.type.selected].resource,
dbInput,
resolvedConfig.type.configuration[resolvedConfig.type.selected].table ?? 'unknown',
columnDef,
resolvedConfig.columnDefs,
value,
data,
oldValue,
resolvedConfig.type.selected as DbType
oldValue
)
}
@@ -193,44 +212,40 @@
}
async function listTables() {
let resource = resolvedConfig.type.configuration?.[resolvedConfig.type.selected]?.resource
if (!dbPath) return
if (lastResource === dbPath) return
lastResource = dbPath
if (!resource) return
if (lastResource === resource) return
lastResource = resource
const gridItem = findGridItem($app, id)
if (!gridItem) {
return
}
if (!gridItem) return
updateOneOfConfiguration(
gridItem.data.configuration.type as OneOfConfiguration,
resolvedConfig.type,
{
table: {
selectOptions: [],
loading: true
}
}
{ table: { selectOptions: [], loading: true } }
)
if (!resolvedConfig.type?.configuration?.[resolvedConfig.type.selected]?.resource) {
if (!dbPath) {
$app = $app
return
}
try {
const dbSchemas: DBSchemas = {}
await getDbSchemas(
resolvedConfig?.type?.selected,
resolvedConfig.type.configuration[resolvedConfig?.type?.selected].resource.split(':')[1],
$workspaceStore,
dbSchemas,
() => {}
)
if (resolvedConfig?.type?.selected === 'ducklake') {
dbSchemas[dbPath] = await getDucklakeSchema({
workspace: $workspaceStore!,
ducklake: dbPath.split('ducklake://')[1]
})
} else {
await getDbSchemas(
resolvedConfig?.type?.selected,
dbPath.split('$res:')[1],
$workspaceStore,
dbSchemas,
() => {}
)
}
updateOneOfConfiguration(
gridItem.data.configuration.type as OneOfConfiguration,
@@ -238,12 +253,7 @@
{
table: {
selectOptions: dbSchemas
? await getTablesByResource(
dbSchemas,
resolvedConfig?.type?.selected,
resource.split(':')[1],
$workspaceStore!
)
? await getTablesByResource(dbSchemas, dbtype, dbPath, $workspaceStore!)
: [],
loading: false
}
@@ -366,14 +376,17 @@
gridItem.data = gridItem.data
$app = $app
let resource = resolvedConfig.type.configuration[selected].resource
assert('resource starts with $res:', resource?.startsWith('$res:'), resource)
let tableMetadata = await loadTableMetaData(
{
type: 'database',
resourcePath: resource.substring(5),
resourceType: selected
},
resolvedConfig.type.selected === 'ducklake'
? {
type: 'ducklake',
ducklake: dbPath.split('ducklake://')[1]
}
: {
type: 'database',
resourcePath: dbPath.split('$res:')[1],
resourceType: dbtype
},
$workspaceStore,
resolvedConfig.type.configuration[selected].table
)
@@ -505,12 +518,11 @@
const selected = resolvedConfig.type.selected
await insertRowRunnable?.insertRow(
resolvedConfig.type.configuration[selected].resource,
dbInput,
$workspaceStore,
resolvedConfig.type.configuration[selected].table,
resolvedConfig.columnDefs,
args,
selected
args
)
insertDrawer?.closeDrawer()
@@ -530,14 +542,11 @@
const data = { ...e.detail }
delete data['__index']
const selected = resolvedConfig.type.selected
deleteRow?.triggerDelete(
resolvedConfig.type.configuration[selected].resource,
resolvedConfig.type.configuration[selected].table ?? 'unknown',
dbInput,
resolvedConfig.type.configuration[resolvedConfig.type.selected].table ?? 'unknown',
resolvedConfig.columnDefs,
data,
selected
data
)
}
@@ -555,18 +564,14 @@
resolvedConfig.type.selected &&
render &&
untrack(() => {
computeInput(
resolvedConfig.columnDefs,
resolvedConfig.whereClause,
resolvedConfig.type.configuration[resolvedConfig.type.selected].resource
)
computeInput(resolvedConfig.columnDefs, resolvedConfig.whereClause, dbPath)
})
})
$effect(() => {
editorContext != undefined &&
$mode == 'dnd' &&
resolvedConfig.type &&
resolvedConfig.type.configuration?.[resolvedConfig.type.selected]?.resource &&
dbPath &&
untrack(() => listTables())
})
$effect(() => {
@@ -628,8 +633,7 @@
{id}
{quicksearch}
{table}
resource={resolvedConfig?.type?.configuration?.[resolvedConfig?.type?.selected]?.resource}
resourceType={resolvedConfig?.type?.selected}
{dbInput}
columnDefs={resolvedConfig?.columnDefs}
whereClause={resolvedConfig?.whereClause}
/>
@@ -672,14 +676,14 @@
{#if hideInsert !== true}
<InsertRowDrawerButton
columnDefs={resolvedConfig.columnDefs}
dbType={resolvedConfig.type.selected}
dbType={dbtype}
onInsert={(args) => insert(args)}
/>
{/if}
</div>
</div>
{/if}
{#if resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.resource && resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.table}
{#if dbPath && resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.table}
<!-- {JSON.stringify(lastInput)} -->
<!-- <span class="text-xs">{JSON.stringify(configuration.columnDefs)}</span> -->
{#key renderCount && render}

View File

@@ -5,30 +5,21 @@
import type RunnableComponent from '../../helpers/RunnableComponent.svelte'
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
import { initOutput } from '../../../editor/appUtils'
import { type ColumnDef, type DbType } from './utils'
import { type ColumnDef } from './utils'
import { getCountInput } from './queries/count'
import type { DbInput } from '$lib/components/dbOps'
interface Props {
id: string
table: string | undefined
resource: string | undefined
renderCount: number
quicksearch: string
resourceType: string
columnDefs: ColumnDef[]
whereClause: string | undefined
dbInput: DbInput
}
let {
id,
table,
resource,
renderCount,
quicksearch,
resourceType,
columnDefs,
whereClause
}: Props = $props()
let { id, table, renderCount, quicksearch, dbInput, columnDefs, whereClause }: Props = $props()
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
@@ -66,16 +57,20 @@
}
}
if (table != undefined && resource !== undefined) {
if (
table != undefined &&
((dbInput.type == 'ducklake' && dbInput.ducklake !== undefined) ||
(dbInput.type == 'database' && dbInput.resourcePath !== undefined))
) {
renderCountLast = renderCount
lastTableCount = table
quicksearchLast = quicksearch
await getCount(resource, table, quicksearch)
await getCount(dbInput, table, quicksearch)
}
}
async function getCount(resource: string, table: string, quicksearch: string) {
input = getCountInput(resource, table, resourceType as DbType, localColumnDefs, whereClause)
async function getCount(dbInput: DbInput, table: string, quicksearch: string) {
input = getCountInput(dbInput, table, localColumnDefs, whereClause)
await tick()

View File

@@ -5,9 +5,10 @@
import type RunnableComponent from '../../helpers/RunnableComponent.svelte'
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
import { initOutput } from '../../../editor/appUtils'
import { getPrimaryKeys, type ColumnDef, type DbType } from './utils'
import { getPrimaryKeys, type ColumnDef } from './utils'
import { sendUserToast } from '$lib/toast'
import { getDeleteInput } from './queries/delete'
import type { DbInput } from '$lib/components/dbOps'
interface Props {
id: string
@@ -31,16 +32,15 @@
const dispatch = createEventDispatcher()
export async function triggerDelete(
resource: string,
dbInput: DbInput,
table: string,
allColumns: ColumnDef[],
data: Record<string, any>,
dbType: DbType
data: Record<string, any>
) {
let primaryColumns = getPrimaryKeys(allColumns)
let columns = allColumns?.filter((x) => primaryColumns.includes(x.field))
input = getDeleteInput(resource, table, columns, dbType)
input = getDeleteInput(dbInput, table, columns)
await tick()

View File

@@ -5,9 +5,10 @@
import type RunnableComponent from '../../helpers/RunnableComponent.svelte'
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
import { initOutput } from '../../../editor/appUtils'
import { type ColumnDef, type DbType } from './utils'
import { type ColumnDef } from './utils'
import { sendUserToast } from '$lib/toast'
import { getInsertInput } from './queries/insert'
import type { DbInput } from '$lib/components/dbOps'
interface Props {
id: string
@@ -30,18 +31,22 @@
const dispatch = createEventDispatcher()
export async function insertRow(
resource: string,
dbInput: DbInput,
workspace: string | undefined,
table: string | undefined,
columns: ColumnDef[],
values: Record<string, any>,
resourceType: string
values: Record<string, any>
): Promise<boolean> {
if (!resource || !table || !workspace) {
if (
(dbInput.type == 'ducklake' && !dbInput.ducklake) ||
(dbInput.type == 'database' && !dbInput.resourcePath) ||
!table ||
!workspace
) {
return false
}
input = getInsertInput(table, columns, resource, resourceType as DbType)
input = getInsertInput(dbInput, table, columns)
await tick()

View File

@@ -5,9 +5,10 @@
import type RunnableComponent from '../../helpers/RunnableComponent.svelte'
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
import { initOutput } from '../../../editor/appUtils'
import { getPrimaryKeys, type ColumnDef, type DbType } from './utils'
import { getPrimaryKeys, type ColumnDef } from './utils'
import { sendUserToast } from '$lib/toast'
import { getUpdateInput } from './queries/update'
import type { DbInput } from '$lib/components/dbOps'
interface Props {
id: string
@@ -28,21 +29,20 @@
let input: AppInput | undefined = $state(undefined)
export async function triggerUpdate(
resource: string,
dbInput: DbInput,
table: string,
column: ColumnDef,
allColumns: ColumnDef[],
valueToUpdate: string,
data: Record<string, any>,
oldValue: string | undefined = undefined,
dbType: DbType
oldValue: string | undefined = undefined
) {
// const datatype = tableMetaData?.find((column) => column.isprimarykey)?.datatype
let primaryColumns = getPrimaryKeys(allColumns)
let columns = allColumns?.filter((x) => primaryColumns.includes(x.field))
input = getUpdateInput(resource, table, column, columns, dbType)
input = getUpdateInput(dbInput, table, column, columns)
await tick()

View File

@@ -1,4 +1,5 @@
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery, type DbInput } from '$lib/components/dbOps'
import { buildParameters, type DbType } from '../utils'
import { getLanguageByResourceType, type ColumnDef, buildVisibleFieldList } from '../utils'
@@ -141,35 +142,41 @@ export function makeCountQuery(
}
export function getCountInput(
resource: string,
dbInput: DbInput,
table: string,
resourceType: DbType,
columnDefs: ColumnDef[],
whereClause: string | undefined
): AppInput | undefined {
if (!resource || !table || !columnDefs) {
// Return undefined if resource or table is not defined
if (
(dbInput.type == 'ducklake' && !dbInput.ducklake) ||
(dbInput.type == 'database' && !dbInput.resourcePath) ||
!table ||
!columnDefs?.length
) {
return undefined
}
const query = makeCountQuery(resourceType, table, whereClause, columnDefs)
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
let query = makeCountQuery(dbType, table, whereClause, columnDefs)
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake)
const updateRunnable: RunnableByName = {
name: 'AppDbExplorer',
type: 'runnableByName',
inlineScript: {
content: query,
language: getLanguageByResourceType(resourceType),
language: getLanguageByResourceType(dbType),
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties: {
database: {
description: 'Database name',
type: 'object',
format: `resource-${resourceType}`
}
},
properties:
dbInput.type === 'database'
? {
database: {
description: 'Database name',
type: 'object',
format: `resource-${dbType}`
}
}
: {},
required: ['database'],
type: 'object'
}
@@ -178,14 +185,17 @@ export function getCountInput(
const updateQuery: AppInput = {
runnable: updateRunnable,
fields: {
database: {
type: 'static',
value: resource,
fieldType: 'object',
format: `resource-${resourceType}`
}
},
fields:
dbInput.type === 'database'
? {
database: {
type: 'static',
value: `$res:${dbInput.resourcePath}`,
fieldType: 'object',
format: `resource-${dbType}`
}
}
: {},
type: 'runnable',
fieldType: 'object'
}

View File

@@ -1,4 +1,5 @@
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery, type DbInput } from '$lib/components/dbOps'
import { getLanguageByResourceType, type ColumnDef, buildParameters, type DbType } from '../utils'
export function makeDeleteQuery(table: string, columns: ColumnDef[], dbType: DbType) {
@@ -65,20 +66,25 @@ export function makeDeleteQuery(table: string, columns: ColumnDef[], dbType: DbT
}
export function getDeleteInput(
resource: string,
dbInput: DbInput,
table: string,
columns: ColumnDef[],
dbType: DbType
columns: ColumnDef[]
): AppInput | undefined {
if (!resource || !table) {
if (
(dbInput.type == 'ducklake' && !dbInput.ducklake) ||
(dbInput.type == 'database' && !dbInput.resourcePath) ||
!table
) {
return undefined
}
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
let query = makeDeleteQuery(table, columns, dbType)
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake)
const deleteRunnable: RunnableByName = {
name: 'AppDbExplorer',
type: 'runnableByName',
inlineScript: {
content: makeDeleteQuery(table, columns, dbType),
content: query,
language: getLanguageByResourceType(dbType),
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
@@ -91,14 +97,17 @@ export function getDeleteInput(
const deleteQuery: AppInput = {
runnable: deleteRunnable,
fields: {
database: {
type: 'static',
value: resource,
fieldType: 'object',
format: `resource-${dbType}`
}
},
fields:
dbInput.type == 'database'
? {
database: {
type: 'static',
value: `$res:${dbInput.resourcePath}`,
fieldType: 'object',
format: `resource-${dbType}`
}
}
: {},
type: 'runnable',
fieldType: 'object'
}

View File

@@ -1,4 +1,5 @@
import type { AppInput } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery, type DbInput } from '$lib/components/dbOps'
import { buildParameters, ColumnIdentity, type DbType } from '../utils'
import { getLanguageByResourceType, type ColumnDef } from '../utils'
@@ -102,18 +103,16 @@ export function makeInsertQuery(table: string, columns: ColumnDef[], dbType: DbT
return query
}
export function getInsertInput(
table: string,
columns: ColumnDef[],
resource: string,
dbType: DbType
): AppInput {
export function getInsertInput(dbInput: DbInput, table: string, columns: ColumnDef[]): AppInput {
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
let query = makeInsertQuery(table, columns, dbType)
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake)
return {
runnable: {
name: 'AppDbExplorer',
type: 'runnableByName',
inlineScript: {
content: makeInsertQuery(table, columns, dbType),
content: query,
language: getLanguageByResourceType(dbType),
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
@@ -123,14 +122,17 @@ export function getInsertInput(
}
}
},
fields: {
database: {
type: 'static',
value: resource,
fieldType: 'object',
format: `resource-${dbType}`
}
},
fields:
dbInput.type === 'database'
? {
database: {
type: 'static',
value: `$res:${dbInput.resourcePath}`,
fieldType: 'object',
format: `resource-${dbType}`
}
}
: {},
type: 'runnable',
fieldType: 'object'
}

View File

@@ -1,4 +1,5 @@
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery, type DbInput } from '$lib/components/dbOps'
import { buildParameters, type DbType } from '../utils'
import { getLanguageByResourceType, type ColumnDef, buildVisibleFieldList } from '../utils'
@@ -258,40 +259,43 @@ function coerceToNumber(value: any): number {
}
export function getSelectInput(
resource: string,
dbInput: DbInput,
table: string | undefined,
columnDefs: ColumnDef[],
whereClause: string | undefined,
dbType: DbType,
options?: { limit?: number; offset?: number }
): AppInput | undefined {
if (!resource || !table || !columnDefs) {
return undefined
}
if (columnDefs.length === 0) {
if (
(dbInput.type == 'ducklake' && !dbInput.ducklake) ||
(dbInput.type == 'database' && !dbInput.resourcePath) ||
!table ||
!columnDefs?.length
) {
return undefined
}
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
let content = makeSelectQuery(table, columnDefs, whereClause, dbType, options)
if (dbInput.type === 'ducklake') content = wrapDucklakeQuery(content, dbInput.ducklake)
const getRunnable: RunnableByName = {
name: 'AppDbExplorer',
type: 'runnableByName',
inlineScript: {
content: makeSelectQuery(table, columnDefs, whereClause, dbType, options),
language: getLanguageByResourceType(dbType)
}
inlineScript: { content, language: getLanguageByResourceType(dbType) }
}
const getQuery: AppInput = {
runnable: getRunnable,
fields: {
database: {
type: 'static',
value: resource,
fieldType: 'object',
format: `resource-${dbType}`
}
},
fields:
dbInput.type === 'database'
? {
database: {
type: 'static',
value: `$res:${dbInput.resourcePath}`,
fieldType: 'object',
format: `resource-${dbType}`
}
}
: {},
type: 'runnable',
fieldType: 'object',
hideRefreshButton: true

View File

@@ -1,4 +1,5 @@
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery, type DbInput } from '$lib/components/dbOps'
import { getLanguageByResourceType, type ColumnDef, buildParameters, type DbType } from '../utils'
export function makeUpdateQuery(
@@ -75,21 +76,27 @@ export function makeUpdateQuery(
}
export function getUpdateInput(
resource: string,
dbInput: DbInput,
table: string,
column: ColumnDef,
columns: ColumnDef[],
dbType: DbType
columns: ColumnDef[]
): AppInput | undefined {
if (!resource || !table) {
if (
(dbInput.type == 'ducklake' && !dbInput.ducklake) ||
(dbInput.type == 'database' && !dbInput.resourcePath) ||
!table
) {
return undefined
}
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
let query = makeUpdateQuery(table, column, columns, dbType)
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake)
const updateRunnable: RunnableByName = {
name: 'AppDbExplorer',
type: 'runnableByName',
inlineScript: {
content: makeUpdateQuery(table, column, columns, dbType),
content: query,
language: getLanguageByResourceType(dbType),
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
@@ -102,14 +109,17 @@ export function getUpdateInput(
const updateQuery: AppInput = {
runnable: updateRunnable,
fields: {
database: {
type: 'static',
value: resource,
fieldType: 'object',
format: `resource-${dbType}`
}
},
fields:
dbInput.type === 'database'
? {
database: {
type: 'static',
value: `$res:${dbInput.resourcePath}`,
fieldType: 'object',
format: `resource-${dbType}`
}
}
: {},
type: 'runnable',
fieldType: 'object'
}

View File

@@ -697,7 +697,7 @@ export function buildVisibleFieldList(columnDefs: ColumnDef[], dbType: DbType) {
case 'duckdb':
return `"${column?.field}"` // DuckDB uses double quotes for identifiers
default:
throw new Error('Unsupported database type')
throw new Error('Unsupported database type: ' + dbType)
}
})
}
@@ -753,7 +753,7 @@ export function getPrimaryKeys(tableMetadata?: TableMetadata): string[] {
export async function getTablesByResource(
schema: Partial<Record<string, DBSchema>>,
dbType: DbType | undefined,
resourcePath: string,
dbPath: string,
workspace: string
): Promise<string[]> {
const s = Object.values(schema)?.[0]
@@ -772,7 +772,7 @@ export async function getTablesByResource(
case 'mysql': {
const resourceObj = (await ResourceService.getResourceValue({
workspace,
path: resourcePath
path: dbPath.split('$res:')[1]
})) as any
const paths: string[] = []
for (const key in s?.schema) {
@@ -821,7 +821,16 @@ export async function getTablesByResource(
}
return paths
}
case 'duckdb': {
const paths: string[] = []
for (const key in s?.schema) {
for (const subKey in s.schema[key]) {
paths.push(`${subKey}`)
}
}
return paths
}
default:
return []
}

View File

@@ -52,7 +52,7 @@
import Dropdown from '$lib/components/DropdownV2.svelte'
import AppEditorTutorial from './AppEditorTutorial.svelte'
import AppReportsDrawer from './AppReportsDrawer.svelte'
import { type ColumnDef, getPrimaryKeys } from '../components/display/dbtable/utils'
import { type ColumnDef, type DbType, getPrimaryKeys } from '../components/display/dbtable/utils'
import DebugPanel from './contextPanel/DebugPanel.svelte'
import { getCountInput } from '../components/display/dbtable/queries/count'
import { getSelectInput } from '../components/display/dbtable/queries/select'
@@ -76,6 +76,7 @@
import AppEditorHeaderDeploy from './AppEditorHeaderDeploy.svelte'
import AppEditorHeaderDeployInitialDraft from './AppEditorHeaderDeployInitialDraft.svelte'
import { computeSecretUrl } from './appDeploy.svelte'
import type { DbInput } from '$lib/components/dbOps'
async function hash(message) {
try {
@@ -227,30 +228,35 @@
let nr: { id: string; input: AppInput }[] = []
let config = c.configuration as any
const dbType = config?.type?.selected
const dbType = config?.type?.selected as DbType
let pg = config?.type?.configuration?.[dbType]
if (pg && dbType) {
const { table, resource } = pg
const { table, resource, ducklake } = pg
const tableValue = table.value
const resourceValue = resource.value
const dbPath =
resource?.value.split('$res:')[1] ??
(ducklake?.value as string | undefined)?.split('ducklake://')[1]
const columnDefs = (c.configuration.columnDefs as any).value as ColumnDef[]
const whereClause = (c.configuration.whereClause as any).value as unknown as
| string
| undefined
if (tableValue && resourceValue && columnDefs) {
if (tableValue && dbPath && columnDefs) {
let dbInput: DbInput = ducklake
? { type: 'ducklake', ducklake: dbPath }
: { type: 'database', resourcePath: dbPath, resourceType: dbType }
r.push({
input: getSelectInput(resourceValue, tableValue, columnDefs, whereClause, dbType),
input: getSelectInput(dbInput, tableValue, columnDefs, whereClause),
id: x.id
})
r.push({
input: getCountInput(resourceValue, tableValue, dbType, columnDefs, whereClause),
input: getCountInput(dbInput, tableValue, columnDefs, whereClause),
id: x.id + '_count'
})
r.push({
input: getInsertInput(tableValue, columnDefs, resourceValue, dbType),
input: getInsertInput(dbInput, tableValue, columnDefs),
id: x.id + '_insert'
})
@@ -258,7 +264,7 @@
let columns = columnDefs?.filter((x) => primaryColumns.includes(x.field))
r.push({
input: getDeleteInput(resourceValue, tableValue, columns, dbType),
input: getDeleteInput(dbInput, tableValue, columns),
id: x.id + '_delete'
})
@@ -266,7 +272,7 @@
.filter((col) => col.editable || config.allEditable.value)
.forEach((column) => {
r.push({
input: getUpdateInput(resourceValue, tableValue, column, columns, dbType),
input: getUpdateInput(dbInput, tableValue, column, columns),
id: x.id + '_update'
})
})

View File

@@ -1400,7 +1400,8 @@ export const components = {
type: 'static',
value: false,
fieldType: 'boolean',
tooltip: 'Run the job in the background without blocking the button. Multiple clicks will trigger multiple jobs.'
tooltip:
'Run the job in the background without blocking the button. Multiple clicks will trigger multiple jobs.'
},
onSuccess: onSuccessClick,
@@ -3953,7 +3954,8 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
ms_sql_server: 'MS SQL Server',
snowflake: 'Snowflake',
bigquery: 'BigQuery',
snowflake_oauth: 'Snowflake OAuth'
snowflake_oauth: 'Snowflake OAuth',
ducklake: 'Ducklake'
},
configuration: {
postgresql: {
@@ -4032,6 +4034,21 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
selectOptions: [],
value: undefined
}
},
ducklake: {
ducklake: {
type: 'static',
fieldType: 'ducklake',
subFieldType: 'ducklake',
value: ''
} as StaticAppInput,
table: {
fieldType: 'select',
subFieldType: 'db-table',
type: 'static',
selectOptions: [],
value: undefined
}
}
}
} as const,

View File

@@ -104,7 +104,8 @@
{#if deletable}
<div class="flex flex-row-reverse -mt-4">
<CloseButton noBg on:close={() => dispatch('delete', k)} />
</div>{/if}
</div>
{/if}
{/if}
{/each}
{#if overridenByComponent.length > 0}

View File

@@ -28,6 +28,7 @@
import JsonEditor from '$lib/components/JsonEditor.svelte'
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
import FileUpload from '$lib/components/common/fileUpload/FileUpload.svelte'
import DucklakePicker from '$lib/components/DucklakePicker.svelte'
interface Props {
componentInput: StaticInput<any> | undefined
@@ -145,6 +146,15 @@
}
resourceType="s3"
/>
{:else if fieldType === 'ducklake'}
<DucklakePicker
class="w-full"
bind:value={
() => componentInput?.value?.split('ducklake://')?.[1],
(v) => componentInput && (componentInput.value = v ? `ducklake://${v}` : undefined)
}
showSchemaExplorer
/>
{:else if fieldType === 'labeledresource'}
{#if componentInput?.value && typeof componentInput?.value == 'object' && 'label' in componentInput?.value && (componentInput.value?.['value'] == undefined || typeof componentInput.value?.['value'] == 'string')}
<div class="flex flex-col gap-1 w-full">

View File

@@ -41,6 +41,7 @@ export type InputType =
| 'snowflake_oauth'
| 'bigquery'
| 'oracledb'
| 'ducklake'
| 'app-path'
// Connection to an output of another component
@@ -251,6 +252,7 @@ export type AppInput =
| AppInputSpec<'resource', string, 'snowflake_oauth'>
| AppInputSpec<'resource', string, 'bigquery'>
| AppInputSpec<'resource', string, 'oracledb'>
| AppInputSpec<'ducklake', string, 'ducklake'>
| AppInputSpec<'array', object[], 'number-tuple'>
| AppInputSpec<'app-path', string>