feat: go to referenced row from foreign-keyed cells in the database manager (#10998)

* feat: go to referenced row from foreign-keyed cells in the database manager

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157Kw1ukbQo7eZnmyM63G4t

* fix: pin foreign keys to their table and escape backslashes on snowflake

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157Kw1ukbQo7eZnmyM63G4t

* fix: address review on foreign key navigation (stale fetch, qualifiers, chip)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157Kw1ukbQo7eZnmyM63G4t

* fix: unicode literals on sql server and hide unreachable foreign key targets

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157Kw1ukbQo7eZnmyM63G4t

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-09-07 16:46:00 +02:00
committed by GitHub
co-authored by Claude Fable 5.1
parent 5da4ea43fb
commit e2b63d177a
7 changed files with 394 additions and 82 deletions
+97 -10
View File
@@ -12,8 +12,8 @@
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { ClearableInput, Drawer, DrawerContent } from './common'
import { sendUserToast } from '$lib/toast'
import { type ColumnDef } from './apps/components/display/dbtable/utils'
import DBTable from './DBTable.svelte'
import { renderDbEqualityFilter, type ColumnDef } from './apps/components/display/dbtable/utils'
import DBTable, { type DbForeignKeyTarget, type DbRowFilter } from './DBTable.svelte'
import type { IDbSchemaOps, IDbTableOps } from './dbOps'
import DropdownV2 from './DropdownV2.svelte'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
@@ -46,7 +46,12 @@
dbSupportsSchemas: boolean
databaseIsEmpty?: boolean
colDefs: Record<string, ColumnDef[]> | undefined
dbTableOpsFactory: (params: { colDefs: ColumnDef[]; tableKey: string }) => IDbTableOps
dbTableOpsFactory: (params: {
colDefs: ColumnDef[]
tableKey: string
/** Raw SQL predicate AND-ed into the reads (already escaped). */
whereClause?: string
}) => IDbTableOps
dbSchemaOps: IDbSchemaOps
refresh?: () => void
initialSchemaKey?: string
@@ -214,6 +219,83 @@
: selected.tableKey
)
// Set by "Go to row" on a foreign-keyed cell; pinned to the table it was
// created for so a schema change can't carry it onto an unrelated table.
let rowFilter: (DbRowFilter & { tableKey: string }) | undefined = $state()
let activeRowFilter = $derived(rowFilter?.tableKey === tableKey ? rowFilter : undefined)
let whereClause = $derived(
activeRowFilter
? renderDbEqualityFilter(activeRowFilter.column, activeRowFilter.value, dbType)
: undefined
)
function selectTable(schemaKey: string | undefined, table: string) {
rowFilter = undefined
selected = { schemaKey, tableKey: table }
}
/** Where a foreign key's `schema.table` target lives in the sidebar, or
* undefined when it cannot be opened from here. */
function resolveForeignKeyTarget(
targetTable: string
): { schemaKey: string; table: string } | undefined {
const parts = targetTable.split('.')
const table = parts[parts.length - 1]
const qualifier = parts.length > 1 ? parts.slice(0, -1).join('.') : undefined
// Without schema support the sidebar browses the connection's default
// schema only, and unqualified reads would hit a same-named local table.
if (!dbSupportsSchemas && qualifier && qualifier !== selected.schemaKey) return undefined
const schemaKey = dbSupportsSchemas && qualifier ? qualifier : selected.schemaKey
if (!schemaKey || !(table in (dbSchema.schema[schemaKey] ?? {}))) return undefined
return { schemaKey, table }
}
function goToRow(target: DbForeignKeyTarget) {
const resolved = resolveForeignKeyTarget(target.table)
if (!resolved) {
sendUserToast(`Table ${target.table} cannot be opened from this schema`, true)
return
}
if (renderDbEqualityFilter(target.column, target.value, dbType) === undefined) {
sendUserToast('This value cannot be used as a filter', true)
return
}
const { schemaKey, table } = resolved
selectTable(schemaKey, table)
rowFilter = {
tableKey: dbSupportsSchemas ? `${schemaKey}.${table}` : table,
column: target.column,
value: target.value
}
}
// The result carries the table it was fetched for: `resource` keeps the
// previous value while refetching, and a stale list would decorate the new
// table's same-named columns as foreign keys.
let foreignKeys = resource(
[() => selected.tableKey, () => selected.schemaKey, () => colDefs],
async ([table, schema], _prev, { signal }) => {
if (!table) return undefined
const forTableKey = dbSupportsSchemas && schema ? `${schema}.${table}` : table
const fks =
features?.foreignKeys === false
? []
: await dbSchemaOps.onFetchForeignKeys({ table, schema })
// A newer selection started meanwhile: an AbortError keeps this result
// out of `current`, where it would shadow the newer table's keys.
if (signal.aborted) throw new DOMException('Superseded', 'AbortError')
return { tableKey: forTableKey, foreignKeys: fks }
}
)
// Only keys whose target the sidebar can open get the "Go to row" affordance.
let currentForeignKeys = $derived.by(() => {
const fetched = foreignKeys.current
if (!fetched || fetched.tableKey !== tableKey) return undefined
return fetched.foreignKeys.filter(
(fk) => fk.targetTable && resolveForeignKeyTarget(fk.targetTable) !== undefined
)
})
let askingForConfirmation:
| (ConfirmationModal['$$prop_def'] & { onConfirm: () => void })
| undefined = $state()
@@ -395,14 +477,12 @@
role="button"
tabindex="0"
onclick={() => {
selected.schemaKey = schemaKey
selected.tableKey = tableKey
selectTable(schemaKey, tableKey)
toggleTableSelection(schemaKey, tableKey)
}}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
selected.schemaKey = schemaKey
selected.tableKey = tableKey
selectTable(schemaKey, tableKey)
toggleTableSelection(schemaKey, tableKey)
}
}}
@@ -468,7 +548,7 @@
<button
class={'w-full text-sm font-normal flex gap-2 items-center h-10 cursor-pointer pl-3 pr-1 ' +
(selected.tableKey === tableKey ? 'bg-surface-secondary' : 'hover:bg-surface-hover')}
onclick={() => (selected.tableKey = tableKey)}
onclick={() => selectTable(selected.schemaKey, tableKey)}
>
{#if asset}
<Star
@@ -541,8 +621,15 @@
</Pane>
<Pane class="p-3 pt-1">
{#if tableKey && colDefs?.[tableKey]?.length}
{@const dbTableOps = dbTableOpsFactory({ colDefs: colDefs[tableKey], tableKey })}
<DBTable {dbTableOps} bind:this={_dbTable} />
{@const dbTableOps = dbTableOpsFactory({ colDefs: colDefs[tableKey], tableKey, whereClause })}
<DBTable
{dbTableOps}
foreignKeys={currentForeignKeys}
onGoToRow={goToRow}
rowFilter={activeRowFilter}
onClearRowFilter={() => (rowFilter = undefined)}
bind:this={_dbTable}
/>
{:else if databaseIsEmpty}
<div class="h-full w-full center-center flex-col gap-4">
<span class="text-hint">Database is empty</span>
@@ -277,10 +277,11 @@
databaseIsEmpty={!Object.values(dbSchema.schema).flatMap((s) => Object.values(s)).length}
{dbSchema}
colDefs={colDefs.current}
dbTableOpsFactory={({ colDefs, tableKey }) =>
dbTableOpsFactory={({ colDefs, tableKey, whereClause }) =>
dbTableOpsWithPreviewScripts({
colDefs,
tableKey,
whereClause,
input: _input,
workspace: ws,
tag: workerTag
+133 -27
View File
@@ -1,11 +1,78 @@
<script lang="ts" module>
import type { ColDef, ITooltipComp, ITooltipParams } from 'ag-grid-community'
import type { TableEditorForeignKey } from './apps/components/display/dbtable/tableEditor'
import { mount, unmount } from 'svelte'
import DbForeignKeyTooltip from './DbForeignKeyTooltip.svelte'
export type DbRowFilter = { column: string; value: unknown }
export type DbForeignKeyTarget = { table: string; column: string; value: unknown }
type FkTooltipValue = { table: string; column: string; value: unknown }
/** AG Grid tooltip hosting the "Go to row" popover of a foreign-keyed cell. */
class ForeignKeyTooltip implements ITooltipComp {
private eGui = document.createElement('div')
private component: ReturnType<typeof mount> | undefined
init(params: ITooltipParams & { onGoToRow: (target: DbForeignKeyTarget) => void }) {
const target = params.value as FkTooltipValue
this.component = mount(DbForeignKeyTooltip, {
target: this.eGui,
props: {
targetTable: target.table,
targetColumn: target.column,
onGoToRow: () => {
params.hideTooltipCallback?.()
params.onGoToRow(target)
}
}
})
}
getGui() {
return this.eGui
}
destroy() {
if (this.component) unmount(this.component)
}
}
const hasForeignKeyValue = (value: unknown) =>
value !== null && value !== undefined && value !== ''
/** Single-column foreign keys only: a composite key has no one cell value to follow. */
function foreignKeyColDefs(
foreignKeys: TableEditorForeignKey[],
onGoToRow: (target: DbForeignKeyTarget) => void
): Record<string, Partial<ColDef>> {
const out: Record<string, Partial<ColDef>> = {}
for (const fk of foreignKeys) {
if (fk.columns.length !== 1) continue
const { sourceColumn, targetColumn } = fk.columns[0]
if (!sourceColumn || !targetColumn || !fk.targetTable) continue
const table = fk.targetTable
out[sourceColumn] = {
tooltipComponent: ForeignKeyTooltip,
tooltipComponentParams: { onGoToRow },
tooltipValueGetter: (p): FkTooltipValue | null =>
hasForeignKeyValue(p.value) ? { table, column: targetColumn, value: p.value } : null,
cellClass: (p) =>
hasForeignKeyValue(p.value)
? 'underline decoration-dotted decoration-tertiary underline-offset-2'
: ''
}
}
return out
}
</script>
<script lang="ts">
import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { createGrid, type GridApi, type IDatasource } from 'ag-grid-community'
import { transformColumnDefs } from './apps/components/display/table/utils'
import DarkModeObserver from './DarkModeObserver.svelte'
import { Button } from './common'
import { Download } from 'lucide-svelte'
import { Badge, Button } from './common'
import { Download, X } from 'lucide-svelte'
import Popover from './Popover.svelte'
import DebouncedInput from './apps/components/helpers/DebouncedInput.svelte'
import InsertRowDrawerButton from './apps/components/display/InsertRowDrawerButton.svelte'
@@ -18,8 +85,14 @@
type Props = {
dbTableOps: IDbTableOps
/** Foreign keys of the displayed table; their cells get a "Go to row" popover. */
foreignKeys?: TableEditorForeignKey[]
onGoToRow?: (target: DbForeignKeyTarget) => void
/** Equality filter already applied by `dbTableOps`, shown as a dismissable chip. */
rowFilter?: DbRowFilter
onClearRowFilter?: () => void
}
let { dbTableOps }: Props = $props()
let { dbTableOps, foreignKeys, onGoToRow, rowFilter, onClearRowFilter }: Props = $props()
let [clientHeight, clientWidth, darkMode, firstRow, lastRow] = $state([0, 0, false, -1, -1])
let quicksearch = $state('')
@@ -78,6 +151,10 @@
}
}),
onViewportChanged: (e) => ([firstRow, lastRow] = [e.firstRow, e.lastRow]),
// Lets the pointer move from a foreign-keyed cell into its "Go to row" popover.
tooltipInteraction: true,
// AG Grid floors the show delay at 200ms whatever value is given.
tooltipShowDelay: 0,
cacheBlockSize: 100,
cacheOverflowSize: 10,
maxBlocksInCache: 20,
@@ -93,34 +170,48 @@
let prevUpdateKey: any = undefined
$effect(() => {
if (!$workspaceStore || !api) return
const key = { quicksearch, colDefs: dbTableOps.colDefs, refreshCount }
const key = { quicksearch, colDefs: dbTableOps.colDefs, refreshCount, rowFilter }
if (deepEqual(key, prevUpdateKey)) return
prevUpdateKey = key
untrack(() => updateGrid())
})
// Foreign keys arrive after the rows: refresh the column defs alone so the
// popovers appear without re-running the row and count queries.
let prevForeignKeys: TableEditorForeignKey[] | undefined = undefined
$effect(() => {
if (!api || foreignKeys === prevForeignKeys) return
prevForeignKeys = foreignKeys
untrack(() => api?.updateGridOptions({ columnDefs: buildColumnDefs() }))
})
function buildColumnDefs() {
const fkColDefs = foreignKeys && onGoToRow ? foreignKeyColDefs(foreignKeys, onGoToRow) : {}
return transformColumnDefs({
columnDefs: (dbTableOps.colDefs ?? []).map((c) =>
c.field && fkColDefs[c.field] ? { ...c, ...fkColDefs[c.field] } : c
),
...(dbTableOps.onDelete && {
onDelete: (values) => {
if (!$workspaceStore) return
dbTableOps
.onDelete?.({ values })
.then(() => {
refresh?.()
sendUserToast('Row deleted')
})
.catch((e) => {
sendUserToast(`Error deleting row: ${e?.message ?? e}`, true)
})
}
})
})
}
function updateGrid() {
dbTableOps.getCount({ quicksearch }).then((result) => (rowCount = result))
api?.purgeInfiniteCache()
api?.updateGridOptions({
datasource,
columnDefs: transformColumnDefs({
columnDefs: dbTableOps.colDefs ?? [],
...(dbTableOps.onDelete && {
onDelete: (values) => {
if (!$workspaceStore) return
dbTableOps
.onDelete?.({ values })
.then(() => {
refresh?.()
sendUserToast('Row deleted')
})
.catch((e) => {
sendUserToast(`Error deleting row: ${e?.message ?? e}`, true)
})
}
})
}),
columnDefs: buildColumnDefs(),
context: {
quicksearch
}
@@ -132,12 +223,27 @@
<div class="h-full relative flex flex-col">
<div class="flex py-2 h-12 justify-between gap-4">
<DebouncedInput
class="w-full max-w-[300px]"
type="text"
bind:value={quicksearch}
placeholder="Search..."
/>
<div class="flex w-full items-center gap-2">
<DebouncedInput
class="w-full max-w-[300px]"
type="text"
bind:value={quicksearch}
placeholder="Search..."
/>
{#if rowFilter}
<Badge rounded class="pr-0.5 text-xs" data-testid="db-row-filter-chip">
<span class="font-mono">{rowFilter.column} = {String(rowFilter.value)}</span>
<Button
iconOnly
unifiedSize="2xs"
variant="subtle"
startIcon={{ icon: X }}
title="Clear filter"
onClick={onClearRowFilter}
/>
</Badge>
{/if}
</div>
{#if dbTableOps.onInsert}
<InsertRowDrawerButton
columnDefs={dbTableOps.colDefs ?? []}
@@ -0,0 +1,23 @@
<script lang="ts">
import { ArrowRight } from 'lucide-svelte'
import Button from './common/button/Button.svelte'
type Props = {
targetTable: string
targetColumn: string
onGoToRow: () => void
}
let { targetTable, targetColumn, onGoToRow }: Props = $props()
</script>
<div
class="flex flex-col gap-1.5 rounded-md border bg-surface-tertiary p-2 text-xs shadow-md"
data-testid="db-fk-tooltip"
>
<div class="text-secondary">
References <span class="font-mono text-primary">{targetTable}.{targetColumn}</span>
</div>
<Button unifiedSize="xs" variant="default" startIcon={{ icon: ArrowRight }} onClick={onGoToRow}>
Go to row
</Button>
</div>
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest'
import { renderDbEqualityFilter, renderDbLiteral } from './utils'
describe('renderDbLiteral', () => {
it('doubles single quotes on every dialect', () => {
expect(renderDbLiteral("O'Brien", 'postgresql')).toBe("'O''Brien'")
expect(renderDbLiteral("O'Brien", 'mysql')).toBe("'O''Brien'")
})
it('doubles backslashes only where the dialect treats them as escapes', () => {
expect(renderDbLiteral('C:\\dir\\', 'postgresql')).toBe("'C:\\dir\\'")
expect(renderDbLiteral('C:\\dir\\', 'mysql')).toBe("'C:\\\\dir\\\\'")
expect(renderDbLiteral('C:\\dir\\', 'snowflake')).toBe("'C:\\\\dir\\\\'")
})
it('marks SQL Server strings as Unicode constants', () => {
expect(renderDbLiteral("Zoë's", 'ms_sql_server')).toBe("N'Zoë''s'")
})
it('renders numbers and booleans without quotes', () => {
expect(renderDbLiteral(42, 'postgresql')).toBe('42')
expect(renderDbLiteral(true, 'postgresql')).toBe('TRUE')
expect(renderDbLiteral(true, 'ms_sql_server')).toBe('1')
})
it('has no literal for values that cannot be compared safely', () => {
expect(renderDbLiteral(null, 'postgresql')).toBeUndefined()
expect(renderDbLiteral({ a: 1 }, 'postgresql')).toBeUndefined()
expect(renderDbLiteral(NaN, 'postgresql')).toBeUndefined()
})
})
describe('renderDbEqualityFilter', () => {
it('quotes the identifier per dialect', () => {
expect(renderDbEqualityFilter('user id', 'x', 'postgresql')).toBe(`"user id" = 'x'`)
expect(renderDbEqualityFilter('user id', 'x', 'ms_sql_server')).toBe(`[user id] = N'x'`)
expect(renderDbEqualityFilter('user id', 'x', 'mysql')).toBe("`user id` = 'x'")
expect(renderDbEqualityFilter('user id', null, 'postgresql')).toBeUndefined()
})
it('doubles a delimiter embedded in the identifier', () => {
expect(renderDbEqualityFilter('a"b', 1, 'postgresql')).toBe(`"a""b" = 1`)
expect(renderDbEqualityFilter('a"b', 1, 'snowflake')).toBe(`"a""b" = 1`)
expect(renderDbEqualityFilter('a"b', 1, 'duckdb')).toBe(`"a""b" = 1`)
expect(renderDbEqualityFilter('a]b', 1, 'ms_sql_server')).toBe(`[a]]b] = 1`)
expect(renderDbEqualityFilter('a`b', 1, 'mysql')).toBe('`a``b` = 1')
})
})
@@ -333,25 +333,58 @@ export function duckdbQuicksearchColumns(columnDefs: ColumnDef[]): string {
.join(', ')
}
/** Mirrors the backend's `render_db_quoted_identifier`, including doubling an
* embedded delimiter. */
export function renderDbQuotedIdentifier(identifier: string, dbType: DbType): string {
switch (dbType) {
case 'postgresql':
return `"${identifier}"` // PostgreSQL uses double quotes for identifiers
case 'ms_sql_server':
return `[${identifier}]` // MSSQL uses square brackets for identifiers
case 'mysql':
return `\`${identifier}\`` // MySQL uses backticks
case 'snowflake':
return `"${identifier}"` // Snowflake uses double quotes for identifiers
case 'bigquery':
return `\`${identifier}\`` // BigQuery uses backticks
case 'duckdb':
return `"${identifier}"` // DuckDB uses double quotes for identifiers
return `"${identifier.replace(/"/g, '""')}"`
case 'ms_sql_server':
return `[${identifier.replace(/]/g, ']]')}]`
case 'mysql':
case 'bigquery':
return `\`${identifier.replace(/`/g, '``')}\``
default:
throw new Error('Unsupported database type: ' + dbType)
}
}
/** Renders a cell value as a SQL literal. Returns undefined for values that
* have no safe literal form (null, objects, non-finite numbers). */
export function renderDbLiteral(value: unknown, dbType: DbType): string | undefined {
if (value === null || value === undefined) return undefined
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : undefined
if (typeof value === 'bigint') return value.toString()
if (typeof value === 'boolean') {
if (dbType === 'ms_sql_server') return value ? '1' : '0'
return value ? 'TRUE' : 'FALSE'
}
if (typeof value !== 'string') return undefined
let escaped = value.replace(/'/g, "''")
// MySQL, Snowflake and BigQuery treat a backslash inside a string literal as
// an escape character.
if (dbType === 'mysql' || dbType === 'snowflake' || dbType === 'bigquery') {
escaped = escaped.replace(/\\/g, '\\\\')
}
// A plain constant is varchar on SQL Server and goes through the database
// code page; the N prefix keeps it Unicode against nvarchar columns.
return dbType === 'ms_sql_server' ? `N'${escaped}'` : `'${escaped}'`
}
/** `"column" = <literal>` predicate, or undefined when the value can't be
* rendered as a literal. */
export function renderDbEqualityFilter(
column: string,
value: unknown,
dbType: DbType
): string | undefined {
const literal = renderDbLiteral(value, dbType)
if (literal === undefined) return undefined
return `${renderDbQuotedIdentifier(column, dbType)} = ${literal}`
}
export function getLanguageByResourceType(name: string): ScriptLang {
const language = {
postgresql: 'postgresql',
+49 -35
View File
@@ -14,6 +14,7 @@ import { WorkspaceService } from '$lib/gen'
import { pendingMigrations } from './workspaceSettings/datatableMigrationUtils'
import {
buildTableEditorValues,
type TableEditorForeignKey,
type TableEditorValues
} from './apps/components/display/dbtable/tableEditor'
import { type AlterTableValues } from './apps/components/display/dbtable/queries/alterTable'
@@ -250,6 +251,10 @@ export type IDbSchemaOps = {
schema?: string
colDefs: TableMetadata
}) => Promise<TableEditorValues>
onFetchForeignKeys: (params: {
table: string
schema?: string
}) => Promise<TableEditorForeignKey[]>
}
/** Thrown by a schema op when the user declines the out-of-order run warning.
@@ -396,6 +401,48 @@ export function dbSchemaOpsWithPreviewScripts({
}
}
/** Resolves to [] when the database has no foreign key introspection
* (BigQuery) or the query fails: callers treat foreign keys as optional. */
async function fetchForeignKeys({
table,
schema
}: {
table: string
schema?: string
}): Promise<TableEditorForeignKey[]> {
if (dbType === 'bigquery') return []
try {
const fkContent = makeMarker('FOREIGN_KEYS', { table, schema })
const fkResult = await runScriptAndPollResult({
workspace,
requestBody: { args: dbArg, content: fkContent, language, tag }
})
let rawForeignKeys: RawForeignKey[]
if (dbType === 'snowflake') {
rawForeignKeys = transformSnowflakeForeignKeys(fkResult as any[])
} else {
rawForeignKeys = fkResult as RawForeignKey[]
if (rawForeignKeys && Array.isArray(rawForeignKeys)) {
rawForeignKeys = rawForeignKeys.map((fk) => {
const lowerFk: any = {}
Object.keys(fk).forEach((key) => {
lowerFk[key.toLowerCase()] = fk[key]
})
return lowerFk
})
}
}
if (rawForeignKeys && Array.isArray(rawForeignKeys)) {
return transformForeignKeys(rawForeignKeys)
}
} catch (e) {
console.warn('Failed to fetch foreign keys:', e)
}
return []
}
return {
onDelete: async ({ tableKey, schema }) => {
const content = makeMarker('DROP_TABLE', { table: tableKey, schema })
@@ -454,44 +501,11 @@ export function dbSchemaOpsWithPreviewScripts({
const downContent = makeMarker('CREATE_SCHEMA', { schema })
await applyDdl(migrationName('drop_schema', schema), content, downContent)
},
onFetchForeignKeys: fetchForeignKeys,
onFetchTableEditorDefinition: async ({ table, schema, colDefs }) => {
let foreignKeys: import('./apps/components/display/dbtable/tableEditor').TableEditorForeignKey[] =
[]
const foreignKeys = await fetchForeignKeys({ table, schema })
let pk_constraint_name: string | undefined
// Fetch foreign keys (not supported for BigQuery)
if (dbType !== 'bigquery') {
try {
const fkContent = makeMarker('FOREIGN_KEYS', { table, schema })
const fkResult = await runScriptAndPollResult({
workspace,
requestBody: { args: dbArg, content: fkContent, language, tag }
})
let rawForeignKeys: RawForeignKey[]
if (dbType === 'snowflake') {
rawForeignKeys = transformSnowflakeForeignKeys(fkResult as any[])
} else {
rawForeignKeys = fkResult as RawForeignKey[]
if (rawForeignKeys && Array.isArray(rawForeignKeys)) {
rawForeignKeys = rawForeignKeys.map((fk) => {
const lowerFk: any = {}
Object.keys(fk).forEach((key) => {
lowerFk[key.toLowerCase()] = fk[key]
})
return lowerFk
})
}
}
if (rawForeignKeys && Array.isArray(rawForeignKeys)) {
foreignKeys = transformForeignKeys(rawForeignKeys)
}
} catch (e) {
console.warn('Failed to fetch foreign keys:', e)
}
}
// Fetch primary key constraint name (not supported for BigQuery/MySQL)
if (dbType !== 'bigquery' && dbType !== 'mysql') {
try {