From 4e7dfd7a900ac1ea85a7934dada4903c218b881b Mon Sep 17 00:00:00 2001
From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com>
Date: Thu, 9 Oct 2025 18:26:42 +0200
Subject: [PATCH] Ducklake support in Db Studio (#6784)
* stash
* Ducklake works in app editor
* fix
* fix missing $res:
---
.../src/lib/components/DucklakePicker.svelte | 58 ++++++++
.../display/dbtable/AppDbExplorer.svelte | 130 +++++++++---------
.../display/dbtable/DbExplorerCount.svelte | 29 ++--
.../display/dbtable/DeleteRow.svelte | 10 +-
.../display/dbtable/InsertRowRunnable.svelte | 17 ++-
.../display/dbtable/UpdateCell.svelte | 10 +-
.../display/dbtable/queries/count.ts | 56 ++++----
.../display/dbtable/queries/delete.ts | 37 +++--
.../display/dbtable/queries/insert.ts | 32 +++--
.../display/dbtable/queries/select.ts | 42 +++---
.../display/dbtable/queries/update.ts | 36 +++--
.../apps/components/display/dbtable/utils.ts | 15 +-
.../apps/editor/AppEditorHeader.svelte | 26 ++--
.../apps/editor/component/components.ts | 21 ++-
.../settingsPanel/InputsSpecsEditor.svelte | 3 +-
.../inputEditor/StaticInputEditor.svelte | 10 ++
frontend/src/lib/components/apps/inputType.ts | 2 +
17 files changed, 338 insertions(+), 196 deletions(-)
create mode 100644 frontend/src/lib/components/DucklakePicker.svelte
diff --git a/frontend/src/lib/components/DucklakePicker.svelte b/frontend/src/lib/components/DucklakePicker.svelte
new file mode 100644
index 0000000000..951b29cd39
--- /dev/null
+++ b/frontend/src/lib/components/DucklakePicker.svelte
@@ -0,0 +1,58 @@
+
+
+
+
diff --git a/frontend/src/lib/components/apps/components/display/dbtable/AppDbExplorer.svelte b/frontend/src/lib/components/apps/components/display/dbtable/AppDbExplorer.svelte
index 321ed518f6..9450debfaa 100644
--- a/frontend/src/lib/components/apps/components/display/dbtable/AppDbExplorer.svelte
+++ b/frontend/src/lib/components/apps/components/display/dbtable/AppDbExplorer.svelte
@@ -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}
insert(args)}
/>
{/if}
{/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}
{#key renderCount && render}
diff --git a/frontend/src/lib/components/apps/components/display/dbtable/DbExplorerCount.svelte b/frontend/src/lib/components/apps/components/display/dbtable/DbExplorerCount.svelte
index 7a18bc8416..66b97cbdaf 100644
--- a/frontend/src/lib/components/apps/components/display/dbtable/DbExplorerCount.svelte
+++ b/frontend/src/lib/components/apps/components/display/dbtable/DbExplorerCount.svelte
@@ -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')
@@ -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()
diff --git a/frontend/src/lib/components/apps/components/display/dbtable/DeleteRow.svelte b/frontend/src/lib/components/apps/components/display/dbtable/DeleteRow.svelte
index d7727f4c7f..36227305af 100644
--- a/frontend/src/lib/components/apps/components/display/dbtable/DeleteRow.svelte
+++ b/frontend/src/lib/components/apps/components/display/dbtable/DeleteRow.svelte
@@ -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,
- dbType: DbType
+ data: Record
) {
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()
diff --git a/frontend/src/lib/components/apps/components/display/dbtable/InsertRowRunnable.svelte b/frontend/src/lib/components/apps/components/display/dbtable/InsertRowRunnable.svelte
index 3e7f2fafc8..3746eb377b 100644
--- a/frontend/src/lib/components/apps/components/display/dbtable/InsertRowRunnable.svelte
+++ b/frontend/src/lib/components/apps/components/display/dbtable/InsertRowRunnable.svelte
@@ -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,
- resourceType: string
+ values: Record
): Promise {
- 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()
diff --git a/frontend/src/lib/components/apps/components/display/dbtable/UpdateCell.svelte b/frontend/src/lib/components/apps/components/display/dbtable/UpdateCell.svelte
index 5f8ae6df77..3e6989170a 100644
--- a/frontend/src/lib/components/apps/components/display/dbtable/UpdateCell.svelte
+++ b/frontend/src/lib/components/apps/components/display/dbtable/UpdateCell.svelte
@@ -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,
- 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()
diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/count.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/count.ts
index 02d5d768ef..c9a8161493 100644
--- a/frontend/src/lib/components/apps/components/display/dbtable/queries/count.ts
+++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/count.ts
@@ -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'
}
diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/delete.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/delete.ts
index 5ff2bba2aa..ac26f33f5a 100644
--- a/frontend/src/lib/components/apps/components/display/dbtable/queries/delete.ts
+++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/delete.ts
@@ -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'
}
diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/insert.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/insert.ts
index 733c95a8ae..66413ec310 100644
--- a/frontend/src/lib/components/apps/components/display/dbtable/queries/insert.ts
+++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/insert.ts
@@ -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'
}
diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/select.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/select.ts
index c440be4b7c..3a83a1e7d4 100644
--- a/frontend/src/lib/components/apps/components/display/dbtable/queries/select.ts
+++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/select.ts
@@ -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
diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/update.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/update.ts
index 38258eeb0b..c6c0b4c4dc 100644
--- a/frontend/src/lib/components/apps/components/display/dbtable/queries/update.ts
+++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/update.ts
@@ -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'
}
diff --git a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts
index dd755bc636..bb9a9b592e 100644
--- a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts
+++ b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts
@@ -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>,
dbType: DbType | undefined,
- resourcePath: string,
+ dbPath: string,
workspace: string
): Promise {
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 []
}
diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte
index 864a5c78ef..58ff64f3d7 100644
--- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte
+++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte
@@ -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'
})
})
diff --git a/frontend/src/lib/components/apps/editor/component/components.ts b/frontend/src/lib/components/apps/editor/component/components.ts
index 185381ee18..c36529f2f8 100644
--- a/frontend/src/lib/components/apps/editor/component/components.ts
+++ b/frontend/src/lib/components/apps/editor/component/components.ts
@@ -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,
diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte
index 4c6b680c2b..9c26dad3c2 100644
--- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte
+++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte
@@ -104,7 +104,8 @@
{#if deletable}
dispatch('delete', k)} />
-
{/if}
+
+ {/if}
{/if}
{/each}
{#if overridenByComponent.length > 0}
diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte
index 82b3e5a985..0020ea3334 100644
--- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte
+++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte
@@ -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 | undefined
@@ -145,6 +146,15 @@
}
resourceType="s3"
/>
+ {:else if fieldType === 'ducklake'}
+ 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')}
diff --git a/frontend/src/lib/components/apps/inputType.ts b/frontend/src/lib/components/apps/inputType.ts
index 96e051a90a..d361bf4a61 100644
--- a/frontend/src/lib/components/apps/inputType.ts
+++ b/frontend/src/lib/components/apps/inputType.ts
@@ -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>