mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 16:02:14 +00:00
Ducklake support in Db Studio (#6784)
* stash * Ducklake works in app editor * fix * fix missing $res:
This commit is contained in:
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
+12
-17
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
+11
-6
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
+10
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user